@wire-dsl/engine 0.9.1 → 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,9 +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
3975
  const isHorizontalStack = node.containerType === "stack" && !isVerticalStack;
3062
- if ((isVerticalStack || isHorizontalStack || node.containerType === "card") && node.children.length > 0) {
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) {
3063
3977
  let containerMaxY = y;
3064
3978
  node.children.forEach((childRef) => {
3065
3979
  const childPos = this.result[childRef.ref];
@@ -3079,8 +3993,22 @@ var LayoutEngine = class {
3079
3993
  const children = node.children;
3080
3994
  if (direction === "vertical") {
3081
3995
  let currentY = y;
3082
- 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) => {
3083
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
+ }
3084
4012
  let childHeight = this.getComponentHeight();
3085
4013
  if (childNode?.kind === "component" && childNode.props.height) {
3086
4014
  childHeight = Number(childNode.props.height);
@@ -3091,12 +4019,17 @@ var LayoutEngine = class {
3091
4019
  }
3092
4020
  this.calculateNode(childRef.ref, x, currentY, width, childHeight, "stack");
3093
4021
  currentY += childHeight;
3094
- if (index < children.length - 1) {
4022
+ flowCount++;
4023
+ if (flowCount < flowChildren.length) {
3095
4024
  currentY += gap;
3096
4025
  }
3097
4026
  });
3098
4027
  let adjustedY = y;
3099
- 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;
3100
4033
  const childPos = this.result[childRef.ref];
3101
4034
  if (childPos) {
3102
4035
  const deltaY = adjustedY - childPos.y;
@@ -3105,7 +4038,8 @@ var LayoutEngine = class {
3105
4038
  this.adjustNodeYPositions(childRef.ref, deltaY);
3106
4039
  }
3107
4040
  adjustedY += childPos.height;
3108
- if (index < children.length - 1) {
4041
+ adjustedCount++;
4042
+ if (adjustedCount < flowChildren.length) {
3109
4043
  adjustedY += gap;
3110
4044
  }
3111
4045
  }
@@ -3115,9 +4049,10 @@ var LayoutEngine = class {
3115
4049
  const crossAlign = node.style.align || "start";
3116
4050
  if (justify === "stretch") {
3117
4051
  let currentX = x;
3118
- 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);
3119
4054
  let stackHeight = 0;
3120
- children.forEach((childRef) => {
4055
+ visibleChildren.forEach((childRef) => {
3121
4056
  const childNode = this.nodes[childRef.ref];
3122
4057
  let childHeight = this.getComponentHeight();
3123
4058
  if (childNode?.kind === "component" && childNode.props.height) {
@@ -3130,6 +4065,10 @@ var LayoutEngine = class {
3130
4065
  stackHeight = Math.max(stackHeight, childHeight);
3131
4066
  });
3132
4067
  children.forEach((childRef) => {
4068
+ if (!this.isNodeVisible(childRef.ref)) {
4069
+ this.calculateNode(childRef.ref, currentX, y, 0, 0, "stack");
4070
+ return;
4071
+ }
3133
4072
  this.calculateNode(childRef.ref, currentX, y, childWidth, stackHeight, "stack");
3134
4073
  currentX += childWidth + gap;
3135
4074
  });
@@ -3138,9 +4077,18 @@ var LayoutEngine = class {
3138
4077
  const childHeights = [];
3139
4078
  const explicitHeightFlags = [];
3140
4079
  const flexIndices = /* @__PURE__ */ new Set();
4080
+ const visibleFlags = [];
3141
4081
  let stackHeight = 0;
3142
4082
  children.forEach((childRef, index) => {
3143
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
+ }
3144
4092
  const hasExplicitHeight = childNode?.kind === "component" && !!childNode.props.height;
3145
4093
  const hasExplicitWidth = childNode?.kind === "component" && !!childNode.props.width;
3146
4094
  const isBlockButton = childNode?.kind === "component" && childNode.componentType === "Button" && !hasExplicitWidth && this.parseBooleanProp(childNode.props.block, false);
@@ -3158,7 +4106,8 @@ var LayoutEngine = class {
3158
4106
  childHeights.push(this.getComponentHeight());
3159
4107
  explicitHeightFlags.push(hasExplicitHeight);
3160
4108
  });
3161
- 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);
3162
4111
  if (flexIndices.size > 0) {
3163
4112
  const fixedWidth = childWidths.reduce((sum, w, idx) => {
3164
4113
  return flexIndices.has(idx) ? sum : sum + w;
@@ -3170,6 +4119,7 @@ var LayoutEngine = class {
3170
4119
  });
3171
4120
  }
3172
4121
  children.forEach((childRef, index) => {
4122
+ if (!visibleFlags[index]) return;
3173
4123
  const childNode = this.nodes[childRef.ref];
3174
4124
  const childWidth = childWidths[index];
3175
4125
  let childHeight = this.getComponentHeight();
@@ -3193,14 +4143,18 @@ var LayoutEngine = class {
3193
4143
  startX = x + width - totalContentWidth;
3194
4144
  } else if (justify === "spaceBetween") {
3195
4145
  startX = x;
3196
- dynamicGap = children.length > 1 ? (width - totalChildWidth) / (children.length - 1) : 0;
4146
+ dynamicGap = visibleCount > 1 ? (width - totalChildWidth) / (visibleCount - 1) : 0;
3197
4147
  } else if (justify === "spaceAround") {
3198
- const spacing = children.length > 0 ? (width - totalChildWidth) / children.length : 0;
4148
+ const spacing = visibleCount > 0 ? (width - totalChildWidth) / visibleCount : 0;
3199
4149
  startX = x + spacing / 2;
3200
4150
  dynamicGap = spacing;
3201
4151
  }
3202
4152
  let currentX = startX;
3203
4153
  children.forEach((childRef, index) => {
4154
+ if (!visibleFlags[index]) {
4155
+ this.calculateNode(childRef.ref, currentX, y, 0, 0, "stack");
4156
+ return;
4157
+ }
3204
4158
  const childWidth = childWidths[index];
3205
4159
  const childHeight = childHeights[index];
3206
4160
  let childY = y;
@@ -3232,6 +4186,7 @@ var LayoutEngine = class {
3232
4186
  let currentRowMaxHeight = 0;
3233
4187
  const rowHeights = [0];
3234
4188
  node.children.forEach((childRef) => {
4189
+ if (!this.isNodeVisible(childRef.ref)) return;
3235
4190
  const child = this.nodes[childRef.ref];
3236
4191
  let span = 1;
3237
4192
  let childHeight = this.getComponentHeight();
@@ -3266,6 +4221,18 @@ var LayoutEngine = class {
3266
4221
  }
3267
4222
  return totalHeight;
3268
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
+ }
3269
4236
  if (node.containerType === "split") {
3270
4237
  const splitGap = this.resolveSpacing(node.style.gap);
3271
4238
  const leftParam = node.params.left;
@@ -3278,6 +4245,7 @@ var LayoutEngine = class {
3278
4245
  const rightWidth = Number.isFinite(rightWidthRaw) && rightWidthRaw > 0 ? rightWidthRaw : availableWidth / 2;
3279
4246
  let maxHeight = 0;
3280
4247
  node.children.forEach((childRef, index) => {
4248
+ if (!this.isNodeVisible(childRef.ref)) return;
3281
4249
  const child = this.nodes[childRef.ref];
3282
4250
  let childHeight = this.getComponentHeight();
3283
4251
  const isFirst = index === 0;
@@ -3305,6 +4273,7 @@ var LayoutEngine = class {
3305
4273
  if (node.containerType === "stack" && direction === "horizontal") {
3306
4274
  let maxHeight = 0;
3307
4275
  node.children.forEach((childRef) => {
4276
+ if (!this.isNodeVisible(childRef.ref)) return;
3308
4277
  const child = this.nodes[childRef.ref];
3309
4278
  let childHeight = this.getComponentHeight();
3310
4279
  if (child?.kind === "component") {
@@ -3321,7 +4290,10 @@ var LayoutEngine = class {
3321
4290
  totalHeight += maxHeight;
3322
4291
  return totalHeight;
3323
4292
  }
3324
- 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;
3325
4297
  const child = this.nodes[childRef.ref];
3326
4298
  let childHeight = this.getComponentHeight();
3327
4299
  if (child?.kind === "component") {
@@ -3334,7 +4306,8 @@ var LayoutEngine = class {
3334
4306
  childHeight = this.calculateContainerHeight(child, availableWidth);
3335
4307
  }
3336
4308
  totalHeight += childHeight;
3337
- if (index < node.children.length - 1) {
4309
+ linearIndex++;
4310
+ if (linearIndex < visibleLinear.length) {
3338
4311
  totalHeight += gap;
3339
4312
  }
3340
4313
  });
@@ -3347,6 +4320,10 @@ var LayoutEngine = class {
3347
4320
  const colWidth = (width - gap * (columns - 1)) / columns;
3348
4321
  const cellHeights = {};
3349
4322
  node.children.forEach((childRef, cellIndex) => {
4323
+ if (!this.isNodeVisible(childRef.ref)) {
4324
+ cellHeights[cellIndex] = 0;
4325
+ return;
4326
+ }
3350
4327
  const child = this.nodes[childRef.ref];
3351
4328
  let cellHeight = this.getComponentHeight();
3352
4329
  let span = 1;
@@ -3371,6 +4348,11 @@ var LayoutEngine = class {
3371
4348
  const rowHeights = [0];
3372
4349
  const cellPositions = [];
3373
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
+ }
3374
4356
  const child = this.nodes[childRef.ref];
3375
4357
  let span = 1;
3376
4358
  if (child?.kind === "container" && child.meta?.source === "cell") {
@@ -3382,13 +4364,17 @@ var LayoutEngine = class {
3382
4364
  currentCol = 0;
3383
4365
  currentRowMaxHeight = 0;
3384
4366
  }
3385
- cellPositions.push({ row: currentRow, col: currentCol, span });
4367
+ cellPositions.push({ row: currentRow, col: currentCol, span, visible: true });
3386
4368
  currentRowMaxHeight = Math.max(currentRowMaxHeight, cellHeights[cellIndex]);
3387
4369
  currentCol += span;
3388
4370
  });
3389
4371
  rowHeights[currentRow] = currentRowMaxHeight;
3390
4372
  node.children.forEach((childRef, cellIndex) => {
3391
- 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
+ }
3392
4378
  const cellHeight = rowHeights[row];
3393
4379
  let cellY = y;
3394
4380
  for (let r = 0; r < row; r++) {
@@ -3459,8 +4445,14 @@ var LayoutEngine = class {
3459
4445
  const innerCardWidth = width - cardPadding * 2;
3460
4446
  const children = node.children;
3461
4447
  let currentY = y + cardPadding;
3462
- children.forEach((childRef, index) => {
4448
+ const flowChildren = children.filter((cr) => this.isNodeVisible(cr.ref));
4449
+ let flowCount = 0;
4450
+ children.forEach((childRef) => {
3463
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
+ }
3464
4456
  let childHeight = this.getComponentHeight();
3465
4457
  if (childNode?.kind === "component" && childNode.props.height) {
3466
4458
  childHeight = Number(childNode.props.height);
@@ -3471,11 +4463,63 @@ var LayoutEngine = class {
3471
4463
  }
3472
4464
  this.calculateNode(childRef.ref, x + cardPadding, currentY, innerCardWidth, childHeight, "card");
3473
4465
  currentY += childHeight;
3474
- if (index < children.length - 1) {
4466
+ flowCount++;
4467
+ if (flowCount < flowChildren.length) {
3475
4468
  currentY += gap;
3476
4469
  }
3477
4470
  });
3478
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
+ }
3479
4523
  /**
3480
4524
  * Calculate layout for an instance node.
3481
4525
  * The instance is a transparent wrapper — its bounding box equals the
@@ -3706,8 +4750,7 @@ var LayoutEngine = class {
3706
4750
  return Math.max(this.getComponentHeight(), wrappedHeight);
3707
4751
  }
3708
4752
  if (node.componentType === "SidebarMenu") {
3709
- const itemsStr = String(node.props.items || "Item 1,Item 2,Item 3");
3710
- const items = itemsStr.split(",").map((s) => s.trim()).filter(Boolean);
4753
+ const items = toStringArray(node.props.items);
3711
4754
  const itemCount = items.length > 0 ? items.length : 3;
3712
4755
  const itemHeight = 40;
3713
4756
  return Math.max(this.getComponentHeight(), itemCount * itemHeight);
@@ -3718,7 +4761,7 @@ var LayoutEngine = class {
3718
4761
  if (node.componentType === "Stat") return 120;
3719
4762
  if (node.componentType === "Chart") return 250;
3720
4763
  if (node.componentType === "List") {
3721
- const itemsFromProps = String(node.props.items || "").split(",").map((item) => item.trim()).filter(Boolean);
4764
+ const itemsFromProps = toStringArray(node.props.items);
3722
4765
  const parsedItemsMock = Number(node.props.itemsMock ?? 4);
3723
4766
  const fallbackCount = Number.isFinite(parsedItemsMock) ? Math.max(0, Math.floor(parsedItemsMock)) : 4;
3724
4767
  const itemCount = itemsFromProps.length > 0 ? itemsFromProps.length : fallbackCount;
@@ -3896,6 +4939,17 @@ var LayoutEngine = class {
3896
4939
  }
3897
4940
  return width;
3898
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
+ }
3899
4953
  parseBooleanProp(value, fallback = false) {
3900
4954
  if (typeof value === "boolean") return value;
3901
4955
  if (typeof value === "string") {
@@ -3986,7 +5040,7 @@ var MockDataGenerator = class {
3986
5040
  for (const [key, rawValues] of Object.entries(mocks)) {
3987
5041
  let values = [];
3988
5042
  if (typeof rawValues === "string") {
3989
- values = rawValues.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
5043
+ values = toStringArray(rawValues);
3990
5044
  } else if (Array.isArray(rawValues)) {
3991
5045
  values = rawValues.map((v) => typeof v === "string" ? v.trim() : "").filter((v) => v.length > 0);
3992
5046
  }
@@ -4078,7 +5132,7 @@ var MockDataGenerator = class {
4078
5132
  * columns: "id,name,status,amount"
4079
5133
  */
4080
5134
  static generateMockRow(columns, rowIndex, random = false) {
4081
- const columnNames = columns.split(",").map((c) => c.trim());
5135
+ const columnNames = toStringArray(columns);
4082
5136
  const row = {};
4083
5137
  columnNames.forEach((col) => {
4084
5138
  const mockType = this.inferMockTypeFromColumn(col);
@@ -5130,6 +6184,9 @@ var SVGRenderer = class {
5130
6184
  if (!node || !pos) return;
5131
6185
  this.renderedNodeIds.add(nodeId);
5132
6186
  if (node.kind === "container") {
6187
+ if (String(node.params.visible) === "false") {
6188
+ return;
6189
+ }
5133
6190
  const containerGroup = [];
5134
6191
  const hasNodeId = node.meta?.nodeId;
5135
6192
  if (hasNodeId) {
@@ -5150,6 +6207,12 @@ var SVGRenderer = class {
5150
6207
  if (node.containerType === "split") {
5151
6208
  this.renderSplitDecoration(node, pos, containerGroup);
5152
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
+ }
5153
6216
  const isCellContainer = node.meta?.source === "cell";
5154
6217
  if (node.children.length === 0 && this.options.showDiagnostics && !isCellContainer) {
5155
6218
  containerGroup.push(this.renderEmptyContainerDiagnostic(pos, node.containerType));
@@ -5176,6 +6239,9 @@ var SVGRenderer = class {
5176
6239
  }
5177
6240
  output.push(...instanceGroup);
5178
6241
  } else if (node.kind === "component") {
6242
+ if (String(node.props.visible) === "false") {
6243
+ return;
6244
+ }
5179
6245
  const componentSvg = this.renderComponent(node, pos);
5180
6246
  if (componentSvg) {
5181
6247
  output.push(componentSvg);
@@ -5237,8 +6303,6 @@ var SVGRenderer = class {
5237
6303
  return this.renderAlert(node, pos);
5238
6304
  case "Badge":
5239
6305
  return this.renderBadge(node, pos);
5240
- case "Modal":
5241
- return this.renderModal(node, pos);
5242
6306
  case "List":
5243
6307
  return this.renderList(node, pos);
5244
6308
  case "Stat":
@@ -5681,8 +6745,8 @@ var SVGRenderer = class {
5681
6745
  }
5682
6746
  renderTable(node, pos) {
5683
6747
  const title = String(node.props.title || "");
5684
- const columnsStr = String(node.props.columns || "Col1,Col2,Col3");
5685
- 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"];
5686
6750
  const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
5687
6751
  const mockStr = String(node.props.mock || "");
5688
6752
  const random = this.parseBooleanProp(node.props.random, false);
@@ -5690,7 +6754,7 @@ var SVGRenderer = class {
5690
6754
  const parsedPageCount = Number(node.props.pages || 5);
5691
6755
  const pageCount = Number.isFinite(parsedPageCount) && parsedPageCount > 0 ? Math.floor(parsedPageCount) : 5;
5692
6756
  const paginationAlign = String(node.props.paginationAlign || "right");
5693
- const actions = String(node.props.actions || "").split(",").map((value) => value.trim()).filter(Boolean);
6757
+ const actions = toStringArray(node.props.actions);
5694
6758
  const hasActions = actions.length > 0;
5695
6759
  const caption = String(node.props.caption || "").trim();
5696
6760
  const hasCaption = caption.length > 0;
@@ -5703,7 +6767,7 @@ var SVGRenderer = class {
5703
6767
  const rawCaptionAlign = String(node.props.captionAlign || "");
5704
6768
  const captionAlign = rawCaptionAlign === "left" || rawCaptionAlign === "center" || rawCaptionAlign === "right" ? rawCaptionAlign : paginationAlign === "left" ? "right" : "left";
5705
6769
  const sameFooterAlign = hasCaption && pagination && captionAlign === paginationAlign;
5706
- const mockTypes = mockStr ? mockStr.split(",").map((m) => m.trim()).filter(Boolean) : [];
6770
+ const mockTypes = toStringArray(mockStr);
5707
6771
  const safeColumns = columns.length > 0 ? columns : ["Column"];
5708
6772
  while (mockTypes.length < safeColumns.length) {
5709
6773
  const inferred = MockDataGenerator.inferMockTypeFromColumn(safeColumns[mockTypes.length] || "item");
@@ -5803,6 +6867,13 @@ var SVGRenderer = class {
5803
6867
  currentX += buttonSize + buttonGap;
5804
6868
  });
5805
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
+ }
5806
6877
  });
5807
6878
  const footerTop = headerY + headerHeight + mockRows.length * rowHeight + 16;
5808
6879
  if (pagination) {
@@ -6192,14 +7263,15 @@ var SVGRenderer = class {
6192
7263
  const label = String(node.props.label || "Checkbox");
6193
7264
  const checked = String(node.props.checked || "false").toLowerCase() === "true";
6194
7265
  const disabled = this.parseBooleanProp(node.props.disabled, false);
7266
+ const clickable = String(node.props.clickable ?? "true") !== "false";
6195
7267
  const controlColor = this.resolveControlColor();
6196
7268
  const checkboxSize = 18;
6197
7269
  const checkboxY = pos.y + pos.height / 2 - checkboxSize / 2;
6198
- return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
6199
- <rect x="${pos.x}" y="${checkboxY}"
6200
- width="${checkboxSize}" height="${checkboxSize}"
6201
- rx="4"
6202
- 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}"
6203
7275
  stroke="${this.renderTheme.border}"
6204
7276
  stroke-width="1"/>
6205
7277
  ${checked ? `<text x="${pos.x + checkboxSize / 2}" y="${checkboxY + 14}"
@@ -6217,13 +7289,14 @@ var SVGRenderer = class {
6217
7289
  const label = String(node.props.label || "Radio");
6218
7290
  const checked = String(node.props.checked || "false").toLowerCase() === "true";
6219
7291
  const disabled = this.parseBooleanProp(node.props.disabled, false);
7292
+ const clickable = String(node.props.clickable ?? "true") !== "false";
6220
7293
  const controlColor = this.resolveControlColor();
6221
7294
  const radioSize = 16;
6222
7295
  const radioY = pos.y + pos.height / 2 - radioSize / 2;
6223
- return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
6224
- <circle cx="${pos.x + radioSize / 2}" cy="${radioY + radioSize / 2}"
6225
- r="${radioSize / 2}"
6226
- 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}"
6227
7300
  stroke="${this.renderTheme.border}"
6228
7301
  stroke-width="1"/>
6229
7302
  ${checked ? `<circle cx="${pos.x + radioSize / 2}" cy="${radioY + radioSize / 2}"
@@ -6239,12 +7312,13 @@ var SVGRenderer = class {
6239
7312
  const label = String(node.props.label || "Toggle");
6240
7313
  const enabled = String(node.props.enabled || "false").toLowerCase() === "true";
6241
7314
  const disabled = this.parseBooleanProp(node.props.disabled, false);
7315
+ const clickable = String(node.props.clickable ?? "true") !== "false";
6242
7316
  const controlColor = this.resolveControlColor();
6243
7317
  const toggleWidth = 40;
6244
7318
  const toggleHeight = 20;
6245
7319
  const toggleY = pos.y + pos.height / 2 - toggleHeight / 2;
6246
- return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
6247
- <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}"
6248
7322
  width="${toggleWidth}" height="${toggleHeight}"
6249
7323
  rx="10"
6250
7324
  fill="${enabled ? controlColor : this.renderTheme.border}"
@@ -6263,12 +7337,9 @@ var SVGRenderer = class {
6263
7337
  // ============================================================================
6264
7338
  renderSidebar(node, pos) {
6265
7339
  const title = String(node.props.title || "Sidebar");
6266
- const itemsStr = String(node.props.items || "");
6267
7340
  const activeItem = String(node.props.active || "");
6268
- let items = [];
6269
- if (itemsStr) {
6270
- items = itemsStr.split(",").map((i) => i.trim());
6271
- } else {
7341
+ let items = toStringArray(node.props.items);
7342
+ if (items.length === 0) {
6272
7343
  const itemCount = Number(node.props.itemsMock || 6);
6273
7344
  items = MockDataGenerator.generateMockList("name", itemCount);
6274
7345
  }
@@ -6307,9 +7378,9 @@ var SVGRenderer = class {
6307
7378
  return svg;
6308
7379
  }
6309
7380
  renderTabs(node, pos) {
6310
- const itemsStr = String(node.props.items || "");
6311
- const tabs = itemsStr ? itemsStr.split(",").map((t) => t.trim()) : ["Tab 1", "Tab 2", "Tab 3"];
6312
- 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;
6313
7384
  const activeIndex = Number.isFinite(Number(activeProp)) ? Math.max(0, Math.floor(Number(activeProp))) : 0;
6314
7385
  const variant = String(node.props.variant || "default");
6315
7386
  const accentColor = variant === "default" ? this.resolveAccentColor() : this.resolveVariantColor(variant, this.resolveAccentColor());
@@ -6319,8 +7390,7 @@ var SVGRenderer = class {
6319
7390
  const tabHeight = pos.height > 0 ? pos.height : sizeMap[String(node.props.size || "md")] ?? 44;
6320
7391
  const fontSize = 13;
6321
7392
  const textY = pos.y + Math.round(tabHeight / 2) + Math.round(fontSize * 0.4);
6322
- const iconsStr = String(node.props.icons || "");
6323
- const iconList = iconsStr ? iconsStr.split(",").map((s) => s.trim()) : [];
7393
+ const iconList = toStringArray(node.props.icons);
6324
7394
  const isFlat = this.parseBooleanProp(node.props.flat, false);
6325
7395
  const showBorder = this.parseBooleanProp(node.props.border, true);
6326
7396
  const tabWidth = pos.width / tabs.length;
@@ -6424,6 +7494,13 @@ var SVGRenderer = class {
6424
7494
  text-anchor="middle">${this.escapeXml(tab)}</text>`;
6425
7495
  }
6426
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
+ }
6427
7504
  });
6428
7505
  const contentY = pos.y + tabHeight;
6429
7506
  const contentH = pos.height - tabHeight;
@@ -6537,66 +7614,51 @@ var SVGRenderer = class {
6537
7614
  text-anchor="middle">${this.escapeXml(text)}</text>
6538
7615
  </g>`;
6539
7616
  }
6540
- renderModal(node, pos) {
6541
- const visible = this.parseBooleanProp(node.props.visible, true);
6542
- if (!visible) {
6543
- return "";
6544
- }
6545
- 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());
6546
7621
  const padding = 16;
6547
7622
  const headerHeight = 48;
6548
- const overlayHeight = Math.max(this.options.height, this.calculateContentHeight());
6549
- const modalX = (this.options.width - pos.width) / 2;
6550
- const modalY = Math.max(40, (overlayHeight - pos.height) / 2);
6551
- return `<g${this.getDataNodeId(node)}>
6552
- <!-- Modal backdrop -->
6553
- <rect x="0" y="0"
6554
- width="${this.options.width}" height="${overlayHeight}"
6555
- fill="black" opacity="0.28"/>
6556
-
6557
- <!-- Modal box -->
6558
- <rect x="${modalX}" y="${modalY}"
6559
- width="${pos.width}" height="${pos.height}"
6560
- rx="8"
6561
- fill="${this.renderTheme.cardBg}"
6562
- stroke="${this.renderTheme.border}"
6563
- stroke-width="1"/>
6564
-
6565
- <!-- Header -->
6566
- <line x1="${modalX}" y1="${modalY + headerHeight}"
6567
- x2="${modalX + pos.width}" y2="${modalY + headerHeight}"
6568
- stroke="${this.renderTheme.border}"
6569
- stroke-width="1"/>
6570
-
6571
- <text x="${modalX + padding}" y="${modalY + padding + 16}"
6572
- font-family="Arial, Helvetica, sans-serif"
6573
- font-size="16"
6574
- font-weight="600"
6575
- fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>
6576
-
6577
- <!-- Close button -->
6578
- <text x="${modalX + pos.width - 16}" y="${modalY + padding + 12}"
6579
- font-family="Arial, Helvetica, sans-serif"
6580
- font-size="18"
6581
- fill="${this.renderTheme.textMuted}">\u2715</text>
6582
-
6583
- <!-- Content placeholder -->
6584
- <text x="${modalX + pos.width / 2}" y="${modalY + headerHeight + (pos.height - headerHeight) / 2}"
6585
- font-family="Arial, Helvetica, sans-serif"
6586
- font-size="13"
6587
- fill="${this.renderTheme.textMuted}"
6588
- text-anchor="middle">Modal content</text>
6589
- </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
+ );
6590
7655
  }
6591
7656
  renderList(node, pos) {
6592
7657
  const title = String(node.props.title || "");
6593
- const itemsStr = String(node.props.items || "");
6594
7658
  const mockType = String(node.props.mock || "").trim();
6595
7659
  const random = this.parseBooleanProp(node.props.random, false);
6596
- let items = [];
6597
- if (itemsStr) {
6598
- items = itemsStr.split(",").map((i) => i.trim()).filter(Boolean);
6599
- } else {
7660
+ let items = toStringArray(node.props.items);
7661
+ if (items.length === 0) {
6600
7662
  const parsedItemsMock = Number(node.props.itemsMock ?? 4);
6601
7663
  const itemCount = Number.isFinite(parsedItemsMock) ? Math.max(0, Math.floor(parsedItemsMock)) : 4;
6602
7664
  const resolvedMockType = mockType || "name";
@@ -6625,6 +7687,7 @@ var SVGRenderer = class {
6625
7687
  items.forEach((item, i) => {
6626
7688
  const itemY = pos.y + titleHeight + i * itemHeight;
6627
7689
  if (itemY + itemHeight <= pos.y + pos.height) {
7690
+ const itemEventAttrs = this.getScopedEventAttrs(node, "onItemClick", { index: i });
6628
7691
  svg += `
6629
7692
  <line x1="${pos.x}" y1="${itemY + itemHeight}"
6630
7693
  x2="${pos.x + pos.width}" y2="${itemY + itemHeight}"
@@ -6633,7 +7696,10 @@ var SVGRenderer = class {
6633
7696
  <text x="${pos.x + padding}" y="${itemY + 24}"
6634
7697
  font-family="Arial, Helvetica, sans-serif"
6635
7698
  font-size="13"
6636
- 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}/>` : ""}`;
6637
7703
  }
6638
7704
  });
6639
7705
  svg += "\n </g>";
@@ -6867,8 +7933,8 @@ var SVGRenderer = class {
6867
7933
  return svg;
6868
7934
  }
6869
7935
  renderBreadcrumbs(node, pos) {
6870
- const itemsStr = String(node.props.items || "Home");
6871
- const items = itemsStr.split(",").map((s) => s.trim());
7936
+ const parsedBreadcrumbs = toStringArray(node.props.items);
7937
+ const items = parsedBreadcrumbs.length > 0 ? parsedBreadcrumbs : ["Home"];
6872
7938
  const separator = String(node.props.separator || "/");
6873
7939
  const fontSize = 12;
6874
7940
  const separatorWidth = 20;
@@ -6900,10 +7966,9 @@ var SVGRenderer = class {
6900
7966
  return svg;
6901
7967
  }
6902
7968
  renderSidebarMenu(node, pos) {
6903
- const itemsStr = String(node.props.items || "Item 1,Item 2,Item 3");
6904
- const iconsStr = String(node.props.icons || "");
6905
- const items = itemsStr.split(",").map((s) => s.trim());
6906
- 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);
6907
7972
  const itemHeight = 40;
6908
7973
  const fontSize = 14;
6909
7974
  const activeIndex = Number(node.props.active || 0);
@@ -6948,6 +8013,13 @@ var SVGRenderer = class {
6948
8013
  font-size="${fontSize}"
6949
8014
  font-weight="${fontWeight}"
6950
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
+ }
6951
8023
  });
6952
8024
  svg += "\n </g>";
6953
8025
  return svg;
@@ -7298,7 +8370,7 @@ var SVGRenderer = class {
7298
8370
  userBadge = { x, y, width, height, label: userLabel };
7299
8371
  rightCursor = x - 8;
7300
8372
  }
7301
- const actionLabels = actions.split(",").map((a) => a.trim()).filter(Boolean);
8373
+ const actionLabels = toStringArray(actions);
7302
8374
  const actionHeight = 32;
7303
8375
  const actionY = pos.y + (pos.height - actionHeight) / 2;
7304
8376
  const actionGap = 8;
@@ -7384,11 +8456,99 @@ var SVGRenderer = class {
7384
8456
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
7385
8457
  }
7386
8458
  /**
7387
- * Get data-node-id attribute string for SVG elements
7388
- * 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).
7389
8462
  */
7390
8463
  getDataNodeId(node) {
7391
- 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
+ }
7392
8552
  }
7393
8553
  };
7394
8554
  function renderToSVG(ir, layout, options) {
@@ -7478,8 +8638,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
7478
8638
  * Render breadcrumbs as skeleton blocks: <rect> / <rect> / <rect accent>
7479
8639
  */
7480
8640
  renderBreadcrumbs(node, pos) {
7481
- const itemsStr = String(node.props.items || "Home");
7482
- const items = itemsStr.split(",").map((s) => s.trim()).filter(Boolean);
8641
+ const items = toStringArray(node.props.items || "Home");
7483
8642
  const separator = String(node.props.separator || "/");
7484
8643
  const blockColor = this.renderTheme.border;
7485
8644
  const charWidth = 6.2;
@@ -7800,10 +8959,9 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
7800
8959
  */
7801
8960
  renderTable(node, pos) {
7802
8961
  const title = String(node.props.title || "");
7803
- const columnsStr = String(node.props.columns || "Col1,Col2,Col3");
7804
- const columns = columnsStr.split(",").map((c) => c.trim()).filter(Boolean);
8962
+ const columns = toStringArray(node.props.columns || "Col1,Col2,Col3");
7805
8963
  const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
7806
- const actions = String(node.props.actions || "").split(",").map((value) => value.trim()).filter(Boolean);
8964
+ const actions = toStringArray(node.props.actions);
7807
8965
  const hasActions = actions.length > 0;
7808
8966
  const pagination = this.parseBooleanProp(node.props.pagination, false);
7809
8967
  const parsedPageCount = Number(node.props.pages || 5);
@@ -8089,7 +9247,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
8089
9247
  const itemsStr = String(node.props.items || "");
8090
9248
  let items = [];
8091
9249
  if (itemsStr) {
8092
- items = itemsStr.split(",").map((i) => i.trim());
9250
+ items = toStringArray(itemsStr);
8093
9251
  } else {
8094
9252
  const itemCount = Number(node.props.itemsMock || 6);
8095
9253
  items = Array(itemCount).fill("Item");
@@ -8126,8 +9284,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
8126
9284
  * Render SidebarMenu with gray blocks instead of text and no icons
8127
9285
  */
8128
9286
  renderSidebarMenu(node, pos) {
8129
- const itemsStr = String(node.props.items || "Item 1,Item 2,Item 3");
8130
- const items = itemsStr.split(",").map((s) => s.trim());
9287
+ const items = toStringArray(node.props.items || "Item 1,Item 2,Item 3");
8131
9288
  const itemHeight = 40;
8132
9289
  const activeIndex = Number(node.props.active || 0);
8133
9290
  const accentColor = this.resolveAccentColor();
@@ -8975,7 +10132,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
8975
10132
  const title = String(node.props.title || "Sidebar");
8976
10133
  const itemsStr = String(node.props.items || "");
8977
10134
  const activeItem = String(node.props.active || "");
8978
- 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"];
8979
10137
  const itemHeight = 40;
8980
10138
  const padding = 16;
8981
10139
  const titleHeight = 40;
@@ -9018,7 +10176,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
9018
10176
  */
9019
10177
  renderTabs(node, pos) {
9020
10178
  const itemsStr = String(node.props.items || "");
9021
- 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"];
9022
10181
  const activeProp = node.props.active ?? 0;
9023
10182
  const activeIndex = Number.isFinite(Number(activeProp)) ? Math.max(0, Math.floor(Number(activeProp))) : 0;
9024
10183
  const variant = String(node.props.variant || "default");
@@ -9030,7 +10189,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
9030
10189
  const fontSize = 13;
9031
10190
  const textY = pos.y + Math.round(tabHeight / 2) + Math.round(fontSize * 0.4);
9032
10191
  const iconsStr = String(node.props.icons || "");
9033
- const iconList = iconsStr ? iconsStr.split(",").map((s) => s.trim()) : [];
10192
+ const iconList = toStringArray(iconsStr);
9034
10193
  const isFlat = this.parseBooleanProp(node.props.flat, false);
9035
10194
  const showBorder = this.parseBooleanProp(node.props.border, true);
9036
10195
  const tabWidth = pos.width / tabs.length;
@@ -9195,7 +10354,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
9195
10354
  <!-- Modal backdrop -->
9196
10355
  <rect x="0" y="0"
9197
10356
  width="${this.options.width}" height="${overlayHeight}"
9198
- fill="black" opacity="0.28"/>
10357
+ fill="black" opacity="0.28" pointer-events="none"/>
9199
10358
 
9200
10359
  <!-- Modal box -->
9201
10360
  <rect x="${modalX}" y="${modalY}"
@@ -9223,7 +10382,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
9223
10382
  <text x="${modalX + pos.width - 16}" y="${modalY + padding + 12}"
9224
10383
  font-family="${this.fontFamily}"
9225
10384
  font-size="18"
9226
- fill="${this.renderTheme.textMuted}">\u2715</text>
10385
+ fill="${this.renderTheme.textMuted}"
10386
+ pointer-events="none">\u2715</text>
9227
10387
 
9228
10388
  <!-- Content placeholder -->
9229
10389
  <text x="${modalX + pos.width / 2}" y="${modalY + headerHeight + (pos.height - headerHeight) / 2}"
@@ -9241,10 +10401,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
9241
10401
  const itemsStr = String(node.props.items || "");
9242
10402
  const mockType = String(node.props.mock || "").trim();
9243
10403
  const random = this.parseBooleanProp(node.props.random, false);
9244
- let items = [];
9245
- if (itemsStr) {
9246
- items = itemsStr.split(",").map((i) => i.trim()).filter(Boolean);
9247
- } else {
10404
+ let items = toStringArray(itemsStr);
10405
+ if (!items.length) {
9248
10406
  const parsedItemsMock = Number(node.props.itemsMock ?? 4);
9249
10407
  const itemCount = Number.isFinite(parsedItemsMock) ? Math.max(0, Math.floor(parsedItemsMock)) : 4;
9250
10408
  const resolvedMockType = mockType || "name";
@@ -9458,7 +10616,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
9458
10616
  */
9459
10617
  renderBreadcrumbs(node, pos) {
9460
10618
  const itemsStr = String(node.props.items || "Home");
9461
- const items = itemsStr.split(",").map((s) => s.trim());
10619
+ const items = toStringArray(itemsStr);
9462
10620
  const separator = String(node.props.separator || "/");
9463
10621
  const fontSize = 12;
9464
10622
  const separatorWidth = 20;
@@ -9495,8 +10653,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
9495
10653
  renderSidebarMenu(node, pos) {
9496
10654
  const itemsStr = String(node.props.items || "Item 1,Item 2,Item 3");
9497
10655
  const iconsStr = String(node.props.icons || "");
9498
- const items = itemsStr.split(",").map((s) => s.trim());
9499
- const icons = iconsStr ? iconsStr.split(",").map((s) => s.trim()) : [];
10656
+ const items = toStringArray(itemsStr);
10657
+ const icons = toStringArray(iconsStr);
9500
10658
  const itemHeight = 40;
9501
10659
  const fontSize = 14;
9502
10660
  const activeIndex = Number(node.props.active || 0);
@@ -9816,11 +10974,13 @@ export {
9816
10974
  DEVICE_PRESETS,
9817
10975
  IRGenerator,
9818
10976
  LayoutEngine,
10977
+ SELF_TARGET,
9819
10978
  SVGRenderer,
9820
10979
  SkeletonSVGRenderer,
9821
10980
  SketchSVGRenderer,
9822
10981
  SourceMapBuilder,
9823
10982
  SourceMapResolver,
10983
+ applyStateChange,
9824
10984
  buildSVG,
9825
10985
  calculateLayout,
9826
10986
  createSVGElement,