@wire-dsl/engine 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -93,6 +93,21 @@ var SourceMapBuilder = class {
93
93
  this.counters.set("cell", idx + 1);
94
94
  return `cell-${idx}`;
95
95
  }
96
+ case "tab": {
97
+ const idx = this.counters.get("tab") || 0;
98
+ this.counters.set("tab", idx + 1);
99
+ return `tab-${idx}`;
100
+ }
101
+ case "modal-body": {
102
+ const idx = this.counters.get("modal-body") || 0;
103
+ this.counters.set("modal-body", idx + 1);
104
+ return `modal-body-${idx}`;
105
+ }
106
+ case "modal-footer": {
107
+ const idx = this.counters.get("modal-footer") || 0;
108
+ this.counters.set("modal-footer", idx + 1);
109
+ return `modal-footer-${idx}`;
110
+ }
96
111
  case "component-definition":
97
112
  return `define-${metadata?.name || "unknown"}`;
98
113
  case "layout-definition":
@@ -155,6 +170,49 @@ var SourceMapBuilder = class {
155
170
  entry.properties[propertyName] = propertySourceMap;
156
171
  return propertySourceMap;
157
172
  }
173
+ /**
174
+ * Add an event handler to an existing node in the SourceMap
175
+ * Events have action expressions as values (e.g., "show(modal) & navigate(Home)")
176
+ *
177
+ * @param nodeId - ID of the node that owns this event
178
+ * @param eventName - Name of the event (e.g., "onClick", "onClose")
179
+ * @param tokens - Captured tokens: name token for event key, CST node for actionChain
180
+ * @returns The EventSourceMap entry created
181
+ */
182
+ addEvent(nodeId, eventName, tokens) {
183
+ const entry = this.entries.find((e) => e.nodeId === nodeId);
184
+ if (!entry) {
185
+ throw new Error(`Cannot add event to non-existent node: ${nodeId}`);
186
+ }
187
+ if (!entry.events) {
188
+ entry.events = {};
189
+ }
190
+ const nameRange = {
191
+ start: this.getTokenStart(tokens.name),
192
+ end: this.getTokenEnd(tokens.name)
193
+ };
194
+ const valueRange = {
195
+ start: this.getTokenStart(tokens.value),
196
+ end: this.getTokenEnd(tokens.value)
197
+ };
198
+ const fullRange = {
199
+ start: nameRange.start,
200
+ end: valueRange.end
201
+ };
202
+ let rawValue = "";
203
+ if (this.sourceCode && valueRange.start.offset !== void 0 && valueRange.end.offset !== void 0) {
204
+ rawValue = this.sourceCode.slice(valueRange.start.offset, valueRange.end.offset + 1);
205
+ }
206
+ const eventSourceMap = {
207
+ name: eventName,
208
+ value: rawValue,
209
+ range: fullRange,
210
+ nameRange,
211
+ valueRange
212
+ };
213
+ entry.events[eventName] = eventSourceMap;
214
+ return eventSourceMap;
215
+ }
158
216
  /**
159
217
  * Push a parent onto the stack (when entering a container node)
160
218
  */
@@ -190,6 +248,9 @@ var SourceMapBuilder = class {
190
248
  "screen",
191
249
  "layout",
192
250
  "cell",
251
+ "tab",
252
+ "modal-body",
253
+ "modal-footer",
193
254
  "component-definition",
194
255
  "layout-definition"
195
256
  ];
@@ -434,12 +495,26 @@ var Style = createToken({ name: "Style", pattern: /style\b/ });
434
495
  var Mocks = createToken({ name: "Mocks", pattern: /mocks\b/ });
435
496
  var Colors = createToken({ name: "Colors", pattern: /colors(?=\s*\{)/ });
436
497
  var Cell = createToken({ name: "Cell", pattern: /cell\b/ });
498
+ var Tab = createToken({ name: "Tab", pattern: /tab\b/ });
499
+ var Body = createToken({ name: "Body", pattern: /body\b/ });
500
+ var Footer = createToken({ name: "Footer", pattern: /footer\b/ });
501
+ var Navigate = createToken({ name: "Navigate", pattern: /navigate\b/ });
502
+ var Show = createToken({ name: "Show", pattern: /show\b/ });
503
+ var Hide = createToken({ name: "Hide", pattern: /hide\b/ });
504
+ var ToggleAction = createToken({ name: "ToggleAction", pattern: /toggle\b/ });
505
+ var EnableAction = createToken({ name: "EnableAction", pattern: /enable\b/ });
506
+ var DisableAction = createToken({ name: "DisableAction", pattern: /disable\b/ });
507
+ var SetTab = createToken({ name: "SetTab", pattern: /setTab\b/ });
508
+ var Self = createToken({ name: "Self", pattern: /self\b/ });
437
509
  var LCurly = createToken({ name: "LCurly", pattern: /{/ });
438
510
  var RCurly = createToken({ name: "RCurly", pattern: /}/ });
439
511
  var LParen = createToken({ name: "LParen", pattern: /\(/ });
440
512
  var RParen = createToken({ name: "RParen", pattern: /\)/ });
441
513
  var Colon = createToken({ name: "Colon", pattern: /:/ });
442
514
  var Comma = createToken({ name: "Comma", pattern: /,/ });
515
+ var Ampersand = createToken({ name: "Ampersand", pattern: /&/ });
516
+ var LBracket = createToken({ name: "LBracket", pattern: /\[/ });
517
+ var RBracket = createToken({ name: "RBracket", pattern: /\]/ });
443
518
  var StringLiteral = createToken({
444
519
  name: "StringLiteral",
445
520
  pattern: /"(?:[^"\\]|\\.)*"/
@@ -488,6 +563,18 @@ var allTokens = [
488
563
  Mocks,
489
564
  Colors,
490
565
  Cell,
566
+ Tab,
567
+ Body,
568
+ Footer,
569
+ // Event action keywords (must come before Identifier)
570
+ Navigate,
571
+ Show,
572
+ Hide,
573
+ ToggleAction,
574
+ EnableAction,
575
+ DisableAction,
576
+ SetTab,
577
+ Self,
491
578
  // Punctuation
492
579
  LCurly,
493
580
  RCurly,
@@ -495,6 +582,9 @@ var allTokens = [
495
582
  RParen,
496
583
  Colon,
497
584
  Comma,
585
+ Ampersand,
586
+ LBracket,
587
+ RBracket,
498
588
  // Literals
499
589
  StringLiteral,
500
590
  NumberLiteral,
@@ -602,6 +692,90 @@ var WireDSLParser = class extends CstParser {
602
692
  this.SUBRULE(this.layout);
603
693
  this.CONSUME(RCurly);
604
694
  });
695
+ // singleAction: navigate(Screen) | show(id|self) | hide(id|self) | toggle(id|self) | setTab(tabsId, index)
696
+ this.singleAction = this.RULE("singleAction", () => {
697
+ this.OR([
698
+ {
699
+ ALT: () => {
700
+ this.CONSUME(Navigate, { LABEL: "navigate" });
701
+ this.CONSUME(LParen);
702
+ this.CONSUME(Identifier, { LABEL: "targetScreen" });
703
+ this.CONSUME(RParen);
704
+ }
705
+ },
706
+ {
707
+ ALT: () => {
708
+ this.OR2([
709
+ { ALT: () => this.CONSUME(Show, { LABEL: "sht" }) },
710
+ { ALT: () => this.CONSUME(Hide, { LABEL: "sht" }) },
711
+ { ALT: () => this.CONSUME(ToggleAction, { LABEL: "sht" }) },
712
+ { ALT: () => this.CONSUME(EnableAction, { LABEL: "sht" }) },
713
+ { ALT: () => this.CONSUME(DisableAction, { LABEL: "sht" }) }
714
+ ]);
715
+ this.CONSUME2(LParen);
716
+ this.OR3([
717
+ { ALT: () => this.CONSUME(Self, { LABEL: "targetId" }) },
718
+ { ALT: () => this.CONSUME2(Identifier, { LABEL: "targetId" }) }
719
+ ]);
720
+ this.CONSUME2(RParen);
721
+ }
722
+ },
723
+ {
724
+ ALT: () => {
725
+ this.CONSUME(SetTab, { LABEL: "setTab" });
726
+ this.CONSUME3(LParen);
727
+ this.CONSUME3(Identifier, { LABEL: "tabsId" });
728
+ this.CONSUME(Comma);
729
+ this.CONSUME(NumberLiteral, { LABEL: "tabIndex" });
730
+ this.CONSUME3(RParen);
731
+ }
732
+ }
733
+ ]);
734
+ });
735
+ // actionChain: singleAction (& singleAction)*
736
+ this.actionChain = this.RULE("actionChain", () => {
737
+ this.SUBRULE(this.singleAction);
738
+ this.MANY(() => {
739
+ this.CONSUME(Ampersand);
740
+ this.SUBRULE2(this.singleAction);
741
+ });
742
+ });
743
+ // tab { ... } — children block inside layout tabs
744
+ this.tab = this.RULE("tab", () => {
745
+ this.CONSUME(Tab);
746
+ this.CONSUME(LCurly);
747
+ this.MANY(() => {
748
+ this.OR([
749
+ { ALT: () => this.SUBRULE(this.component) },
750
+ { ALT: () => this.SUBRULE(this.layout) }
751
+ ]);
752
+ });
753
+ this.CONSUME(RCurly);
754
+ });
755
+ // body { ... } — content section inside layout modal
756
+ this.body = this.RULE("body", () => {
757
+ this.CONSUME(Body);
758
+ this.CONSUME(LCurly);
759
+ this.MANY(() => {
760
+ this.OR([
761
+ { ALT: () => this.SUBRULE(this.component) },
762
+ { ALT: () => this.SUBRULE(this.layout) }
763
+ ]);
764
+ });
765
+ this.CONSUME(RCurly);
766
+ });
767
+ // footer { ... } — footer section inside layout modal
768
+ this.footer = this.RULE("footer", () => {
769
+ this.CONSUME(Footer);
770
+ this.CONSUME(LCurly);
771
+ this.MANY(() => {
772
+ this.OR([
773
+ { ALT: () => this.SUBRULE(this.component) },
774
+ { ALT: () => this.SUBRULE(this.layout) }
775
+ ]);
776
+ });
777
+ this.CONSUME(RCurly);
778
+ });
605
779
  // layout stack(...) { ... }
606
780
  this.layout = this.RULE("layout", () => {
607
781
  this.CONSUME(Layout);
@@ -614,7 +788,10 @@ var WireDSLParser = class extends CstParser {
614
788
  this.OR([
615
789
  { ALT: () => this.SUBRULE(this.component) },
616
790
  { ALT: () => this.SUBRULE2(this.layout) },
617
- { ALT: () => this.SUBRULE(this.cell) }
791
+ { ALT: () => this.SUBRULE(this.cell) },
792
+ { ALT: () => this.SUBRULE(this.tab) },
793
+ { ALT: () => this.SUBRULE(this.body) },
794
+ { ALT: () => this.SUBRULE(this.footer) }
618
795
  ]);
619
796
  });
620
797
  this.CONSUME(RCurly);
@@ -642,16 +819,27 @@ var WireDSLParser = class extends CstParser {
642
819
  this.SUBRULE(this.property);
643
820
  });
644
821
  });
645
- // property: key: value
822
+ // property: key: value (value can be string, number, identifier, or action chain)
646
823
  this.property = this.RULE("property", () => {
647
824
  this.CONSUME(Identifier, { LABEL: "propKey" });
648
825
  this.CONSUME(Colon);
649
826
  this.OR([
827
+ { ALT: () => this.SUBRULE(this.actionChain) },
828
+ { ALT: () => this.SUBRULE(this.arrayLiteral) },
650
829
  { ALT: () => this.CONSUME(StringLiteral, { LABEL: "propValue" }) },
651
830
  { ALT: () => this.CONSUME(NumberLiteral, { LABEL: "propValue" }) },
652
831
  { ALT: () => this.CONSUME2(Identifier, { LABEL: "propValue" }) }
653
832
  ]);
654
833
  });
834
+ // ["item1", "item2", "item3"]
835
+ this.arrayLiteral = this.RULE("arrayLiteral", () => {
836
+ this.CONSUME(LBracket);
837
+ this.MANY_SEP({
838
+ SEP: Comma,
839
+ DEF: () => this.CONSUME(StringLiteral, { LABEL: "arrayItem" })
840
+ });
841
+ this.CONSUME(RBracket);
842
+ });
655
843
  // (param1: value1, param2: value2)
656
844
  this.paramList = this.RULE("paramList", () => {
657
845
  this.CONSUME(LParen);
@@ -790,7 +978,7 @@ var WireDSLVisitor = class extends BaseCstVisitor {
790
978
  };
791
979
  }
792
980
  screen(ctx) {
793
- const params = ctx.paramList ? this.visit(ctx.paramList[0]) : {};
981
+ const { params } = ctx.paramList ? this.visit(ctx.paramList[0]) : { params: {} };
794
982
  return {
795
983
  type: "screen",
796
984
  name: ctx.screenName[0].image,
@@ -801,10 +989,12 @@ var WireDSLVisitor = class extends BaseCstVisitor {
801
989
  layout(ctx) {
802
990
  const layoutType = ctx.layoutType[0].image;
803
991
  const params = {};
992
+ const events = [];
804
993
  const children = [];
805
994
  if (ctx.paramList) {
806
995
  const paramResult = this.visit(ctx.paramList);
807
- Object.assign(params, paramResult);
996
+ Object.assign(params, paramResult.params);
997
+ events.push(...paramResult.events);
808
998
  }
809
999
  const childNodes = [];
810
1000
  if (ctx.component) {
@@ -825,20 +1015,33 @@ var WireDSLVisitor = class extends BaseCstVisitor {
825
1015
  childNodes.push({ type: "cell", node: cell, index: startToken.startOffset });
826
1016
  });
827
1017
  }
1018
+ if (ctx.tab) {
1019
+ ctx.tab.forEach((tab) => {
1020
+ const startToken = tab.children?.Tab?.[0];
1021
+ childNodes.push({ type: "tab", node: tab, index: startToken.startOffset });
1022
+ });
1023
+ }
1024
+ if (ctx.body) {
1025
+ ctx.body.forEach((body) => {
1026
+ const startToken = body.children?.Body?.[0];
1027
+ childNodes.push({ type: "body", node: body, index: startToken.startOffset });
1028
+ });
1029
+ }
1030
+ if (ctx.footer) {
1031
+ ctx.footer.forEach((footer) => {
1032
+ const startToken = footer.children?.Footer?.[0];
1033
+ childNodes.push({ type: "footer", node: footer, index: startToken.startOffset });
1034
+ });
1035
+ }
828
1036
  childNodes.sort((a, b) => a.index - b.index);
829
1037
  childNodes.forEach((item) => {
830
- if (item.type === "component") {
831
- children.push(this.visit(item.node));
832
- } else if (item.type === "layout") {
833
- children.push(this.visit(item.node));
834
- } else if (item.type === "cell") {
835
- children.push(this.visit(item.node));
836
- }
1038
+ children.push(this.visit(item.node));
837
1039
  });
838
1040
  return {
839
1041
  type: "layout",
840
1042
  layoutType,
841
1043
  params,
1044
+ events,
842
1045
  children
843
1046
  };
844
1047
  }
@@ -878,23 +1081,137 @@ var WireDSLVisitor = class extends BaseCstVisitor {
878
1081
  children
879
1082
  };
880
1083
  }
1084
+ singleAction(ctx) {
1085
+ if (ctx.navigate) {
1086
+ return { type: "navigate", screen: ctx.targetScreen[0].image };
1087
+ }
1088
+ if (ctx.sht) {
1089
+ const tokenName = ctx.sht[0].tokenType.name;
1090
+ const typeMap = {
1091
+ Show: "show",
1092
+ Hide: "hide",
1093
+ ToggleAction: "toggle",
1094
+ EnableAction: "enable",
1095
+ DisableAction: "disable"
1096
+ };
1097
+ const type = typeMap[tokenName] ?? "show";
1098
+ const targetToken = ctx.targetId[0];
1099
+ const isSelf = targetToken.tokenType.name === "Self";
1100
+ const targetId = isSelf ? "_self" : targetToken.image;
1101
+ return { type, targetId };
1102
+ }
1103
+ if (ctx.setTab) {
1104
+ return {
1105
+ type: "setTab",
1106
+ tabsId: ctx.tabsId[0].image,
1107
+ index: Number(ctx.tabIndex[0].image)
1108
+ };
1109
+ }
1110
+ throw new Error("Unknown action type in singleAction visitor");
1111
+ }
1112
+ actionChain(ctx) {
1113
+ const actions = [];
1114
+ if (ctx.singleAction) {
1115
+ ctx.singleAction.forEach((action) => {
1116
+ actions.push(this.visit(action));
1117
+ });
1118
+ }
1119
+ return actions;
1120
+ }
1121
+ tab(ctx) {
1122
+ const children = [];
1123
+ const childNodes = [];
1124
+ if (ctx.component) {
1125
+ ctx.component.forEach((comp) => {
1126
+ const startToken = comp.children?.Component?.[0] || comp.children?.componentType?.[0];
1127
+ childNodes.push({ type: "component", node: comp, index: startToken.startOffset });
1128
+ });
1129
+ }
1130
+ if (ctx.layout) {
1131
+ ctx.layout.forEach((layout) => {
1132
+ const startToken = layout.children?.Layout?.[0] || layout.children?.layoutType?.[0];
1133
+ childNodes.push({ type: "layout", node: layout, index: startToken.startOffset });
1134
+ });
1135
+ }
1136
+ childNodes.sort((a, b) => a.index - b.index);
1137
+ childNodes.forEach((item) => {
1138
+ children.push(this.visit(item.node));
1139
+ });
1140
+ return { type: "tab", children };
1141
+ }
1142
+ body(ctx) {
1143
+ const children = [];
1144
+ const childNodes = [];
1145
+ if (ctx.component) {
1146
+ ctx.component.forEach((comp) => {
1147
+ const startToken = comp.children?.Component?.[0] || comp.children?.componentType?.[0];
1148
+ childNodes.push({ type: "component", node: comp, index: startToken.startOffset });
1149
+ });
1150
+ }
1151
+ if (ctx.layout) {
1152
+ ctx.layout.forEach((layout) => {
1153
+ const startToken = layout.children?.Layout?.[0] || layout.children?.layoutType?.[0];
1154
+ childNodes.push({ type: "layout", node: layout, index: startToken.startOffset });
1155
+ });
1156
+ }
1157
+ childNodes.sort((a, b) => a.index - b.index);
1158
+ childNodes.forEach((item) => {
1159
+ children.push(this.visit(item.node));
1160
+ });
1161
+ return { type: "modal-body", children };
1162
+ }
1163
+ footer(ctx) {
1164
+ const children = [];
1165
+ const childNodes = [];
1166
+ if (ctx.component) {
1167
+ ctx.component.forEach((comp) => {
1168
+ const startToken = comp.children?.Component?.[0] || comp.children?.componentType?.[0];
1169
+ childNodes.push({ type: "component", node: comp, index: startToken.startOffset });
1170
+ });
1171
+ }
1172
+ if (ctx.layout) {
1173
+ ctx.layout.forEach((layout) => {
1174
+ const startToken = layout.children?.Layout?.[0] || layout.children?.layoutType?.[0];
1175
+ childNodes.push({ type: "layout", node: layout, index: startToken.startOffset });
1176
+ });
1177
+ }
1178
+ childNodes.sort((a, b) => a.index - b.index);
1179
+ childNodes.forEach((item) => {
1180
+ children.push(this.visit(item.node));
1181
+ });
1182
+ return { type: "modal-footer", children };
1183
+ }
881
1184
  component(ctx) {
882
1185
  const componentType = ctx.componentType[0].image;
883
1186
  const props = {};
1187
+ const events = [];
884
1188
  if (ctx.property) {
885
1189
  ctx.property.forEach((prop) => {
886
1190
  const result = this.visit(prop);
887
- props[result.key] = result.value;
1191
+ if (result.isEvent) {
1192
+ events.push({ event: result.key, actions: result.actions });
1193
+ } else {
1194
+ props[result.key] = result.value;
1195
+ }
888
1196
  });
889
1197
  }
890
1198
  return {
891
1199
  type: "component",
892
1200
  componentType,
893
- props
1201
+ props,
1202
+ events
894
1203
  };
895
1204
  }
896
1205
  property(ctx) {
897
1206
  const key = ctx.propKey[0].image;
1207
+ if (ctx.actionChain && ctx.actionChain.length > 0) {
1208
+ const actions = this.visit(ctx.actionChain[0]);
1209
+ return { key, isEvent: true, actions };
1210
+ }
1211
+ if (ctx.arrayLiteral && ctx.arrayLiteral.length > 0) {
1212
+ const items = this.visit(ctx.arrayLiteral[0]);
1213
+ return { key, value: items };
1214
+ }
898
1215
  const rawValue = ctx.propValue[0].image;
899
1216
  let value = rawValue;
900
1217
  if (typeof rawValue === "string" && rawValue.startsWith('"')) {
@@ -904,15 +1221,27 @@ var WireDSLVisitor = class extends BaseCstVisitor {
904
1221
  }
905
1222
  return { key, value };
906
1223
  }
1224
+ arrayLiteral(ctx) {
1225
+ if (!ctx.arrayItem) return [];
1226
+ return ctx.arrayItem.map((token) => {
1227
+ const raw = token.image;
1228
+ return raw.startsWith('"') ? raw.slice(1, -1) : raw;
1229
+ });
1230
+ }
907
1231
  paramList(ctx) {
908
1232
  const params = {};
1233
+ const events = [];
909
1234
  if (ctx.property) {
910
1235
  ctx.property.forEach((prop) => {
911
1236
  const result = this.visit(prop);
912
- params[result.key] = result.value;
1237
+ if (result.isEvent) {
1238
+ events.push({ event: result.key, actions: result.actions });
1239
+ } else {
1240
+ params[result.key] = result.value;
1241
+ }
913
1242
  });
914
1243
  }
915
- return params;
1244
+ return { params, events };
916
1245
  }
917
1246
  };
918
1247
  var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
@@ -987,7 +1316,7 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
987
1316
  return ast;
988
1317
  }
989
1318
  screen(ctx) {
990
- const params = ctx.paramList ? this.visit(ctx.paramList[0]) : {};
1319
+ const { params } = ctx.paramList ? this.visit(ctx.paramList[0]) : { params: {} };
991
1320
  const screenName = ctx.screenName[0].image;
992
1321
  const tokens = {
993
1322
  keyword: ctx.Screen[0],
@@ -1020,9 +1349,11 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
1020
1349
  layout(ctx) {
1021
1350
  const layoutType = ctx.layoutType[0].image;
1022
1351
  const params = {};
1352
+ const events = [];
1023
1353
  if (ctx.paramList) {
1024
1354
  const paramResult = this.visit(ctx.paramList);
1025
- Object.assign(params, paramResult);
1355
+ Object.assign(params, paramResult.params);
1356
+ events.push(...paramResult.events);
1026
1357
  }
1027
1358
  const tokens = {
1028
1359
  keyword: ctx.Layout[0],
@@ -1034,6 +1365,7 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
1034
1365
  type: "layout",
1035
1366
  layoutType,
1036
1367
  params,
1368
+ events,
1037
1369
  children: []
1038
1370
  // Will be filled after push
1039
1371
  };
@@ -1047,13 +1379,21 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
1047
1379
  if (ctx.paramList && ctx.paramList[0]?.children?.property) {
1048
1380
  ctx.paramList[0].children.property.forEach((propCtx) => {
1049
1381
  const propResult = this.visit(propCtx);
1382
+ if (propResult.isEvent) {
1383
+ this.sourceMapBuilder.addEvent(nodeId, propResult.key, {
1384
+ name: propCtx.children.propKey[0],
1385
+ value: propCtx.children.actionChain[0]
1386
+ });
1387
+ return;
1388
+ }
1389
+ const valueToken = propCtx.children.propValue?.[0] ?? propCtx.children.arrayLiteral?.[0];
1050
1390
  this.sourceMapBuilder.addProperty(
1051
1391
  nodeId,
1052
1392
  propResult.key,
1053
1393
  propResult.value,
1054
1394
  {
1055
1395
  name: propCtx.children.propKey[0],
1056
- value: propCtx.children.propValue[0]
1396
+ value: valueToken
1057
1397
  }
1058
1398
  );
1059
1399
  });
@@ -1079,6 +1419,24 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
1079
1419
  childNodes.push({ type: "cell", node: cell, index: startToken.startOffset });
1080
1420
  });
1081
1421
  }
1422
+ if (ctx.tab) {
1423
+ ctx.tab.forEach((tab) => {
1424
+ const startToken = tab.children?.Tab?.[0];
1425
+ childNodes.push({ type: "tab", node: tab, index: startToken.startOffset });
1426
+ });
1427
+ }
1428
+ if (ctx.body) {
1429
+ ctx.body.forEach((body) => {
1430
+ const startToken = body.children?.Body?.[0];
1431
+ childNodes.push({ type: "body", node: body, index: startToken.startOffset });
1432
+ });
1433
+ }
1434
+ if (ctx.footer) {
1435
+ ctx.footer.forEach((footer) => {
1436
+ const startToken = footer.children?.Footer?.[0];
1437
+ childNodes.push({ type: "footer", node: footer, index: startToken.startOffset });
1438
+ });
1439
+ }
1082
1440
  childNodes.sort((a, b) => a.index - b.index);
1083
1441
  childNodes.forEach((item) => {
1084
1442
  ast.children.push(this.visit(item.node));
@@ -1116,13 +1474,21 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
1116
1474
  if (ctx.property) {
1117
1475
  ctx.property.forEach((propCtx) => {
1118
1476
  const propResult = this.visit(propCtx);
1477
+ if (propResult.isEvent) {
1478
+ this.sourceMapBuilder.addEvent(nodeId, propResult.key, {
1479
+ name: propCtx.children.propKey[0],
1480
+ value: propCtx.children.actionChain[0]
1481
+ });
1482
+ return;
1483
+ }
1484
+ const valueToken = propCtx.children.propValue?.[0] ?? propCtx.children.arrayLiteral?.[0];
1119
1485
  this.sourceMapBuilder.addProperty(
1120
1486
  nodeId,
1121
1487
  propResult.key,
1122
1488
  propResult.value,
1123
1489
  {
1124
1490
  name: propCtx.children.propKey[0],
1125
- value: propCtx.children.propValue[0]
1491
+ value: valueToken
1126
1492
  }
1127
1493
  );
1128
1494
  });
@@ -1151,6 +1517,114 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
1151
1517
  }
1152
1518
  return ast;
1153
1519
  }
1520
+ body(ctx) {
1521
+ const tokens = {
1522
+ keyword: ctx.Body[0],
1523
+ body: ctx.RCurly[0]
1524
+ };
1525
+ const ast = {
1526
+ type: "modal-body",
1527
+ children: []
1528
+ };
1529
+ if (this.sourceMapBuilder) {
1530
+ const nodeId = this.sourceMapBuilder.addNode("modal-body", tokens);
1531
+ ast._meta = { nodeId };
1532
+ this.sourceMapBuilder.pushParent(nodeId);
1533
+ }
1534
+ const childNodes = [];
1535
+ if (ctx.component) {
1536
+ ctx.component.forEach((comp) => {
1537
+ const startToken = comp.children?.Component?.[0] || comp.children?.componentType?.[0];
1538
+ childNodes.push({ type: "component", node: comp, index: startToken.startOffset });
1539
+ });
1540
+ }
1541
+ if (ctx.layout) {
1542
+ ctx.layout.forEach((layout) => {
1543
+ const startToken = layout.children?.Layout?.[0] || layout.children?.layoutType?.[0];
1544
+ childNodes.push({ type: "layout", node: layout, index: startToken.startOffset });
1545
+ });
1546
+ }
1547
+ childNodes.sort((a, b) => a.index - b.index);
1548
+ childNodes.forEach((item) => {
1549
+ ast.children.push(this.visit(item.node));
1550
+ });
1551
+ if (this.sourceMapBuilder) {
1552
+ this.sourceMapBuilder.popParent();
1553
+ }
1554
+ return ast;
1555
+ }
1556
+ tab(ctx) {
1557
+ const tokens = {
1558
+ keyword: ctx.Tab[0],
1559
+ body: ctx.RCurly[0]
1560
+ };
1561
+ const ast = {
1562
+ type: "tab",
1563
+ children: []
1564
+ };
1565
+ if (this.sourceMapBuilder) {
1566
+ const nodeId = this.sourceMapBuilder.addNode("tab", tokens);
1567
+ ast._meta = { nodeId };
1568
+ this.sourceMapBuilder.pushParent(nodeId);
1569
+ }
1570
+ const childNodes = [];
1571
+ if (ctx.component) {
1572
+ ctx.component.forEach((comp) => {
1573
+ const startToken = comp.children?.Component?.[0] || comp.children?.componentType?.[0];
1574
+ childNodes.push({ type: "component", node: comp, index: startToken.startOffset });
1575
+ });
1576
+ }
1577
+ if (ctx.layout) {
1578
+ ctx.layout.forEach((layout) => {
1579
+ const startToken = layout.children?.Layout?.[0] || layout.children?.layoutType?.[0];
1580
+ childNodes.push({ type: "layout", node: layout, index: startToken.startOffset });
1581
+ });
1582
+ }
1583
+ childNodes.sort((a, b) => a.index - b.index);
1584
+ childNodes.forEach((item) => {
1585
+ ast.children.push(this.visit(item.node));
1586
+ });
1587
+ if (this.sourceMapBuilder) {
1588
+ this.sourceMapBuilder.popParent();
1589
+ }
1590
+ return ast;
1591
+ }
1592
+ footer(ctx) {
1593
+ const tokens = {
1594
+ keyword: ctx.Footer[0],
1595
+ body: ctx.RCurly[0]
1596
+ };
1597
+ const ast = {
1598
+ type: "modal-footer",
1599
+ children: []
1600
+ };
1601
+ if (this.sourceMapBuilder) {
1602
+ const nodeId = this.sourceMapBuilder.addNode("modal-footer", tokens);
1603
+ ast._meta = { nodeId };
1604
+ this.sourceMapBuilder.pushParent(nodeId);
1605
+ }
1606
+ const childNodes = [];
1607
+ if (ctx.component) {
1608
+ ctx.component.forEach((comp) => {
1609
+ const startToken = comp.children?.Component?.[0] || comp.children?.componentType?.[0];
1610
+ childNodes.push({ type: "component", node: comp, index: startToken.startOffset });
1611
+ });
1612
+ }
1613
+ if (ctx.layout) {
1614
+ ctx.layout.forEach((layout) => {
1615
+ const startToken = layout.children?.Layout?.[0] || layout.children?.layoutType?.[0];
1616
+ childNodes.push({ type: "layout", node: layout, index: startToken.startOffset });
1617
+ });
1618
+ }
1619
+ childNodes.sort((a, b) => a.index - b.index);
1620
+ childNodes.forEach((item) => {
1621
+ ast.children.push(this.visit(item.node));
1622
+ });
1623
+ if (this.sourceMapBuilder) {
1624
+ this.sourceMapBuilder.popParent();
1625
+ }
1626
+ return ast;
1627
+ }
1154
1628
  component(ctx) {
1155
1629
  const tokens = {
1156
1630
  keyword: ctx.Component[0],
@@ -1172,13 +1646,21 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
1172
1646
  if (ctx.property) {
1173
1647
  ctx.property.forEach((propCtx) => {
1174
1648
  const propResult = this.visit(propCtx);
1649
+ if (propResult.isEvent) {
1650
+ this.sourceMapBuilder.addEvent(nodeId, propResult.key, {
1651
+ name: propCtx.children.propKey[0],
1652
+ value: propCtx.children.actionChain[0]
1653
+ });
1654
+ return;
1655
+ }
1656
+ const valueToken = propCtx.children.propValue?.[0] ?? propCtx.children.arrayLiteral?.[0];
1175
1657
  this.sourceMapBuilder.addProperty(
1176
1658
  nodeId,
1177
1659
  propResult.key,
1178
1660
  propResult.value,
1179
1661
  {
1180
1662
  name: propCtx.children.propKey[0],
1181
- value: propCtx.children.propValue[0]
1663
+ value: valueToken
1182
1664
  }
1183
1665
  );
1184
1666
  });
@@ -1525,11 +2007,13 @@ function createParserDiagnostic(error) {
1525
2007
  };
1526
2008
  }
1527
2009
  function isBooleanLike(value) {
2010
+ if (Array.isArray(value)) return false;
1528
2011
  if (typeof value === "number") return value === 0 || value === 1;
1529
2012
  const normalized = String(value).trim().toLowerCase();
1530
2013
  return normalized === "true" || normalized === "false";
1531
2014
  }
1532
2015
  function parseBooleanLike(value, fallback = false) {
2016
+ if (Array.isArray(value)) return fallback;
1533
2017
  if (typeof value === "number") {
1534
2018
  if (value === 1) return true;
1535
2019
  if (value === 0) return false;
@@ -1586,6 +2070,14 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1586
2070
  count += countChildrenSlots(cellChild);
1587
2071
  }
1588
2072
  }
2073
+ } else if (child.type === "tab") {
2074
+ for (const tabChild of child.children) {
2075
+ if (tabChild.type === "component") {
2076
+ if (tabChild.componentType === "Children") count += 1;
2077
+ } else if (tabChild.type === "layout") {
2078
+ count += countChildrenSlots(tabChild);
2079
+ }
2080
+ }
1589
2081
  }
1590
2082
  }
1591
2083
  return count;
@@ -1832,6 +2324,11 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1832
2324
  checkLayout(child, insideDefinedLayout);
1833
2325
  } else if (child.type === "cell") {
1834
2326
  checkCell(child, insideDefinedLayout);
2327
+ } else if (child.type === "tab") {
2328
+ for (const tabChild of child.children) {
2329
+ if (tabChild.type === "component") checkComponent(tabChild, insideDefinedLayout);
2330
+ if (tabChild.type === "layout") checkLayout(tabChild, insideDefinedLayout);
2331
+ }
1835
2332
  }
1836
2333
  }
1837
2334
  };
@@ -2009,6 +2506,16 @@ function validateDefinitionCycles(ast) {
2009
2506
  collectLayoutDependencies(cellChild, deps);
2010
2507
  }
2011
2508
  }
2509
+ } else if (child.type === "tab") {
2510
+ for (const tabChild of child.children) {
2511
+ if (tabChild.type === "component") {
2512
+ if (shouldTrackComponentDependency(tabChild.componentType)) {
2513
+ deps.add(makeComponentKey(tabChild.componentType));
2514
+ }
2515
+ } else if (tabChild.type === "layout") {
2516
+ collectLayoutDependencies(tabChild, deps);
2517
+ }
2518
+ }
2012
2519
  }
2013
2520
  }
2014
2521
  };
@@ -2076,6 +2583,13 @@ import {
2076
2583
  LAYOUTS as LAYOUTS2
2077
2584
  } from "@wire-dsl/language-support/components";
2078
2585
 
2586
+ // src/shared/list-utils.ts
2587
+ function toStringArray(value) {
2588
+ if (Array.isArray(value)) return value;
2589
+ if (value === void 0 || value === "") return [];
2590
+ return String(value).split(",").map((s) => s.trim()).filter(Boolean);
2591
+ }
2592
+
2079
2593
  // src/ir/device-presets.ts
2080
2594
  var DEVICE_PRESETS = {
2081
2595
  mobile: {
@@ -2161,6 +2675,7 @@ function isValidDevice(device) {
2161
2675
  }
2162
2676
 
2163
2677
  // src/ir/index.ts
2678
+ var SELF_TARGET = "_self";
2164
2679
  var IRStyleSchema = z.object({
2165
2680
  density: z.enum(["compact", "normal", "comfortable"]),
2166
2681
  spacing: z.enum(["xs", "sm", "md", "lg", "xl"]),
@@ -2182,12 +2697,27 @@ var IRMetaSchema = z.object({
2182
2697
  source: z.string().optional(),
2183
2698
  nodeId: z.string().optional()
2184
2699
  });
2700
+ var IREventActionSchema = z.union([
2701
+ z.object({ type: z.literal("navigate"), screen: z.string() }),
2702
+ z.object({ type: z.literal("show"), targetId: z.string() }),
2703
+ z.object({ type: z.literal("hide"), targetId: z.string() }),
2704
+ z.object({ type: z.literal("toggle"), targetId: z.string() }),
2705
+ z.object({ type: z.literal("enable"), targetId: z.string() }),
2706
+ z.object({ type: z.literal("disable"), targetId: z.string() }),
2707
+ z.object({ type: z.literal("setTab"), tabsId: z.string(), index: z.number().int().min(0) }),
2708
+ z.object({ type: z.literal("navigateItems"), screens: z.array(z.string()) })
2709
+ ]);
2710
+ var IREventHandlerSchema = z.object({
2711
+ event: z.enum(["onClick", "onChange", "onActive", "onInactive", "onItemsClick", "onItemClick", "onRowClick", "onClose"]),
2712
+ actions: z.array(IREventActionSchema)
2713
+ });
2185
2714
  var IRContainerNodeSchema = z.object({
2186
2715
  id: z.string(),
2187
2716
  kind: z.literal("container"),
2188
- containerType: z.enum(["stack", "grid", "split", "panel", "card"]),
2189
- params: z.record(z.string(), z.union([z.string(), z.number()])),
2717
+ containerType: z.enum(["stack", "grid", "split", "panel", "card", "tabs", "tab", "modal", "modal-body", "modal-footer"]),
2718
+ params: z.record(z.string(), z.union([z.string(), z.number(), z.array(z.string())])),
2190
2719
  children: z.array(z.object({ ref: z.string() })),
2720
+ events: z.array(IREventHandlerSchema).optional(),
2191
2721
  style: IRNodeStyleSchema,
2192
2722
  meta: IRMetaSchema
2193
2723
  });
@@ -2195,7 +2725,9 @@ var IRComponentNodeSchema = z.object({
2195
2725
  id: z.string(),
2196
2726
  kind: z.literal("component"),
2197
2727
  componentType: z.string(),
2198
- props: z.record(z.string(), z.union([z.string(), z.number()])),
2728
+ props: z.record(z.string(), z.union([z.string(), z.number(), z.array(z.string())])),
2729
+ userDefinedId: z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/, "ID must match [a-zA-Z_][a-zA-Z0-9_]*").optional(),
2730
+ events: z.array(IREventHandlerSchema).optional(),
2199
2731
  style: IRNodeStyleSchema,
2200
2732
  meta: IRMetaSchema
2201
2733
  });
@@ -2204,7 +2736,7 @@ var IRInstanceNodeSchema = z.object({
2204
2736
  kind: z.literal("instance"),
2205
2737
  definitionName: z.string(),
2206
2738
  definitionKind: z.enum(["component", "layout"]),
2207
- invocationProps: z.record(z.string(), z.union([z.string(), z.number()])),
2739
+ invocationProps: z.record(z.string(), z.union([z.string(), z.number(), z.array(z.string())])),
2208
2740
  expandedRoot: z.object({ ref: z.string() }),
2209
2741
  style: IRNodeStyleSchema,
2210
2742
  meta: IRMetaSchema
@@ -2398,25 +2930,29 @@ ${messages}`);
2398
2930
  if (layout.children && layout.children.length > 0) {
2399
2931
  layout.children.forEach((child) => {
2400
2932
  if (child.type === "component") {
2401
- found.push({
2402
- componentType: child.componentType,
2403
- location: "layout"
2404
- });
2933
+ found.push({ componentType: child.componentType, location: "layout" });
2405
2934
  } else if (child.type === "layout") {
2406
2935
  this.findComponentsInLayout(child, found);
2407
2936
  } else if (child.type === "cell") {
2408
2937
  if (child.children) {
2409
2938
  child.children.forEach((cellChild) => {
2410
2939
  if (cellChild.type === "component") {
2411
- found.push({
2412
- componentType: cellChild.componentType,
2413
- location: "cell"
2414
- });
2940
+ found.push({ componentType: cellChild.componentType, location: "cell" });
2415
2941
  } else if (cellChild.type === "layout") {
2416
2942
  this.findComponentsInLayout(cellChild, found);
2417
2943
  }
2418
2944
  });
2419
2945
  }
2946
+ } else if (child.type === "tab") {
2947
+ if (child.children) {
2948
+ child.children.forEach((tabChild) => {
2949
+ if (tabChild.type === "component") {
2950
+ found.push({ componentType: tabChild.componentType, location: "tab" });
2951
+ } else if (tabChild.type === "layout") {
2952
+ this.findComponentsInLayout(tabChild, found);
2953
+ }
2954
+ });
2955
+ }
2420
2956
  }
2421
2957
  });
2422
2958
  }
@@ -2433,53 +2969,179 @@ ${messages}`);
2433
2969
  if (layout.layoutType === "split") {
2434
2970
  layoutParams = this.normalizeSplitParams(layoutParams);
2435
2971
  }
2436
- const layoutChildren = layout.children;
2972
+ const nonTabChildren = layout.children.filter((c) => c.type !== "tab");
2437
2973
  const layoutDefinition = this.definedLayouts.get(layout.layoutType);
2438
2974
  if (layoutDefinition) {
2439
- return this.expandDefinedLayout(layoutDefinition, layoutParams, layoutChildren, context, layout._meta?.nodeId);
2975
+ return this.expandDefinedLayout(layoutDefinition, layoutParams, nonTabChildren, context, layout._meta?.nodeId);
2976
+ }
2977
+ const nodeId = this.idGen.generate("node");
2978
+ const childRefs = [];
2979
+ if (layout.layoutType === "modal") {
2980
+ const bodyChildren = layout.children.filter((c) => c.type === "modal-body");
2981
+ const footerChildren = layout.children.filter((c) => c.type === "modal-footer");
2982
+ const normalChildren = layout.children.filter((c) => c.type !== "modal-body" && c.type !== "modal-footer");
2983
+ if (bodyChildren.length > 1 || footerChildren.length > 1) {
2984
+ if (bodyChildren.length > 1) {
2985
+ this.warnings.push({
2986
+ type: "modal-003-duplicate-body",
2987
+ message: "MODAL-003: A modal can only have one body section."
2988
+ });
2989
+ }
2990
+ if (footerChildren.length > 1) {
2991
+ this.warnings.push({
2992
+ type: "modal-004-duplicate-footer",
2993
+ message: "MODAL-004: A modal can only have one footer section."
2994
+ });
2995
+ }
2996
+ }
2997
+ if ((bodyChildren.length > 0 || footerChildren.length > 0) && normalChildren.length > 0) {
2998
+ this.warnings.push({
2999
+ type: "modal-002-mixed-children",
3000
+ message: "MODAL-002: Cannot mix body/footer sections with direct children in a modal. Use either body/footer sections or direct children, not both."
3001
+ });
3002
+ }
3003
+ }
3004
+ for (const child of layout.children) {
3005
+ if (child.type === "modal-body" || child.type === "modal-footer") {
3006
+ if (layout.layoutType !== "modal") {
3007
+ this.warnings.push({
3008
+ type: "modal-001-invalid-context",
3009
+ message: `MODAL-001: "${child.type}" sections are only valid inside layout modal.`
3010
+ });
3011
+ continue;
3012
+ }
3013
+ const childId = child.type === "modal-body" ? this.convertModalBody(child, context) : this.convertModalFooter(child, context);
3014
+ if (childId) childRefs.push({ ref: childId });
3015
+ } else if (child.type === "layout") {
3016
+ const childId = this.convertLayout(child, context);
3017
+ if (childId) childRefs.push({ ref: childId });
3018
+ } else if (child.type === "component") {
3019
+ const childId = this.convertComponent(child, context);
3020
+ if (childId) childRefs.push({ ref: childId });
3021
+ } else if (child.type === "cell") {
3022
+ const childId = this.convertCell(child, context);
3023
+ if (childId) childRefs.push({ ref: childId });
3024
+ } else if (child.type === "tab") {
3025
+ const childId = this.convertTab(child, context);
3026
+ if (childId) childRefs.push({ ref: childId });
3027
+ }
3028
+ }
3029
+ const style = {};
3030
+ if (layoutParams.padding !== void 0) {
3031
+ style.padding = String(layoutParams.padding);
3032
+ } else {
3033
+ style.padding = "none";
3034
+ }
3035
+ if (layoutParams.gap !== void 0) {
3036
+ style.gap = String(layoutParams.gap);
3037
+ }
3038
+ if (layoutParams.justify !== void 0) {
3039
+ style.justify = layoutParams.justify;
3040
+ }
3041
+ if (layoutParams.align !== void 0) {
3042
+ style.align = layoutParams.align;
3043
+ }
3044
+ if (layoutParams.background !== void 0) {
3045
+ style.background = String(layoutParams.background);
3046
+ }
3047
+ if (layoutParams.id !== void 0) {
3048
+ const layoutId = String(layoutParams.id);
3049
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(layoutId)) {
3050
+ this.errors.push({
3051
+ type: "evt-009-invalid-id",
3052
+ message: `EVT-009: id "${layoutId}" is not a valid identifier. Must match [a-zA-Z_][a-zA-Z0-9_]* (cannot start with a digit or contain hyphens).`
3053
+ });
3054
+ }
2440
3055
  }
3056
+ const irEvents = this.convertASTEvents(layout.events || []);
3057
+ const containerNode = {
3058
+ id: nodeId,
3059
+ kind: "container",
3060
+ containerType: layout.layoutType,
3061
+ params: this.cleanParams(layoutParams),
3062
+ children: childRefs,
3063
+ ...irEvents.length > 0 ? { events: irEvents } : {},
3064
+ style,
3065
+ meta: {
3066
+ nodeId: context?.instanceScope ? `${layout._meta?.nodeId}@${context.instanceScope}` : layout._meta?.nodeId
3067
+ }
3068
+ };
3069
+ this.nodes[nodeId] = containerNode;
3070
+ return nodeId;
3071
+ }
3072
+ convertTab(tab, context) {
3073
+ const nodeId = this.idGen.generate("node");
3074
+ const childRefs = [];
3075
+ for (const child of tab.children) {
3076
+ if (child.type === "layout") {
3077
+ const childId = this.convertLayout(child, context);
3078
+ if (childId) childRefs.push({ ref: childId });
3079
+ } else if (child.type === "component") {
3080
+ const childId = this.convertComponent(child, context);
3081
+ if (childId) childRefs.push({ ref: childId });
3082
+ }
3083
+ }
3084
+ const containerNode = {
3085
+ id: nodeId,
3086
+ kind: "container",
3087
+ containerType: "tab",
3088
+ params: {},
3089
+ children: childRefs,
3090
+ style: { padding: "none" },
3091
+ meta: {
3092
+ nodeId: context?.instanceScope ? `${tab._meta?.nodeId}@${context.instanceScope}` : tab._meta?.nodeId
3093
+ }
3094
+ };
3095
+ this.nodes[nodeId] = containerNode;
3096
+ return nodeId;
3097
+ }
3098
+ convertModalBody(body, context) {
2441
3099
  const nodeId = this.idGen.generate("node");
2442
3100
  const childRefs = [];
2443
- for (const child of layoutChildren) {
3101
+ for (const child of body.children) {
2444
3102
  if (child.type === "layout") {
2445
3103
  const childId = this.convertLayout(child, context);
2446
3104
  if (childId) childRefs.push({ ref: childId });
2447
3105
  } else if (child.type === "component") {
2448
3106
  const childId = this.convertComponent(child, context);
2449
3107
  if (childId) childRefs.push({ ref: childId });
2450
- } else if (child.type === "cell") {
2451
- const childId = this.convertCell(child, context);
2452
- if (childId) childRefs.push({ ref: childId });
2453
3108
  }
2454
3109
  }
2455
- const style = {};
2456
- if (layoutParams.padding !== void 0) {
2457
- style.padding = String(layoutParams.padding);
2458
- } else {
2459
- style.padding = "none";
2460
- }
2461
- if (layoutParams.gap !== void 0) {
2462
- style.gap = String(layoutParams.gap);
2463
- }
2464
- if (layoutParams.justify !== void 0) {
2465
- style.justify = layoutParams.justify;
2466
- }
2467
- if (layoutParams.align !== void 0) {
2468
- style.align = layoutParams.align;
2469
- }
2470
- if (layoutParams.background !== void 0) {
2471
- style.background = String(layoutParams.background);
3110
+ const containerNode = {
3111
+ id: nodeId,
3112
+ kind: "container",
3113
+ containerType: "modal-body",
3114
+ params: { direction: "vertical" },
3115
+ children: childRefs,
3116
+ style: { padding: "md", gap: "md" },
3117
+ meta: {
3118
+ nodeId: context?.instanceScope ? `${body._meta?.nodeId}@${context.instanceScope}` : body._meta?.nodeId
3119
+ }
3120
+ };
3121
+ this.nodes[nodeId] = containerNode;
3122
+ return nodeId;
3123
+ }
3124
+ convertModalFooter(footer, context) {
3125
+ const nodeId = this.idGen.generate("node");
3126
+ const childRefs = [];
3127
+ for (const child of footer.children) {
3128
+ if (child.type === "layout") {
3129
+ const childId = this.convertLayout(child, context);
3130
+ if (childId) childRefs.push({ ref: childId });
3131
+ } else if (child.type === "component") {
3132
+ const childId = this.convertComponent(child, context);
3133
+ if (childId) childRefs.push({ ref: childId });
3134
+ }
2472
3135
  }
2473
3136
  const containerNode = {
2474
3137
  id: nodeId,
2475
3138
  kind: "container",
2476
- containerType: layout.layoutType,
2477
- params: this.cleanParams(layoutParams),
3139
+ containerType: "modal-footer",
3140
+ params: { direction: "horizontal" },
2478
3141
  children: childRefs,
2479
- style,
3142
+ style: { padding: "md", justify: "spaceBetween" },
2480
3143
  meta: {
2481
- // Scope nodeId per instance so each expansion gets a unique identifier
2482
- nodeId: context?.instanceScope ? `${layout._meta?.nodeId}@${context.instanceScope}` : layout._meta?.nodeId
3144
+ nodeId: context?.instanceScope ? `${footer._meta?.nodeId}@${context.instanceScope}` : footer._meta?.nodeId
2483
3145
  }
2484
3146
  };
2485
3147
  this.nodes[nodeId] = containerNode;
@@ -2562,7 +3224,6 @@ ${messages}`);
2562
3224
  "IconButton",
2563
3225
  "Alert",
2564
3226
  "Badge",
2565
- "Modal",
2566
3227
  "List",
2567
3228
  "Sidebar",
2568
3229
  "Tabs",
@@ -2574,20 +3235,70 @@ ${messages}`);
2574
3235
  this.undefinedComponentsUsed.add(component.componentType);
2575
3236
  }
2576
3237
  const nodeId = this.idGen.generate("node");
3238
+ const rawId = resolvedProps.id !== void 0 ? String(resolvedProps.id) : void 0;
3239
+ if (rawId !== void 0 && !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(rawId)) {
3240
+ this.errors.push({
3241
+ type: "evt-009-invalid-id",
3242
+ message: `EVT-009: id "${rawId}" is not a valid identifier. Must match [a-zA-Z_][a-zA-Z0-9_]* (cannot start with a digit or contain hyphens).`
3243
+ });
3244
+ }
3245
+ const userDefinedId = rawId;
3246
+ const propsWithoutId = { ...resolvedProps };
3247
+ delete propsWithoutId.id;
3248
+ const irEvents = this.convertASTEvents(component.events || []);
3249
+ const onItemsClickEvent = this.extractOnItemsClickEvent(propsWithoutId);
3250
+ if (onItemsClickEvent) {
3251
+ irEvents.push(onItemsClickEvent);
3252
+ delete propsWithoutId.onItemsClick;
3253
+ }
2577
3254
  const componentNode = {
2578
3255
  id: nodeId,
2579
3256
  kind: "component",
2580
3257
  componentType: component.componentType,
2581
- props: resolvedProps,
3258
+ props: propsWithoutId,
3259
+ ...userDefinedId ? { userDefinedId } : {},
3260
+ ...irEvents.length > 0 ? { events: irEvents } : {},
2582
3261
  style: {},
2583
3262
  meta: {
2584
- // Scope nodeId per instance so each expansion gets a unique identifier
2585
3263
  nodeId: context?.instanceScope ? `${component._meta?.nodeId}@${context.instanceScope}` : component._meta?.nodeId
2586
3264
  }
2587
3265
  };
2588
3266
  this.nodes[nodeId] = componentNode;
2589
3267
  return nodeId;
2590
3268
  }
3269
+ convertASTEventAction(action) {
3270
+ switch (action.type) {
3271
+ case "navigate":
3272
+ return { type: "navigate", screen: action.screen };
3273
+ case "show":
3274
+ return { type: "show", targetId: action.targetId };
3275
+ case "hide":
3276
+ return { type: "hide", targetId: action.targetId };
3277
+ case "toggle":
3278
+ return { type: "toggle", targetId: action.targetId };
3279
+ case "enable":
3280
+ return { type: "enable", targetId: action.targetId };
3281
+ case "disable":
3282
+ return { type: "disable", targetId: action.targetId };
3283
+ case "setTab":
3284
+ return { type: "setTab", tabsId: action.tabsId, index: action.index };
3285
+ }
3286
+ }
3287
+ convertASTEvents(events) {
3288
+ return events.map((handler) => ({
3289
+ event: handler.event,
3290
+ actions: handler.actions.map((a) => this.convertASTEventAction(a))
3291
+ }));
3292
+ }
3293
+ extractOnItemsClickEvent(props) {
3294
+ const value = props.onItemsClick;
3295
+ if (!value) return null;
3296
+ const screens = toStringArray(value);
3297
+ return {
3298
+ event: "onItemsClick",
3299
+ actions: [{ type: "navigateItems", screens }]
3300
+ };
3301
+ }
2591
3302
  expandDefinedComponent(definition, invocationArgs, callSiteNodeId, parentContext) {
2592
3303
  const context = {
2593
3304
  args: invocationArgs,
@@ -2767,21 +3478,23 @@ ${messages}`);
2767
3478
  key
2768
3479
  );
2769
3480
  if (resolvedValue !== void 0) {
3481
+ const metadata = COMPONENTS2[componentType];
3482
+ const propMeta = metadata?.properties?.[key];
2770
3483
  const wasPropReference = typeof value === "string" && value.startsWith("prop_");
2771
- if (wasPropReference) {
2772
- const metadata = COMPONENTS2[componentType];
2773
- const property = metadata?.properties?.[key];
2774
- if (property?.type === "enum" && Array.isArray(property.options)) {
2775
- const normalizedValue = String(resolvedValue);
2776
- if (!property.options.includes(normalizedValue)) {
2777
- this.warnings.push({
2778
- type: "invalid-bound-enum-value",
2779
- message: `Invalid value "${normalizedValue}" for property "${key}" in component "${componentType}". Expected one of: ${property.options.join(", ")}.`
2780
- });
2781
- }
3484
+ if (wasPropReference && propMeta?.type === "enum" && Array.isArray(propMeta.options)) {
3485
+ const normalizedValue = String(resolvedValue);
3486
+ if (!propMeta.options.includes(normalizedValue)) {
3487
+ this.warnings.push({
3488
+ type: "invalid-bound-enum-value",
3489
+ message: `Invalid value "${normalizedValue}" for property "${key}" in component "${componentType}". Expected one of: ${propMeta.options.join(", ")}.`
3490
+ });
2782
3491
  }
2783
3492
  }
2784
- resolved[key] = resolvedValue;
3493
+ if (propMeta?.type === "list" && !Array.isArray(resolvedValue)) {
3494
+ resolved[key] = toStringArray(resolvedValue);
3495
+ } else {
3496
+ resolved[key] = resolvedValue;
3497
+ }
2785
3498
  }
2786
3499
  }
2787
3500
  return resolved;
@@ -2853,6 +3566,195 @@ function generateIR(ast) {
2853
3566
  return generator.generate(ast);
2854
3567
  }
2855
3568
 
3569
+ // src/state.ts
3570
+ function applyStateChange(ir, change, originNodeId) {
3571
+ switch (change.type) {
3572
+ case "setVisible":
3573
+ return applySetVisible(ir, change.targetId, change.visible, originNodeId);
3574
+ case "toggleVisible":
3575
+ return applyToggleVisible(ir, change.targetId, originNodeId);
3576
+ case "setActiveTab":
3577
+ return applySetActiveTab(ir, change.tabsId, change.index);
3578
+ case "setChecked":
3579
+ return applySetBooleanProp(ir, change.targetId, "checked", change.checked, originNodeId);
3580
+ case "toggleChecked":
3581
+ return applyToggleBooleanProp(ir, change.targetId, "checked", originNodeId);
3582
+ case "setEnabled":
3583
+ return applySetBooleanProp(ir, change.targetId, "enabled", change.enabled, originNodeId);
3584
+ case "toggleEnabled":
3585
+ return applyToggleBooleanProp(ir, change.targetId, "enabled", originNodeId);
3586
+ case "setDisabled":
3587
+ return applySetBooleanProp(ir, change.targetId, "disabled", change.disabled, originNodeId);
3588
+ case "navigateTo":
3589
+ return applyNavigateTo(ir, change.screen);
3590
+ }
3591
+ }
3592
+ function applySetVisible(ir, targetId, visible, originNodeId) {
3593
+ const resolvedId = resolveTargetId(ir, targetId, originNodeId);
3594
+ if (!resolvedId) return ir;
3595
+ const nodes = mutateNodeVisible(ir.project.nodes, resolvedId, visible);
3596
+ return { ...ir, project: { ...ir.project, nodes } };
3597
+ }
3598
+ function applyToggleVisible(ir, targetId, originNodeId) {
3599
+ const resolvedId = resolveTargetId(ir, targetId, originNodeId);
3600
+ if (!resolvedId) return ir;
3601
+ const targetNodeEntry = findNodeByUserDefinedId(ir.project.nodes, resolvedId) || (resolvedId === originNodeId ? findNodeByMetaNodeId(ir.project.nodes, resolvedId) : null);
3602
+ const targetContainerEntry = !targetNodeEntry ? Object.values(ir.project.nodes).find(
3603
+ (n) => n.kind === "container" && (String(n.params.id) === resolvedId || n.meta.nodeId === resolvedId)
3604
+ ) : void 0;
3605
+ let currentVisible = true;
3606
+ if (targetNodeEntry?.kind === "component") {
3607
+ currentVisible = targetNodeEntry.props?.visible !== "false";
3608
+ } else if (targetContainerEntry?.kind === "container") {
3609
+ currentVisible = String(targetContainerEntry.params.visible) !== "false";
3610
+ }
3611
+ const nodes = mutateNodeVisible(ir.project.nodes, resolvedId, !currentVisible);
3612
+ return { ...ir, project: { ...ir.project, nodes } };
3613
+ }
3614
+ function applySetActiveTab(ir, tabsId, index) {
3615
+ let found = false;
3616
+ const nodes = {};
3617
+ for (const [nodeKey, node] of Object.entries(ir.project.nodes)) {
3618
+ if (node.kind === "container" && node.containerType === "tabs" && node.params.id === tabsId) {
3619
+ nodes[nodeKey] = {
3620
+ ...node,
3621
+ params: { ...node.params, active: index }
3622
+ };
3623
+ found = true;
3624
+ } else if (node.kind === "component" && node.componentType === "Tabs" && node.props.tabsId === tabsId) {
3625
+ nodes[nodeKey] = {
3626
+ ...node,
3627
+ props: { ...node.props, active: index }
3628
+ };
3629
+ found = true;
3630
+ } else {
3631
+ nodes[nodeKey] = node;
3632
+ }
3633
+ }
3634
+ if (!found) {
3635
+ console.warn(`[applyStateChange] setActiveTab: no layout tabs found with id "${tabsId}"`);
3636
+ return ir;
3637
+ }
3638
+ return { ...ir, project: { ...ir.project, nodes } };
3639
+ }
3640
+ function applySetBooleanProp(ir, targetId, propName, value, originNodeId) {
3641
+ const resolvedId = resolveTargetId(ir, targetId, originNodeId);
3642
+ if (!resolvedId) return ir;
3643
+ const target = findTargetComponentNode(ir.project.nodes, resolvedId);
3644
+ if (!target) {
3645
+ console.warn(`[applyStateChange] ${propName}: no node found with id "${resolvedId}"`);
3646
+ return ir;
3647
+ }
3648
+ const nodes = mutateNodeBooleanProp(ir.project.nodes, resolvedId, propName, value);
3649
+ return { ...ir, project: { ...ir.project, nodes } };
3650
+ }
3651
+ function applyToggleBooleanProp(ir, targetId, propName, originNodeId) {
3652
+ const resolvedId = resolveTargetId(ir, targetId, originNodeId);
3653
+ if (!resolvedId) return ir;
3654
+ const targetNodeEntry = findTargetComponentNode(ir.project.nodes, resolvedId);
3655
+ const currentValue = targetNodeEntry ? String(targetNodeEntry.props[propName] || "false").toLowerCase() === "true" : false;
3656
+ const nodes = mutateNodeBooleanProp(ir.project.nodes, resolvedId, propName, !currentValue);
3657
+ return { ...ir, project: { ...ir.project, nodes } };
3658
+ }
3659
+ function applyNavigateTo(ir, screenName) {
3660
+ const updatedProject = {
3661
+ ...ir.project,
3662
+ activeScreen: screenName
3663
+ };
3664
+ return { ...ir, project: updatedProject };
3665
+ }
3666
+ function resolveTargetId(ir, targetId, originNodeId) {
3667
+ if (targetId === SELF_TARGET) {
3668
+ if (!originNodeId) {
3669
+ console.warn("[applyStateChange] targetId is _self but no originNodeId was provided");
3670
+ return null;
3671
+ }
3672
+ return originNodeId;
3673
+ }
3674
+ return targetId;
3675
+ }
3676
+ function findNodeByUserDefinedId(nodes, userDefinedId) {
3677
+ for (const node of Object.values(nodes)) {
3678
+ if (node.kind === "component" && node.userDefinedId === userDefinedId) {
3679
+ return node;
3680
+ }
3681
+ }
3682
+ return null;
3683
+ }
3684
+ function findNodeByMetaNodeId(nodes, metaNodeId) {
3685
+ for (const node of Object.values(nodes)) {
3686
+ if (node.kind === "component" && node.meta.nodeId === metaNodeId) {
3687
+ return node;
3688
+ }
3689
+ }
3690
+ return null;
3691
+ }
3692
+ function findTargetComponentNode(nodes, targetId) {
3693
+ return findNodeByUserDefinedId(nodes, targetId) || findNodeByMetaNodeId(nodes, targetId);
3694
+ }
3695
+ function mutateNodeVisible(nodes, targetId, visible) {
3696
+ let found = false;
3697
+ const result = {};
3698
+ for (const [key, node] of Object.entries(nodes)) {
3699
+ if (node.kind === "component") {
3700
+ const matchByUserDefined = node.userDefinedId === targetId;
3701
+ const matchByMetaNodeId = node.meta.nodeId === targetId;
3702
+ if (matchByUserDefined || matchByMetaNodeId) {
3703
+ result[key] = {
3704
+ ...node,
3705
+ props: { ...node.props, visible: visible ? "true" : "false" }
3706
+ };
3707
+ found = true;
3708
+ } else {
3709
+ result[key] = node;
3710
+ }
3711
+ } else if (node.kind === "container") {
3712
+ const matchByParamsId = node.params.id !== void 0 && String(node.params.id) === targetId;
3713
+ const matchByMetaNodeId = node.meta.nodeId === targetId;
3714
+ if (matchByParamsId || matchByMetaNodeId) {
3715
+ result[key] = {
3716
+ ...node,
3717
+ params: { ...node.params, visible: visible ? "true" : "false" }
3718
+ };
3719
+ found = true;
3720
+ } else {
3721
+ result[key] = node;
3722
+ }
3723
+ } else {
3724
+ result[key] = node;
3725
+ }
3726
+ }
3727
+ if (!found) {
3728
+ console.warn(`[applyStateChange] setVisible: no node found with id "${targetId}"`);
3729
+ }
3730
+ return result;
3731
+ }
3732
+ function mutateNodeBooleanProp(nodes, targetId, propName, value) {
3733
+ let found = false;
3734
+ const result = {};
3735
+ for (const [key, node] of Object.entries(nodes)) {
3736
+ if (node.kind === "component") {
3737
+ const matchByUserDefined = node.userDefinedId === targetId;
3738
+ const matchByMetaNodeId = node.meta.nodeId === targetId;
3739
+ if (matchByUserDefined || matchByMetaNodeId) {
3740
+ result[key] = {
3741
+ ...node,
3742
+ props: { ...node.props, [propName]: value ? "true" : "false" }
3743
+ };
3744
+ found = true;
3745
+ } else {
3746
+ result[key] = node;
3747
+ }
3748
+ } else {
3749
+ result[key] = node;
3750
+ }
3751
+ }
3752
+ if (!found) {
3753
+ console.warn(`[applyStateChange] ${propName}: no node found with id "${targetId}"`);
3754
+ }
3755
+ return result;
3756
+ }
3757
+
2856
3758
  // src/shared/spacing.ts
2857
3759
  var SPACING_VALUES = {
2858
3760
  none: 0,
@@ -3032,6 +3934,10 @@ var LayoutEngine = class {
3032
3934
  }
3033
3935
  calculateContainer(node, nodeId, x, y, width, height) {
3034
3936
  if (node.kind !== "container") return;
3937
+ if (node.containerType === "modal") {
3938
+ this.calculateModal(node, nodeId, x, y, width, height);
3939
+ return;
3940
+ }
3035
3941
  const usesOuterPadding = node.containerType !== "card";
3036
3942
  const padding = usesOuterPadding ? this.resolveSpacing(node.style.padding) : 0;
3037
3943
  const innerX = x + padding;
@@ -3043,6 +3949,11 @@ var LayoutEngine = class {
3043
3949
  this.result[nodeId] = { x, y, width, height };
3044
3950
  switch (node.containerType) {
3045
3951
  case "stack":
3952
+ case "tab":
3953
+ case "modal-body":
3954
+ this.calculateStack(node, innerX, innerY, innerWidth, innerHeight);
3955
+ break;
3956
+ case "modal-footer":
3046
3957
  this.calculateStack(node, innerX, innerY, innerWidth, innerHeight);
3047
3958
  break;
3048
3959
  case "grid":
@@ -3057,8 +3968,12 @@ var LayoutEngine = class {
3057
3968
  case "card":
3058
3969
  this.calculateCard(node, innerX, innerY, innerWidth, innerHeight);
3059
3970
  break;
3971
+ case "tabs":
3972
+ this.calculateTabs(node, innerX, innerY, innerWidth);
3973
+ break;
3060
3974
  }
3061
- if ((isVerticalStack || node.containerType === "card") && node.children.length > 0) {
3975
+ const isHorizontalStack = node.containerType === "stack" && !isVerticalStack;
3976
+ if ((isVerticalStack || isHorizontalStack || node.containerType === "card" || node.containerType === "tabs" || node.containerType === "tab" || node.containerType === "modal-body" || node.containerType === "modal-footer") && node.children.length > 0) {
3062
3977
  let containerMaxY = y;
3063
3978
  node.children.forEach((childRef) => {
3064
3979
  const childPos = this.result[childRef.ref];
@@ -3078,8 +3993,22 @@ var LayoutEngine = class {
3078
3993
  const children = node.children;
3079
3994
  if (direction === "vertical") {
3080
3995
  let currentY = y;
3081
- children.forEach((childRef, index) => {
3996
+ const flowChildren = children.filter((cr) => {
3997
+ const n = this.nodes[cr.ref];
3998
+ if (n?.kind === "container" && n.containerType === "modal") return false;
3999
+ return this.isNodeVisible(cr.ref);
4000
+ });
4001
+ let flowCount = 0;
4002
+ children.forEach((childRef) => {
3082
4003
  const childNode = this.nodes[childRef.ref];
4004
+ if (childNode?.kind === "container" && childNode.containerType === "modal") {
4005
+ this.calculateNode(childRef.ref, x, currentY, width, 0, "stack");
4006
+ return;
4007
+ }
4008
+ if (!this.isNodeVisible(childRef.ref)) {
4009
+ this.calculateNode(childRef.ref, x, currentY, width, 0, "stack");
4010
+ return;
4011
+ }
3083
4012
  let childHeight = this.getComponentHeight();
3084
4013
  if (childNode?.kind === "component" && childNode.props.height) {
3085
4014
  childHeight = Number(childNode.props.height);
@@ -3090,12 +4019,17 @@ var LayoutEngine = class {
3090
4019
  }
3091
4020
  this.calculateNode(childRef.ref, x, currentY, width, childHeight, "stack");
3092
4021
  currentY += childHeight;
3093
- if (index < children.length - 1) {
4022
+ flowCount++;
4023
+ if (flowCount < flowChildren.length) {
3094
4024
  currentY += gap;
3095
4025
  }
3096
4026
  });
3097
4027
  let adjustedY = y;
3098
- children.forEach((childRef, index) => {
4028
+ let adjustedCount = 0;
4029
+ children.forEach((childRef) => {
4030
+ const childNode = this.nodes[childRef.ref];
4031
+ if (childNode?.kind === "container" && childNode.containerType === "modal") return;
4032
+ if (!this.isNodeVisible(childRef.ref)) return;
3099
4033
  const childPos = this.result[childRef.ref];
3100
4034
  if (childPos) {
3101
4035
  const deltaY = adjustedY - childPos.y;
@@ -3104,7 +4038,8 @@ var LayoutEngine = class {
3104
4038
  this.adjustNodeYPositions(childRef.ref, deltaY);
3105
4039
  }
3106
4040
  adjustedY += childPos.height;
3107
- if (index < children.length - 1) {
4041
+ adjustedCount++;
4042
+ if (adjustedCount < flowChildren.length) {
3108
4043
  adjustedY += gap;
3109
4044
  }
3110
4045
  }
@@ -3114,9 +4049,10 @@ var LayoutEngine = class {
3114
4049
  const crossAlign = node.style.align || "start";
3115
4050
  if (justify === "stretch") {
3116
4051
  let currentX = x;
3117
- const childWidth = this.calculateChildWidth(children.length, width, gap);
4052
+ const visibleChildren = children.filter((cr) => this.isNodeVisible(cr.ref));
4053
+ const childWidth = this.calculateChildWidth(visibleChildren.length, width, gap);
3118
4054
  let stackHeight = 0;
3119
- children.forEach((childRef) => {
4055
+ visibleChildren.forEach((childRef) => {
3120
4056
  const childNode = this.nodes[childRef.ref];
3121
4057
  let childHeight = this.getComponentHeight();
3122
4058
  if (childNode?.kind === "component" && childNode.props.height) {
@@ -3129,6 +4065,10 @@ var LayoutEngine = class {
3129
4065
  stackHeight = Math.max(stackHeight, childHeight);
3130
4066
  });
3131
4067
  children.forEach((childRef) => {
4068
+ if (!this.isNodeVisible(childRef.ref)) {
4069
+ this.calculateNode(childRef.ref, currentX, y, 0, 0, "stack");
4070
+ return;
4071
+ }
3132
4072
  this.calculateNode(childRef.ref, currentX, y, childWidth, stackHeight, "stack");
3133
4073
  currentX += childWidth + gap;
3134
4074
  });
@@ -3137,9 +4077,18 @@ var LayoutEngine = class {
3137
4077
  const childHeights = [];
3138
4078
  const explicitHeightFlags = [];
3139
4079
  const flexIndices = /* @__PURE__ */ new Set();
4080
+ const visibleFlags = [];
3140
4081
  let stackHeight = 0;
3141
4082
  children.forEach((childRef, index) => {
3142
4083
  const childNode = this.nodes[childRef.ref];
4084
+ const visible = this.isNodeVisible(childRef.ref);
4085
+ visibleFlags.push(visible);
4086
+ if (!visible) {
4087
+ childWidths.push(0);
4088
+ childHeights.push(0);
4089
+ explicitHeightFlags.push(false);
4090
+ return;
4091
+ }
3143
4092
  const hasExplicitHeight = childNode?.kind === "component" && !!childNode.props.height;
3144
4093
  const hasExplicitWidth = childNode?.kind === "component" && !!childNode.props.width;
3145
4094
  const isBlockButton = childNode?.kind === "component" && childNode.componentType === "Button" && !hasExplicitWidth && this.parseBooleanProp(childNode.props.block, false);
@@ -3157,7 +4106,8 @@ var LayoutEngine = class {
3157
4106
  childHeights.push(this.getComponentHeight());
3158
4107
  explicitHeightFlags.push(hasExplicitHeight);
3159
4108
  });
3160
- const totalGapWidth = gap * Math.max(0, children.length - 1);
4109
+ const visibleCount = visibleFlags.filter(Boolean).length;
4110
+ const totalGapWidth = gap * Math.max(0, visibleCount - 1);
3161
4111
  if (flexIndices.size > 0) {
3162
4112
  const fixedWidth = childWidths.reduce((sum, w, idx) => {
3163
4113
  return flexIndices.has(idx) ? sum : sum + w;
@@ -3169,6 +4119,7 @@ var LayoutEngine = class {
3169
4119
  });
3170
4120
  }
3171
4121
  children.forEach((childRef, index) => {
4122
+ if (!visibleFlags[index]) return;
3172
4123
  const childNode = this.nodes[childRef.ref];
3173
4124
  const childWidth = childWidths[index];
3174
4125
  let childHeight = this.getComponentHeight();
@@ -3192,14 +4143,18 @@ var LayoutEngine = class {
3192
4143
  startX = x + width - totalContentWidth;
3193
4144
  } else if (justify === "spaceBetween") {
3194
4145
  startX = x;
3195
- dynamicGap = children.length > 1 ? (width - totalChildWidth) / (children.length - 1) : 0;
4146
+ dynamicGap = visibleCount > 1 ? (width - totalChildWidth) / (visibleCount - 1) : 0;
3196
4147
  } else if (justify === "spaceAround") {
3197
- const spacing = children.length > 0 ? (width - totalChildWidth) / children.length : 0;
4148
+ const spacing = visibleCount > 0 ? (width - totalChildWidth) / visibleCount : 0;
3198
4149
  startX = x + spacing / 2;
3199
4150
  dynamicGap = spacing;
3200
4151
  }
3201
4152
  let currentX = startX;
3202
4153
  children.forEach((childRef, index) => {
4154
+ if (!visibleFlags[index]) {
4155
+ this.calculateNode(childRef.ref, currentX, y, 0, 0, "stack");
4156
+ return;
4157
+ }
3203
4158
  const childWidth = childWidths[index];
3204
4159
  const childHeight = childHeights[index];
3205
4160
  let childY = y;
@@ -3231,6 +4186,7 @@ var LayoutEngine = class {
3231
4186
  let currentRowMaxHeight = 0;
3232
4187
  const rowHeights = [0];
3233
4188
  node.children.forEach((childRef) => {
4189
+ if (!this.isNodeVisible(childRef.ref)) return;
3234
4190
  const child = this.nodes[childRef.ref];
3235
4191
  let span = 1;
3236
4192
  let childHeight = this.getComponentHeight();
@@ -3265,6 +4221,18 @@ var LayoutEngine = class {
3265
4221
  }
3266
4222
  return totalHeight;
3267
4223
  }
4224
+ if (node.containerType === "tabs") {
4225
+ const activeIndex = Number(node.params.active) || 0;
4226
+ const activeChildRef = node.children[activeIndex] ?? node.children[0];
4227
+ if (!activeChildRef) return totalHeight;
4228
+ const activeChild = this.nodes[activeChildRef.ref];
4229
+ if (activeChild?.kind === "container") {
4230
+ totalHeight += this.calculateContainerHeight(activeChild, availableWidth);
4231
+ } else if (activeChild?.kind === "component") {
4232
+ totalHeight += this.getIntrinsicComponentHeight(activeChild, availableWidth);
4233
+ }
4234
+ return totalHeight;
4235
+ }
3268
4236
  if (node.containerType === "split") {
3269
4237
  const splitGap = this.resolveSpacing(node.style.gap);
3270
4238
  const leftParam = node.params.left;
@@ -3277,6 +4245,7 @@ var LayoutEngine = class {
3277
4245
  const rightWidth = Number.isFinite(rightWidthRaw) && rightWidthRaw > 0 ? rightWidthRaw : availableWidth / 2;
3278
4246
  let maxHeight = 0;
3279
4247
  node.children.forEach((childRef, index) => {
4248
+ if (!this.isNodeVisible(childRef.ref)) return;
3280
4249
  const child = this.nodes[childRef.ref];
3281
4250
  let childHeight = this.getComponentHeight();
3282
4251
  const isFirst = index === 0;
@@ -3304,6 +4273,7 @@ var LayoutEngine = class {
3304
4273
  if (node.containerType === "stack" && direction === "horizontal") {
3305
4274
  let maxHeight = 0;
3306
4275
  node.children.forEach((childRef) => {
4276
+ if (!this.isNodeVisible(childRef.ref)) return;
3307
4277
  const child = this.nodes[childRef.ref];
3308
4278
  let childHeight = this.getComponentHeight();
3309
4279
  if (child?.kind === "component") {
@@ -3320,7 +4290,10 @@ var LayoutEngine = class {
3320
4290
  totalHeight += maxHeight;
3321
4291
  return totalHeight;
3322
4292
  }
3323
- node.children.forEach((childRef, index) => {
4293
+ const visibleLinear = node.children.filter((cr) => this.isNodeVisible(cr.ref));
4294
+ let linearIndex = 0;
4295
+ node.children.forEach((childRef) => {
4296
+ if (!this.isNodeVisible(childRef.ref)) return;
3324
4297
  const child = this.nodes[childRef.ref];
3325
4298
  let childHeight = this.getComponentHeight();
3326
4299
  if (child?.kind === "component") {
@@ -3333,7 +4306,8 @@ var LayoutEngine = class {
3333
4306
  childHeight = this.calculateContainerHeight(child, availableWidth);
3334
4307
  }
3335
4308
  totalHeight += childHeight;
3336
- if (index < node.children.length - 1) {
4309
+ linearIndex++;
4310
+ if (linearIndex < visibleLinear.length) {
3337
4311
  totalHeight += gap;
3338
4312
  }
3339
4313
  });
@@ -3346,6 +4320,10 @@ var LayoutEngine = class {
3346
4320
  const colWidth = (width - gap * (columns - 1)) / columns;
3347
4321
  const cellHeights = {};
3348
4322
  node.children.forEach((childRef, cellIndex) => {
4323
+ if (!this.isNodeVisible(childRef.ref)) {
4324
+ cellHeights[cellIndex] = 0;
4325
+ return;
4326
+ }
3349
4327
  const child = this.nodes[childRef.ref];
3350
4328
  let cellHeight = this.getComponentHeight();
3351
4329
  let span = 1;
@@ -3370,6 +4348,11 @@ var LayoutEngine = class {
3370
4348
  const rowHeights = [0];
3371
4349
  const cellPositions = [];
3372
4350
  node.children.forEach((childRef, cellIndex) => {
4351
+ const visible = this.isNodeVisible(childRef.ref);
4352
+ if (!visible) {
4353
+ cellPositions.push({ row: currentRow, col: currentCol, span: 0, visible: false });
4354
+ return;
4355
+ }
3373
4356
  const child = this.nodes[childRef.ref];
3374
4357
  let span = 1;
3375
4358
  if (child?.kind === "container" && child.meta?.source === "cell") {
@@ -3381,13 +4364,17 @@ var LayoutEngine = class {
3381
4364
  currentCol = 0;
3382
4365
  currentRowMaxHeight = 0;
3383
4366
  }
3384
- cellPositions.push({ row: currentRow, col: currentCol, span });
4367
+ cellPositions.push({ row: currentRow, col: currentCol, span, visible: true });
3385
4368
  currentRowMaxHeight = Math.max(currentRowMaxHeight, cellHeights[cellIndex]);
3386
4369
  currentCol += span;
3387
4370
  });
3388
4371
  rowHeights[currentRow] = currentRowMaxHeight;
3389
4372
  node.children.forEach((childRef, cellIndex) => {
3390
- const { row, col, span } = cellPositions[cellIndex];
4373
+ const { row, col, span, visible } = cellPositions[cellIndex];
4374
+ if (!visible) {
4375
+ this.calculateNode(childRef.ref, x, y, 0, 0, "grid");
4376
+ return;
4377
+ }
3391
4378
  const cellHeight = rowHeights[row];
3392
4379
  let cellY = y;
3393
4380
  for (let r = 0; r < row; r++) {
@@ -3458,8 +4445,14 @@ var LayoutEngine = class {
3458
4445
  const innerCardWidth = width - cardPadding * 2;
3459
4446
  const children = node.children;
3460
4447
  let currentY = y + cardPadding;
3461
- children.forEach((childRef, index) => {
4448
+ const flowChildren = children.filter((cr) => this.isNodeVisible(cr.ref));
4449
+ let flowCount = 0;
4450
+ children.forEach((childRef) => {
3462
4451
  const childNode = this.nodes[childRef.ref];
4452
+ if (!this.isNodeVisible(childRef.ref)) {
4453
+ this.calculateNode(childRef.ref, x + cardPadding, currentY, innerCardWidth, 0, "card");
4454
+ return;
4455
+ }
3463
4456
  let childHeight = this.getComponentHeight();
3464
4457
  if (childNode?.kind === "component" && childNode.props.height) {
3465
4458
  childHeight = Number(childNode.props.height);
@@ -3470,11 +4463,63 @@ var LayoutEngine = class {
3470
4463
  }
3471
4464
  this.calculateNode(childRef.ref, x + cardPadding, currentY, innerCardWidth, childHeight, "card");
3472
4465
  currentY += childHeight;
3473
- if (index < children.length - 1) {
4466
+ flowCount++;
4467
+ if (flowCount < flowChildren.length) {
3474
4468
  currentY += gap;
3475
4469
  }
3476
4470
  });
3477
4471
  }
4472
+ calculateTabs(node, x, y, width) {
4473
+ if (node.kind !== "container") return;
4474
+ const activeIndex = Number(node.params.active) || 0;
4475
+ node.children.forEach((childRef, index) => {
4476
+ if (index === activeIndex) {
4477
+ const tabNode = this.nodes[childRef.ref];
4478
+ let tabHeight = 40;
4479
+ if (tabNode?.kind === "container") {
4480
+ tabHeight = this.calculateContainerHeight(tabNode, width);
4481
+ } else if (tabNode?.kind === "component") {
4482
+ tabHeight = this.getIntrinsicComponentHeight(tabNode, width);
4483
+ }
4484
+ this.calculateNode(childRef.ref, x, y, width, tabHeight, "tabs");
4485
+ }
4486
+ });
4487
+ }
4488
+ calculateModal(node, nodeId, _canvasX, _canvasY, _canvasWidth, _canvasHeight) {
4489
+ if (node.kind !== "container") return;
4490
+ const viewportWidth = this.viewport.width;
4491
+ const size = String(node.params.size || "md");
4492
+ const modalWidths = { sm: 380, md: 520, lg: 720 };
4493
+ const modalWidth = Math.min(modalWidths[size] ?? 520, viewportWidth - 32);
4494
+ const modalX = Math.round((viewportWidth - modalWidth) / 2);
4495
+ const modalY = 64;
4496
+ const hasHeader = node.params.title !== void 0 && node.params.title !== "";
4497
+ const headerHeight = hasHeader ? 48 : 0;
4498
+ let childrenHeight = 0;
4499
+ const innerWidth = modalWidth;
4500
+ let currentY = modalY + headerHeight;
4501
+ node.children.forEach((childRef, index) => {
4502
+ const childNode = this.nodes[childRef.ref];
4503
+ let childHeight = 0;
4504
+ if (childNode?.kind === "container") {
4505
+ childHeight = this.calculateContainerHeight(childNode, innerWidth);
4506
+ } else if (childNode?.kind === "component") {
4507
+ childHeight = this.getIntrinsicComponentHeight(childNode, innerWidth);
4508
+ } else {
4509
+ childHeight = this.getComponentHeight();
4510
+ }
4511
+ this.calculateNode(childRef.ref, modalX, currentY, innerWidth, childHeight, "modal");
4512
+ const resolvedChildHeight = this.result[childRef.ref]?.height ?? childHeight;
4513
+ currentY += resolvedChildHeight;
4514
+ childrenHeight += resolvedChildHeight;
4515
+ if (index < node.children.length - 1) {
4516
+ currentY += 8;
4517
+ childrenHeight += 8;
4518
+ }
4519
+ });
4520
+ const modalHeight = headerHeight + childrenHeight;
4521
+ this.result[nodeId] = { x: modalX, y: modalY, width: modalWidth, height: modalHeight };
4522
+ }
3478
4523
  /**
3479
4524
  * Calculate layout for an instance node.
3480
4525
  * The instance is a transparent wrapper — its bounding box equals the
@@ -3705,8 +4750,7 @@ var LayoutEngine = class {
3705
4750
  return Math.max(this.getComponentHeight(), wrappedHeight);
3706
4751
  }
3707
4752
  if (node.componentType === "SidebarMenu") {
3708
- const itemsStr = String(node.props.items || "Item 1,Item 2,Item 3");
3709
- const items = itemsStr.split(",").map((s) => s.trim()).filter(Boolean);
4753
+ const items = toStringArray(node.props.items);
3710
4754
  const itemCount = items.length > 0 ? items.length : 3;
3711
4755
  const itemHeight = 40;
3712
4756
  return Math.max(this.getComponentHeight(), itemCount * itemHeight);
@@ -3717,7 +4761,7 @@ var LayoutEngine = class {
3717
4761
  if (node.componentType === "Stat") return 120;
3718
4762
  if (node.componentType === "Chart") return 250;
3719
4763
  if (node.componentType === "List") {
3720
- const itemsFromProps = String(node.props.items || "").split(",").map((item) => item.trim()).filter(Boolean);
4764
+ const itemsFromProps = toStringArray(node.props.items);
3721
4765
  const parsedItemsMock = Number(node.props.itemsMock ?? 4);
3722
4766
  const fallbackCount = Number.isFinite(parsedItemsMock) ? Math.max(0, Math.floor(parsedItemsMock)) : 4;
3723
4767
  const itemCount = itemsFromProps.length > 0 ? itemsFromProps.length : fallbackCount;
@@ -3895,6 +4939,17 @@ var LayoutEngine = class {
3895
4939
  }
3896
4940
  return width;
3897
4941
  }
4942
+ /**
4943
+ * Returns false only when the node has an explicit `visible: 'false'` param/prop.
4944
+ * Missing or any other value is treated as visible.
4945
+ */
4946
+ isNodeVisible(nodeId) {
4947
+ const node = this.nodes[nodeId];
4948
+ if (!node) return true;
4949
+ if (node.kind === "component") return String(node.props.visible) !== "false";
4950
+ if (node.kind === "container") return String(node.params.visible) !== "false";
4951
+ return true;
4952
+ }
3898
4953
  parseBooleanProp(value, fallback = false) {
3899
4954
  if (typeof value === "boolean") return value;
3900
4955
  if (typeof value === "string") {
@@ -3985,7 +5040,7 @@ var MockDataGenerator = class {
3985
5040
  for (const [key, rawValues] of Object.entries(mocks)) {
3986
5041
  let values = [];
3987
5042
  if (typeof rawValues === "string") {
3988
- values = rawValues.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
5043
+ values = toStringArray(rawValues);
3989
5044
  } else if (Array.isArray(rawValues)) {
3990
5045
  values = rawValues.map((v) => typeof v === "string" ? v.trim() : "").filter((v) => v.length > 0);
3991
5046
  }
@@ -4077,7 +5132,7 @@ var MockDataGenerator = class {
4077
5132
  * columns: "id,name,status,amount"
4078
5133
  */
4079
5134
  static generateMockRow(columns, rowIndex, random = false) {
4080
- const columnNames = columns.split(",").map((c) => c.trim());
5135
+ const columnNames = toStringArray(columns);
4081
5136
  const row = {};
4082
5137
  columnNames.forEach((col) => {
4083
5138
  const mockType = this.inferMockTypeFromColumn(col);
@@ -5129,6 +6184,9 @@ var SVGRenderer = class {
5129
6184
  if (!node || !pos) return;
5130
6185
  this.renderedNodeIds.add(nodeId);
5131
6186
  if (node.kind === "container") {
6187
+ if (String(node.params.visible) === "false") {
6188
+ return;
6189
+ }
5132
6190
  const containerGroup = [];
5133
6191
  const hasNodeId = node.meta?.nodeId;
5134
6192
  if (hasNodeId) {
@@ -5149,6 +6207,12 @@ var SVGRenderer = class {
5149
6207
  if (node.containerType === "split") {
5150
6208
  this.renderSplitDecoration(node, pos, containerGroup);
5151
6209
  }
6210
+ if (node.containerType === "modal") {
6211
+ this.renderModalDecoration(node, pos, containerGroup);
6212
+ }
6213
+ if (node.containerType === "modal-footer") {
6214
+ this.renderModalFooterDecoration(pos, containerGroup);
6215
+ }
5152
6216
  const isCellContainer = node.meta?.source === "cell";
5153
6217
  if (node.children.length === 0 && this.options.showDiagnostics && !isCellContainer) {
5154
6218
  containerGroup.push(this.renderEmptyContainerDiagnostic(pos, node.containerType));
@@ -5175,6 +6239,9 @@ var SVGRenderer = class {
5175
6239
  }
5176
6240
  output.push(...instanceGroup);
5177
6241
  } else if (node.kind === "component") {
6242
+ if (String(node.props.visible) === "false") {
6243
+ return;
6244
+ }
5178
6245
  const componentSvg = this.renderComponent(node, pos);
5179
6246
  if (componentSvg) {
5180
6247
  output.push(componentSvg);
@@ -5236,8 +6303,6 @@ var SVGRenderer = class {
5236
6303
  return this.renderAlert(node, pos);
5237
6304
  case "Badge":
5238
6305
  return this.renderBadge(node, pos);
5239
- case "Modal":
5240
- return this.renderModal(node, pos);
5241
6306
  case "List":
5242
6307
  return this.renderList(node, pos);
5243
6308
  case "Stat":
@@ -5680,8 +6745,8 @@ var SVGRenderer = class {
5680
6745
  }
5681
6746
  renderTable(node, pos) {
5682
6747
  const title = String(node.props.title || "");
5683
- const columnsStr = String(node.props.columns || "Col1,Col2,Col3");
5684
- const columns = columnsStr.split(",").map((c) => c.trim()).filter(Boolean);
6748
+ const parsedColumns = toStringArray(node.props.columns);
6749
+ const columns = parsedColumns.length > 0 ? parsedColumns : ["Col1", "Col2", "Col3"];
5685
6750
  const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
5686
6751
  const mockStr = String(node.props.mock || "");
5687
6752
  const random = this.parseBooleanProp(node.props.random, false);
@@ -5689,7 +6754,7 @@ var SVGRenderer = class {
5689
6754
  const parsedPageCount = Number(node.props.pages || 5);
5690
6755
  const pageCount = Number.isFinite(parsedPageCount) && parsedPageCount > 0 ? Math.floor(parsedPageCount) : 5;
5691
6756
  const paginationAlign = String(node.props.paginationAlign || "right");
5692
- const actions = String(node.props.actions || "").split(",").map((value) => value.trim()).filter(Boolean);
6757
+ const actions = toStringArray(node.props.actions);
5693
6758
  const hasActions = actions.length > 0;
5694
6759
  const caption = String(node.props.caption || "").trim();
5695
6760
  const hasCaption = caption.length > 0;
@@ -5702,7 +6767,7 @@ var SVGRenderer = class {
5702
6767
  const rawCaptionAlign = String(node.props.captionAlign || "");
5703
6768
  const captionAlign = rawCaptionAlign === "left" || rawCaptionAlign === "center" || rawCaptionAlign === "right" ? rawCaptionAlign : paginationAlign === "left" ? "right" : "left";
5704
6769
  const sameFooterAlign = hasCaption && pagination && captionAlign === paginationAlign;
5705
- const mockTypes = mockStr ? mockStr.split(",").map((m) => m.trim()).filter(Boolean) : [];
6770
+ const mockTypes = toStringArray(mockStr);
5706
6771
  const safeColumns = columns.length > 0 ? columns : ["Column"];
5707
6772
  while (mockTypes.length < safeColumns.length) {
5708
6773
  const inferred = MockDataGenerator.inferMockTypeFromColumn(safeColumns[mockTypes.length] || "item");
@@ -5802,6 +6867,13 @@ var SVGRenderer = class {
5802
6867
  currentX += buttonSize + buttonGap;
5803
6868
  });
5804
6869
  }
6870
+ const rowEventAttrs = this.getScopedEventAttrs(node, "onRowClick", { index: rowIdx });
6871
+ if (rowEventAttrs) {
6872
+ svg += `
6873
+ <rect x="${pos.x}" y="${rowY}"
6874
+ width="${pos.width}" height="${rowHeight}"
6875
+ fill="transparent" stroke="none" pointer-events="all"${rowEventAttrs}/>`;
6876
+ }
5805
6877
  });
5806
6878
  const footerTop = headerY + headerHeight + mockRows.length * rowHeight + 16;
5807
6879
  if (pagination) {
@@ -6191,14 +7263,15 @@ var SVGRenderer = class {
6191
7263
  const label = String(node.props.label || "Checkbox");
6192
7264
  const checked = String(node.props.checked || "false").toLowerCase() === "true";
6193
7265
  const disabled = this.parseBooleanProp(node.props.disabled, false);
7266
+ const clickable = String(node.props.clickable ?? "true") !== "false";
6194
7267
  const controlColor = this.resolveControlColor();
6195
7268
  const checkboxSize = 18;
6196
7269
  const checkboxY = pos.y + pos.height / 2 - checkboxSize / 2;
6197
- return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
6198
- <rect x="${pos.x}" y="${checkboxY}"
6199
- width="${checkboxSize}" height="${checkboxSize}"
6200
- rx="4"
6201
- fill="${checked ? controlColor : this.renderTheme.cardBg}"
7270
+ return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}${!clickable ? ' data-clickable="false"' : ""}>
7271
+ <rect x="${pos.x}" y="${checkboxY}"
7272
+ width="${checkboxSize}" height="${checkboxSize}"
7273
+ rx="4"
7274
+ fill="${checked ? controlColor : this.renderTheme.cardBg}"
6202
7275
  stroke="${this.renderTheme.border}"
6203
7276
  stroke-width="1"/>
6204
7277
  ${checked ? `<text x="${pos.x + checkboxSize / 2}" y="${checkboxY + 14}"
@@ -6216,13 +7289,14 @@ var SVGRenderer = class {
6216
7289
  const label = String(node.props.label || "Radio");
6217
7290
  const checked = String(node.props.checked || "false").toLowerCase() === "true";
6218
7291
  const disabled = this.parseBooleanProp(node.props.disabled, false);
7292
+ const clickable = String(node.props.clickable ?? "true") !== "false";
6219
7293
  const controlColor = this.resolveControlColor();
6220
7294
  const radioSize = 16;
6221
7295
  const radioY = pos.y + pos.height / 2 - radioSize / 2;
6222
- return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
6223
- <circle cx="${pos.x + radioSize / 2}" cy="${radioY + radioSize / 2}"
6224
- r="${radioSize / 2}"
6225
- fill="${this.renderTheme.cardBg}"
7296
+ return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}${!clickable ? ' data-clickable="false"' : ""}>
7297
+ <circle cx="${pos.x + radioSize / 2}" cy="${radioY + radioSize / 2}"
7298
+ r="${radioSize / 2}"
7299
+ fill="${this.renderTheme.cardBg}"
6226
7300
  stroke="${this.renderTheme.border}"
6227
7301
  stroke-width="1"/>
6228
7302
  ${checked ? `<circle cx="${pos.x + radioSize / 2}" cy="${radioY + radioSize / 2}"
@@ -6238,12 +7312,13 @@ var SVGRenderer = class {
6238
7312
  const label = String(node.props.label || "Toggle");
6239
7313
  const enabled = String(node.props.enabled || "false").toLowerCase() === "true";
6240
7314
  const disabled = this.parseBooleanProp(node.props.disabled, false);
7315
+ const clickable = String(node.props.clickable ?? "true") !== "false";
6241
7316
  const controlColor = this.resolveControlColor();
6242
7317
  const toggleWidth = 40;
6243
7318
  const toggleHeight = 20;
6244
7319
  const toggleY = pos.y + pos.height / 2 - toggleHeight / 2;
6245
- return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
6246
- <rect x="${pos.x}" y="${toggleY}"
7320
+ return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}${!clickable ? ' data-clickable="false"' : ""}>
7321
+ <rect x="${pos.x}" y="${toggleY}"
6247
7322
  width="${toggleWidth}" height="${toggleHeight}"
6248
7323
  rx="10"
6249
7324
  fill="${enabled ? controlColor : this.renderTheme.border}"
@@ -6262,12 +7337,9 @@ var SVGRenderer = class {
6262
7337
  // ============================================================================
6263
7338
  renderSidebar(node, pos) {
6264
7339
  const title = String(node.props.title || "Sidebar");
6265
- const itemsStr = String(node.props.items || "");
6266
7340
  const activeItem = String(node.props.active || "");
6267
- let items = [];
6268
- if (itemsStr) {
6269
- items = itemsStr.split(",").map((i) => i.trim());
6270
- } else {
7341
+ let items = toStringArray(node.props.items);
7342
+ if (items.length === 0) {
6271
7343
  const itemCount = Number(node.props.itemsMock || 6);
6272
7344
  items = MockDataGenerator.generateMockList("name", itemCount);
6273
7345
  }
@@ -6306,9 +7378,9 @@ var SVGRenderer = class {
6306
7378
  return svg;
6307
7379
  }
6308
7380
  renderTabs(node, pos) {
6309
- const itemsStr = String(node.props.items || "");
6310
- const tabs = itemsStr ? itemsStr.split(",").map((t) => t.trim()) : ["Tab 1", "Tab 2", "Tab 3"];
6311
- const activeProp = node.props.active ?? 0;
7381
+ const parsedTabs = toStringArray(node.props.items);
7382
+ const tabs = parsedTabs.length > 0 ? parsedTabs : ["Tab 1", "Tab 2", "Tab 3"];
7383
+ const activeProp = node.props.active ?? node.props.initialActive ?? 0;
6312
7384
  const activeIndex = Number.isFinite(Number(activeProp)) ? Math.max(0, Math.floor(Number(activeProp))) : 0;
6313
7385
  const variant = String(node.props.variant || "default");
6314
7386
  const accentColor = variant === "default" ? this.resolveAccentColor() : this.resolveVariantColor(variant, this.resolveAccentColor());
@@ -6318,8 +7390,7 @@ var SVGRenderer = class {
6318
7390
  const tabHeight = pos.height > 0 ? pos.height : sizeMap[String(node.props.size || "md")] ?? 44;
6319
7391
  const fontSize = 13;
6320
7392
  const textY = pos.y + Math.round(tabHeight / 2) + Math.round(fontSize * 0.4);
6321
- const iconsStr = String(node.props.icons || "");
6322
- const iconList = iconsStr ? iconsStr.split(",").map((s) => s.trim()) : [];
7393
+ const iconList = toStringArray(node.props.icons);
6323
7394
  const isFlat = this.parseBooleanProp(node.props.flat, false);
6324
7395
  const showBorder = this.parseBooleanProp(node.props.border, true);
6325
7396
  const tabWidth = pos.width / tabs.length;
@@ -6423,6 +7494,13 @@ var SVGRenderer = class {
6423
7494
  text-anchor="middle">${this.escapeXml(tab)}</text>`;
6424
7495
  }
6425
7496
  }
7497
+ const tabsTriggerAttrs = this.getTabsTriggerAttrs(node, i);
7498
+ if (tabsTriggerAttrs) {
7499
+ svg += `
7500
+ <rect x="${tabX}" y="${pos.y}"
7501
+ width="${tabWidth}" height="${tabHeight}"
7502
+ fill="transparent" stroke="none" pointer-events="all"${tabsTriggerAttrs}/>`;
7503
+ }
6426
7504
  });
6427
7505
  const contentY = pos.y + tabHeight;
6428
7506
  const contentH = pos.height - tabHeight;
@@ -6536,66 +7614,51 @@ var SVGRenderer = class {
6536
7614
  text-anchor="middle">${this.escapeXml(text)}</text>
6537
7615
  </g>`;
6538
7616
  }
6539
- renderModal(node, pos) {
6540
- const visible = this.parseBooleanProp(node.props.visible, true);
6541
- if (!visible) {
6542
- return "";
6543
- }
6544
- const title = String(node.props.title || "Modal");
7617
+ renderModalDecoration(node, pos, output) {
7618
+ if (node.kind !== "container") return;
7619
+ const canvasWidth = this.options.width;
7620
+ const canvasHeight = Math.max(this.options.height, this.calculateContentHeight());
6545
7621
  const padding = 16;
6546
7622
  const headerHeight = 48;
6547
- const overlayHeight = Math.max(this.options.height, this.calculateContentHeight());
6548
- const modalX = (this.options.width - pos.width) / 2;
6549
- const modalY = Math.max(40, (overlayHeight - pos.height) / 2);
6550
- return `<g${this.getDataNodeId(node)}>
6551
- <!-- Modal backdrop -->
6552
- <rect x="0" y="0"
6553
- width="${this.options.width}" height="${overlayHeight}"
6554
- fill="black" opacity="0.28"/>
6555
-
6556
- <!-- Modal box -->
6557
- <rect x="${modalX}" y="${modalY}"
6558
- width="${pos.width}" height="${pos.height}"
6559
- rx="8"
6560
- fill="${this.renderTheme.cardBg}"
6561
- stroke="${this.renderTheme.border}"
6562
- stroke-width="1"/>
6563
-
6564
- <!-- Header -->
6565
- <line x1="${modalX}" y1="${modalY + headerHeight}"
6566
- x2="${modalX + pos.width}" y2="${modalY + headerHeight}"
6567
- stroke="${this.renderTheme.border}"
6568
- stroke-width="1"/>
6569
-
6570
- <text x="${modalX + padding}" y="${modalY + padding + 16}"
6571
- font-family="Arial, Helvetica, sans-serif"
6572
- font-size="16"
6573
- font-weight="600"
6574
- fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>
6575
-
6576
- <!-- Close button -->
6577
- <text x="${modalX + pos.width - 16}" y="${modalY + padding + 12}"
6578
- font-family="Arial, Helvetica, sans-serif"
6579
- font-size="18"
6580
- fill="${this.renderTheme.textMuted}">\u2715</text>
6581
-
6582
- <!-- Content placeholder -->
6583
- <text x="${modalX + pos.width / 2}" y="${modalY + headerHeight + (pos.height - headerHeight) / 2}"
6584
- font-family="Arial, Helvetica, sans-serif"
6585
- font-size="13"
6586
- fill="${this.renderTheme.textMuted}"
6587
- text-anchor="middle">Modal content</text>
6588
- </g>`;
7623
+ const hasTitle = node.params.title !== void 0 && node.params.title !== "";
7624
+ const closable = node.params.closable !== "false" && node.params.closable !== 0;
7625
+ const title = hasTitle ? String(node.params.title) : "";
7626
+ output.push(
7627
+ `<rect x="0" y="0" width="${canvasWidth}" height="${canvasHeight}" fill="black" opacity="0.28" pointer-events="none"/>`
7628
+ );
7629
+ output.push(
7630
+ `<rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" rx="8" fill="${this.renderTheme.cardBg}" stroke="${this.renderTheme.border}" stroke-width="1"/>`
7631
+ );
7632
+ if (hasTitle) {
7633
+ output.push(
7634
+ `<line x1="${pos.x}" y1="${pos.y + headerHeight}" x2="${pos.x + pos.width}" y2="${pos.y + headerHeight}" stroke="${this.renderTheme.border}" stroke-width="1"/>`
7635
+ );
7636
+ output.push(
7637
+ `<text x="${pos.x + padding}" y="${pos.y + padding + 15}" font-family="Arial, Helvetica, sans-serif" font-size="15" font-weight="600" fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>`
7638
+ );
7639
+ if (closable) {
7640
+ const events = node.events?.find((e) => e.event === "onClose");
7641
+ const closeEventAttr = events ? this.serializeEventHandler(events) : "";
7642
+ const closeX = pos.x + pos.width - padding - 14;
7643
+ const closeY = pos.y + padding + 14;
7644
+ output.push(
7645
+ `<rect x="${closeX - 12}" y="${closeY - 12}" width="24" height="24" rx="4" fill="transparent" stroke="none" pointer-events="all"${closeEventAttr}/>`,
7646
+ `<text x="${closeX}" y="${closeY}" font-family="Arial, Helvetica, sans-serif" font-size="16" fill="${this.renderTheme.textMuted}" text-anchor="middle" dominant-baseline="central" pointer-events="none">\u2715</text>`
7647
+ );
7648
+ }
7649
+ }
7650
+ }
7651
+ renderModalFooterDecoration(pos, output) {
7652
+ output.push(
7653
+ `<line x1="${pos.x}" y1="${pos.y}" x2="${pos.x + pos.width}" y2="${pos.y}" stroke="${this.renderTheme.border}" stroke-width="1"/>`
7654
+ );
6589
7655
  }
6590
7656
  renderList(node, pos) {
6591
7657
  const title = String(node.props.title || "");
6592
- const itemsStr = String(node.props.items || "");
6593
7658
  const mockType = String(node.props.mock || "").trim();
6594
7659
  const random = this.parseBooleanProp(node.props.random, false);
6595
- let items = [];
6596
- if (itemsStr) {
6597
- items = itemsStr.split(",").map((i) => i.trim()).filter(Boolean);
6598
- } else {
7660
+ let items = toStringArray(node.props.items);
7661
+ if (items.length === 0) {
6599
7662
  const parsedItemsMock = Number(node.props.itemsMock ?? 4);
6600
7663
  const itemCount = Number.isFinite(parsedItemsMock) ? Math.max(0, Math.floor(parsedItemsMock)) : 4;
6601
7664
  const resolvedMockType = mockType || "name";
@@ -6624,6 +7687,7 @@ var SVGRenderer = class {
6624
7687
  items.forEach((item, i) => {
6625
7688
  const itemY = pos.y + titleHeight + i * itemHeight;
6626
7689
  if (itemY + itemHeight <= pos.y + pos.height) {
7690
+ const itemEventAttrs = this.getScopedEventAttrs(node, "onItemClick", { index: i });
6627
7691
  svg += `
6628
7692
  <line x1="${pos.x}" y1="${itemY + itemHeight}"
6629
7693
  x2="${pos.x + pos.width}" y2="${itemY + itemHeight}"
@@ -6632,7 +7696,10 @@ var SVGRenderer = class {
6632
7696
  <text x="${pos.x + padding}" y="${itemY + 24}"
6633
7697
  font-family="Arial, Helvetica, sans-serif"
6634
7698
  font-size="13"
6635
- fill="${this.renderTheme.text}">${this.escapeXml(item)}</text>`;
7699
+ fill="${this.renderTheme.text}">${this.escapeXml(item)}</text>
7700
+ ${itemEventAttrs ? `<rect x="${pos.x}" y="${itemY}"
7701
+ width="${pos.width}" height="${itemHeight}"
7702
+ fill="transparent" stroke="none" pointer-events="all"${itemEventAttrs}/>` : ""}`;
6636
7703
  }
6637
7704
  });
6638
7705
  svg += "\n </g>";
@@ -6866,8 +7933,8 @@ var SVGRenderer = class {
6866
7933
  return svg;
6867
7934
  }
6868
7935
  renderBreadcrumbs(node, pos) {
6869
- const itemsStr = String(node.props.items || "Home");
6870
- const items = itemsStr.split(",").map((s) => s.trim());
7936
+ const parsedBreadcrumbs = toStringArray(node.props.items);
7937
+ const items = parsedBreadcrumbs.length > 0 ? parsedBreadcrumbs : ["Home"];
6871
7938
  const separator = String(node.props.separator || "/");
6872
7939
  const fontSize = 12;
6873
7940
  const separatorWidth = 20;
@@ -6899,10 +7966,9 @@ var SVGRenderer = class {
6899
7966
  return svg;
6900
7967
  }
6901
7968
  renderSidebarMenu(node, pos) {
6902
- const itemsStr = String(node.props.items || "Item 1,Item 2,Item 3");
6903
- const iconsStr = String(node.props.icons || "");
6904
- const items = itemsStr.split(",").map((s) => s.trim());
6905
- const icons = iconsStr ? iconsStr.split(",").map((s) => s.trim()) : [];
7969
+ const parsedMenuItems = toStringArray(node.props.items);
7970
+ const items = parsedMenuItems.length > 0 ? parsedMenuItems : ["Item 1", "Item 2", "Item 3"];
7971
+ const icons = toStringArray(node.props.icons);
6906
7972
  const itemHeight = 40;
6907
7973
  const fontSize = 14;
6908
7974
  const activeIndex = Number(node.props.active || 0);
@@ -6947,6 +8013,13 @@ var SVGRenderer = class {
6947
8013
  font-size="${fontSize}"
6948
8014
  font-weight="${fontWeight}"
6949
8015
  fill="${textColor}">${this.escapeXml(item)}</text>`;
8016
+ const itemEventAttrs = this.getScopedEventAttrs(node, "onItemsClick", { index });
8017
+ if (itemEventAttrs) {
8018
+ svg += `
8019
+ <rect x="${pos.x}" y="${itemY}"
8020
+ width="${pos.width}" height="${itemHeight}"
8021
+ fill="transparent" stroke="none" pointer-events="all"${itemEventAttrs}/>`;
8022
+ }
6950
8023
  });
6951
8024
  svg += "\n </g>";
6952
8025
  return svg;
@@ -7297,7 +8370,7 @@ var SVGRenderer = class {
7297
8370
  userBadge = { x, y, width, height, label: userLabel };
7298
8371
  rightCursor = x - 8;
7299
8372
  }
7300
- const actionLabels = actions.split(",").map((a) => a.trim()).filter(Boolean);
8373
+ const actionLabels = toStringArray(actions);
7301
8374
  const actionHeight = 32;
7302
8375
  const actionY = pos.y + (pos.height - actionHeight) / 2;
7303
8376
  const actionGap = 8;
@@ -7383,11 +8456,99 @@ var SVGRenderer = class {
7383
8456
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
7384
8457
  }
7385
8458
  /**
7386
- * Get data-node-id attribute string for SVG elements
7387
- * Enables bidirectional selection between code and canvas
8459
+ * Get data-node-id and event data attributes for SVG elements.
8460
+ * Enables bidirectional selection between code and canvas (data-node-id)
8461
+ * and play test interactivity (data-event-*, data-user-id, data-tabs-id).
7388
8462
  */
7389
8463
  getDataNodeId(node) {
7390
- return node.meta.nodeId ? ` data-node-id="${node.meta.nodeId}"` : "";
8464
+ let attrs = "";
8465
+ if (node.meta.nodeId) {
8466
+ attrs += ` data-node-id="${node.meta.nodeId}"`;
8467
+ }
8468
+ if (node.kind === "component") {
8469
+ if (node.userDefinedId) {
8470
+ attrs += ` data-user-id="${node.userDefinedId}"`;
8471
+ }
8472
+ if (node.events && node.events.length > 0) {
8473
+ for (const handler of node.events) {
8474
+ if (!this.isScopedEvent(handler.event)) {
8475
+ attrs += this.serializeEventHandler(handler);
8476
+ }
8477
+ }
8478
+ }
8479
+ }
8480
+ if (node.kind === "container") {
8481
+ if (node.containerType === "tabs" && node.params.id) {
8482
+ attrs += ` data-tabs-id="${node.params.id}"`;
8483
+ if (node.params.active !== void 0) {
8484
+ attrs += ` data-tabs-active="${node.params.active}"`;
8485
+ }
8486
+ }
8487
+ if (node.events && node.events.length > 0) {
8488
+ for (const handler of node.events) {
8489
+ if (node.containerType === "modal" && handler.event === "onClose") continue;
8490
+ attrs += this.serializeEventHandler(handler);
8491
+ }
8492
+ }
8493
+ }
8494
+ return attrs;
8495
+ }
8496
+ isScopedEvent(event) {
8497
+ return event === "onClose" || event === "onItemClick" || event === "onRowClick" || event === "onItemsClick";
8498
+ }
8499
+ getScopedEventAttrs(node, eventName, options = {}) {
8500
+ const handler = node.events?.find((event) => event.event === eventName);
8501
+ if (!handler) return "";
8502
+ let attrs = "";
8503
+ if (node.meta.nodeId) {
8504
+ attrs += ` data-node-id="${node.meta.nodeId}"`;
8505
+ }
8506
+ if (node.userDefinedId) {
8507
+ attrs += ` data-user-id="${node.userDefinedId}"`;
8508
+ }
8509
+ attrs += this.serializeEventHandler(handler);
8510
+ if (options.index !== void 0) {
8511
+ attrs += ` data-event-index="${options.index}"`;
8512
+ }
8513
+ return attrs;
8514
+ }
8515
+ getTabsTriggerAttrs(node, index) {
8516
+ const tabsId = String(node.props.tabsId || "").trim();
8517
+ if (!tabsId) return "";
8518
+ let attrs = "";
8519
+ if (node.meta.nodeId) {
8520
+ attrs += ` data-node-id="${node.meta.nodeId}"`;
8521
+ }
8522
+ attrs += ` data-tabs-id="${tabsId}" data-tabs-trigger-index="${index}"`;
8523
+ return attrs;
8524
+ }
8525
+ serializeEventHandler(handler) {
8526
+ const attrName = this.eventNameToDataAttr(handler.event);
8527
+ const value = handler.actions.map((a) => this.serializeEventAction(a)).join("|");
8528
+ return ` data-event-${attrName}="${value}"`;
8529
+ }
8530
+ eventNameToDataAttr(event) {
8531
+ return event.replace(/^on/, "").toLowerCase();
8532
+ }
8533
+ serializeEventAction(action) {
8534
+ switch (action.type) {
8535
+ case "navigate":
8536
+ return `navigate:${action.screen}`;
8537
+ case "show":
8538
+ return `show:${action.targetId}`;
8539
+ case "hide":
8540
+ return `hide:${action.targetId}`;
8541
+ case "toggle":
8542
+ return `toggle:${action.targetId}`;
8543
+ case "enable":
8544
+ return `enable:${action.targetId}`;
8545
+ case "disable":
8546
+ return `disable:${action.targetId}`;
8547
+ case "setTab":
8548
+ return `setTab:${action.tabsId}:${action.index}`;
8549
+ case "navigateItems":
8550
+ return action.screens.map((s) => `navigate:${s}`).join(",");
8551
+ }
7391
8552
  }
7392
8553
  };
7393
8554
  function renderToSVG(ir, layout, options) {
@@ -7477,8 +8638,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
7477
8638
  * Render breadcrumbs as skeleton blocks: <rect> / <rect> / <rect accent>
7478
8639
  */
7479
8640
  renderBreadcrumbs(node, pos) {
7480
- const itemsStr = String(node.props.items || "Home");
7481
- const items = itemsStr.split(",").map((s) => s.trim()).filter(Boolean);
8641
+ const items = toStringArray(node.props.items || "Home");
7482
8642
  const separator = String(node.props.separator || "/");
7483
8643
  const blockColor = this.renderTheme.border;
7484
8644
  const charWidth = 6.2;
@@ -7799,10 +8959,9 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
7799
8959
  */
7800
8960
  renderTable(node, pos) {
7801
8961
  const title = String(node.props.title || "");
7802
- const columnsStr = String(node.props.columns || "Col1,Col2,Col3");
7803
- const columns = columnsStr.split(",").map((c) => c.trim()).filter(Boolean);
8962
+ const columns = toStringArray(node.props.columns || "Col1,Col2,Col3");
7804
8963
  const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
7805
- const actions = String(node.props.actions || "").split(",").map((value) => value.trim()).filter(Boolean);
8964
+ const actions = toStringArray(node.props.actions);
7806
8965
  const hasActions = actions.length > 0;
7807
8966
  const pagination = this.parseBooleanProp(node.props.pagination, false);
7808
8967
  const parsedPageCount = Number(node.props.pages || 5);
@@ -8088,7 +9247,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
8088
9247
  const itemsStr = String(node.props.items || "");
8089
9248
  let items = [];
8090
9249
  if (itemsStr) {
8091
- items = itemsStr.split(",").map((i) => i.trim());
9250
+ items = toStringArray(itemsStr);
8092
9251
  } else {
8093
9252
  const itemCount = Number(node.props.itemsMock || 6);
8094
9253
  items = Array(itemCount).fill("Item");
@@ -8125,8 +9284,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
8125
9284
  * Render SidebarMenu with gray blocks instead of text and no icons
8126
9285
  */
8127
9286
  renderSidebarMenu(node, pos) {
8128
- const itemsStr = String(node.props.items || "Item 1,Item 2,Item 3");
8129
- const items = itemsStr.split(",").map((s) => s.trim());
9287
+ const items = toStringArray(node.props.items || "Item 1,Item 2,Item 3");
8130
9288
  const itemHeight = 40;
8131
9289
  const activeIndex = Number(node.props.active || 0);
8132
9290
  const accentColor = this.resolveAccentColor();
@@ -8974,7 +10132,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
8974
10132
  const title = String(node.props.title || "Sidebar");
8975
10133
  const itemsStr = String(node.props.items || "");
8976
10134
  const activeItem = String(node.props.active || "");
8977
- const items = itemsStr ? itemsStr.split(",").map((i) => i.trim()) : ["Item 1", "Item 2", "Item 3"];
10135
+ const parsed = toStringArray(itemsStr);
10136
+ const items = parsed.length ? parsed : ["Item 1", "Item 2", "Item 3"];
8978
10137
  const itemHeight = 40;
8979
10138
  const padding = 16;
8980
10139
  const titleHeight = 40;
@@ -9017,7 +10176,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
9017
10176
  */
9018
10177
  renderTabs(node, pos) {
9019
10178
  const itemsStr = String(node.props.items || "");
9020
- const tabs = itemsStr ? itemsStr.split(",").map((t) => t.trim()) : ["Tab 1", "Tab 2", "Tab 3"];
10179
+ const parsedTabs = toStringArray(itemsStr);
10180
+ const tabs = parsedTabs.length ? parsedTabs : ["Tab 1", "Tab 2", "Tab 3"];
9021
10181
  const activeProp = node.props.active ?? 0;
9022
10182
  const activeIndex = Number.isFinite(Number(activeProp)) ? Math.max(0, Math.floor(Number(activeProp))) : 0;
9023
10183
  const variant = String(node.props.variant || "default");
@@ -9029,7 +10189,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
9029
10189
  const fontSize = 13;
9030
10190
  const textY = pos.y + Math.round(tabHeight / 2) + Math.round(fontSize * 0.4);
9031
10191
  const iconsStr = String(node.props.icons || "");
9032
- const iconList = iconsStr ? iconsStr.split(",").map((s) => s.trim()) : [];
10192
+ const iconList = toStringArray(iconsStr);
9033
10193
  const isFlat = this.parseBooleanProp(node.props.flat, false);
9034
10194
  const showBorder = this.parseBooleanProp(node.props.border, true);
9035
10195
  const tabWidth = pos.width / tabs.length;
@@ -9194,7 +10354,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
9194
10354
  <!-- Modal backdrop -->
9195
10355
  <rect x="0" y="0"
9196
10356
  width="${this.options.width}" height="${overlayHeight}"
9197
- fill="black" opacity="0.28"/>
10357
+ fill="black" opacity="0.28" pointer-events="none"/>
9198
10358
 
9199
10359
  <!-- Modal box -->
9200
10360
  <rect x="${modalX}" y="${modalY}"
@@ -9222,7 +10382,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
9222
10382
  <text x="${modalX + pos.width - 16}" y="${modalY + padding + 12}"
9223
10383
  font-family="${this.fontFamily}"
9224
10384
  font-size="18"
9225
- fill="${this.renderTheme.textMuted}">\u2715</text>
10385
+ fill="${this.renderTheme.textMuted}"
10386
+ pointer-events="none">\u2715</text>
9226
10387
 
9227
10388
  <!-- Content placeholder -->
9228
10389
  <text x="${modalX + pos.width / 2}" y="${modalY + headerHeight + (pos.height - headerHeight) / 2}"
@@ -9240,10 +10401,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
9240
10401
  const itemsStr = String(node.props.items || "");
9241
10402
  const mockType = String(node.props.mock || "").trim();
9242
10403
  const random = this.parseBooleanProp(node.props.random, false);
9243
- let items = [];
9244
- if (itemsStr) {
9245
- items = itemsStr.split(",").map((i) => i.trim()).filter(Boolean);
9246
- } else {
10404
+ let items = toStringArray(itemsStr);
10405
+ if (!items.length) {
9247
10406
  const parsedItemsMock = Number(node.props.itemsMock ?? 4);
9248
10407
  const itemCount = Number.isFinite(parsedItemsMock) ? Math.max(0, Math.floor(parsedItemsMock)) : 4;
9249
10408
  const resolvedMockType = mockType || "name";
@@ -9457,7 +10616,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
9457
10616
  */
9458
10617
  renderBreadcrumbs(node, pos) {
9459
10618
  const itemsStr = String(node.props.items || "Home");
9460
- const items = itemsStr.split(",").map((s) => s.trim());
10619
+ const items = toStringArray(itemsStr);
9461
10620
  const separator = String(node.props.separator || "/");
9462
10621
  const fontSize = 12;
9463
10622
  const separatorWidth = 20;
@@ -9494,8 +10653,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
9494
10653
  renderSidebarMenu(node, pos) {
9495
10654
  const itemsStr = String(node.props.items || "Item 1,Item 2,Item 3");
9496
10655
  const iconsStr = String(node.props.icons || "");
9497
- const items = itemsStr.split(",").map((s) => s.trim());
9498
- const icons = iconsStr ? iconsStr.split(",").map((s) => s.trim()) : [];
10656
+ const items = toStringArray(itemsStr);
10657
+ const icons = toStringArray(iconsStr);
9499
10658
  const itemHeight = 40;
9500
10659
  const fontSize = 14;
9501
10660
  const activeIndex = Number(node.props.active || 0);
@@ -9815,11 +10974,13 @@ export {
9815
10974
  DEVICE_PRESETS,
9816
10975
  IRGenerator,
9817
10976
  LayoutEngine,
10977
+ SELF_TARGET,
9818
10978
  SVGRenderer,
9819
10979
  SkeletonSVGRenderer,
9820
10980
  SketchSVGRenderer,
9821
10981
  SourceMapBuilder,
9822
10982
  SourceMapResolver,
10983
+ applyStateChange,
9823
10984
  buildSVG,
9824
10985
  calculateLayout,
9825
10986
  createSVGElement,