@wire-dsl/engine 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -24,11 +24,13 @@ __export(index_exports, {
24
24
  DEVICE_PRESETS: () => DEVICE_PRESETS,
25
25
  IRGenerator: () => IRGenerator,
26
26
  LayoutEngine: () => LayoutEngine,
27
+ SELF_TARGET: () => SELF_TARGET,
27
28
  SVGRenderer: () => SVGRenderer,
28
29
  SkeletonSVGRenderer: () => SkeletonSVGRenderer,
29
30
  SketchSVGRenderer: () => SketchSVGRenderer,
30
31
  SourceMapBuilder: () => SourceMapBuilder,
31
32
  SourceMapResolver: () => SourceMapResolver,
33
+ applyStateChange: () => applyStateChange,
32
34
  buildSVG: () => buildSVG,
33
35
  calculateLayout: () => calculateLayout,
34
36
  createSVGElement: () => createSVGElement,
@@ -142,6 +144,21 @@ var SourceMapBuilder = class {
142
144
  this.counters.set("cell", idx + 1);
143
145
  return `cell-${idx}`;
144
146
  }
147
+ case "tab": {
148
+ const idx = this.counters.get("tab") || 0;
149
+ this.counters.set("tab", idx + 1);
150
+ return `tab-${idx}`;
151
+ }
152
+ case "modal-body": {
153
+ const idx = this.counters.get("modal-body") || 0;
154
+ this.counters.set("modal-body", idx + 1);
155
+ return `modal-body-${idx}`;
156
+ }
157
+ case "modal-footer": {
158
+ const idx = this.counters.get("modal-footer") || 0;
159
+ this.counters.set("modal-footer", idx + 1);
160
+ return `modal-footer-${idx}`;
161
+ }
145
162
  case "component-definition":
146
163
  return `define-${metadata?.name || "unknown"}`;
147
164
  case "layout-definition":
@@ -204,6 +221,49 @@ var SourceMapBuilder = class {
204
221
  entry.properties[propertyName] = propertySourceMap;
205
222
  return propertySourceMap;
206
223
  }
224
+ /**
225
+ * Add an event handler to an existing node in the SourceMap
226
+ * Events have action expressions as values (e.g., "show(modal) & navigate(Home)")
227
+ *
228
+ * @param nodeId - ID of the node that owns this event
229
+ * @param eventName - Name of the event (e.g., "onClick", "onClose")
230
+ * @param tokens - Captured tokens: name token for event key, CST node for actionChain
231
+ * @returns The EventSourceMap entry created
232
+ */
233
+ addEvent(nodeId, eventName, tokens) {
234
+ const entry = this.entries.find((e) => e.nodeId === nodeId);
235
+ if (!entry) {
236
+ throw new Error(`Cannot add event to non-existent node: ${nodeId}`);
237
+ }
238
+ if (!entry.events) {
239
+ entry.events = {};
240
+ }
241
+ const nameRange = {
242
+ start: this.getTokenStart(tokens.name),
243
+ end: this.getTokenEnd(tokens.name)
244
+ };
245
+ const valueRange = {
246
+ start: this.getTokenStart(tokens.value),
247
+ end: this.getTokenEnd(tokens.value)
248
+ };
249
+ const fullRange = {
250
+ start: nameRange.start,
251
+ end: valueRange.end
252
+ };
253
+ let rawValue = "";
254
+ if (this.sourceCode && valueRange.start.offset !== void 0 && valueRange.end.offset !== void 0) {
255
+ rawValue = this.sourceCode.slice(valueRange.start.offset, valueRange.end.offset + 1);
256
+ }
257
+ const eventSourceMap = {
258
+ name: eventName,
259
+ value: rawValue,
260
+ range: fullRange,
261
+ nameRange,
262
+ valueRange
263
+ };
264
+ entry.events[eventName] = eventSourceMap;
265
+ return eventSourceMap;
266
+ }
207
267
  /**
208
268
  * Push a parent onto the stack (when entering a container node)
209
269
  */
@@ -239,6 +299,9 @@ var SourceMapBuilder = class {
239
299
  "screen",
240
300
  "layout",
241
301
  "cell",
302
+ "tab",
303
+ "modal-body",
304
+ "modal-footer",
242
305
  "component-definition",
243
306
  "layout-definition"
244
307
  ];
@@ -483,12 +546,26 @@ var Style = (0, import_chevrotain.createToken)({ name: "Style", pattern: /style\
483
546
  var Mocks = (0, import_chevrotain.createToken)({ name: "Mocks", pattern: /mocks\b/ });
484
547
  var Colors = (0, import_chevrotain.createToken)({ name: "Colors", pattern: /colors(?=\s*\{)/ });
485
548
  var Cell = (0, import_chevrotain.createToken)({ name: "Cell", pattern: /cell\b/ });
549
+ var Tab = (0, import_chevrotain.createToken)({ name: "Tab", pattern: /tab\b/ });
550
+ var Body = (0, import_chevrotain.createToken)({ name: "Body", pattern: /body\b/ });
551
+ var Footer = (0, import_chevrotain.createToken)({ name: "Footer", pattern: /footer\b/ });
552
+ var Navigate = (0, import_chevrotain.createToken)({ name: "Navigate", pattern: /navigate\b/ });
553
+ var Show = (0, import_chevrotain.createToken)({ name: "Show", pattern: /show\b/ });
554
+ var Hide = (0, import_chevrotain.createToken)({ name: "Hide", pattern: /hide\b/ });
555
+ var ToggleAction = (0, import_chevrotain.createToken)({ name: "ToggleAction", pattern: /toggle\b/ });
556
+ var EnableAction = (0, import_chevrotain.createToken)({ name: "EnableAction", pattern: /enable\b/ });
557
+ var DisableAction = (0, import_chevrotain.createToken)({ name: "DisableAction", pattern: /disable\b/ });
558
+ var SetTab = (0, import_chevrotain.createToken)({ name: "SetTab", pattern: /setTab\b/ });
559
+ var Self = (0, import_chevrotain.createToken)({ name: "Self", pattern: /self\b/ });
486
560
  var LCurly = (0, import_chevrotain.createToken)({ name: "LCurly", pattern: /{/ });
487
561
  var RCurly = (0, import_chevrotain.createToken)({ name: "RCurly", pattern: /}/ });
488
562
  var LParen = (0, import_chevrotain.createToken)({ name: "LParen", pattern: /\(/ });
489
563
  var RParen = (0, import_chevrotain.createToken)({ name: "RParen", pattern: /\)/ });
490
564
  var Colon = (0, import_chevrotain.createToken)({ name: "Colon", pattern: /:/ });
491
565
  var Comma = (0, import_chevrotain.createToken)({ name: "Comma", pattern: /,/ });
566
+ var Ampersand = (0, import_chevrotain.createToken)({ name: "Ampersand", pattern: /&/ });
567
+ var LBracket = (0, import_chevrotain.createToken)({ name: "LBracket", pattern: /\[/ });
568
+ var RBracket = (0, import_chevrotain.createToken)({ name: "RBracket", pattern: /\]/ });
492
569
  var StringLiteral = (0, import_chevrotain.createToken)({
493
570
  name: "StringLiteral",
494
571
  pattern: /"(?:[^"\\]|\\.)*"/
@@ -537,6 +614,18 @@ var allTokens = [
537
614
  Mocks,
538
615
  Colors,
539
616
  Cell,
617
+ Tab,
618
+ Body,
619
+ Footer,
620
+ // Event action keywords (must come before Identifier)
621
+ Navigate,
622
+ Show,
623
+ Hide,
624
+ ToggleAction,
625
+ EnableAction,
626
+ DisableAction,
627
+ SetTab,
628
+ Self,
540
629
  // Punctuation
541
630
  LCurly,
542
631
  RCurly,
@@ -544,6 +633,9 @@ var allTokens = [
544
633
  RParen,
545
634
  Colon,
546
635
  Comma,
636
+ Ampersand,
637
+ LBracket,
638
+ RBracket,
547
639
  // Literals
548
640
  StringLiteral,
549
641
  NumberLiteral,
@@ -651,6 +743,90 @@ var WireDSLParser = class extends import_chevrotain.CstParser {
651
743
  this.SUBRULE(this.layout);
652
744
  this.CONSUME(RCurly);
653
745
  });
746
+ // singleAction: navigate(Screen) | show(id|self) | hide(id|self) | toggle(id|self) | setTab(tabsId, index)
747
+ this.singleAction = this.RULE("singleAction", () => {
748
+ this.OR([
749
+ {
750
+ ALT: () => {
751
+ this.CONSUME(Navigate, { LABEL: "navigate" });
752
+ this.CONSUME(LParen);
753
+ this.CONSUME(Identifier, { LABEL: "targetScreen" });
754
+ this.CONSUME(RParen);
755
+ }
756
+ },
757
+ {
758
+ ALT: () => {
759
+ this.OR2([
760
+ { ALT: () => this.CONSUME(Show, { LABEL: "sht" }) },
761
+ { ALT: () => this.CONSUME(Hide, { LABEL: "sht" }) },
762
+ { ALT: () => this.CONSUME(ToggleAction, { LABEL: "sht" }) },
763
+ { ALT: () => this.CONSUME(EnableAction, { LABEL: "sht" }) },
764
+ { ALT: () => this.CONSUME(DisableAction, { LABEL: "sht" }) }
765
+ ]);
766
+ this.CONSUME2(LParen);
767
+ this.OR3([
768
+ { ALT: () => this.CONSUME(Self, { LABEL: "targetId" }) },
769
+ { ALT: () => this.CONSUME2(Identifier, { LABEL: "targetId" }) }
770
+ ]);
771
+ this.CONSUME2(RParen);
772
+ }
773
+ },
774
+ {
775
+ ALT: () => {
776
+ this.CONSUME(SetTab, { LABEL: "setTab" });
777
+ this.CONSUME3(LParen);
778
+ this.CONSUME3(Identifier, { LABEL: "tabsId" });
779
+ this.CONSUME(Comma);
780
+ this.CONSUME(NumberLiteral, { LABEL: "tabIndex" });
781
+ this.CONSUME3(RParen);
782
+ }
783
+ }
784
+ ]);
785
+ });
786
+ // actionChain: singleAction (& singleAction)*
787
+ this.actionChain = this.RULE("actionChain", () => {
788
+ this.SUBRULE(this.singleAction);
789
+ this.MANY(() => {
790
+ this.CONSUME(Ampersand);
791
+ this.SUBRULE2(this.singleAction);
792
+ });
793
+ });
794
+ // tab { ... } — children block inside layout tabs
795
+ this.tab = this.RULE("tab", () => {
796
+ this.CONSUME(Tab);
797
+ this.CONSUME(LCurly);
798
+ this.MANY(() => {
799
+ this.OR([
800
+ { ALT: () => this.SUBRULE(this.component) },
801
+ { ALT: () => this.SUBRULE(this.layout) }
802
+ ]);
803
+ });
804
+ this.CONSUME(RCurly);
805
+ });
806
+ // body { ... } — content section inside layout modal
807
+ this.body = this.RULE("body", () => {
808
+ this.CONSUME(Body);
809
+ this.CONSUME(LCurly);
810
+ this.MANY(() => {
811
+ this.OR([
812
+ { ALT: () => this.SUBRULE(this.component) },
813
+ { ALT: () => this.SUBRULE(this.layout) }
814
+ ]);
815
+ });
816
+ this.CONSUME(RCurly);
817
+ });
818
+ // footer { ... } — footer section inside layout modal
819
+ this.footer = this.RULE("footer", () => {
820
+ this.CONSUME(Footer);
821
+ this.CONSUME(LCurly);
822
+ this.MANY(() => {
823
+ this.OR([
824
+ { ALT: () => this.SUBRULE(this.component) },
825
+ { ALT: () => this.SUBRULE(this.layout) }
826
+ ]);
827
+ });
828
+ this.CONSUME(RCurly);
829
+ });
654
830
  // layout stack(...) { ... }
655
831
  this.layout = this.RULE("layout", () => {
656
832
  this.CONSUME(Layout);
@@ -663,7 +839,10 @@ var WireDSLParser = class extends import_chevrotain.CstParser {
663
839
  this.OR([
664
840
  { ALT: () => this.SUBRULE(this.component) },
665
841
  { ALT: () => this.SUBRULE2(this.layout) },
666
- { ALT: () => this.SUBRULE(this.cell) }
842
+ { ALT: () => this.SUBRULE(this.cell) },
843
+ { ALT: () => this.SUBRULE(this.tab) },
844
+ { ALT: () => this.SUBRULE(this.body) },
845
+ { ALT: () => this.SUBRULE(this.footer) }
667
846
  ]);
668
847
  });
669
848
  this.CONSUME(RCurly);
@@ -691,16 +870,27 @@ var WireDSLParser = class extends import_chevrotain.CstParser {
691
870
  this.SUBRULE(this.property);
692
871
  });
693
872
  });
694
- // property: key: value
873
+ // property: key: value (value can be string, number, identifier, or action chain)
695
874
  this.property = this.RULE("property", () => {
696
875
  this.CONSUME(Identifier, { LABEL: "propKey" });
697
876
  this.CONSUME(Colon);
698
877
  this.OR([
878
+ { ALT: () => this.SUBRULE(this.actionChain) },
879
+ { ALT: () => this.SUBRULE(this.arrayLiteral) },
699
880
  { ALT: () => this.CONSUME(StringLiteral, { LABEL: "propValue" }) },
700
881
  { ALT: () => this.CONSUME(NumberLiteral, { LABEL: "propValue" }) },
701
882
  { ALT: () => this.CONSUME2(Identifier, { LABEL: "propValue" }) }
702
883
  ]);
703
884
  });
885
+ // ["item1", "item2", "item3"]
886
+ this.arrayLiteral = this.RULE("arrayLiteral", () => {
887
+ this.CONSUME(LBracket);
888
+ this.MANY_SEP({
889
+ SEP: Comma,
890
+ DEF: () => this.CONSUME(StringLiteral, { LABEL: "arrayItem" })
891
+ });
892
+ this.CONSUME(RBracket);
893
+ });
704
894
  // (param1: value1, param2: value2)
705
895
  this.paramList = this.RULE("paramList", () => {
706
896
  this.CONSUME(LParen);
@@ -839,7 +1029,7 @@ var WireDSLVisitor = class extends BaseCstVisitor {
839
1029
  };
840
1030
  }
841
1031
  screen(ctx) {
842
- const params = ctx.paramList ? this.visit(ctx.paramList[0]) : {};
1032
+ const { params } = ctx.paramList ? this.visit(ctx.paramList[0]) : { params: {} };
843
1033
  return {
844
1034
  type: "screen",
845
1035
  name: ctx.screenName[0].image,
@@ -850,10 +1040,12 @@ var WireDSLVisitor = class extends BaseCstVisitor {
850
1040
  layout(ctx) {
851
1041
  const layoutType = ctx.layoutType[0].image;
852
1042
  const params = {};
1043
+ const events = [];
853
1044
  const children = [];
854
1045
  if (ctx.paramList) {
855
1046
  const paramResult = this.visit(ctx.paramList);
856
- Object.assign(params, paramResult);
1047
+ Object.assign(params, paramResult.params);
1048
+ events.push(...paramResult.events);
857
1049
  }
858
1050
  const childNodes = [];
859
1051
  if (ctx.component) {
@@ -874,20 +1066,33 @@ var WireDSLVisitor = class extends BaseCstVisitor {
874
1066
  childNodes.push({ type: "cell", node: cell, index: startToken.startOffset });
875
1067
  });
876
1068
  }
1069
+ if (ctx.tab) {
1070
+ ctx.tab.forEach((tab) => {
1071
+ const startToken = tab.children?.Tab?.[0];
1072
+ childNodes.push({ type: "tab", node: tab, index: startToken.startOffset });
1073
+ });
1074
+ }
1075
+ if (ctx.body) {
1076
+ ctx.body.forEach((body) => {
1077
+ const startToken = body.children?.Body?.[0];
1078
+ childNodes.push({ type: "body", node: body, index: startToken.startOffset });
1079
+ });
1080
+ }
1081
+ if (ctx.footer) {
1082
+ ctx.footer.forEach((footer) => {
1083
+ const startToken = footer.children?.Footer?.[0];
1084
+ childNodes.push({ type: "footer", node: footer, index: startToken.startOffset });
1085
+ });
1086
+ }
877
1087
  childNodes.sort((a, b) => a.index - b.index);
878
1088
  childNodes.forEach((item) => {
879
- if (item.type === "component") {
880
- children.push(this.visit(item.node));
881
- } else if (item.type === "layout") {
882
- children.push(this.visit(item.node));
883
- } else if (item.type === "cell") {
884
- children.push(this.visit(item.node));
885
- }
1089
+ children.push(this.visit(item.node));
886
1090
  });
887
1091
  return {
888
1092
  type: "layout",
889
1093
  layoutType,
890
1094
  params,
1095
+ events,
891
1096
  children
892
1097
  };
893
1098
  }
@@ -927,23 +1132,137 @@ var WireDSLVisitor = class extends BaseCstVisitor {
927
1132
  children
928
1133
  };
929
1134
  }
1135
+ singleAction(ctx) {
1136
+ if (ctx.navigate) {
1137
+ return { type: "navigate", screen: ctx.targetScreen[0].image };
1138
+ }
1139
+ if (ctx.sht) {
1140
+ const tokenName = ctx.sht[0].tokenType.name;
1141
+ const typeMap = {
1142
+ Show: "show",
1143
+ Hide: "hide",
1144
+ ToggleAction: "toggle",
1145
+ EnableAction: "enable",
1146
+ DisableAction: "disable"
1147
+ };
1148
+ const type = typeMap[tokenName] ?? "show";
1149
+ const targetToken = ctx.targetId[0];
1150
+ const isSelf = targetToken.tokenType.name === "Self";
1151
+ const targetId = isSelf ? "_self" : targetToken.image;
1152
+ return { type, targetId };
1153
+ }
1154
+ if (ctx.setTab) {
1155
+ return {
1156
+ type: "setTab",
1157
+ tabsId: ctx.tabsId[0].image,
1158
+ index: Number(ctx.tabIndex[0].image)
1159
+ };
1160
+ }
1161
+ throw new Error("Unknown action type in singleAction visitor");
1162
+ }
1163
+ actionChain(ctx) {
1164
+ const actions = [];
1165
+ if (ctx.singleAction) {
1166
+ ctx.singleAction.forEach((action) => {
1167
+ actions.push(this.visit(action));
1168
+ });
1169
+ }
1170
+ return actions;
1171
+ }
1172
+ tab(ctx) {
1173
+ const children = [];
1174
+ const childNodes = [];
1175
+ if (ctx.component) {
1176
+ ctx.component.forEach((comp) => {
1177
+ const startToken = comp.children?.Component?.[0] || comp.children?.componentType?.[0];
1178
+ childNodes.push({ type: "component", node: comp, index: startToken.startOffset });
1179
+ });
1180
+ }
1181
+ if (ctx.layout) {
1182
+ ctx.layout.forEach((layout) => {
1183
+ const startToken = layout.children?.Layout?.[0] || layout.children?.layoutType?.[0];
1184
+ childNodes.push({ type: "layout", node: layout, index: startToken.startOffset });
1185
+ });
1186
+ }
1187
+ childNodes.sort((a, b) => a.index - b.index);
1188
+ childNodes.forEach((item) => {
1189
+ children.push(this.visit(item.node));
1190
+ });
1191
+ return { type: "tab", children };
1192
+ }
1193
+ body(ctx) {
1194
+ const children = [];
1195
+ const childNodes = [];
1196
+ if (ctx.component) {
1197
+ ctx.component.forEach((comp) => {
1198
+ const startToken = comp.children?.Component?.[0] || comp.children?.componentType?.[0];
1199
+ childNodes.push({ type: "component", node: comp, index: startToken.startOffset });
1200
+ });
1201
+ }
1202
+ if (ctx.layout) {
1203
+ ctx.layout.forEach((layout) => {
1204
+ const startToken = layout.children?.Layout?.[0] || layout.children?.layoutType?.[0];
1205
+ childNodes.push({ type: "layout", node: layout, index: startToken.startOffset });
1206
+ });
1207
+ }
1208
+ childNodes.sort((a, b) => a.index - b.index);
1209
+ childNodes.forEach((item) => {
1210
+ children.push(this.visit(item.node));
1211
+ });
1212
+ return { type: "modal-body", children };
1213
+ }
1214
+ footer(ctx) {
1215
+ const children = [];
1216
+ const childNodes = [];
1217
+ if (ctx.component) {
1218
+ ctx.component.forEach((comp) => {
1219
+ const startToken = comp.children?.Component?.[0] || comp.children?.componentType?.[0];
1220
+ childNodes.push({ type: "component", node: comp, index: startToken.startOffset });
1221
+ });
1222
+ }
1223
+ if (ctx.layout) {
1224
+ ctx.layout.forEach((layout) => {
1225
+ const startToken = layout.children?.Layout?.[0] || layout.children?.layoutType?.[0];
1226
+ childNodes.push({ type: "layout", node: layout, index: startToken.startOffset });
1227
+ });
1228
+ }
1229
+ childNodes.sort((a, b) => a.index - b.index);
1230
+ childNodes.forEach((item) => {
1231
+ children.push(this.visit(item.node));
1232
+ });
1233
+ return { type: "modal-footer", children };
1234
+ }
930
1235
  component(ctx) {
931
1236
  const componentType = ctx.componentType[0].image;
932
1237
  const props = {};
1238
+ const events = [];
933
1239
  if (ctx.property) {
934
1240
  ctx.property.forEach((prop) => {
935
1241
  const result = this.visit(prop);
936
- props[result.key] = result.value;
1242
+ if (result.isEvent) {
1243
+ events.push({ event: result.key, actions: result.actions });
1244
+ } else {
1245
+ props[result.key] = result.value;
1246
+ }
937
1247
  });
938
1248
  }
939
1249
  return {
940
1250
  type: "component",
941
1251
  componentType,
942
- props
1252
+ props,
1253
+ events
943
1254
  };
944
1255
  }
945
1256
  property(ctx) {
946
1257
  const key = ctx.propKey[0].image;
1258
+ if (ctx.actionChain && ctx.actionChain.length > 0) {
1259
+ const actions = this.visit(ctx.actionChain[0]);
1260
+ return { key, isEvent: true, actions };
1261
+ }
1262
+ if (ctx.arrayLiteral && ctx.arrayLiteral.length > 0) {
1263
+ const items = this.visit(ctx.arrayLiteral[0]);
1264
+ return { key, value: items };
1265
+ }
947
1266
  const rawValue = ctx.propValue[0].image;
948
1267
  let value = rawValue;
949
1268
  if (typeof rawValue === "string" && rawValue.startsWith('"')) {
@@ -953,15 +1272,27 @@ var WireDSLVisitor = class extends BaseCstVisitor {
953
1272
  }
954
1273
  return { key, value };
955
1274
  }
1275
+ arrayLiteral(ctx) {
1276
+ if (!ctx.arrayItem) return [];
1277
+ return ctx.arrayItem.map((token) => {
1278
+ const raw = token.image;
1279
+ return raw.startsWith('"') ? raw.slice(1, -1) : raw;
1280
+ });
1281
+ }
956
1282
  paramList(ctx) {
957
1283
  const params = {};
1284
+ const events = [];
958
1285
  if (ctx.property) {
959
1286
  ctx.property.forEach((prop) => {
960
1287
  const result = this.visit(prop);
961
- params[result.key] = result.value;
1288
+ if (result.isEvent) {
1289
+ events.push({ event: result.key, actions: result.actions });
1290
+ } else {
1291
+ params[result.key] = result.value;
1292
+ }
962
1293
  });
963
1294
  }
964
- return params;
1295
+ return { params, events };
965
1296
  }
966
1297
  };
967
1298
  var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
@@ -1036,7 +1367,7 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
1036
1367
  return ast;
1037
1368
  }
1038
1369
  screen(ctx) {
1039
- const params = ctx.paramList ? this.visit(ctx.paramList[0]) : {};
1370
+ const { params } = ctx.paramList ? this.visit(ctx.paramList[0]) : { params: {} };
1040
1371
  const screenName = ctx.screenName[0].image;
1041
1372
  const tokens = {
1042
1373
  keyword: ctx.Screen[0],
@@ -1069,9 +1400,11 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
1069
1400
  layout(ctx) {
1070
1401
  const layoutType = ctx.layoutType[0].image;
1071
1402
  const params = {};
1403
+ const events = [];
1072
1404
  if (ctx.paramList) {
1073
1405
  const paramResult = this.visit(ctx.paramList);
1074
- Object.assign(params, paramResult);
1406
+ Object.assign(params, paramResult.params);
1407
+ events.push(...paramResult.events);
1075
1408
  }
1076
1409
  const tokens = {
1077
1410
  keyword: ctx.Layout[0],
@@ -1083,6 +1416,7 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
1083
1416
  type: "layout",
1084
1417
  layoutType,
1085
1418
  params,
1419
+ events,
1086
1420
  children: []
1087
1421
  // Will be filled after push
1088
1422
  };
@@ -1096,13 +1430,21 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
1096
1430
  if (ctx.paramList && ctx.paramList[0]?.children?.property) {
1097
1431
  ctx.paramList[0].children.property.forEach((propCtx) => {
1098
1432
  const propResult = this.visit(propCtx);
1433
+ if (propResult.isEvent) {
1434
+ this.sourceMapBuilder.addEvent(nodeId, propResult.key, {
1435
+ name: propCtx.children.propKey[0],
1436
+ value: propCtx.children.actionChain[0]
1437
+ });
1438
+ return;
1439
+ }
1440
+ const valueToken = propCtx.children.propValue?.[0] ?? propCtx.children.arrayLiteral?.[0];
1099
1441
  this.sourceMapBuilder.addProperty(
1100
1442
  nodeId,
1101
1443
  propResult.key,
1102
1444
  propResult.value,
1103
1445
  {
1104
1446
  name: propCtx.children.propKey[0],
1105
- value: propCtx.children.propValue[0]
1447
+ value: valueToken
1106
1448
  }
1107
1449
  );
1108
1450
  });
@@ -1128,6 +1470,24 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
1128
1470
  childNodes.push({ type: "cell", node: cell, index: startToken.startOffset });
1129
1471
  });
1130
1472
  }
1473
+ if (ctx.tab) {
1474
+ ctx.tab.forEach((tab) => {
1475
+ const startToken = tab.children?.Tab?.[0];
1476
+ childNodes.push({ type: "tab", node: tab, index: startToken.startOffset });
1477
+ });
1478
+ }
1479
+ if (ctx.body) {
1480
+ ctx.body.forEach((body) => {
1481
+ const startToken = body.children?.Body?.[0];
1482
+ childNodes.push({ type: "body", node: body, index: startToken.startOffset });
1483
+ });
1484
+ }
1485
+ if (ctx.footer) {
1486
+ ctx.footer.forEach((footer) => {
1487
+ const startToken = footer.children?.Footer?.[0];
1488
+ childNodes.push({ type: "footer", node: footer, index: startToken.startOffset });
1489
+ });
1490
+ }
1131
1491
  childNodes.sort((a, b) => a.index - b.index);
1132
1492
  childNodes.forEach((item) => {
1133
1493
  ast.children.push(this.visit(item.node));
@@ -1165,13 +1525,21 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
1165
1525
  if (ctx.property) {
1166
1526
  ctx.property.forEach((propCtx) => {
1167
1527
  const propResult = this.visit(propCtx);
1528
+ if (propResult.isEvent) {
1529
+ this.sourceMapBuilder.addEvent(nodeId, propResult.key, {
1530
+ name: propCtx.children.propKey[0],
1531
+ value: propCtx.children.actionChain[0]
1532
+ });
1533
+ return;
1534
+ }
1535
+ const valueToken = propCtx.children.propValue?.[0] ?? propCtx.children.arrayLiteral?.[0];
1168
1536
  this.sourceMapBuilder.addProperty(
1169
1537
  nodeId,
1170
1538
  propResult.key,
1171
1539
  propResult.value,
1172
1540
  {
1173
1541
  name: propCtx.children.propKey[0],
1174
- value: propCtx.children.propValue[0]
1542
+ value: valueToken
1175
1543
  }
1176
1544
  );
1177
1545
  });
@@ -1200,6 +1568,114 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
1200
1568
  }
1201
1569
  return ast;
1202
1570
  }
1571
+ body(ctx) {
1572
+ const tokens = {
1573
+ keyword: ctx.Body[0],
1574
+ body: ctx.RCurly[0]
1575
+ };
1576
+ const ast = {
1577
+ type: "modal-body",
1578
+ children: []
1579
+ };
1580
+ if (this.sourceMapBuilder) {
1581
+ const nodeId = this.sourceMapBuilder.addNode("modal-body", tokens);
1582
+ ast._meta = { nodeId };
1583
+ this.sourceMapBuilder.pushParent(nodeId);
1584
+ }
1585
+ const childNodes = [];
1586
+ if (ctx.component) {
1587
+ ctx.component.forEach((comp) => {
1588
+ const startToken = comp.children?.Component?.[0] || comp.children?.componentType?.[0];
1589
+ childNodes.push({ type: "component", node: comp, index: startToken.startOffset });
1590
+ });
1591
+ }
1592
+ if (ctx.layout) {
1593
+ ctx.layout.forEach((layout) => {
1594
+ const startToken = layout.children?.Layout?.[0] || layout.children?.layoutType?.[0];
1595
+ childNodes.push({ type: "layout", node: layout, index: startToken.startOffset });
1596
+ });
1597
+ }
1598
+ childNodes.sort((a, b) => a.index - b.index);
1599
+ childNodes.forEach((item) => {
1600
+ ast.children.push(this.visit(item.node));
1601
+ });
1602
+ if (this.sourceMapBuilder) {
1603
+ this.sourceMapBuilder.popParent();
1604
+ }
1605
+ return ast;
1606
+ }
1607
+ tab(ctx) {
1608
+ const tokens = {
1609
+ keyword: ctx.Tab[0],
1610
+ body: ctx.RCurly[0]
1611
+ };
1612
+ const ast = {
1613
+ type: "tab",
1614
+ children: []
1615
+ };
1616
+ if (this.sourceMapBuilder) {
1617
+ const nodeId = this.sourceMapBuilder.addNode("tab", tokens);
1618
+ ast._meta = { nodeId };
1619
+ this.sourceMapBuilder.pushParent(nodeId);
1620
+ }
1621
+ const childNodes = [];
1622
+ if (ctx.component) {
1623
+ ctx.component.forEach((comp) => {
1624
+ const startToken = comp.children?.Component?.[0] || comp.children?.componentType?.[0];
1625
+ childNodes.push({ type: "component", node: comp, index: startToken.startOffset });
1626
+ });
1627
+ }
1628
+ if (ctx.layout) {
1629
+ ctx.layout.forEach((layout) => {
1630
+ const startToken = layout.children?.Layout?.[0] || layout.children?.layoutType?.[0];
1631
+ childNodes.push({ type: "layout", node: layout, index: startToken.startOffset });
1632
+ });
1633
+ }
1634
+ childNodes.sort((a, b) => a.index - b.index);
1635
+ childNodes.forEach((item) => {
1636
+ ast.children.push(this.visit(item.node));
1637
+ });
1638
+ if (this.sourceMapBuilder) {
1639
+ this.sourceMapBuilder.popParent();
1640
+ }
1641
+ return ast;
1642
+ }
1643
+ footer(ctx) {
1644
+ const tokens = {
1645
+ keyword: ctx.Footer[0],
1646
+ body: ctx.RCurly[0]
1647
+ };
1648
+ const ast = {
1649
+ type: "modal-footer",
1650
+ children: []
1651
+ };
1652
+ if (this.sourceMapBuilder) {
1653
+ const nodeId = this.sourceMapBuilder.addNode("modal-footer", tokens);
1654
+ ast._meta = { nodeId };
1655
+ this.sourceMapBuilder.pushParent(nodeId);
1656
+ }
1657
+ const childNodes = [];
1658
+ if (ctx.component) {
1659
+ ctx.component.forEach((comp) => {
1660
+ const startToken = comp.children?.Component?.[0] || comp.children?.componentType?.[0];
1661
+ childNodes.push({ type: "component", node: comp, index: startToken.startOffset });
1662
+ });
1663
+ }
1664
+ if (ctx.layout) {
1665
+ ctx.layout.forEach((layout) => {
1666
+ const startToken = layout.children?.Layout?.[0] || layout.children?.layoutType?.[0];
1667
+ childNodes.push({ type: "layout", node: layout, index: startToken.startOffset });
1668
+ });
1669
+ }
1670
+ childNodes.sort((a, b) => a.index - b.index);
1671
+ childNodes.forEach((item) => {
1672
+ ast.children.push(this.visit(item.node));
1673
+ });
1674
+ if (this.sourceMapBuilder) {
1675
+ this.sourceMapBuilder.popParent();
1676
+ }
1677
+ return ast;
1678
+ }
1203
1679
  component(ctx) {
1204
1680
  const tokens = {
1205
1681
  keyword: ctx.Component[0],
@@ -1221,13 +1697,21 @@ var WireDSLVisitorWithSourceMap = class extends WireDSLVisitor {
1221
1697
  if (ctx.property) {
1222
1698
  ctx.property.forEach((propCtx) => {
1223
1699
  const propResult = this.visit(propCtx);
1700
+ if (propResult.isEvent) {
1701
+ this.sourceMapBuilder.addEvent(nodeId, propResult.key, {
1702
+ name: propCtx.children.propKey[0],
1703
+ value: propCtx.children.actionChain[0]
1704
+ });
1705
+ return;
1706
+ }
1707
+ const valueToken = propCtx.children.propValue?.[0] ?? propCtx.children.arrayLiteral?.[0];
1224
1708
  this.sourceMapBuilder.addProperty(
1225
1709
  nodeId,
1226
1710
  propResult.key,
1227
1711
  propResult.value,
1228
1712
  {
1229
1713
  name: propCtx.children.propKey[0],
1230
- value: propCtx.children.propValue[0]
1714
+ value: valueToken
1231
1715
  }
1232
1716
  );
1233
1717
  });
@@ -1574,11 +2058,13 @@ function createParserDiagnostic(error) {
1574
2058
  };
1575
2059
  }
1576
2060
  function isBooleanLike(value) {
2061
+ if (Array.isArray(value)) return false;
1577
2062
  if (typeof value === "number") return value === 0 || value === 1;
1578
2063
  const normalized = String(value).trim().toLowerCase();
1579
2064
  return normalized === "true" || normalized === "false";
1580
2065
  }
1581
2066
  function parseBooleanLike(value, fallback = false) {
2067
+ if (Array.isArray(value)) return fallback;
1582
2068
  if (typeof value === "number") {
1583
2069
  if (value === 1) return true;
1584
2070
  if (value === 0) return false;
@@ -1635,6 +2121,14 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1635
2121
  count += countChildrenSlots(cellChild);
1636
2122
  }
1637
2123
  }
2124
+ } else if (child.type === "tab") {
2125
+ for (const tabChild of child.children) {
2126
+ if (tabChild.type === "component") {
2127
+ if (tabChild.componentType === "Children") count += 1;
2128
+ } else if (tabChild.type === "layout") {
2129
+ count += countChildrenSlots(tabChild);
2130
+ }
2131
+ }
1638
2132
  }
1639
2133
  }
1640
2134
  return count;
@@ -1881,6 +2375,11 @@ function validateSemanticDiagnostics(ast, sourceMap) {
1881
2375
  checkLayout(child, insideDefinedLayout);
1882
2376
  } else if (child.type === "cell") {
1883
2377
  checkCell(child, insideDefinedLayout);
2378
+ } else if (child.type === "tab") {
2379
+ for (const tabChild of child.children) {
2380
+ if (tabChild.type === "component") checkComponent(tabChild, insideDefinedLayout);
2381
+ if (tabChild.type === "layout") checkLayout(tabChild, insideDefinedLayout);
2382
+ }
1884
2383
  }
1885
2384
  }
1886
2385
  };
@@ -2058,6 +2557,16 @@ function validateDefinitionCycles(ast) {
2058
2557
  collectLayoutDependencies(cellChild, deps);
2059
2558
  }
2060
2559
  }
2560
+ } else if (child.type === "tab") {
2561
+ for (const tabChild of child.children) {
2562
+ if (tabChild.type === "component") {
2563
+ if (shouldTrackComponentDependency(tabChild.componentType)) {
2564
+ deps.add(makeComponentKey(tabChild.componentType));
2565
+ }
2566
+ } else if (tabChild.type === "layout") {
2567
+ collectLayoutDependencies(tabChild, deps);
2568
+ }
2569
+ }
2061
2570
  }
2062
2571
  }
2063
2572
  };
@@ -2122,6 +2631,13 @@ Components and layouts cannot reference each other in a cycle.`
2122
2631
  var import_zod = require("zod");
2123
2632
  var import_components2 = require("@wire-dsl/language-support/components");
2124
2633
 
2634
+ // src/shared/list-utils.ts
2635
+ function toStringArray(value) {
2636
+ if (Array.isArray(value)) return value;
2637
+ if (value === void 0 || value === "") return [];
2638
+ return String(value).split(",").map((s) => s.trim()).filter(Boolean);
2639
+ }
2640
+
2125
2641
  // src/ir/device-presets.ts
2126
2642
  var DEVICE_PRESETS = {
2127
2643
  mobile: {
@@ -2207,6 +2723,7 @@ function isValidDevice(device) {
2207
2723
  }
2208
2724
 
2209
2725
  // src/ir/index.ts
2726
+ var SELF_TARGET = "_self";
2210
2727
  var IRStyleSchema = import_zod.z.object({
2211
2728
  density: import_zod.z.enum(["compact", "normal", "comfortable"]),
2212
2729
  spacing: import_zod.z.enum(["xs", "sm", "md", "lg", "xl"]),
@@ -2228,12 +2745,27 @@ var IRMetaSchema = import_zod.z.object({
2228
2745
  source: import_zod.z.string().optional(),
2229
2746
  nodeId: import_zod.z.string().optional()
2230
2747
  });
2748
+ var IREventActionSchema = import_zod.z.union([
2749
+ import_zod.z.object({ type: import_zod.z.literal("navigate"), screen: import_zod.z.string() }),
2750
+ import_zod.z.object({ type: import_zod.z.literal("show"), targetId: import_zod.z.string() }),
2751
+ import_zod.z.object({ type: import_zod.z.literal("hide"), targetId: import_zod.z.string() }),
2752
+ import_zod.z.object({ type: import_zod.z.literal("toggle"), targetId: import_zod.z.string() }),
2753
+ import_zod.z.object({ type: import_zod.z.literal("enable"), targetId: import_zod.z.string() }),
2754
+ import_zod.z.object({ type: import_zod.z.literal("disable"), targetId: import_zod.z.string() }),
2755
+ import_zod.z.object({ type: import_zod.z.literal("setTab"), tabsId: import_zod.z.string(), index: import_zod.z.number().int().min(0) }),
2756
+ import_zod.z.object({ type: import_zod.z.literal("navigateItems"), screens: import_zod.z.array(import_zod.z.string()) })
2757
+ ]);
2758
+ var IREventHandlerSchema = import_zod.z.object({
2759
+ event: import_zod.z.enum(["onClick", "onChange", "onActive", "onInactive", "onItemsClick", "onItemClick", "onRowClick", "onClose"]),
2760
+ actions: import_zod.z.array(IREventActionSchema)
2761
+ });
2231
2762
  var IRContainerNodeSchema = import_zod.z.object({
2232
2763
  id: import_zod.z.string(),
2233
2764
  kind: import_zod.z.literal("container"),
2234
- containerType: import_zod.z.enum(["stack", "grid", "split", "panel", "card"]),
2235
- params: import_zod.z.record(import_zod.z.string(), import_zod.z.union([import_zod.z.string(), import_zod.z.number()])),
2765
+ containerType: import_zod.z.enum(["stack", "grid", "split", "panel", "card", "tabs", "tab", "modal", "modal-body", "modal-footer"]),
2766
+ params: import_zod.z.record(import_zod.z.string(), import_zod.z.union([import_zod.z.string(), import_zod.z.number(), import_zod.z.array(import_zod.z.string())])),
2236
2767
  children: import_zod.z.array(import_zod.z.object({ ref: import_zod.z.string() })),
2768
+ events: import_zod.z.array(IREventHandlerSchema).optional(),
2237
2769
  style: IRNodeStyleSchema,
2238
2770
  meta: IRMetaSchema
2239
2771
  });
@@ -2241,7 +2773,9 @@ var IRComponentNodeSchema = import_zod.z.object({
2241
2773
  id: import_zod.z.string(),
2242
2774
  kind: import_zod.z.literal("component"),
2243
2775
  componentType: import_zod.z.string(),
2244
- props: import_zod.z.record(import_zod.z.string(), import_zod.z.union([import_zod.z.string(), import_zod.z.number()])),
2776
+ props: import_zod.z.record(import_zod.z.string(), import_zod.z.union([import_zod.z.string(), import_zod.z.number(), import_zod.z.array(import_zod.z.string())])),
2777
+ userDefinedId: import_zod.z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/, "ID must match [a-zA-Z_][a-zA-Z0-9_]*").optional(),
2778
+ events: import_zod.z.array(IREventHandlerSchema).optional(),
2245
2779
  style: IRNodeStyleSchema,
2246
2780
  meta: IRMetaSchema
2247
2781
  });
@@ -2250,7 +2784,7 @@ var IRInstanceNodeSchema = import_zod.z.object({
2250
2784
  kind: import_zod.z.literal("instance"),
2251
2785
  definitionName: import_zod.z.string(),
2252
2786
  definitionKind: import_zod.z.enum(["component", "layout"]),
2253
- invocationProps: import_zod.z.record(import_zod.z.string(), import_zod.z.union([import_zod.z.string(), import_zod.z.number()])),
2787
+ invocationProps: import_zod.z.record(import_zod.z.string(), import_zod.z.union([import_zod.z.string(), import_zod.z.number(), import_zod.z.array(import_zod.z.string())])),
2254
2788
  expandedRoot: import_zod.z.object({ ref: import_zod.z.string() }),
2255
2789
  style: IRNodeStyleSchema,
2256
2790
  meta: IRMetaSchema
@@ -2444,25 +2978,29 @@ ${messages}`);
2444
2978
  if (layout.children && layout.children.length > 0) {
2445
2979
  layout.children.forEach((child) => {
2446
2980
  if (child.type === "component") {
2447
- found.push({
2448
- componentType: child.componentType,
2449
- location: "layout"
2450
- });
2981
+ found.push({ componentType: child.componentType, location: "layout" });
2451
2982
  } else if (child.type === "layout") {
2452
2983
  this.findComponentsInLayout(child, found);
2453
2984
  } else if (child.type === "cell") {
2454
2985
  if (child.children) {
2455
2986
  child.children.forEach((cellChild) => {
2456
2987
  if (cellChild.type === "component") {
2457
- found.push({
2458
- componentType: cellChild.componentType,
2459
- location: "cell"
2460
- });
2988
+ found.push({ componentType: cellChild.componentType, location: "cell" });
2461
2989
  } else if (cellChild.type === "layout") {
2462
2990
  this.findComponentsInLayout(cellChild, found);
2463
2991
  }
2464
2992
  });
2465
2993
  }
2994
+ } else if (child.type === "tab") {
2995
+ if (child.children) {
2996
+ child.children.forEach((tabChild) => {
2997
+ if (tabChild.type === "component") {
2998
+ found.push({ componentType: tabChild.componentType, location: "tab" });
2999
+ } else if (tabChild.type === "layout") {
3000
+ this.findComponentsInLayout(tabChild, found);
3001
+ }
3002
+ });
3003
+ }
2466
3004
  }
2467
3005
  });
2468
3006
  }
@@ -2479,53 +3017,179 @@ ${messages}`);
2479
3017
  if (layout.layoutType === "split") {
2480
3018
  layoutParams = this.normalizeSplitParams(layoutParams);
2481
3019
  }
2482
- const layoutChildren = layout.children;
3020
+ const nonTabChildren = layout.children.filter((c) => c.type !== "tab");
2483
3021
  const layoutDefinition = this.definedLayouts.get(layout.layoutType);
2484
3022
  if (layoutDefinition) {
2485
- return this.expandDefinedLayout(layoutDefinition, layoutParams, layoutChildren, context, layout._meta?.nodeId);
3023
+ return this.expandDefinedLayout(layoutDefinition, layoutParams, nonTabChildren, context, layout._meta?.nodeId);
3024
+ }
3025
+ const nodeId = this.idGen.generate("node");
3026
+ const childRefs = [];
3027
+ if (layout.layoutType === "modal") {
3028
+ const bodyChildren = layout.children.filter((c) => c.type === "modal-body");
3029
+ const footerChildren = layout.children.filter((c) => c.type === "modal-footer");
3030
+ const normalChildren = layout.children.filter((c) => c.type !== "modal-body" && c.type !== "modal-footer");
3031
+ if (bodyChildren.length > 1 || footerChildren.length > 1) {
3032
+ if (bodyChildren.length > 1) {
3033
+ this.warnings.push({
3034
+ type: "modal-003-duplicate-body",
3035
+ message: "MODAL-003: A modal can only have one body section."
3036
+ });
3037
+ }
3038
+ if (footerChildren.length > 1) {
3039
+ this.warnings.push({
3040
+ type: "modal-004-duplicate-footer",
3041
+ message: "MODAL-004: A modal can only have one footer section."
3042
+ });
3043
+ }
3044
+ }
3045
+ if ((bodyChildren.length > 0 || footerChildren.length > 0) && normalChildren.length > 0) {
3046
+ this.warnings.push({
3047
+ type: "modal-002-mixed-children",
3048
+ message: "MODAL-002: Cannot mix body/footer sections with direct children in a modal. Use either body/footer sections or direct children, not both."
3049
+ });
3050
+ }
3051
+ }
3052
+ for (const child of layout.children) {
3053
+ if (child.type === "modal-body" || child.type === "modal-footer") {
3054
+ if (layout.layoutType !== "modal") {
3055
+ this.warnings.push({
3056
+ type: "modal-001-invalid-context",
3057
+ message: `MODAL-001: "${child.type}" sections are only valid inside layout modal.`
3058
+ });
3059
+ continue;
3060
+ }
3061
+ const childId = child.type === "modal-body" ? this.convertModalBody(child, context) : this.convertModalFooter(child, context);
3062
+ if (childId) childRefs.push({ ref: childId });
3063
+ } else if (child.type === "layout") {
3064
+ const childId = this.convertLayout(child, context);
3065
+ if (childId) childRefs.push({ ref: childId });
3066
+ } else if (child.type === "component") {
3067
+ const childId = this.convertComponent(child, context);
3068
+ if (childId) childRefs.push({ ref: childId });
3069
+ } else if (child.type === "cell") {
3070
+ const childId = this.convertCell(child, context);
3071
+ if (childId) childRefs.push({ ref: childId });
3072
+ } else if (child.type === "tab") {
3073
+ const childId = this.convertTab(child, context);
3074
+ if (childId) childRefs.push({ ref: childId });
3075
+ }
3076
+ }
3077
+ const style = {};
3078
+ if (layoutParams.padding !== void 0) {
3079
+ style.padding = String(layoutParams.padding);
3080
+ } else {
3081
+ style.padding = "none";
3082
+ }
3083
+ if (layoutParams.gap !== void 0) {
3084
+ style.gap = String(layoutParams.gap);
3085
+ }
3086
+ if (layoutParams.justify !== void 0) {
3087
+ style.justify = layoutParams.justify;
3088
+ }
3089
+ if (layoutParams.align !== void 0) {
3090
+ style.align = layoutParams.align;
3091
+ }
3092
+ if (layoutParams.background !== void 0) {
3093
+ style.background = String(layoutParams.background);
3094
+ }
3095
+ if (layoutParams.id !== void 0) {
3096
+ const layoutId = String(layoutParams.id);
3097
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(layoutId)) {
3098
+ this.errors.push({
3099
+ type: "evt-009-invalid-id",
3100
+ 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).`
3101
+ });
3102
+ }
2486
3103
  }
3104
+ const irEvents = this.convertASTEvents(layout.events || []);
3105
+ const containerNode = {
3106
+ id: nodeId,
3107
+ kind: "container",
3108
+ containerType: layout.layoutType,
3109
+ params: this.cleanParams(layoutParams),
3110
+ children: childRefs,
3111
+ ...irEvents.length > 0 ? { events: irEvents } : {},
3112
+ style,
3113
+ meta: {
3114
+ nodeId: context?.instanceScope ? `${layout._meta?.nodeId}@${context.instanceScope}` : layout._meta?.nodeId
3115
+ }
3116
+ };
3117
+ this.nodes[nodeId] = containerNode;
3118
+ return nodeId;
3119
+ }
3120
+ convertTab(tab, context) {
3121
+ const nodeId = this.idGen.generate("node");
3122
+ const childRefs = [];
3123
+ for (const child of tab.children) {
3124
+ if (child.type === "layout") {
3125
+ const childId = this.convertLayout(child, context);
3126
+ if (childId) childRefs.push({ ref: childId });
3127
+ } else if (child.type === "component") {
3128
+ const childId = this.convertComponent(child, context);
3129
+ if (childId) childRefs.push({ ref: childId });
3130
+ }
3131
+ }
3132
+ const containerNode = {
3133
+ id: nodeId,
3134
+ kind: "container",
3135
+ containerType: "tab",
3136
+ params: {},
3137
+ children: childRefs,
3138
+ style: { padding: "none" },
3139
+ meta: {
3140
+ nodeId: context?.instanceScope ? `${tab._meta?.nodeId}@${context.instanceScope}` : tab._meta?.nodeId
3141
+ }
3142
+ };
3143
+ this.nodes[nodeId] = containerNode;
3144
+ return nodeId;
3145
+ }
3146
+ convertModalBody(body, context) {
2487
3147
  const nodeId = this.idGen.generate("node");
2488
3148
  const childRefs = [];
2489
- for (const child of layoutChildren) {
3149
+ for (const child of body.children) {
2490
3150
  if (child.type === "layout") {
2491
3151
  const childId = this.convertLayout(child, context);
2492
3152
  if (childId) childRefs.push({ ref: childId });
2493
3153
  } else if (child.type === "component") {
2494
3154
  const childId = this.convertComponent(child, context);
2495
3155
  if (childId) childRefs.push({ ref: childId });
2496
- } else if (child.type === "cell") {
2497
- const childId = this.convertCell(child, context);
2498
- if (childId) childRefs.push({ ref: childId });
2499
3156
  }
2500
3157
  }
2501
- const style = {};
2502
- if (layoutParams.padding !== void 0) {
2503
- style.padding = String(layoutParams.padding);
2504
- } else {
2505
- style.padding = "none";
2506
- }
2507
- if (layoutParams.gap !== void 0) {
2508
- style.gap = String(layoutParams.gap);
2509
- }
2510
- if (layoutParams.justify !== void 0) {
2511
- style.justify = layoutParams.justify;
2512
- }
2513
- if (layoutParams.align !== void 0) {
2514
- style.align = layoutParams.align;
2515
- }
2516
- if (layoutParams.background !== void 0) {
2517
- style.background = String(layoutParams.background);
3158
+ const containerNode = {
3159
+ id: nodeId,
3160
+ kind: "container",
3161
+ containerType: "modal-body",
3162
+ params: { direction: "vertical" },
3163
+ children: childRefs,
3164
+ style: { padding: "md", gap: "md" },
3165
+ meta: {
3166
+ nodeId: context?.instanceScope ? `${body._meta?.nodeId}@${context.instanceScope}` : body._meta?.nodeId
3167
+ }
3168
+ };
3169
+ this.nodes[nodeId] = containerNode;
3170
+ return nodeId;
3171
+ }
3172
+ convertModalFooter(footer, context) {
3173
+ const nodeId = this.idGen.generate("node");
3174
+ const childRefs = [];
3175
+ for (const child of footer.children) {
3176
+ if (child.type === "layout") {
3177
+ const childId = this.convertLayout(child, context);
3178
+ if (childId) childRefs.push({ ref: childId });
3179
+ } else if (child.type === "component") {
3180
+ const childId = this.convertComponent(child, context);
3181
+ if (childId) childRefs.push({ ref: childId });
3182
+ }
2518
3183
  }
2519
3184
  const containerNode = {
2520
3185
  id: nodeId,
2521
3186
  kind: "container",
2522
- containerType: layout.layoutType,
2523
- params: this.cleanParams(layoutParams),
3187
+ containerType: "modal-footer",
3188
+ params: { direction: "horizontal" },
2524
3189
  children: childRefs,
2525
- style,
3190
+ style: { padding: "md", justify: "spaceBetween" },
2526
3191
  meta: {
2527
- // Scope nodeId per instance so each expansion gets a unique identifier
2528
- nodeId: context?.instanceScope ? `${layout._meta?.nodeId}@${context.instanceScope}` : layout._meta?.nodeId
3192
+ nodeId: context?.instanceScope ? `${footer._meta?.nodeId}@${context.instanceScope}` : footer._meta?.nodeId
2529
3193
  }
2530
3194
  };
2531
3195
  this.nodes[nodeId] = containerNode;
@@ -2608,7 +3272,6 @@ ${messages}`);
2608
3272
  "IconButton",
2609
3273
  "Alert",
2610
3274
  "Badge",
2611
- "Modal",
2612
3275
  "List",
2613
3276
  "Sidebar",
2614
3277
  "Tabs",
@@ -2620,20 +3283,70 @@ ${messages}`);
2620
3283
  this.undefinedComponentsUsed.add(component.componentType);
2621
3284
  }
2622
3285
  const nodeId = this.idGen.generate("node");
3286
+ const rawId = resolvedProps.id !== void 0 ? String(resolvedProps.id) : void 0;
3287
+ if (rawId !== void 0 && !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(rawId)) {
3288
+ this.errors.push({
3289
+ type: "evt-009-invalid-id",
3290
+ 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).`
3291
+ });
3292
+ }
3293
+ const userDefinedId = rawId;
3294
+ const propsWithoutId = { ...resolvedProps };
3295
+ delete propsWithoutId.id;
3296
+ const irEvents = this.convertASTEvents(component.events || []);
3297
+ const onItemsClickEvent = this.extractOnItemsClickEvent(propsWithoutId);
3298
+ if (onItemsClickEvent) {
3299
+ irEvents.push(onItemsClickEvent);
3300
+ delete propsWithoutId.onItemsClick;
3301
+ }
2623
3302
  const componentNode = {
2624
3303
  id: nodeId,
2625
3304
  kind: "component",
2626
3305
  componentType: component.componentType,
2627
- props: resolvedProps,
3306
+ props: propsWithoutId,
3307
+ ...userDefinedId ? { userDefinedId } : {},
3308
+ ...irEvents.length > 0 ? { events: irEvents } : {},
2628
3309
  style: {},
2629
3310
  meta: {
2630
- // Scope nodeId per instance so each expansion gets a unique identifier
2631
3311
  nodeId: context?.instanceScope ? `${component._meta?.nodeId}@${context.instanceScope}` : component._meta?.nodeId
2632
3312
  }
2633
3313
  };
2634
3314
  this.nodes[nodeId] = componentNode;
2635
3315
  return nodeId;
2636
3316
  }
3317
+ convertASTEventAction(action) {
3318
+ switch (action.type) {
3319
+ case "navigate":
3320
+ return { type: "navigate", screen: action.screen };
3321
+ case "show":
3322
+ return { type: "show", targetId: action.targetId };
3323
+ case "hide":
3324
+ return { type: "hide", targetId: action.targetId };
3325
+ case "toggle":
3326
+ return { type: "toggle", targetId: action.targetId };
3327
+ case "enable":
3328
+ return { type: "enable", targetId: action.targetId };
3329
+ case "disable":
3330
+ return { type: "disable", targetId: action.targetId };
3331
+ case "setTab":
3332
+ return { type: "setTab", tabsId: action.tabsId, index: action.index };
3333
+ }
3334
+ }
3335
+ convertASTEvents(events) {
3336
+ return events.map((handler) => ({
3337
+ event: handler.event,
3338
+ actions: handler.actions.map((a) => this.convertASTEventAction(a))
3339
+ }));
3340
+ }
3341
+ extractOnItemsClickEvent(props) {
3342
+ const value = props.onItemsClick;
3343
+ if (!value) return null;
3344
+ const screens = toStringArray(value);
3345
+ return {
3346
+ event: "onItemsClick",
3347
+ actions: [{ type: "navigateItems", screens }]
3348
+ };
3349
+ }
2637
3350
  expandDefinedComponent(definition, invocationArgs, callSiteNodeId, parentContext) {
2638
3351
  const context = {
2639
3352
  args: invocationArgs,
@@ -2813,21 +3526,23 @@ ${messages}`);
2813
3526
  key
2814
3527
  );
2815
3528
  if (resolvedValue !== void 0) {
3529
+ const metadata = import_components2.COMPONENTS[componentType];
3530
+ const propMeta = metadata?.properties?.[key];
2816
3531
  const wasPropReference = typeof value === "string" && value.startsWith("prop_");
2817
- if (wasPropReference) {
2818
- const metadata = import_components2.COMPONENTS[componentType];
2819
- const property = metadata?.properties?.[key];
2820
- if (property?.type === "enum" && Array.isArray(property.options)) {
2821
- const normalizedValue = String(resolvedValue);
2822
- if (!property.options.includes(normalizedValue)) {
2823
- this.warnings.push({
2824
- type: "invalid-bound-enum-value",
2825
- message: `Invalid value "${normalizedValue}" for property "${key}" in component "${componentType}". Expected one of: ${property.options.join(", ")}.`
2826
- });
2827
- }
3532
+ if (wasPropReference && propMeta?.type === "enum" && Array.isArray(propMeta.options)) {
3533
+ const normalizedValue = String(resolvedValue);
3534
+ if (!propMeta.options.includes(normalizedValue)) {
3535
+ this.warnings.push({
3536
+ type: "invalid-bound-enum-value",
3537
+ message: `Invalid value "${normalizedValue}" for property "${key}" in component "${componentType}". Expected one of: ${propMeta.options.join(", ")}.`
3538
+ });
2828
3539
  }
2829
3540
  }
2830
- resolved[key] = resolvedValue;
3541
+ if (propMeta?.type === "list" && !Array.isArray(resolvedValue)) {
3542
+ resolved[key] = toStringArray(resolvedValue);
3543
+ } else {
3544
+ resolved[key] = resolvedValue;
3545
+ }
2831
3546
  }
2832
3547
  }
2833
3548
  return resolved;
@@ -2899,6 +3614,195 @@ function generateIR(ast) {
2899
3614
  return generator.generate(ast);
2900
3615
  }
2901
3616
 
3617
+ // src/state.ts
3618
+ function applyStateChange(ir, change, originNodeId) {
3619
+ switch (change.type) {
3620
+ case "setVisible":
3621
+ return applySetVisible(ir, change.targetId, change.visible, originNodeId);
3622
+ case "toggleVisible":
3623
+ return applyToggleVisible(ir, change.targetId, originNodeId);
3624
+ case "setActiveTab":
3625
+ return applySetActiveTab(ir, change.tabsId, change.index);
3626
+ case "setChecked":
3627
+ return applySetBooleanProp(ir, change.targetId, "checked", change.checked, originNodeId);
3628
+ case "toggleChecked":
3629
+ return applyToggleBooleanProp(ir, change.targetId, "checked", originNodeId);
3630
+ case "setEnabled":
3631
+ return applySetBooleanProp(ir, change.targetId, "enabled", change.enabled, originNodeId);
3632
+ case "toggleEnabled":
3633
+ return applyToggleBooleanProp(ir, change.targetId, "enabled", originNodeId);
3634
+ case "setDisabled":
3635
+ return applySetBooleanProp(ir, change.targetId, "disabled", change.disabled, originNodeId);
3636
+ case "navigateTo":
3637
+ return applyNavigateTo(ir, change.screen);
3638
+ }
3639
+ }
3640
+ function applySetVisible(ir, targetId, visible, originNodeId) {
3641
+ const resolvedId = resolveTargetId(ir, targetId, originNodeId);
3642
+ if (!resolvedId) return ir;
3643
+ const nodes = mutateNodeVisible(ir.project.nodes, resolvedId, visible);
3644
+ return { ...ir, project: { ...ir.project, nodes } };
3645
+ }
3646
+ function applyToggleVisible(ir, targetId, originNodeId) {
3647
+ const resolvedId = resolveTargetId(ir, targetId, originNodeId);
3648
+ if (!resolvedId) return ir;
3649
+ const targetNodeEntry = findNodeByUserDefinedId(ir.project.nodes, resolvedId) || (resolvedId === originNodeId ? findNodeByMetaNodeId(ir.project.nodes, resolvedId) : null);
3650
+ const targetContainerEntry = !targetNodeEntry ? Object.values(ir.project.nodes).find(
3651
+ (n) => n.kind === "container" && (String(n.params.id) === resolvedId || n.meta.nodeId === resolvedId)
3652
+ ) : void 0;
3653
+ let currentVisible = true;
3654
+ if (targetNodeEntry?.kind === "component") {
3655
+ currentVisible = targetNodeEntry.props?.visible !== "false";
3656
+ } else if (targetContainerEntry?.kind === "container") {
3657
+ currentVisible = String(targetContainerEntry.params.visible) !== "false";
3658
+ }
3659
+ const nodes = mutateNodeVisible(ir.project.nodes, resolvedId, !currentVisible);
3660
+ return { ...ir, project: { ...ir.project, nodes } };
3661
+ }
3662
+ function applySetActiveTab(ir, tabsId, index) {
3663
+ let found = false;
3664
+ const nodes = {};
3665
+ for (const [nodeKey, node] of Object.entries(ir.project.nodes)) {
3666
+ if (node.kind === "container" && node.containerType === "tabs" && node.params.id === tabsId) {
3667
+ nodes[nodeKey] = {
3668
+ ...node,
3669
+ params: { ...node.params, active: index }
3670
+ };
3671
+ found = true;
3672
+ } else if (node.kind === "component" && node.componentType === "Tabs" && node.props.tabsId === tabsId) {
3673
+ nodes[nodeKey] = {
3674
+ ...node,
3675
+ props: { ...node.props, active: index }
3676
+ };
3677
+ found = true;
3678
+ } else {
3679
+ nodes[nodeKey] = node;
3680
+ }
3681
+ }
3682
+ if (!found) {
3683
+ console.warn(`[applyStateChange] setActiveTab: no layout tabs found with id "${tabsId}"`);
3684
+ return ir;
3685
+ }
3686
+ return { ...ir, project: { ...ir.project, nodes } };
3687
+ }
3688
+ function applySetBooleanProp(ir, targetId, propName, value, originNodeId) {
3689
+ const resolvedId = resolveTargetId(ir, targetId, originNodeId);
3690
+ if (!resolvedId) return ir;
3691
+ const target = findTargetComponentNode(ir.project.nodes, resolvedId);
3692
+ if (!target) {
3693
+ console.warn(`[applyStateChange] ${propName}: no node found with id "${resolvedId}"`);
3694
+ return ir;
3695
+ }
3696
+ const nodes = mutateNodeBooleanProp(ir.project.nodes, resolvedId, propName, value);
3697
+ return { ...ir, project: { ...ir.project, nodes } };
3698
+ }
3699
+ function applyToggleBooleanProp(ir, targetId, propName, originNodeId) {
3700
+ const resolvedId = resolveTargetId(ir, targetId, originNodeId);
3701
+ if (!resolvedId) return ir;
3702
+ const targetNodeEntry = findTargetComponentNode(ir.project.nodes, resolvedId);
3703
+ const currentValue = targetNodeEntry ? String(targetNodeEntry.props[propName] || "false").toLowerCase() === "true" : false;
3704
+ const nodes = mutateNodeBooleanProp(ir.project.nodes, resolvedId, propName, !currentValue);
3705
+ return { ...ir, project: { ...ir.project, nodes } };
3706
+ }
3707
+ function applyNavigateTo(ir, screenName) {
3708
+ const updatedProject = {
3709
+ ...ir.project,
3710
+ activeScreen: screenName
3711
+ };
3712
+ return { ...ir, project: updatedProject };
3713
+ }
3714
+ function resolveTargetId(ir, targetId, originNodeId) {
3715
+ if (targetId === SELF_TARGET) {
3716
+ if (!originNodeId) {
3717
+ console.warn("[applyStateChange] targetId is _self but no originNodeId was provided");
3718
+ return null;
3719
+ }
3720
+ return originNodeId;
3721
+ }
3722
+ return targetId;
3723
+ }
3724
+ function findNodeByUserDefinedId(nodes, userDefinedId) {
3725
+ for (const node of Object.values(nodes)) {
3726
+ if (node.kind === "component" && node.userDefinedId === userDefinedId) {
3727
+ return node;
3728
+ }
3729
+ }
3730
+ return null;
3731
+ }
3732
+ function findNodeByMetaNodeId(nodes, metaNodeId) {
3733
+ for (const node of Object.values(nodes)) {
3734
+ if (node.kind === "component" && node.meta.nodeId === metaNodeId) {
3735
+ return node;
3736
+ }
3737
+ }
3738
+ return null;
3739
+ }
3740
+ function findTargetComponentNode(nodes, targetId) {
3741
+ return findNodeByUserDefinedId(nodes, targetId) || findNodeByMetaNodeId(nodes, targetId);
3742
+ }
3743
+ function mutateNodeVisible(nodes, targetId, visible) {
3744
+ let found = false;
3745
+ const result = {};
3746
+ for (const [key, node] of Object.entries(nodes)) {
3747
+ if (node.kind === "component") {
3748
+ const matchByUserDefined = node.userDefinedId === targetId;
3749
+ const matchByMetaNodeId = node.meta.nodeId === targetId;
3750
+ if (matchByUserDefined || matchByMetaNodeId) {
3751
+ result[key] = {
3752
+ ...node,
3753
+ props: { ...node.props, visible: visible ? "true" : "false" }
3754
+ };
3755
+ found = true;
3756
+ } else {
3757
+ result[key] = node;
3758
+ }
3759
+ } else if (node.kind === "container") {
3760
+ const matchByParamsId = node.params.id !== void 0 && String(node.params.id) === targetId;
3761
+ const matchByMetaNodeId = node.meta.nodeId === targetId;
3762
+ if (matchByParamsId || matchByMetaNodeId) {
3763
+ result[key] = {
3764
+ ...node,
3765
+ params: { ...node.params, visible: visible ? "true" : "false" }
3766
+ };
3767
+ found = true;
3768
+ } else {
3769
+ result[key] = node;
3770
+ }
3771
+ } else {
3772
+ result[key] = node;
3773
+ }
3774
+ }
3775
+ if (!found) {
3776
+ console.warn(`[applyStateChange] setVisible: no node found with id "${targetId}"`);
3777
+ }
3778
+ return result;
3779
+ }
3780
+ function mutateNodeBooleanProp(nodes, targetId, propName, value) {
3781
+ let found = false;
3782
+ const result = {};
3783
+ for (const [key, node] of Object.entries(nodes)) {
3784
+ if (node.kind === "component") {
3785
+ const matchByUserDefined = node.userDefinedId === targetId;
3786
+ const matchByMetaNodeId = node.meta.nodeId === targetId;
3787
+ if (matchByUserDefined || matchByMetaNodeId) {
3788
+ result[key] = {
3789
+ ...node,
3790
+ props: { ...node.props, [propName]: value ? "true" : "false" }
3791
+ };
3792
+ found = true;
3793
+ } else {
3794
+ result[key] = node;
3795
+ }
3796
+ } else {
3797
+ result[key] = node;
3798
+ }
3799
+ }
3800
+ if (!found) {
3801
+ console.warn(`[applyStateChange] ${propName}: no node found with id "${targetId}"`);
3802
+ }
3803
+ return result;
3804
+ }
3805
+
2902
3806
  // src/shared/spacing.ts
2903
3807
  var SPACING_VALUES = {
2904
3808
  none: 0,
@@ -3078,6 +3982,10 @@ var LayoutEngine = class {
3078
3982
  }
3079
3983
  calculateContainer(node, nodeId, x, y, width, height) {
3080
3984
  if (node.kind !== "container") return;
3985
+ if (node.containerType === "modal") {
3986
+ this.calculateModal(node, nodeId, x, y, width, height);
3987
+ return;
3988
+ }
3081
3989
  const usesOuterPadding = node.containerType !== "card";
3082
3990
  const padding = usesOuterPadding ? this.resolveSpacing(node.style.padding) : 0;
3083
3991
  const innerX = x + padding;
@@ -3089,6 +3997,11 @@ var LayoutEngine = class {
3089
3997
  this.result[nodeId] = { x, y, width, height };
3090
3998
  switch (node.containerType) {
3091
3999
  case "stack":
4000
+ case "tab":
4001
+ case "modal-body":
4002
+ this.calculateStack(node, innerX, innerY, innerWidth, innerHeight);
4003
+ break;
4004
+ case "modal-footer":
3092
4005
  this.calculateStack(node, innerX, innerY, innerWidth, innerHeight);
3093
4006
  break;
3094
4007
  case "grid":
@@ -3103,8 +4016,12 @@ var LayoutEngine = class {
3103
4016
  case "card":
3104
4017
  this.calculateCard(node, innerX, innerY, innerWidth, innerHeight);
3105
4018
  break;
4019
+ case "tabs":
4020
+ this.calculateTabs(node, innerX, innerY, innerWidth);
4021
+ break;
3106
4022
  }
3107
- if ((isVerticalStack || node.containerType === "card") && node.children.length > 0) {
4023
+ const isHorizontalStack = node.containerType === "stack" && !isVerticalStack;
4024
+ if ((isVerticalStack || isHorizontalStack || node.containerType === "card" || node.containerType === "tabs" || node.containerType === "tab" || node.containerType === "modal-body" || node.containerType === "modal-footer") && node.children.length > 0) {
3108
4025
  let containerMaxY = y;
3109
4026
  node.children.forEach((childRef) => {
3110
4027
  const childPos = this.result[childRef.ref];
@@ -3124,8 +4041,22 @@ var LayoutEngine = class {
3124
4041
  const children = node.children;
3125
4042
  if (direction === "vertical") {
3126
4043
  let currentY = y;
3127
- children.forEach((childRef, index) => {
4044
+ const flowChildren = children.filter((cr) => {
4045
+ const n = this.nodes[cr.ref];
4046
+ if (n?.kind === "container" && n.containerType === "modal") return false;
4047
+ return this.isNodeVisible(cr.ref);
4048
+ });
4049
+ let flowCount = 0;
4050
+ children.forEach((childRef) => {
3128
4051
  const childNode = this.nodes[childRef.ref];
4052
+ if (childNode?.kind === "container" && childNode.containerType === "modal") {
4053
+ this.calculateNode(childRef.ref, x, currentY, width, 0, "stack");
4054
+ return;
4055
+ }
4056
+ if (!this.isNodeVisible(childRef.ref)) {
4057
+ this.calculateNode(childRef.ref, x, currentY, width, 0, "stack");
4058
+ return;
4059
+ }
3129
4060
  let childHeight = this.getComponentHeight();
3130
4061
  if (childNode?.kind === "component" && childNode.props.height) {
3131
4062
  childHeight = Number(childNode.props.height);
@@ -3136,12 +4067,17 @@ var LayoutEngine = class {
3136
4067
  }
3137
4068
  this.calculateNode(childRef.ref, x, currentY, width, childHeight, "stack");
3138
4069
  currentY += childHeight;
3139
- if (index < children.length - 1) {
4070
+ flowCount++;
4071
+ if (flowCount < flowChildren.length) {
3140
4072
  currentY += gap;
3141
4073
  }
3142
4074
  });
3143
4075
  let adjustedY = y;
3144
- children.forEach((childRef, index) => {
4076
+ let adjustedCount = 0;
4077
+ children.forEach((childRef) => {
4078
+ const childNode = this.nodes[childRef.ref];
4079
+ if (childNode?.kind === "container" && childNode.containerType === "modal") return;
4080
+ if (!this.isNodeVisible(childRef.ref)) return;
3145
4081
  const childPos = this.result[childRef.ref];
3146
4082
  if (childPos) {
3147
4083
  const deltaY = adjustedY - childPos.y;
@@ -3150,7 +4086,8 @@ var LayoutEngine = class {
3150
4086
  this.adjustNodeYPositions(childRef.ref, deltaY);
3151
4087
  }
3152
4088
  adjustedY += childPos.height;
3153
- if (index < children.length - 1) {
4089
+ adjustedCount++;
4090
+ if (adjustedCount < flowChildren.length) {
3154
4091
  adjustedY += gap;
3155
4092
  }
3156
4093
  }
@@ -3160,9 +4097,10 @@ var LayoutEngine = class {
3160
4097
  const crossAlign = node.style.align || "start";
3161
4098
  if (justify === "stretch") {
3162
4099
  let currentX = x;
3163
- const childWidth = this.calculateChildWidth(children.length, width, gap);
4100
+ const visibleChildren = children.filter((cr) => this.isNodeVisible(cr.ref));
4101
+ const childWidth = this.calculateChildWidth(visibleChildren.length, width, gap);
3164
4102
  let stackHeight = 0;
3165
- children.forEach((childRef) => {
4103
+ visibleChildren.forEach((childRef) => {
3166
4104
  const childNode = this.nodes[childRef.ref];
3167
4105
  let childHeight = this.getComponentHeight();
3168
4106
  if (childNode?.kind === "component" && childNode.props.height) {
@@ -3175,6 +4113,10 @@ var LayoutEngine = class {
3175
4113
  stackHeight = Math.max(stackHeight, childHeight);
3176
4114
  });
3177
4115
  children.forEach((childRef) => {
4116
+ if (!this.isNodeVisible(childRef.ref)) {
4117
+ this.calculateNode(childRef.ref, currentX, y, 0, 0, "stack");
4118
+ return;
4119
+ }
3178
4120
  this.calculateNode(childRef.ref, currentX, y, childWidth, stackHeight, "stack");
3179
4121
  currentX += childWidth + gap;
3180
4122
  });
@@ -3183,9 +4125,18 @@ var LayoutEngine = class {
3183
4125
  const childHeights = [];
3184
4126
  const explicitHeightFlags = [];
3185
4127
  const flexIndices = /* @__PURE__ */ new Set();
4128
+ const visibleFlags = [];
3186
4129
  let stackHeight = 0;
3187
4130
  children.forEach((childRef, index) => {
3188
4131
  const childNode = this.nodes[childRef.ref];
4132
+ const visible = this.isNodeVisible(childRef.ref);
4133
+ visibleFlags.push(visible);
4134
+ if (!visible) {
4135
+ childWidths.push(0);
4136
+ childHeights.push(0);
4137
+ explicitHeightFlags.push(false);
4138
+ return;
4139
+ }
3189
4140
  const hasExplicitHeight = childNode?.kind === "component" && !!childNode.props.height;
3190
4141
  const hasExplicitWidth = childNode?.kind === "component" && !!childNode.props.width;
3191
4142
  const isBlockButton = childNode?.kind === "component" && childNode.componentType === "Button" && !hasExplicitWidth && this.parseBooleanProp(childNode.props.block, false);
@@ -3203,7 +4154,8 @@ var LayoutEngine = class {
3203
4154
  childHeights.push(this.getComponentHeight());
3204
4155
  explicitHeightFlags.push(hasExplicitHeight);
3205
4156
  });
3206
- const totalGapWidth = gap * Math.max(0, children.length - 1);
4157
+ const visibleCount = visibleFlags.filter(Boolean).length;
4158
+ const totalGapWidth = gap * Math.max(0, visibleCount - 1);
3207
4159
  if (flexIndices.size > 0) {
3208
4160
  const fixedWidth = childWidths.reduce((sum, w, idx) => {
3209
4161
  return flexIndices.has(idx) ? sum : sum + w;
@@ -3215,6 +4167,7 @@ var LayoutEngine = class {
3215
4167
  });
3216
4168
  }
3217
4169
  children.forEach((childRef, index) => {
4170
+ if (!visibleFlags[index]) return;
3218
4171
  const childNode = this.nodes[childRef.ref];
3219
4172
  const childWidth = childWidths[index];
3220
4173
  let childHeight = this.getComponentHeight();
@@ -3238,14 +4191,18 @@ var LayoutEngine = class {
3238
4191
  startX = x + width - totalContentWidth;
3239
4192
  } else if (justify === "spaceBetween") {
3240
4193
  startX = x;
3241
- dynamicGap = children.length > 1 ? (width - totalChildWidth) / (children.length - 1) : 0;
4194
+ dynamicGap = visibleCount > 1 ? (width - totalChildWidth) / (visibleCount - 1) : 0;
3242
4195
  } else if (justify === "spaceAround") {
3243
- const spacing = children.length > 0 ? (width - totalChildWidth) / children.length : 0;
4196
+ const spacing = visibleCount > 0 ? (width - totalChildWidth) / visibleCount : 0;
3244
4197
  startX = x + spacing / 2;
3245
4198
  dynamicGap = spacing;
3246
4199
  }
3247
4200
  let currentX = startX;
3248
4201
  children.forEach((childRef, index) => {
4202
+ if (!visibleFlags[index]) {
4203
+ this.calculateNode(childRef.ref, currentX, y, 0, 0, "stack");
4204
+ return;
4205
+ }
3249
4206
  const childWidth = childWidths[index];
3250
4207
  const childHeight = childHeights[index];
3251
4208
  let childY = y;
@@ -3277,6 +4234,7 @@ var LayoutEngine = class {
3277
4234
  let currentRowMaxHeight = 0;
3278
4235
  const rowHeights = [0];
3279
4236
  node.children.forEach((childRef) => {
4237
+ if (!this.isNodeVisible(childRef.ref)) return;
3280
4238
  const child = this.nodes[childRef.ref];
3281
4239
  let span = 1;
3282
4240
  let childHeight = this.getComponentHeight();
@@ -3311,6 +4269,18 @@ var LayoutEngine = class {
3311
4269
  }
3312
4270
  return totalHeight;
3313
4271
  }
4272
+ if (node.containerType === "tabs") {
4273
+ const activeIndex = Number(node.params.active) || 0;
4274
+ const activeChildRef = node.children[activeIndex] ?? node.children[0];
4275
+ if (!activeChildRef) return totalHeight;
4276
+ const activeChild = this.nodes[activeChildRef.ref];
4277
+ if (activeChild?.kind === "container") {
4278
+ totalHeight += this.calculateContainerHeight(activeChild, availableWidth);
4279
+ } else if (activeChild?.kind === "component") {
4280
+ totalHeight += this.getIntrinsicComponentHeight(activeChild, availableWidth);
4281
+ }
4282
+ return totalHeight;
4283
+ }
3314
4284
  if (node.containerType === "split") {
3315
4285
  const splitGap = this.resolveSpacing(node.style.gap);
3316
4286
  const leftParam = node.params.left;
@@ -3323,6 +4293,7 @@ var LayoutEngine = class {
3323
4293
  const rightWidth = Number.isFinite(rightWidthRaw) && rightWidthRaw > 0 ? rightWidthRaw : availableWidth / 2;
3324
4294
  let maxHeight = 0;
3325
4295
  node.children.forEach((childRef, index) => {
4296
+ if (!this.isNodeVisible(childRef.ref)) return;
3326
4297
  const child = this.nodes[childRef.ref];
3327
4298
  let childHeight = this.getComponentHeight();
3328
4299
  const isFirst = index === 0;
@@ -3350,6 +4321,7 @@ var LayoutEngine = class {
3350
4321
  if (node.containerType === "stack" && direction === "horizontal") {
3351
4322
  let maxHeight = 0;
3352
4323
  node.children.forEach((childRef) => {
4324
+ if (!this.isNodeVisible(childRef.ref)) return;
3353
4325
  const child = this.nodes[childRef.ref];
3354
4326
  let childHeight = this.getComponentHeight();
3355
4327
  if (child?.kind === "component") {
@@ -3366,7 +4338,10 @@ var LayoutEngine = class {
3366
4338
  totalHeight += maxHeight;
3367
4339
  return totalHeight;
3368
4340
  }
3369
- node.children.forEach((childRef, index) => {
4341
+ const visibleLinear = node.children.filter((cr) => this.isNodeVisible(cr.ref));
4342
+ let linearIndex = 0;
4343
+ node.children.forEach((childRef) => {
4344
+ if (!this.isNodeVisible(childRef.ref)) return;
3370
4345
  const child = this.nodes[childRef.ref];
3371
4346
  let childHeight = this.getComponentHeight();
3372
4347
  if (child?.kind === "component") {
@@ -3379,7 +4354,8 @@ var LayoutEngine = class {
3379
4354
  childHeight = this.calculateContainerHeight(child, availableWidth);
3380
4355
  }
3381
4356
  totalHeight += childHeight;
3382
- if (index < node.children.length - 1) {
4357
+ linearIndex++;
4358
+ if (linearIndex < visibleLinear.length) {
3383
4359
  totalHeight += gap;
3384
4360
  }
3385
4361
  });
@@ -3392,6 +4368,10 @@ var LayoutEngine = class {
3392
4368
  const colWidth = (width - gap * (columns - 1)) / columns;
3393
4369
  const cellHeights = {};
3394
4370
  node.children.forEach((childRef, cellIndex) => {
4371
+ if (!this.isNodeVisible(childRef.ref)) {
4372
+ cellHeights[cellIndex] = 0;
4373
+ return;
4374
+ }
3395
4375
  const child = this.nodes[childRef.ref];
3396
4376
  let cellHeight = this.getComponentHeight();
3397
4377
  let span = 1;
@@ -3416,6 +4396,11 @@ var LayoutEngine = class {
3416
4396
  const rowHeights = [0];
3417
4397
  const cellPositions = [];
3418
4398
  node.children.forEach((childRef, cellIndex) => {
4399
+ const visible = this.isNodeVisible(childRef.ref);
4400
+ if (!visible) {
4401
+ cellPositions.push({ row: currentRow, col: currentCol, span: 0, visible: false });
4402
+ return;
4403
+ }
3419
4404
  const child = this.nodes[childRef.ref];
3420
4405
  let span = 1;
3421
4406
  if (child?.kind === "container" && child.meta?.source === "cell") {
@@ -3427,13 +4412,17 @@ var LayoutEngine = class {
3427
4412
  currentCol = 0;
3428
4413
  currentRowMaxHeight = 0;
3429
4414
  }
3430
- cellPositions.push({ row: currentRow, col: currentCol, span });
4415
+ cellPositions.push({ row: currentRow, col: currentCol, span, visible: true });
3431
4416
  currentRowMaxHeight = Math.max(currentRowMaxHeight, cellHeights[cellIndex]);
3432
4417
  currentCol += span;
3433
4418
  });
3434
4419
  rowHeights[currentRow] = currentRowMaxHeight;
3435
4420
  node.children.forEach((childRef, cellIndex) => {
3436
- const { row, col, span } = cellPositions[cellIndex];
4421
+ const { row, col, span, visible } = cellPositions[cellIndex];
4422
+ if (!visible) {
4423
+ this.calculateNode(childRef.ref, x, y, 0, 0, "grid");
4424
+ return;
4425
+ }
3437
4426
  const cellHeight = rowHeights[row];
3438
4427
  let cellY = y;
3439
4428
  for (let r = 0; r < row; r++) {
@@ -3504,8 +4493,14 @@ var LayoutEngine = class {
3504
4493
  const innerCardWidth = width - cardPadding * 2;
3505
4494
  const children = node.children;
3506
4495
  let currentY = y + cardPadding;
3507
- children.forEach((childRef, index) => {
4496
+ const flowChildren = children.filter((cr) => this.isNodeVisible(cr.ref));
4497
+ let flowCount = 0;
4498
+ children.forEach((childRef) => {
3508
4499
  const childNode = this.nodes[childRef.ref];
4500
+ if (!this.isNodeVisible(childRef.ref)) {
4501
+ this.calculateNode(childRef.ref, x + cardPadding, currentY, innerCardWidth, 0, "card");
4502
+ return;
4503
+ }
3509
4504
  let childHeight = this.getComponentHeight();
3510
4505
  if (childNode?.kind === "component" && childNode.props.height) {
3511
4506
  childHeight = Number(childNode.props.height);
@@ -3516,11 +4511,63 @@ var LayoutEngine = class {
3516
4511
  }
3517
4512
  this.calculateNode(childRef.ref, x + cardPadding, currentY, innerCardWidth, childHeight, "card");
3518
4513
  currentY += childHeight;
3519
- if (index < children.length - 1) {
4514
+ flowCount++;
4515
+ if (flowCount < flowChildren.length) {
3520
4516
  currentY += gap;
3521
4517
  }
3522
4518
  });
3523
4519
  }
4520
+ calculateTabs(node, x, y, width) {
4521
+ if (node.kind !== "container") return;
4522
+ const activeIndex = Number(node.params.active) || 0;
4523
+ node.children.forEach((childRef, index) => {
4524
+ if (index === activeIndex) {
4525
+ const tabNode = this.nodes[childRef.ref];
4526
+ let tabHeight = 40;
4527
+ if (tabNode?.kind === "container") {
4528
+ tabHeight = this.calculateContainerHeight(tabNode, width);
4529
+ } else if (tabNode?.kind === "component") {
4530
+ tabHeight = this.getIntrinsicComponentHeight(tabNode, width);
4531
+ }
4532
+ this.calculateNode(childRef.ref, x, y, width, tabHeight, "tabs");
4533
+ }
4534
+ });
4535
+ }
4536
+ calculateModal(node, nodeId, _canvasX, _canvasY, _canvasWidth, _canvasHeight) {
4537
+ if (node.kind !== "container") return;
4538
+ const viewportWidth = this.viewport.width;
4539
+ const size = String(node.params.size || "md");
4540
+ const modalWidths = { sm: 380, md: 520, lg: 720 };
4541
+ const modalWidth = Math.min(modalWidths[size] ?? 520, viewportWidth - 32);
4542
+ const modalX = Math.round((viewportWidth - modalWidth) / 2);
4543
+ const modalY = 64;
4544
+ const hasHeader = node.params.title !== void 0 && node.params.title !== "";
4545
+ const headerHeight = hasHeader ? 48 : 0;
4546
+ let childrenHeight = 0;
4547
+ const innerWidth = modalWidth;
4548
+ let currentY = modalY + headerHeight;
4549
+ node.children.forEach((childRef, index) => {
4550
+ const childNode = this.nodes[childRef.ref];
4551
+ let childHeight = 0;
4552
+ if (childNode?.kind === "container") {
4553
+ childHeight = this.calculateContainerHeight(childNode, innerWidth);
4554
+ } else if (childNode?.kind === "component") {
4555
+ childHeight = this.getIntrinsicComponentHeight(childNode, innerWidth);
4556
+ } else {
4557
+ childHeight = this.getComponentHeight();
4558
+ }
4559
+ this.calculateNode(childRef.ref, modalX, currentY, innerWidth, childHeight, "modal");
4560
+ const resolvedChildHeight = this.result[childRef.ref]?.height ?? childHeight;
4561
+ currentY += resolvedChildHeight;
4562
+ childrenHeight += resolvedChildHeight;
4563
+ if (index < node.children.length - 1) {
4564
+ currentY += 8;
4565
+ childrenHeight += 8;
4566
+ }
4567
+ });
4568
+ const modalHeight = headerHeight + childrenHeight;
4569
+ this.result[nodeId] = { x: modalX, y: modalY, width: modalWidth, height: modalHeight };
4570
+ }
3524
4571
  /**
3525
4572
  * Calculate layout for an instance node.
3526
4573
  * The instance is a transparent wrapper — its bounding box equals the
@@ -3751,8 +4798,7 @@ var LayoutEngine = class {
3751
4798
  return Math.max(this.getComponentHeight(), wrappedHeight);
3752
4799
  }
3753
4800
  if (node.componentType === "SidebarMenu") {
3754
- const itemsStr = String(node.props.items || "Item 1,Item 2,Item 3");
3755
- const items = itemsStr.split(",").map((s) => s.trim()).filter(Boolean);
4801
+ const items = toStringArray(node.props.items);
3756
4802
  const itemCount = items.length > 0 ? items.length : 3;
3757
4803
  const itemHeight = 40;
3758
4804
  return Math.max(this.getComponentHeight(), itemCount * itemHeight);
@@ -3763,7 +4809,7 @@ var LayoutEngine = class {
3763
4809
  if (node.componentType === "Stat") return 120;
3764
4810
  if (node.componentType === "Chart") return 250;
3765
4811
  if (node.componentType === "List") {
3766
- const itemsFromProps = String(node.props.items || "").split(",").map((item) => item.trim()).filter(Boolean);
4812
+ const itemsFromProps = toStringArray(node.props.items);
3767
4813
  const parsedItemsMock = Number(node.props.itemsMock ?? 4);
3768
4814
  const fallbackCount = Number.isFinite(parsedItemsMock) ? Math.max(0, Math.floor(parsedItemsMock)) : 4;
3769
4815
  const itemCount = itemsFromProps.length > 0 ? itemsFromProps.length : fallbackCount;
@@ -3941,6 +4987,17 @@ var LayoutEngine = class {
3941
4987
  }
3942
4988
  return width;
3943
4989
  }
4990
+ /**
4991
+ * Returns false only when the node has an explicit `visible: 'false'` param/prop.
4992
+ * Missing or any other value is treated as visible.
4993
+ */
4994
+ isNodeVisible(nodeId) {
4995
+ const node = this.nodes[nodeId];
4996
+ if (!node) return true;
4997
+ if (node.kind === "component") return String(node.props.visible) !== "false";
4998
+ if (node.kind === "container") return String(node.params.visible) !== "false";
4999
+ return true;
5000
+ }
3944
5001
  parseBooleanProp(value, fallback = false) {
3945
5002
  if (typeof value === "boolean") return value;
3946
5003
  if (typeof value === "string") {
@@ -4031,7 +5088,7 @@ var MockDataGenerator = class {
4031
5088
  for (const [key, rawValues] of Object.entries(mocks)) {
4032
5089
  let values = [];
4033
5090
  if (typeof rawValues === "string") {
4034
- values = rawValues.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
5091
+ values = toStringArray(rawValues);
4035
5092
  } else if (Array.isArray(rawValues)) {
4036
5093
  values = rawValues.map((v) => typeof v === "string" ? v.trim() : "").filter((v) => v.length > 0);
4037
5094
  }
@@ -4123,7 +5180,7 @@ var MockDataGenerator = class {
4123
5180
  * columns: "id,name,status,amount"
4124
5181
  */
4125
5182
  static generateMockRow(columns, rowIndex, random = false) {
4126
- const columnNames = columns.split(",").map((c) => c.trim());
5183
+ const columnNames = toStringArray(columns);
4127
5184
  const row = {};
4128
5185
  columnNames.forEach((col) => {
4129
5186
  const mockType = this.inferMockTypeFromColumn(col);
@@ -5175,6 +6232,9 @@ var SVGRenderer = class {
5175
6232
  if (!node || !pos) return;
5176
6233
  this.renderedNodeIds.add(nodeId);
5177
6234
  if (node.kind === "container") {
6235
+ if (String(node.params.visible) === "false") {
6236
+ return;
6237
+ }
5178
6238
  const containerGroup = [];
5179
6239
  const hasNodeId = node.meta?.nodeId;
5180
6240
  if (hasNodeId) {
@@ -5195,6 +6255,12 @@ var SVGRenderer = class {
5195
6255
  if (node.containerType === "split") {
5196
6256
  this.renderSplitDecoration(node, pos, containerGroup);
5197
6257
  }
6258
+ if (node.containerType === "modal") {
6259
+ this.renderModalDecoration(node, pos, containerGroup);
6260
+ }
6261
+ if (node.containerType === "modal-footer") {
6262
+ this.renderModalFooterDecoration(pos, containerGroup);
6263
+ }
5198
6264
  const isCellContainer = node.meta?.source === "cell";
5199
6265
  if (node.children.length === 0 && this.options.showDiagnostics && !isCellContainer) {
5200
6266
  containerGroup.push(this.renderEmptyContainerDiagnostic(pos, node.containerType));
@@ -5221,6 +6287,9 @@ var SVGRenderer = class {
5221
6287
  }
5222
6288
  output.push(...instanceGroup);
5223
6289
  } else if (node.kind === "component") {
6290
+ if (String(node.props.visible) === "false") {
6291
+ return;
6292
+ }
5224
6293
  const componentSvg = this.renderComponent(node, pos);
5225
6294
  if (componentSvg) {
5226
6295
  output.push(componentSvg);
@@ -5282,8 +6351,6 @@ var SVGRenderer = class {
5282
6351
  return this.renderAlert(node, pos);
5283
6352
  case "Badge":
5284
6353
  return this.renderBadge(node, pos);
5285
- case "Modal":
5286
- return this.renderModal(node, pos);
5287
6354
  case "List":
5288
6355
  return this.renderList(node, pos);
5289
6356
  case "Stat":
@@ -5726,8 +6793,8 @@ var SVGRenderer = class {
5726
6793
  }
5727
6794
  renderTable(node, pos) {
5728
6795
  const title = String(node.props.title || "");
5729
- const columnsStr = String(node.props.columns || "Col1,Col2,Col3");
5730
- const columns = columnsStr.split(",").map((c) => c.trim()).filter(Boolean);
6796
+ const parsedColumns = toStringArray(node.props.columns);
6797
+ const columns = parsedColumns.length > 0 ? parsedColumns : ["Col1", "Col2", "Col3"];
5731
6798
  const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
5732
6799
  const mockStr = String(node.props.mock || "");
5733
6800
  const random = this.parseBooleanProp(node.props.random, false);
@@ -5735,7 +6802,7 @@ var SVGRenderer = class {
5735
6802
  const parsedPageCount = Number(node.props.pages || 5);
5736
6803
  const pageCount = Number.isFinite(parsedPageCount) && parsedPageCount > 0 ? Math.floor(parsedPageCount) : 5;
5737
6804
  const paginationAlign = String(node.props.paginationAlign || "right");
5738
- const actions = String(node.props.actions || "").split(",").map((value) => value.trim()).filter(Boolean);
6805
+ const actions = toStringArray(node.props.actions);
5739
6806
  const hasActions = actions.length > 0;
5740
6807
  const caption = String(node.props.caption || "").trim();
5741
6808
  const hasCaption = caption.length > 0;
@@ -5748,7 +6815,7 @@ var SVGRenderer = class {
5748
6815
  const rawCaptionAlign = String(node.props.captionAlign || "");
5749
6816
  const captionAlign = rawCaptionAlign === "left" || rawCaptionAlign === "center" || rawCaptionAlign === "right" ? rawCaptionAlign : paginationAlign === "left" ? "right" : "left";
5750
6817
  const sameFooterAlign = hasCaption && pagination && captionAlign === paginationAlign;
5751
- const mockTypes = mockStr ? mockStr.split(",").map((m) => m.trim()).filter(Boolean) : [];
6818
+ const mockTypes = toStringArray(mockStr);
5752
6819
  const safeColumns = columns.length > 0 ? columns : ["Column"];
5753
6820
  while (mockTypes.length < safeColumns.length) {
5754
6821
  const inferred = MockDataGenerator.inferMockTypeFromColumn(safeColumns[mockTypes.length] || "item");
@@ -5848,6 +6915,13 @@ var SVGRenderer = class {
5848
6915
  currentX += buttonSize + buttonGap;
5849
6916
  });
5850
6917
  }
6918
+ const rowEventAttrs = this.getScopedEventAttrs(node, "onRowClick", { index: rowIdx });
6919
+ if (rowEventAttrs) {
6920
+ svg += `
6921
+ <rect x="${pos.x}" y="${rowY}"
6922
+ width="${pos.width}" height="${rowHeight}"
6923
+ fill="transparent" stroke="none" pointer-events="all"${rowEventAttrs}/>`;
6924
+ }
5851
6925
  });
5852
6926
  const footerTop = headerY + headerHeight + mockRows.length * rowHeight + 16;
5853
6927
  if (pagination) {
@@ -6237,14 +7311,15 @@ var SVGRenderer = class {
6237
7311
  const label = String(node.props.label || "Checkbox");
6238
7312
  const checked = String(node.props.checked || "false").toLowerCase() === "true";
6239
7313
  const disabled = this.parseBooleanProp(node.props.disabled, false);
7314
+ const clickable = String(node.props.clickable ?? "true") !== "false";
6240
7315
  const controlColor = this.resolveControlColor();
6241
7316
  const checkboxSize = 18;
6242
7317
  const checkboxY = pos.y + pos.height / 2 - checkboxSize / 2;
6243
- return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
6244
- <rect x="${pos.x}" y="${checkboxY}"
6245
- width="${checkboxSize}" height="${checkboxSize}"
6246
- rx="4"
6247
- fill="${checked ? controlColor : this.renderTheme.cardBg}"
7318
+ return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}${!clickable ? ' data-clickable="false"' : ""}>
7319
+ <rect x="${pos.x}" y="${checkboxY}"
7320
+ width="${checkboxSize}" height="${checkboxSize}"
7321
+ rx="4"
7322
+ fill="${checked ? controlColor : this.renderTheme.cardBg}"
6248
7323
  stroke="${this.renderTheme.border}"
6249
7324
  stroke-width="1"/>
6250
7325
  ${checked ? `<text x="${pos.x + checkboxSize / 2}" y="${checkboxY + 14}"
@@ -6262,13 +7337,14 @@ var SVGRenderer = class {
6262
7337
  const label = String(node.props.label || "Radio");
6263
7338
  const checked = String(node.props.checked || "false").toLowerCase() === "true";
6264
7339
  const disabled = this.parseBooleanProp(node.props.disabled, false);
7340
+ const clickable = String(node.props.clickable ?? "true") !== "false";
6265
7341
  const controlColor = this.resolveControlColor();
6266
7342
  const radioSize = 16;
6267
7343
  const radioY = pos.y + pos.height / 2 - radioSize / 2;
6268
- return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
6269
- <circle cx="${pos.x + radioSize / 2}" cy="${radioY + radioSize / 2}"
6270
- r="${radioSize / 2}"
6271
- fill="${this.renderTheme.cardBg}"
7344
+ return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}${!clickable ? ' data-clickable="false"' : ""}>
7345
+ <circle cx="${pos.x + radioSize / 2}" cy="${radioY + radioSize / 2}"
7346
+ r="${radioSize / 2}"
7347
+ fill="${this.renderTheme.cardBg}"
6272
7348
  stroke="${this.renderTheme.border}"
6273
7349
  stroke-width="1"/>
6274
7350
  ${checked ? `<circle cx="${pos.x + radioSize / 2}" cy="${radioY + radioSize / 2}"
@@ -6284,12 +7360,13 @@ var SVGRenderer = class {
6284
7360
  const label = String(node.props.label || "Toggle");
6285
7361
  const enabled = String(node.props.enabled || "false").toLowerCase() === "true";
6286
7362
  const disabled = this.parseBooleanProp(node.props.disabled, false);
7363
+ const clickable = String(node.props.clickable ?? "true") !== "false";
6287
7364
  const controlColor = this.resolveControlColor();
6288
7365
  const toggleWidth = 40;
6289
7366
  const toggleHeight = 20;
6290
7367
  const toggleY = pos.y + pos.height / 2 - toggleHeight / 2;
6291
- return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
6292
- <rect x="${pos.x}" y="${toggleY}"
7368
+ return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}${!clickable ? ' data-clickable="false"' : ""}>
7369
+ <rect x="${pos.x}" y="${toggleY}"
6293
7370
  width="${toggleWidth}" height="${toggleHeight}"
6294
7371
  rx="10"
6295
7372
  fill="${enabled ? controlColor : this.renderTheme.border}"
@@ -6308,12 +7385,9 @@ var SVGRenderer = class {
6308
7385
  // ============================================================================
6309
7386
  renderSidebar(node, pos) {
6310
7387
  const title = String(node.props.title || "Sidebar");
6311
- const itemsStr = String(node.props.items || "");
6312
7388
  const activeItem = String(node.props.active || "");
6313
- let items = [];
6314
- if (itemsStr) {
6315
- items = itemsStr.split(",").map((i) => i.trim());
6316
- } else {
7389
+ let items = toStringArray(node.props.items);
7390
+ if (items.length === 0) {
6317
7391
  const itemCount = Number(node.props.itemsMock || 6);
6318
7392
  items = MockDataGenerator.generateMockList("name", itemCount);
6319
7393
  }
@@ -6352,9 +7426,9 @@ var SVGRenderer = class {
6352
7426
  return svg;
6353
7427
  }
6354
7428
  renderTabs(node, pos) {
6355
- const itemsStr = String(node.props.items || "");
6356
- const tabs = itemsStr ? itemsStr.split(",").map((t) => t.trim()) : ["Tab 1", "Tab 2", "Tab 3"];
6357
- const activeProp = node.props.active ?? 0;
7429
+ const parsedTabs = toStringArray(node.props.items);
7430
+ const tabs = parsedTabs.length > 0 ? parsedTabs : ["Tab 1", "Tab 2", "Tab 3"];
7431
+ const activeProp = node.props.active ?? node.props.initialActive ?? 0;
6358
7432
  const activeIndex = Number.isFinite(Number(activeProp)) ? Math.max(0, Math.floor(Number(activeProp))) : 0;
6359
7433
  const variant = String(node.props.variant || "default");
6360
7434
  const accentColor = variant === "default" ? this.resolveAccentColor() : this.resolveVariantColor(variant, this.resolveAccentColor());
@@ -6364,8 +7438,7 @@ var SVGRenderer = class {
6364
7438
  const tabHeight = pos.height > 0 ? pos.height : sizeMap[String(node.props.size || "md")] ?? 44;
6365
7439
  const fontSize = 13;
6366
7440
  const textY = pos.y + Math.round(tabHeight / 2) + Math.round(fontSize * 0.4);
6367
- const iconsStr = String(node.props.icons || "");
6368
- const iconList = iconsStr ? iconsStr.split(",").map((s) => s.trim()) : [];
7441
+ const iconList = toStringArray(node.props.icons);
6369
7442
  const isFlat = this.parseBooleanProp(node.props.flat, false);
6370
7443
  const showBorder = this.parseBooleanProp(node.props.border, true);
6371
7444
  const tabWidth = pos.width / tabs.length;
@@ -6469,6 +7542,13 @@ var SVGRenderer = class {
6469
7542
  text-anchor="middle">${this.escapeXml(tab)}</text>`;
6470
7543
  }
6471
7544
  }
7545
+ const tabsTriggerAttrs = this.getTabsTriggerAttrs(node, i);
7546
+ if (tabsTriggerAttrs) {
7547
+ svg += `
7548
+ <rect x="${tabX}" y="${pos.y}"
7549
+ width="${tabWidth}" height="${tabHeight}"
7550
+ fill="transparent" stroke="none" pointer-events="all"${tabsTriggerAttrs}/>`;
7551
+ }
6472
7552
  });
6473
7553
  const contentY = pos.y + tabHeight;
6474
7554
  const contentH = pos.height - tabHeight;
@@ -6582,66 +7662,51 @@ var SVGRenderer = class {
6582
7662
  text-anchor="middle">${this.escapeXml(text)}</text>
6583
7663
  </g>`;
6584
7664
  }
6585
- renderModal(node, pos) {
6586
- const visible = this.parseBooleanProp(node.props.visible, true);
6587
- if (!visible) {
6588
- return "";
6589
- }
6590
- const title = String(node.props.title || "Modal");
7665
+ renderModalDecoration(node, pos, output) {
7666
+ if (node.kind !== "container") return;
7667
+ const canvasWidth = this.options.width;
7668
+ const canvasHeight = Math.max(this.options.height, this.calculateContentHeight());
6591
7669
  const padding = 16;
6592
7670
  const headerHeight = 48;
6593
- const overlayHeight = Math.max(this.options.height, this.calculateContentHeight());
6594
- const modalX = (this.options.width - pos.width) / 2;
6595
- const modalY = Math.max(40, (overlayHeight - pos.height) / 2);
6596
- return `<g${this.getDataNodeId(node)}>
6597
- <!-- Modal backdrop -->
6598
- <rect x="0" y="0"
6599
- width="${this.options.width}" height="${overlayHeight}"
6600
- fill="black" opacity="0.28"/>
6601
-
6602
- <!-- Modal box -->
6603
- <rect x="${modalX}" y="${modalY}"
6604
- width="${pos.width}" height="${pos.height}"
6605
- rx="8"
6606
- fill="${this.renderTheme.cardBg}"
6607
- stroke="${this.renderTheme.border}"
6608
- stroke-width="1"/>
6609
-
6610
- <!-- Header -->
6611
- <line x1="${modalX}" y1="${modalY + headerHeight}"
6612
- x2="${modalX + pos.width}" y2="${modalY + headerHeight}"
6613
- stroke="${this.renderTheme.border}"
6614
- stroke-width="1"/>
6615
-
6616
- <text x="${modalX + padding}" y="${modalY + padding + 16}"
6617
- font-family="Arial, Helvetica, sans-serif"
6618
- font-size="16"
6619
- font-weight="600"
6620
- fill="${this.renderTheme.text}">${this.escapeXml(title)}</text>
6621
-
6622
- <!-- Close button -->
6623
- <text x="${modalX + pos.width - 16}" y="${modalY + padding + 12}"
6624
- font-family="Arial, Helvetica, sans-serif"
6625
- font-size="18"
6626
- fill="${this.renderTheme.textMuted}">\u2715</text>
6627
-
6628
- <!-- Content placeholder -->
6629
- <text x="${modalX + pos.width / 2}" y="${modalY + headerHeight + (pos.height - headerHeight) / 2}"
6630
- font-family="Arial, Helvetica, sans-serif"
6631
- font-size="13"
6632
- fill="${this.renderTheme.textMuted}"
6633
- text-anchor="middle">Modal content</text>
6634
- </g>`;
7671
+ const hasTitle = node.params.title !== void 0 && node.params.title !== "";
7672
+ const closable = node.params.closable !== "false" && node.params.closable !== 0;
7673
+ const title = hasTitle ? String(node.params.title) : "";
7674
+ output.push(
7675
+ `<rect x="0" y="0" width="${canvasWidth}" height="${canvasHeight}" fill="black" opacity="0.28" pointer-events="none"/>`
7676
+ );
7677
+ output.push(
7678
+ `<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"/>`
7679
+ );
7680
+ if (hasTitle) {
7681
+ output.push(
7682
+ `<line x1="${pos.x}" y1="${pos.y + headerHeight}" x2="${pos.x + pos.width}" y2="${pos.y + headerHeight}" stroke="${this.renderTheme.border}" stroke-width="1"/>`
7683
+ );
7684
+ output.push(
7685
+ `<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>`
7686
+ );
7687
+ if (closable) {
7688
+ const events = node.events?.find((e) => e.event === "onClose");
7689
+ const closeEventAttr = events ? this.serializeEventHandler(events) : "";
7690
+ const closeX = pos.x + pos.width - padding - 14;
7691
+ const closeY = pos.y + padding + 14;
7692
+ output.push(
7693
+ `<rect x="${closeX - 12}" y="${closeY - 12}" width="24" height="24" rx="4" fill="transparent" stroke="none" pointer-events="all"${closeEventAttr}/>`,
7694
+ `<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>`
7695
+ );
7696
+ }
7697
+ }
7698
+ }
7699
+ renderModalFooterDecoration(pos, output) {
7700
+ output.push(
7701
+ `<line x1="${pos.x}" y1="${pos.y}" x2="${pos.x + pos.width}" y2="${pos.y}" stroke="${this.renderTheme.border}" stroke-width="1"/>`
7702
+ );
6635
7703
  }
6636
7704
  renderList(node, pos) {
6637
7705
  const title = String(node.props.title || "");
6638
- const itemsStr = String(node.props.items || "");
6639
7706
  const mockType = String(node.props.mock || "").trim();
6640
7707
  const random = this.parseBooleanProp(node.props.random, false);
6641
- let items = [];
6642
- if (itemsStr) {
6643
- items = itemsStr.split(",").map((i) => i.trim()).filter(Boolean);
6644
- } else {
7708
+ let items = toStringArray(node.props.items);
7709
+ if (items.length === 0) {
6645
7710
  const parsedItemsMock = Number(node.props.itemsMock ?? 4);
6646
7711
  const itemCount = Number.isFinite(parsedItemsMock) ? Math.max(0, Math.floor(parsedItemsMock)) : 4;
6647
7712
  const resolvedMockType = mockType || "name";
@@ -6670,6 +7735,7 @@ var SVGRenderer = class {
6670
7735
  items.forEach((item, i) => {
6671
7736
  const itemY = pos.y + titleHeight + i * itemHeight;
6672
7737
  if (itemY + itemHeight <= pos.y + pos.height) {
7738
+ const itemEventAttrs = this.getScopedEventAttrs(node, "onItemClick", { index: i });
6673
7739
  svg += `
6674
7740
  <line x1="${pos.x}" y1="${itemY + itemHeight}"
6675
7741
  x2="${pos.x + pos.width}" y2="${itemY + itemHeight}"
@@ -6678,7 +7744,10 @@ var SVGRenderer = class {
6678
7744
  <text x="${pos.x + padding}" y="${itemY + 24}"
6679
7745
  font-family="Arial, Helvetica, sans-serif"
6680
7746
  font-size="13"
6681
- fill="${this.renderTheme.text}">${this.escapeXml(item)}</text>`;
7747
+ fill="${this.renderTheme.text}">${this.escapeXml(item)}</text>
7748
+ ${itemEventAttrs ? `<rect x="${pos.x}" y="${itemY}"
7749
+ width="${pos.width}" height="${itemHeight}"
7750
+ fill="transparent" stroke="none" pointer-events="all"${itemEventAttrs}/>` : ""}`;
6682
7751
  }
6683
7752
  });
6684
7753
  svg += "\n </g>";
@@ -6912,8 +7981,8 @@ var SVGRenderer = class {
6912
7981
  return svg;
6913
7982
  }
6914
7983
  renderBreadcrumbs(node, pos) {
6915
- const itemsStr = String(node.props.items || "Home");
6916
- const items = itemsStr.split(",").map((s) => s.trim());
7984
+ const parsedBreadcrumbs = toStringArray(node.props.items);
7985
+ const items = parsedBreadcrumbs.length > 0 ? parsedBreadcrumbs : ["Home"];
6917
7986
  const separator = String(node.props.separator || "/");
6918
7987
  const fontSize = 12;
6919
7988
  const separatorWidth = 20;
@@ -6945,10 +8014,9 @@ var SVGRenderer = class {
6945
8014
  return svg;
6946
8015
  }
6947
8016
  renderSidebarMenu(node, pos) {
6948
- const itemsStr = String(node.props.items || "Item 1,Item 2,Item 3");
6949
- const iconsStr = String(node.props.icons || "");
6950
- const items = itemsStr.split(",").map((s) => s.trim());
6951
- const icons = iconsStr ? iconsStr.split(",").map((s) => s.trim()) : [];
8017
+ const parsedMenuItems = toStringArray(node.props.items);
8018
+ const items = parsedMenuItems.length > 0 ? parsedMenuItems : ["Item 1", "Item 2", "Item 3"];
8019
+ const icons = toStringArray(node.props.icons);
6952
8020
  const itemHeight = 40;
6953
8021
  const fontSize = 14;
6954
8022
  const activeIndex = Number(node.props.active || 0);
@@ -6993,6 +8061,13 @@ var SVGRenderer = class {
6993
8061
  font-size="${fontSize}"
6994
8062
  font-weight="${fontWeight}"
6995
8063
  fill="${textColor}">${this.escapeXml(item)}</text>`;
8064
+ const itemEventAttrs = this.getScopedEventAttrs(node, "onItemsClick", { index });
8065
+ if (itemEventAttrs) {
8066
+ svg += `
8067
+ <rect x="${pos.x}" y="${itemY}"
8068
+ width="${pos.width}" height="${itemHeight}"
8069
+ fill="transparent" stroke="none" pointer-events="all"${itemEventAttrs}/>`;
8070
+ }
6996
8071
  });
6997
8072
  svg += "\n </g>";
6998
8073
  return svg;
@@ -7343,7 +8418,7 @@ var SVGRenderer = class {
7343
8418
  userBadge = { x, y, width, height, label: userLabel };
7344
8419
  rightCursor = x - 8;
7345
8420
  }
7346
- const actionLabels = actions.split(",").map((a) => a.trim()).filter(Boolean);
8421
+ const actionLabels = toStringArray(actions);
7347
8422
  const actionHeight = 32;
7348
8423
  const actionY = pos.y + (pos.height - actionHeight) / 2;
7349
8424
  const actionGap = 8;
@@ -7429,11 +8504,99 @@ var SVGRenderer = class {
7429
8504
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
7430
8505
  }
7431
8506
  /**
7432
- * Get data-node-id attribute string for SVG elements
7433
- * Enables bidirectional selection between code and canvas
8507
+ * Get data-node-id and event data attributes for SVG elements.
8508
+ * Enables bidirectional selection between code and canvas (data-node-id)
8509
+ * and play test interactivity (data-event-*, data-user-id, data-tabs-id).
7434
8510
  */
7435
8511
  getDataNodeId(node) {
7436
- return node.meta.nodeId ? ` data-node-id="${node.meta.nodeId}"` : "";
8512
+ let attrs = "";
8513
+ if (node.meta.nodeId) {
8514
+ attrs += ` data-node-id="${node.meta.nodeId}"`;
8515
+ }
8516
+ if (node.kind === "component") {
8517
+ if (node.userDefinedId) {
8518
+ attrs += ` data-user-id="${node.userDefinedId}"`;
8519
+ }
8520
+ if (node.events && node.events.length > 0) {
8521
+ for (const handler of node.events) {
8522
+ if (!this.isScopedEvent(handler.event)) {
8523
+ attrs += this.serializeEventHandler(handler);
8524
+ }
8525
+ }
8526
+ }
8527
+ }
8528
+ if (node.kind === "container") {
8529
+ if (node.containerType === "tabs" && node.params.id) {
8530
+ attrs += ` data-tabs-id="${node.params.id}"`;
8531
+ if (node.params.active !== void 0) {
8532
+ attrs += ` data-tabs-active="${node.params.active}"`;
8533
+ }
8534
+ }
8535
+ if (node.events && node.events.length > 0) {
8536
+ for (const handler of node.events) {
8537
+ if (node.containerType === "modal" && handler.event === "onClose") continue;
8538
+ attrs += this.serializeEventHandler(handler);
8539
+ }
8540
+ }
8541
+ }
8542
+ return attrs;
8543
+ }
8544
+ isScopedEvent(event) {
8545
+ return event === "onClose" || event === "onItemClick" || event === "onRowClick" || event === "onItemsClick";
8546
+ }
8547
+ getScopedEventAttrs(node, eventName, options = {}) {
8548
+ const handler = node.events?.find((event) => event.event === eventName);
8549
+ if (!handler) return "";
8550
+ let attrs = "";
8551
+ if (node.meta.nodeId) {
8552
+ attrs += ` data-node-id="${node.meta.nodeId}"`;
8553
+ }
8554
+ if (node.userDefinedId) {
8555
+ attrs += ` data-user-id="${node.userDefinedId}"`;
8556
+ }
8557
+ attrs += this.serializeEventHandler(handler);
8558
+ if (options.index !== void 0) {
8559
+ attrs += ` data-event-index="${options.index}"`;
8560
+ }
8561
+ return attrs;
8562
+ }
8563
+ getTabsTriggerAttrs(node, index) {
8564
+ const tabsId = String(node.props.tabsId || "").trim();
8565
+ if (!tabsId) return "";
8566
+ let attrs = "";
8567
+ if (node.meta.nodeId) {
8568
+ attrs += ` data-node-id="${node.meta.nodeId}"`;
8569
+ }
8570
+ attrs += ` data-tabs-id="${tabsId}" data-tabs-trigger-index="${index}"`;
8571
+ return attrs;
8572
+ }
8573
+ serializeEventHandler(handler) {
8574
+ const attrName = this.eventNameToDataAttr(handler.event);
8575
+ const value = handler.actions.map((a) => this.serializeEventAction(a)).join("|");
8576
+ return ` data-event-${attrName}="${value}"`;
8577
+ }
8578
+ eventNameToDataAttr(event) {
8579
+ return event.replace(/^on/, "").toLowerCase();
8580
+ }
8581
+ serializeEventAction(action) {
8582
+ switch (action.type) {
8583
+ case "navigate":
8584
+ return `navigate:${action.screen}`;
8585
+ case "show":
8586
+ return `show:${action.targetId}`;
8587
+ case "hide":
8588
+ return `hide:${action.targetId}`;
8589
+ case "toggle":
8590
+ return `toggle:${action.targetId}`;
8591
+ case "enable":
8592
+ return `enable:${action.targetId}`;
8593
+ case "disable":
8594
+ return `disable:${action.targetId}`;
8595
+ case "setTab":
8596
+ return `setTab:${action.tabsId}:${action.index}`;
8597
+ case "navigateItems":
8598
+ return action.screens.map((s) => `navigate:${s}`).join(",");
8599
+ }
7437
8600
  }
7438
8601
  };
7439
8602
  function renderToSVG(ir, layout, options) {
@@ -7523,8 +8686,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
7523
8686
  * Render breadcrumbs as skeleton blocks: <rect> / <rect> / <rect accent>
7524
8687
  */
7525
8688
  renderBreadcrumbs(node, pos) {
7526
- const itemsStr = String(node.props.items || "Home");
7527
- const items = itemsStr.split(",").map((s) => s.trim()).filter(Boolean);
8689
+ const items = toStringArray(node.props.items || "Home");
7528
8690
  const separator = String(node.props.separator || "/");
7529
8691
  const blockColor = this.renderTheme.border;
7530
8692
  const charWidth = 6.2;
@@ -7845,10 +9007,9 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
7845
9007
  */
7846
9008
  renderTable(node, pos) {
7847
9009
  const title = String(node.props.title || "");
7848
- const columnsStr = String(node.props.columns || "Col1,Col2,Col3");
7849
- const columns = columnsStr.split(",").map((c) => c.trim()).filter(Boolean);
9010
+ const columns = toStringArray(node.props.columns || "Col1,Col2,Col3");
7850
9011
  const rowCount = Number(node.props.rows || node.props.rowsMock || 5);
7851
- const actions = String(node.props.actions || "").split(",").map((value) => value.trim()).filter(Boolean);
9012
+ const actions = toStringArray(node.props.actions);
7852
9013
  const hasActions = actions.length > 0;
7853
9014
  const pagination = this.parseBooleanProp(node.props.pagination, false);
7854
9015
  const parsedPageCount = Number(node.props.pages || 5);
@@ -8134,7 +9295,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
8134
9295
  const itemsStr = String(node.props.items || "");
8135
9296
  let items = [];
8136
9297
  if (itemsStr) {
8137
- items = itemsStr.split(",").map((i) => i.trim());
9298
+ items = toStringArray(itemsStr);
8138
9299
  } else {
8139
9300
  const itemCount = Number(node.props.itemsMock || 6);
8140
9301
  items = Array(itemCount).fill("Item");
@@ -8171,8 +9332,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
8171
9332
  * Render SidebarMenu with gray blocks instead of text and no icons
8172
9333
  */
8173
9334
  renderSidebarMenu(node, pos) {
8174
- const itemsStr = String(node.props.items || "Item 1,Item 2,Item 3");
8175
- const items = itemsStr.split(",").map((s) => s.trim());
9335
+ const items = toStringArray(node.props.items || "Item 1,Item 2,Item 3");
8176
9336
  const itemHeight = 40;
8177
9337
  const activeIndex = Number(node.props.active || 0);
8178
9338
  const accentColor = this.resolveAccentColor();
@@ -9020,7 +10180,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
9020
10180
  const title = String(node.props.title || "Sidebar");
9021
10181
  const itemsStr = String(node.props.items || "");
9022
10182
  const activeItem = String(node.props.active || "");
9023
- const items = itemsStr ? itemsStr.split(",").map((i) => i.trim()) : ["Item 1", "Item 2", "Item 3"];
10183
+ const parsed = toStringArray(itemsStr);
10184
+ const items = parsed.length ? parsed : ["Item 1", "Item 2", "Item 3"];
9024
10185
  const itemHeight = 40;
9025
10186
  const padding = 16;
9026
10187
  const titleHeight = 40;
@@ -9063,7 +10224,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
9063
10224
  */
9064
10225
  renderTabs(node, pos) {
9065
10226
  const itemsStr = String(node.props.items || "");
9066
- const tabs = itemsStr ? itemsStr.split(",").map((t) => t.trim()) : ["Tab 1", "Tab 2", "Tab 3"];
10227
+ const parsedTabs = toStringArray(itemsStr);
10228
+ const tabs = parsedTabs.length ? parsedTabs : ["Tab 1", "Tab 2", "Tab 3"];
9067
10229
  const activeProp = node.props.active ?? 0;
9068
10230
  const activeIndex = Number.isFinite(Number(activeProp)) ? Math.max(0, Math.floor(Number(activeProp))) : 0;
9069
10231
  const variant = String(node.props.variant || "default");
@@ -9075,7 +10237,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
9075
10237
  const fontSize = 13;
9076
10238
  const textY = pos.y + Math.round(tabHeight / 2) + Math.round(fontSize * 0.4);
9077
10239
  const iconsStr = String(node.props.icons || "");
9078
- const iconList = iconsStr ? iconsStr.split(",").map((s) => s.trim()) : [];
10240
+ const iconList = toStringArray(iconsStr);
9079
10241
  const isFlat = this.parseBooleanProp(node.props.flat, false);
9080
10242
  const showBorder = this.parseBooleanProp(node.props.border, true);
9081
10243
  const tabWidth = pos.width / tabs.length;
@@ -9240,7 +10402,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
9240
10402
  <!-- Modal backdrop -->
9241
10403
  <rect x="0" y="0"
9242
10404
  width="${this.options.width}" height="${overlayHeight}"
9243
- fill="black" opacity="0.28"/>
10405
+ fill="black" opacity="0.28" pointer-events="none"/>
9244
10406
 
9245
10407
  <!-- Modal box -->
9246
10408
  <rect x="${modalX}" y="${modalY}"
@@ -9268,7 +10430,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
9268
10430
  <text x="${modalX + pos.width - 16}" y="${modalY + padding + 12}"
9269
10431
  font-family="${this.fontFamily}"
9270
10432
  font-size="18"
9271
- fill="${this.renderTheme.textMuted}">\u2715</text>
10433
+ fill="${this.renderTheme.textMuted}"
10434
+ pointer-events="none">\u2715</text>
9272
10435
 
9273
10436
  <!-- Content placeholder -->
9274
10437
  <text x="${modalX + pos.width / 2}" y="${modalY + headerHeight + (pos.height - headerHeight) / 2}"
@@ -9286,10 +10449,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
9286
10449
  const itemsStr = String(node.props.items || "");
9287
10450
  const mockType = String(node.props.mock || "").trim();
9288
10451
  const random = this.parseBooleanProp(node.props.random, false);
9289
- let items = [];
9290
- if (itemsStr) {
9291
- items = itemsStr.split(",").map((i) => i.trim()).filter(Boolean);
9292
- } else {
10452
+ let items = toStringArray(itemsStr);
10453
+ if (!items.length) {
9293
10454
  const parsedItemsMock = Number(node.props.itemsMock ?? 4);
9294
10455
  const itemCount = Number.isFinite(parsedItemsMock) ? Math.max(0, Math.floor(parsedItemsMock)) : 4;
9295
10456
  const resolvedMockType = mockType || "name";
@@ -9503,7 +10664,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
9503
10664
  */
9504
10665
  renderBreadcrumbs(node, pos) {
9505
10666
  const itemsStr = String(node.props.items || "Home");
9506
- const items = itemsStr.split(",").map((s) => s.trim());
10667
+ const items = toStringArray(itemsStr);
9507
10668
  const separator = String(node.props.separator || "/");
9508
10669
  const fontSize = 12;
9509
10670
  const separatorWidth = 20;
@@ -9540,8 +10701,8 @@ var SketchSVGRenderer = class extends SVGRenderer {
9540
10701
  renderSidebarMenu(node, pos) {
9541
10702
  const itemsStr = String(node.props.items || "Item 1,Item 2,Item 3");
9542
10703
  const iconsStr = String(node.props.icons || "");
9543
- const items = itemsStr.split(",").map((s) => s.trim());
9544
- const icons = iconsStr ? iconsStr.split(",").map((s) => s.trim()) : [];
10704
+ const items = toStringArray(itemsStr);
10705
+ const icons = toStringArray(iconsStr);
9545
10706
  const itemHeight = 40;
9546
10707
  const fontSize = 14;
9547
10708
  const activeIndex = Number(node.props.active || 0);
@@ -9862,11 +11023,13 @@ var version = "0.0.1";
9862
11023
  DEVICE_PRESETS,
9863
11024
  IRGenerator,
9864
11025
  LayoutEngine,
11026
+ SELF_TARGET,
9865
11027
  SVGRenderer,
9866
11028
  SkeletonSVGRenderer,
9867
11029
  SketchSVGRenderer,
9868
11030
  SourceMapBuilder,
9869
11031
  SourceMapResolver,
11032
+ applyStateChange,
9870
11033
  buildSVG,
9871
11034
  calculateLayout,
9872
11035
  createSVGElement,