@lumiastream/ui 0.5.3 → 0.5.4

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/se-import.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/se-import/index.ts
2
- import { nanoid as nanoid10 } from "nanoid";
2
+ import { nanoid as nanoid8 } from "nanoid";
3
3
 
4
4
  // src/se-import/translate.ts
5
5
  var SE_TO_LUMIA_PLACEHOLDER = {
@@ -1064,12 +1064,26 @@ function mapImage(widget, ctx) {
1064
1064
  );
1065
1065
  const innerWidth = widget.image?.css?.width;
1066
1066
  const innerHeight = widget.image?.css?.height;
1067
+ const outerWidthExplicit = isExplicitPxSize(widget.css?.width);
1068
+ const outerHeightExplicit = isExplicitPxSize(widget.css?.height);
1067
1069
  const bounds = unit.layer.bounds;
1068
- if (innerWidth === "auto") bounds.autoWidth = true;
1069
- if (innerHeight === "auto") bounds.autoHeight = true;
1070
+ if (innerWidth === "auto" && !outerWidthExplicit) bounds.autoWidth = true;
1071
+ if (innerHeight === "auto" && !outerHeightExplicit) bounds.autoHeight = true;
1070
1072
  return unit;
1071
1073
  }
1074
+ function isExplicitPxSize(value) {
1075
+ if (value == null) return false;
1076
+ if (typeof value === "number") return Number.isFinite(value) && value > 0;
1077
+ if (typeof value !== "string") return false;
1078
+ const s = value.trim().toLowerCase();
1079
+ if (s === "" || s === "auto") return false;
1080
+ if (s.endsWith("%")) return false;
1081
+ const parsed = parseFloat(s);
1082
+ return Number.isFinite(parsed) && parsed > 0;
1083
+ }
1072
1084
  function mapVideo(widget, ctx) {
1085
+ const innerWidth = widget.video?.css?.width;
1086
+ const isInnerAutoWidth = innerWidth === "auto";
1073
1087
  const unit = buildUnit(
1074
1088
  widget,
1075
1089
  "video",
@@ -1078,12 +1092,19 @@ function mapVideo(widget, ctx) {
1078
1092
  src: widget.video?.src ?? "",
1079
1093
  volume: widget.video?.volume ?? 0.5,
1080
1094
  loop: true,
1081
- muted: false
1095
+ muted: false,
1096
+ ...isInnerAutoWidth ? { objectFit: "contain" } : {}
1082
1097
  }
1083
1098
  },
1084
1099
  ctx
1085
1100
  );
1086
- unit.layer.bounds.autoHeight = true;
1101
+ const bounds = unit.layer.bounds;
1102
+ if (isInnerAutoWidth) {
1103
+ bounds.autoWidth = true;
1104
+ bounds.autoHeight = true;
1105
+ } else {
1106
+ bounds.autoHeight = true;
1107
+ }
1087
1108
  return unit;
1088
1109
  }
1089
1110
  var LUMIA_SNOW_WEBM = "https://storage.lumiastream.com/overlays/global/seasonal/snow.webm";
@@ -1142,118 +1163,157 @@ function mapReadout(widget, fallbackVar, ctx) {
1142
1163
 
1143
1164
  // src/se-import/mappers/alert.ts
1144
1165
  import { LumiaAlertConfigs } from "@lumiastream/lumia-types";
1166
+ import { nanoid as nanoid2 } from "nanoid";
1167
+
1168
+ // src/se-import/import-utils.ts
1169
+ function pxToNumber(raw, fallback) {
1170
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw;
1171
+ if (typeof raw !== "string") return fallback;
1172
+ const trimmed = raw.trim();
1173
+ if (!trimmed) return fallback;
1174
+ if (trimmed.endsWith("px")) {
1175
+ const n2 = parseFloat(trimmed.slice(0, -2));
1176
+ return Number.isFinite(n2) ? n2 : fallback;
1177
+ }
1178
+ const n = parseFloat(trimmed);
1179
+ return Number.isFinite(n) ? n : fallback;
1180
+ }
1181
+ function parseDurationMs(raw, fallbackMs) {
1182
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw;
1183
+ if (typeof raw !== "string") return fallbackMs;
1184
+ const trimmed = raw.trim();
1185
+ if (!trimmed) return fallbackMs;
1186
+ if (trimmed.endsWith("ms")) {
1187
+ const n2 = parseFloat(trimmed.slice(0, -2));
1188
+ return Number.isFinite(n2) ? n2 : fallbackMs;
1189
+ }
1190
+ if (trimmed.endsWith("s")) {
1191
+ const n2 = parseFloat(trimmed.slice(0, -1));
1192
+ return Number.isFinite(n2) ? n2 * 1e3 : fallbackMs;
1193
+ }
1194
+ const n = parseFloat(trimmed);
1195
+ return Number.isFinite(n) ? n : fallbackMs;
1196
+ }
1197
+ function buildImportDescription(head, sourceUrl) {
1198
+ if (typeof sourceUrl === "string" && sourceUrl.trim().length > 0) {
1199
+ return `${head}
1200
+ Source: ${sourceUrl.trim()}`;
1201
+ }
1202
+ return head;
1203
+ }
1145
1204
 
1146
1205
  // src/se-import/mappers/provider-alerts.ts
1206
+ import { LumiaAlertValues } from "@lumiastream/lumia-types";
1147
1207
  var ALERT_BOX_KEYS = {
1148
1208
  twitch: {
1149
- follower: ["twitch-follower"],
1150
- subscriber: ["twitch-subscriber"],
1151
- tip: ["lumiastream-donation", "streamelements-donation"],
1152
- cheer: ["twitch-bits"],
1153
- raid: ["twitch-raid"],
1154
- merch: ["fourthwall-shopOrder"],
1155
- purchase: ["fourthwall-shopOrder"],
1156
- charityCampaignDonation: ["twitch-charityDonation"]
1209
+ follower: [LumiaAlertValues.TWITCH_FOLLOWER],
1210
+ subscriber: [LumiaAlertValues.TWITCH_SUBSCRIBER],
1211
+ tip: [LumiaAlertValues.LUMIASTREAM_DONATION, LumiaAlertValues.STREAMELEMENTS_DONATION],
1212
+ cheer: [LumiaAlertValues.TWITCH_BITS],
1213
+ raid: [LumiaAlertValues.TWITCH_RAID],
1214
+ merch: [LumiaAlertValues.FOURTHWALL_SHOPORDER],
1215
+ purchase: [LumiaAlertValues.FOURTHWALL_SHOPORDER],
1216
+ charityCampaignDonation: [LumiaAlertValues.TWITCH_CHARITY_DONATION]
1157
1217
  },
1158
1218
  kick: {
1159
- follower: ["kick-follower"],
1160
- subscriber: ["kick-subscriber"],
1161
- tip: ["lumiastream-donation", "streamelements-donation"],
1162
- cheer: ["kick-kicks"],
1163
- raid: ["kick-host"],
1164
- host: ["kick-host"],
1165
- merch: ["fourthwall-shopOrder"],
1166
- purchase: ["fourthwall-shopOrder"]
1219
+ follower: [LumiaAlertValues.KICK_FOLLOWER],
1220
+ subscriber: [LumiaAlertValues.KICK_SUBSCRIBER],
1221
+ tip: [LumiaAlertValues.LUMIASTREAM_DONATION, LumiaAlertValues.STREAMELEMENTS_DONATION],
1222
+ cheer: [LumiaAlertValues.KICK_KICKS],
1223
+ raid: [LumiaAlertValues.KICK_HOST],
1224
+ host: [LumiaAlertValues.KICK_HOST],
1225
+ merch: [LumiaAlertValues.FOURTHWALL_SHOPORDER],
1226
+ purchase: [LumiaAlertValues.FOURTHWALL_SHOPORDER]
1167
1227
  },
1168
1228
  youtube: {
1169
- follower: ["youtube-subscriber"],
1170
- subscriber: ["youtube-member"],
1171
- tip: ["lumiastream-donation", "streamelements-donation"],
1172
- cheer: ["youtube-superchat"],
1173
- merch: ["fourthwall-shopOrder"],
1174
- purchase: ["fourthwall-shopOrder"]
1229
+ follower: [LumiaAlertValues.YOUTUBE_SUBSCRIBER],
1230
+ subscriber: [LumiaAlertValues.YOUTUBE_MEMBER],
1231
+ tip: [LumiaAlertValues.LUMIASTREAM_DONATION, LumiaAlertValues.STREAMELEMENTS_DONATION],
1232
+ cheer: [LumiaAlertValues.YOUTUBE_SUPERCHAT],
1233
+ merch: [LumiaAlertValues.FOURTHWALL_SHOPORDER],
1234
+ purchase: [LumiaAlertValues.FOURTHWALL_SHOPORDER]
1175
1235
  }
1176
1236
  };
1177
1237
  var KAPPAGEN_KEYS = {
1178
1238
  twitch: {
1179
- follower: "twitch-follower",
1180
- subscriber: "twitch-subscriber",
1181
- cheer: "twitch-bits",
1182
- raid: "twitch-raid",
1183
- tip: "lumiastream-donation"
1239
+ follower: LumiaAlertValues.TWITCH_FOLLOWER,
1240
+ subscriber: LumiaAlertValues.TWITCH_SUBSCRIBER,
1241
+ cheer: LumiaAlertValues.TWITCH_BITS,
1242
+ raid: LumiaAlertValues.TWITCH_RAID,
1243
+ tip: LumiaAlertValues.LUMIASTREAM_DONATION
1184
1244
  },
1185
1245
  kick: {
1186
- follower: "kick-follower",
1187
- subscriber: "kick-subscriber",
1188
- cheer: "kick-kicks",
1189
- raid: "kick-host",
1190
- tip: "lumiastream-donation"
1246
+ follower: LumiaAlertValues.KICK_FOLLOWER,
1247
+ subscriber: LumiaAlertValues.KICK_SUBSCRIBER,
1248
+ cheer: LumiaAlertValues.KICK_KICKS,
1249
+ raid: LumiaAlertValues.KICK_HOST,
1250
+ tip: LumiaAlertValues.LUMIASTREAM_DONATION
1191
1251
  },
1192
1252
  youtube: {
1193
- follower: "youtube-subscriber",
1194
- subscriber: "youtube-member",
1195
- cheer: "youtube-superchat",
1196
- tip: "lumiastream-donation"
1253
+ follower: LumiaAlertValues.YOUTUBE_SUBSCRIBER,
1254
+ subscriber: LumiaAlertValues.YOUTUBE_MEMBER,
1255
+ cheer: LumiaAlertValues.YOUTUBE_SUPERCHAT,
1256
+ tip: LumiaAlertValues.LUMIASTREAM_DONATION
1197
1257
  }
1198
1258
  };
1199
1259
  var STREAMBOSS_LISTENER_KEYS = {
1200
1260
  twitch: {
1201
- "follower-latest": ["twitch-follower"],
1202
- "subscriber-latest": ["twitch-subscriber"],
1203
- "tip-latest": ["lumiastream-donation", "streamelements-donation"],
1204
- "cheer-latest": ["twitch-bits"],
1205
- "raid-latest": ["twitch-raid"]
1261
+ "follower-latest": [LumiaAlertValues.TWITCH_FOLLOWER],
1262
+ "subscriber-latest": [LumiaAlertValues.TWITCH_SUBSCRIBER],
1263
+ "tip-latest": [LumiaAlertValues.LUMIASTREAM_DONATION, LumiaAlertValues.STREAMELEMENTS_DONATION],
1264
+ "cheer-latest": [LumiaAlertValues.TWITCH_BITS],
1265
+ "raid-latest": [LumiaAlertValues.TWITCH_RAID]
1206
1266
  },
1207
1267
  kick: {
1208
- "follower-latest": ["kick-follower"],
1209
- "subscriber-latest": ["kick-subscriber"],
1210
- "tip-latest": ["lumiastream-donation", "streamelements-donation"],
1211
- "cheer-latest": ["kick-kicks"],
1212
- "raid-latest": ["kick-host"]
1268
+ "follower-latest": [LumiaAlertValues.KICK_FOLLOWER],
1269
+ "subscriber-latest": [LumiaAlertValues.KICK_SUBSCRIBER],
1270
+ "tip-latest": [LumiaAlertValues.LUMIASTREAM_DONATION, LumiaAlertValues.STREAMELEMENTS_DONATION],
1271
+ "cheer-latest": [LumiaAlertValues.KICK_KICKS],
1272
+ "raid-latest": [LumiaAlertValues.KICK_HOST]
1213
1273
  },
1214
1274
  youtube: {
1215
- "follower-latest": ["youtube-subscriber"],
1216
- "subscriber-latest": ["youtube-member"],
1217
- "tip-latest": ["lumiastream-donation", "streamelements-donation"],
1218
- "cheer-latest": ["youtube-superchat"]
1275
+ "follower-latest": [LumiaAlertValues.YOUTUBE_SUBSCRIBER],
1276
+ "subscriber-latest": [LumiaAlertValues.YOUTUBE_MEMBER],
1277
+ "tip-latest": [LumiaAlertValues.LUMIASTREAM_DONATION, LumiaAlertValues.STREAMELEMENTS_DONATION],
1278
+ "cheer-latest": [LumiaAlertValues.YOUTUBE_SUPERCHAT]
1219
1279
  }
1220
1280
  };
1221
1281
  var CURATED_ALERTS = {
1222
1282
  twitch: [
1223
- "twitch-follower",
1224
- "twitch-subscriber",
1225
- "twitch-giftSubscription",
1226
- "twitch-bits",
1227
- "twitch-bitsCombo",
1228
- "twitch-raid",
1229
- "twitch-points",
1230
- "twitch-redemption",
1231
- "twitch-charityDonation",
1232
- "twitch-hypetrainStarted",
1233
- "twitch-clip",
1234
- "twitch-shoutoutReceive",
1235
- "twitch-watchStreak",
1236
- "twitch-firstChatter",
1237
- "twitch-extension"
1283
+ LumiaAlertValues.TWITCH_FOLLOWER,
1284
+ LumiaAlertValues.TWITCH_SUBSCRIBER,
1285
+ LumiaAlertValues.TWITCH_GIFT_SUBSCRIPTION,
1286
+ LumiaAlertValues.TWITCH_BITS,
1287
+ LumiaAlertValues.TWITCH_BITS_COMBO,
1288
+ LumiaAlertValues.TWITCH_RAID,
1289
+ LumiaAlertValues.TWITCH_POINTS,
1290
+ LumiaAlertValues.TWITCH_REDEMPTION,
1291
+ LumiaAlertValues.TWITCH_CHARITY_DONATION,
1292
+ LumiaAlertValues.TWITCH_HYPETRAIN_STARTED,
1293
+ LumiaAlertValues.TWITCH_CLIP,
1294
+ LumiaAlertValues.TWITCH_SHOUTOUT_RECEIVE,
1295
+ LumiaAlertValues.TWITCH_WATCH_STREAK,
1296
+ LumiaAlertValues.TWITCH_FIRST_CHATTER,
1297
+ LumiaAlertValues.TWITCH_EXTENSION
1238
1298
  ],
1239
1299
  kick: [
1240
- "kick-follower",
1241
- "kick-subscriber",
1242
- "kick-subscriptionGift",
1243
- "kick-kicks",
1244
- "kick-host",
1245
- "kick-points",
1246
- "kick-firstChatter"
1300
+ LumiaAlertValues.KICK_FOLLOWER,
1301
+ LumiaAlertValues.KICK_SUBSCRIBER,
1302
+ LumiaAlertValues.KICK_GIFT_SUBSCRIPTION,
1303
+ LumiaAlertValues.KICK_KICKS,
1304
+ LumiaAlertValues.KICK_HOST,
1305
+ LumiaAlertValues.KICK_POINTS,
1306
+ LumiaAlertValues.KICK_FIRST_CHATTER
1247
1307
  ],
1248
1308
  youtube: [
1249
- "youtube-subscriber",
1250
- "youtube-member",
1251
- "youtube-giftMembers",
1252
- "youtube-superchat",
1253
- "youtube-supersticker",
1254
- "youtube-gifts",
1255
- "youtube-like",
1256
- "youtube-firstChatter"
1309
+ LumiaAlertValues.YOUTUBE_SUBSCRIBER,
1310
+ LumiaAlertValues.YOUTUBE_MEMBER,
1311
+ LumiaAlertValues.YOUTUBE_GIFT_MEMBERS,
1312
+ LumiaAlertValues.YOUTUBE_SUPERCHAT,
1313
+ LumiaAlertValues.YOUTUBE_SUPERSTICKER,
1314
+ LumiaAlertValues.YOUTUBE_GIFTS,
1315
+ LumiaAlertValues.YOUTUBE_LIKE,
1316
+ LumiaAlertValues.YOUTUBE_FIRST_CHATTER
1257
1317
  ]
1258
1318
  };
1259
1319
  function curatedAlertsFor(provider = "twitch") {
@@ -1321,8 +1381,12 @@ function buildAlertEvent(seEvent, seEventType) {
1321
1381
  const shadowEnabled = text.enableShadow !== false;
1322
1382
  const importedTextShadow = css["text-shadow"] ?? "rgba(0, 0, 0, 0.8) 1px 1px 1px";
1323
1383
  const textShadow = shadowEnabled ? importedTextShadow : "none";
1324
- const marginTopRaw = Number(css["margin-top"]);
1325
- const pushTextUp = seLayoutUsesPushText(seEvent.layout) && Number.isFinite(marginTopRaw) ? -marginTopRaw : 0;
1384
+ const allowPush = seLayoutUsesPushText(seEvent.layout);
1385
+ const pushTextUp = allowPush ? pxToNumber(css["margin-top"], 0) : 0;
1386
+ const pushTextLeft = allowPush ? pxToNumber(css["margin-left"], 0) : 0;
1387
+ const usesColumnLayout = !seEvent.layout || seEvent.layout === "column";
1388
+ const usesRowLayout = seEvent.layout === "row";
1389
+ const tightLayout = allowPush && (usesColumnLayout || usesRowLayout);
1326
1390
  const variations = (seEvent.variations ?? []).map((variation) => buildAlertVariation(variation, seEventType, seEvent)).filter((v) => v != null);
1327
1391
  const defaultDuration = (seEventType && SE_DEFAULT_DURATION_BY_EVENT[seEventType]) ?? 8;
1328
1392
  return {
@@ -1349,7 +1413,7 @@ function buildAlertEvent(seEvent, seEventType) {
1349
1413
  showBrandIcon: true
1350
1414
  },
1351
1415
  settings: { duration: (seEvent.duration ?? defaultDuration) * 1e3 },
1352
- layout: { value: translateLayout(seEvent.layout), css: {}, customCss: "", pushTextUp, pushTextLeft: 0 },
1416
+ layout: { value: translateLayout(seEvent.layout), css: {}, customCss: "", pushTextUp, pushTextLeft, tight: tightLayout },
1353
1417
  text: {
1354
1418
  css: {
1355
1419
  fontFamily: css["font-family"] ?? "Roboto",
@@ -1358,6 +1422,10 @@ function buildAlertEvent(seEvent, seEventType) {
1358
1422
  fontWeight: css["font-weight"] ?? "bold",
1359
1423
  textShadow,
1360
1424
  textAlign: css["text-align"] ?? "center"
1425
+ // SE's per-event `margin-top` / `margin-left` are routed to
1426
+ // `layout.pushTextUp` / `layout.pushTextLeft` above (not
1427
+ // stamped on `text.css`) so they land in the alert editor's
1428
+ // dedicated sliders. See the long comment near `pushTextUp`.
1361
1429
  },
1362
1430
  messageCss,
1363
1431
  content: { value: translateSeText(text.message ?? "") }
@@ -1427,6 +1495,15 @@ function buildAlertVariation(v, seEventType, parentSettings) {
1427
1495
  const mergedSettings = deepMergeSeSettings(parentSettings ? parentInheritable : void 0, v.settings);
1428
1496
  const base = buildAlertEvent(mergedSettings, seEventType);
1429
1497
  return {
1498
+ // Stable per-variation ID. Lumia's alert editor (Settings.tsx) auto-
1499
+ // stamps a nanoid on first open for any variation missing an id, but
1500
+ // that leaves a window where the imported overlay's variations have
1501
+ // no stable key — list components rendering them key on
1502
+ // `variation.id || variation.name || idx` and adjacent variations
1503
+ // with the same name collide. Stamping at import time gives every
1504
+ // variation a stable identity from save #1 without waiting for the
1505
+ // editor's first mount.
1506
+ id: nanoid2(),
1430
1507
  name: v.name ?? "Variation",
1431
1508
  on: v.enabled !== false,
1432
1509
  randomChance: v.chance ?? 100,
@@ -1510,7 +1587,7 @@ function mapSingleAlert(widget, lumiaAlertKey, ctx) {
1510
1587
  css: { ...topCss, ...innerCss }
1511
1588
  };
1512
1589
  }
1513
- const built = buildAlertEvent(merged);
1590
+ const built = buildAlertEvent(merged, void 0);
1514
1591
  const events = {
1515
1592
  [lumiaAlertKey]: built
1516
1593
  };
@@ -1656,7 +1733,7 @@ function mapGoal(widget, ctx) {
1656
1733
  }
1657
1734
 
1658
1735
  // src/se-import/mappers/custom.ts
1659
- import { nanoid as nanoid2 } from "nanoid";
1736
+ import { nanoid as nanoid3 } from "nanoid";
1660
1737
  var SOURCE_TO_LUMIA_FIELD_TYPE = {
1661
1738
  text: "input",
1662
1739
  number: "number",
@@ -1746,7 +1823,7 @@ function mapCustom(widget, ctx) {
1746
1823
  // imports — codeId is the scoping key for variables, storage, and
1747
1824
  // event listeners. Mint a fresh nanoid so every imported widget
1748
1825
  // has its own scope.
1749
- codeId: nanoid2(),
1826
+ codeId: nanoid3(),
1750
1827
  html: v.html ?? "",
1751
1828
  css: v.css ?? "",
1752
1829
  js: v.js ?? "",
@@ -1883,6 +1960,63 @@ var TIPJAR_DEFAULT_MODULE = {
1883
1960
  version: 1,
1884
1961
  content: TIPJAR_DEFAULT_CONTENT_FIELDS
1885
1962
  };
1963
+ var EVENTLIST_DEFAULT_CONTENT_FIELDS = {
1964
+ version: 2,
1965
+ eventListType: {},
1966
+ theme: "basic",
1967
+ themeConfig: {},
1968
+ borderRadius: "10px",
1969
+ condensedText: true,
1970
+ src: "",
1971
+ // Category blacklist — events whose Lumia type appears here are
1972
+ // hidden. SL `show_<type>: false` maps to inserting the equivalent
1973
+ // Lumia category here.
1974
+ filters: [],
1975
+ sites: [],
1976
+ maxItemsToShow: 20,
1977
+ removeAfter: 0,
1978
+ reverse: false,
1979
+ horizontal: false,
1980
+ animations: {
1981
+ enterAnimation: "fadeIn",
1982
+ exitAnimation: "fadeOut",
1983
+ enterAnimationDuration: 1e3,
1984
+ exitAnimationDuration: 1e3,
1985
+ enterAnimationDelay: 0,
1986
+ exitAnimationDelay: 0
1987
+ },
1988
+ showDetailedText: true,
1989
+ hideAlertMessage: false,
1990
+ showAlertIcon: true,
1991
+ showSiteIcon: false,
1992
+ showUserAvatar: false,
1993
+ fadeOutAfterDelay: false,
1994
+ fadeOutDelayTime: 5,
1995
+ image: "",
1996
+ itemGap: 16,
1997
+ audio: {
1998
+ enter: "",
1999
+ volume: 1
2000
+ }
2001
+ };
2002
+ var EVENTLIST_DEFAULT_MODULE = {
2003
+ version: 2,
2004
+ content: EVENTLIST_DEFAULT_CONTENT_FIELDS,
2005
+ css: {
2006
+ borderRadius: "0px",
2007
+ borderStyle: "solid",
2008
+ borderWidth: "0px",
2009
+ borderColor: "transparent",
2010
+ fontSize: 16,
2011
+ lineHeight: 1.2,
2012
+ textAlign: "left",
2013
+ fontFamily: "Roboto",
2014
+ fontWeight: "normal",
2015
+ color: "#ffffff",
2016
+ textShadow: "",
2017
+ background: "transparent"
2018
+ }
2019
+ };
1886
2020
 
1887
2021
  // src/se-import/mappers/misc.ts
1888
2022
  function mapChatboxTheme(seTheme) {
@@ -3832,11 +3966,6 @@ function rewriteAssetURLs(overlay, mapping) {
3832
3966
  };
3833
3967
  }
3834
3968
  var MAX_MIRROR_ASSET_BYTES = 100 * 1024 * 1024;
3835
- function looksLikeCorsError2(err) {
3836
- if (!(err instanceof TypeError)) return false;
3837
- const msg = (err.message ?? "").toLowerCase();
3838
- return msg.includes("failed to fetch") || msg.includes("networkerror") || msg.includes("network error") || msg.includes("cors") || msg.includes("access-control") || err.message.length > 0;
3839
- }
3840
3969
  async function mirrorOneAsset(url, upload, signal, proxyAssetFetch) {
3841
3970
  const blob = await fetchAssetBlob(url, signal, proxyAssetFetch);
3842
3971
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
@@ -3868,7 +3997,7 @@ async function fetchAssetBlob(url, signal, proxyAssetFetch) {
3868
3997
  return res.blob();
3869
3998
  }
3870
3999
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
3871
- if (proxyAssetFetch && looksLikeCorsError2(directErr)) {
4000
+ if (proxyAssetFetch && looksLikeCorsError(directErr)) {
3872
4001
  try {
3873
4002
  return await proxyAssetFetch(url);
3874
4003
  } catch (proxyErr) {
@@ -3919,7 +4048,7 @@ function guessMime(name) {
3919
4048
  }
3920
4049
 
3921
4050
  // src/se-import/transplant.ts
3922
- import { nanoid as nanoid3 } from "nanoid";
4051
+ import { nanoid as nanoid4 } from "nanoid";
3923
4052
  function cloneModuleForTransplant(srcModule, newId) {
3924
4053
  const clonedContent = srcModule.content ? structuredClone(srcModule.content) : srcModule.content;
3925
4054
  if (srcModule.settings?.type === "custom" && clonedContent) {
@@ -3937,7 +4066,7 @@ function transplantLayer(source, sourceLayerId) {
3937
4066
  const srcLayer = layers.find((l) => l.id === sourceLayerId);
3938
4067
  const srcModule = modules[sourceLayerId];
3939
4068
  if (!srcLayer || !srcModule) return null;
3940
- const rootNewId = nanoid3();
4069
+ const rootNewId = nanoid4();
3941
4070
  if (srcLayer.type !== "group") {
3942
4071
  const layer = { ...srcLayer, id: rootNewId };
3943
4072
  const module = cloneModuleForTransplant(srcModule, rootNewId);
@@ -3955,7 +4084,7 @@ function transplantLayer(source, sourceLayerId) {
3955
4084
  if (seen.has(child.id)) continue;
3956
4085
  seen.add(child.id);
3957
4086
  descendants.push(child);
3958
- idRemap.set(child.id, nanoid3());
4087
+ idRemap.set(child.id, nanoid4());
3959
4088
  if (child.type === "group") queue.push(child.id);
3960
4089
  }
3961
4090
  }
@@ -6441,7 +6570,74 @@ function readSLProvider(response) {
6441
6570
  }
6442
6571
 
6443
6572
  // src/sl-import/mappers/alert-box.ts
6444
- import { nanoid as nanoid4 } from "nanoid";
6573
+ import { nanoid as nanoid6 } from "nanoid";
6574
+ import { LumiaAlertValues as LumiaAlertValues2 } from "@lumiastream/lumia-types";
6575
+
6576
+ // src/se-import/import-envelope.ts
6577
+ import { nanoid as nanoid5 } from "nanoid";
6578
+ function buildModuleEnvelope(opts) {
6579
+ const id = opts.id ?? nanoid5();
6580
+ const locked = opts.locked ?? false;
6581
+ const visible = opts.visible ?? true;
6582
+ const layer = {
6583
+ id,
6584
+ ...opts.parentGroupId !== void 0 ? { group: opts.parentGroupId } : {},
6585
+ ...opts.type_layer ? { type: opts.type_layer } : {},
6586
+ state: {
6587
+ visible,
6588
+ locked,
6589
+ ...opts.expanded !== void 0 ? { expanded: opts.expanded } : {}
6590
+ },
6591
+ bounds: {
6592
+ x: opts.bounds.x,
6593
+ y: opts.bounds.y,
6594
+ width: opts.bounds.width,
6595
+ height: opts.bounds.height,
6596
+ scale: [1, 1],
6597
+ ...opts.zIndex !== void 0 ? { zIndex: opts.zIndex } : {}
6598
+ }
6599
+ };
6600
+ const module = {
6601
+ id,
6602
+ version: opts.version ?? 1,
6603
+ loaded: true,
6604
+ settings: { type: opts.type, title: opts.title, locked },
6605
+ lights: opts.lights ?? [],
6606
+ css: opts.css ?? {},
6607
+ content: opts.content ?? {},
6608
+ variables: opts.variables ?? {},
6609
+ events: opts.events ?? {},
6610
+ ...opts.alert !== void 0 ? { alert: opts.alert } : {}
6611
+ };
6612
+ return { id, layer, module };
6613
+ }
6614
+ var IMPORT_META_KEY = "importMeta";
6615
+ function stampImportMetaMerge(module, meta) {
6616
+ const existing = module.content?.[IMPORT_META_KEY] ?? {};
6617
+ module.content = {
6618
+ ...module.content ?? {},
6619
+ [IMPORT_META_KEY]: {
6620
+ importedAt: (/* @__PURE__ */ new Date()).toISOString(),
6621
+ ...existing,
6622
+ ...meta
6623
+ }
6624
+ };
6625
+ }
6626
+ function customHtmlReviewItem(moduleId, importSourceType, settings, reason) {
6627
+ return {
6628
+ moduleId,
6629
+ // ReviewItem's `seWidget` is typed against SEWidget but the review
6630
+ // UI just reads `.type` and `.settings`. Cast through unknown is
6631
+ // the only place we touch the shape mismatch; widening the
6632
+ // ReviewItem type is a future cleanup.
6633
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6634
+ seWidget: { type: importSourceType, settings },
6635
+ status: "template",
6636
+ reason
6637
+ };
6638
+ }
6639
+
6640
+ // src/sl-import/mappers/alert-box.ts
6445
6641
  var SL_EVENT_ROUTING = {
6446
6642
  // Platform-native — provider routes (Twitch / Kick / YouTube).
6447
6643
  follow: { pickProviderKeys: "follower" },
@@ -6453,47 +6649,35 @@ var SL_EVENT_ROUTING = {
6453
6649
  // `subscriber` here is YouTube subscribe (free); `sponsor` is YouTube
6454
6650
  // channel membership; `fanfunding` is YouTube superchat. SL ships these
6455
6651
  // even on Twitch-primary accounts so we always map them.
6456
- subscriber: { keys: ["youtube-subscriber"] },
6457
- sponsor: { keys: ["youtube-member"] },
6458
- fanfunding: { keys: ["youtube-superchat"] },
6459
- super_sticker: { keys: ["youtube-supersticker"] },
6652
+ subscriber: { keys: [LumiaAlertValues2.YOUTUBE_SUBSCRIBER] },
6653
+ sponsor: { keys: [LumiaAlertValues2.YOUTUBE_MEMBER] },
6654
+ fanfunding: { keys: [LumiaAlertValues2.YOUTUBE_SUPERCHAT] },
6655
+ super_sticker: { keys: [LumiaAlertValues2.YOUTUBE_SUPERSTICKER] },
6460
6656
  // Donation / tip channels — Lumia ships dedicated alert keys for each
6461
6657
  // integration. `donation` itself fans out to `streamlabs-donation` AND
6462
6658
  // `lumiastream-donation` so the imported alert fires regardless of which
6463
6659
  // channel the donation actually came in on.
6464
- donation: { keys: ["streamlabs-donation", "lumiastream-donation"] },
6465
- eldonation: { keys: ["extralife-donation"] },
6466
- tiltifydonation: { keys: ["tiltify-campaignDonation"] },
6467
- donordrivedonation: { keys: ["donordrive-donation"] },
6468
- justgivingdonation: { keys: ["streamlabs-donation"] },
6469
- pledge: { keys: ["patreon-campaignPledge"] },
6660
+ donation: { keys: [LumiaAlertValues2.STREAMLABS_DONATION, LumiaAlertValues2.LUMIASTREAM_DONATION] },
6661
+ eldonation: { keys: [LumiaAlertValues2.EXTRALIFE_DONATION] },
6662
+ tiltifydonation: { keys: [LumiaAlertValues2.TILTIFY_DONATION] },
6663
+ donordrivedonation: { keys: [LumiaAlertValues2.DONORDRIVE_DONATION] },
6664
+ justgivingdonation: { keys: [LumiaAlertValues2.STREAMLABS_DONATION] },
6665
+ pledge: { keys: [LumiaAlertValues2.PATREON_PLEDGE] },
6470
6666
  // Charity / merch / loyalty / prime gifting — SL-flavored channels with
6471
6667
  // matching Lumia keys.
6472
- streamlabscharitydonation: { keys: ["streamlabs-charity"] },
6473
- twitchcharitydonation: { keys: ["twitch-charityDonation"] },
6474
- merch: { keys: ["streamlabs-merch"] },
6475
- loyalty_store_redemption: { keys: ["streamlabs-redemption"] },
6476
- prime_sub_gift: { keys: ["streamlabs-primegift"] },
6477
- treat: { keys: ["treatstream-treat"] },
6668
+ streamlabscharitydonation: { keys: [LumiaAlertValues2.STREAMLABS_CHARITY] },
6669
+ twitchcharitydonation: { keys: [LumiaAlertValues2.TWITCH_CHARITY_DONATION] },
6670
+ merch: { keys: [LumiaAlertValues2.STREAMLABS_MERCH] },
6671
+ loyalty_store_redemption: { keys: [LumiaAlertValues2.STREAMLABS_REDEMPTION] },
6672
+ prime_sub_gift: { keys: [LumiaAlertValues2.STREAMLABS_PRIMEGIFT] },
6673
+ treat: { keys: [LumiaAlertValues2.TREATSTREAM_TREAT] },
6478
6674
  // TikTok-flavored (jewel gift is TikTok-ish in concept).
6479
- jewel_gift: { keys: ["tiktok-gift"] }
6675
+ jewel_gift: { keys: [LumiaAlertValues2.TIKTOK_GIFT] }
6480
6676
  };
6481
6677
  function getField(settings, prefix, field) {
6482
6678
  const key = `${prefix}_${field}`;
6483
6679
  return settings[key];
6484
6680
  }
6485
- function pxToNumber(raw, fallback) {
6486
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
6487
- if (typeof raw !== "string") return fallback;
6488
- const trimmed = raw.trim();
6489
- if (!trimmed) return fallback;
6490
- if (trimmed.endsWith("px")) {
6491
- const n2 = parseFloat(trimmed.slice(0, -2));
6492
- return Number.isFinite(n2) ? n2 : fallback;
6493
- }
6494
- const n = parseFloat(trimmed);
6495
- return Number.isFinite(n) ? n : fallback;
6496
- }
6497
6681
  function translateLayout2(slLayout) {
6498
6682
  switch (slLayout) {
6499
6683
  case "above":
@@ -6603,9 +6787,7 @@ function buildBaseAlertEvent(settings, prefix, defaultMessage) {
6603
6787
  tts: {
6604
6788
  enabled: getField(settings, prefix, "tts_enabled") ?? false,
6605
6789
  voice: getField(settings, prefix, "tts_language") ?? "Brian",
6606
- volume: clampVolume(
6607
- getField(settings, prefix, "tts_volume") ?? 75
6608
- ),
6790
+ volume: clampVolume(getField(settings, prefix, "tts_volume") ?? 75),
6609
6791
  minAmount: getField(settings, prefix, "tts_min_amount") ?? 0
6610
6792
  },
6611
6793
  variations: []
@@ -6620,10 +6802,7 @@ function readSecondaryMessageConfig(settings, prefix, primaryFontFamily) {
6620
6802
  const enabled = settings[showKey];
6621
6803
  if (enabled !== true) return null;
6622
6804
  const fontFamily = getField(settings, secondaryPrefix, "message_font") ?? primaryFontFamily;
6623
- const fontSize = pxToNumber(
6624
- getField(settings, secondaryPrefix, "message_font_size"),
6625
- 20
6626
- );
6805
+ const fontSize = pxToNumber(getField(settings, secondaryPrefix, "message_font_size"), 20);
6627
6806
  const fontWeight = getField(settings, secondaryPrefix, "message_font_weight") ?? 400;
6628
6807
  const fontColor = getField(settings, secondaryPrefix, "message_font_color") ?? "#FFFFFF";
6629
6808
  return {
@@ -6650,11 +6829,20 @@ function translateSLVariationCondition(v) {
6650
6829
  const numericData = typeof data === "number" ? data : Number(data);
6651
6830
  switch (v.condition) {
6652
6831
  case "RANDOM":
6653
- return { conditionType: "RANDOM", condition: Number.isFinite(numericData) ? numericData : 100 };
6832
+ return {
6833
+ conditionType: "RANDOM",
6834
+ condition: Number.isFinite(numericData) ? numericData : 100
6835
+ };
6654
6836
  case "MIN_BITS_USED":
6655
- return { conditionType: "GREATER_NUMBER", condition: Number.isFinite(numericData) ? numericData : 0 };
6837
+ return {
6838
+ conditionType: "GREATER_NUMBER",
6839
+ condition: Number.isFinite(numericData) ? numericData : 0
6840
+ };
6656
6841
  case "EXACT_BITS_USED":
6657
- return { conditionType: "EQUAL_NUMBER", condition: Number.isFinite(numericData) ? numericData : 0 };
6842
+ return {
6843
+ conditionType: "EQUAL_NUMBER",
6844
+ condition: Number.isFinite(numericData) ? numericData : 0
6845
+ };
6658
6846
  case "MIN_MONTHS_SUBSCRIBED":
6659
6847
  return {
6660
6848
  conditionType: "SUBSCRIBED_MONTHS_GREATER",
@@ -6719,6 +6907,14 @@ function buildVariationEvent(v, parentBase) {
6719
6907
  if (s.hideAnimation) inherited.animations.alertExitAnimation = s.hideAnimation;
6720
6908
  if (s.layout) inherited.layout.value = translateLayout2(s.layout);
6721
6909
  return {
6910
+ // Reuse SL's own `uuid` when present so re-importing the same SL
6911
+ // alert-box keeps the variation identity stable across import
6912
+ // runs. SL always emits a uuid in their config (every fixture we've
6913
+ // seen has one); fall back to nanoid for the edge case of a
6914
+ // hand-crafted / older payload that omits it. Matches the SE
6915
+ // alert mapper's variation-id pattern — see Settings.tsx auto-fill
6916
+ // note there for why a stable id matters from save #1.
6917
+ id: typeof v.uuid === "string" && v.uuid.length > 0 ? v.uuid : nanoid6(),
6722
6918
  name: v.name ?? "Variation",
6723
6919
  on: v.paused !== true,
6724
6920
  randomChance: 100,
@@ -6784,11 +6980,7 @@ function buildSLAlertBox(settings, options = {}) {
6784
6980
  for (const [prefix, routing] of Object.entries(SL_EVENT_ROUTING)) {
6785
6981
  const lumiaKeys = routing.pickProviderKeys ? alertBoxKeysFor(routing.pickProviderKeys, provider) : routing.keys ?? [];
6786
6982
  if (lumiaKeys.length === 0) continue;
6787
- const built = buildBaseAlertEvent(
6788
- settings,
6789
- prefix,
6790
- DEFAULT_MESSAGES[prefix] ?? "{name}"
6791
- );
6983
+ const built = buildBaseAlertEvent(settings, prefix, DEFAULT_MESSAGES[prefix] ?? "{name}");
6792
6984
  if (!built) continue;
6793
6985
  const variationKey = VARIATION_KEY_FOR_PREFIX[prefix];
6794
6986
  const variations = variationKey ? settings[variationKey] : void 0;
@@ -6805,53 +6997,32 @@ function buildSLAlertBox(settings, options = {}) {
6805
6997
  channelsImported.push(prefix);
6806
6998
  if (firstEvent == null) firstEvent = built;
6807
6999
  }
6808
- const id = nanoid4();
6809
- const layer = {
6810
- id,
6811
- state: { visible: true, locked: false },
6812
- bounds: { x: 100, y: 100, width: 700, height: 600, scale: [1, 1] }
6813
- };
6814
7000
  const baseAlert = firstEvent ? stripEventOnlyFields(firstEvent) : {};
6815
- const module = {
6816
- id,
6817
- version: 1,
6818
- loaded: true,
6819
- settings: { type: "alert", title: "Streamlabs Alerts", locked: false },
6820
- lights: [],
6821
- css: {},
6822
- content: {},
6823
- variables: {},
6824
- events: {},
7001
+ const { layer, module } = buildModuleEnvelope({
7002
+ type: "alert",
7003
+ title: "Streamlabs Alerts",
7004
+ bounds: { x: 100, y: 100, width: 700, height: 600 },
6825
7005
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
6826
7006
  alert: { ...baseAlert, events }
6827
- };
7007
+ });
6828
7008
  return { layer, module, channelsImported };
6829
7009
  }
6830
7010
  function stripEventOnlyFields(event) {
6831
- const { on: _on, variations: _variations, matchEmptyCondition: _mec, ...base } = event;
7011
+ const {
7012
+ on: _on,
7013
+ variations: _variations,
7014
+ matchEmptyCondition: _mec,
7015
+ ...base
7016
+ } = event;
6832
7017
  return base;
6833
7018
  }
6834
7019
 
6835
7020
  // src/sl-import/mappers/tip-jar.ts
6836
- import { nanoid as nanoid5 } from "nanoid";
6837
- function pxToNumber2(raw, fallback) {
6838
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
6839
- if (typeof raw !== "string") return fallback;
6840
- const trimmed = raw.trim();
6841
- if (!trimmed) return fallback;
6842
- if (trimmed.endsWith("px")) {
6843
- const n2 = parseFloat(trimmed.slice(0, -2));
6844
- return Number.isFinite(n2) ? n2 : fallback;
6845
- }
6846
- const n = parseFloat(trimmed);
6847
- return Number.isFinite(n) ? n : fallback;
6848
- }
6849
7021
  function buildSLTipJar(settings) {
6850
- const id = nanoid5();
6851
7022
  const backgroundColor = settings.background?.color ?? "transparent";
6852
7023
  const textColor = settings.text?.color ?? "#FFFFFF";
6853
7024
  const fontFamily = settings.text?.font ?? "Open Sans";
6854
- const fontSize = pxToNumber2(settings.text?.size, 32);
7025
+ const fontSize = pxToNumber(settings.text?.size, 32);
6855
7026
  const types = settings.types ?? {};
6856
7027
  const tipsConfig = types.tips ?? {};
6857
7028
  const slEvents = {
@@ -6867,17 +7038,11 @@ function buildSLTipJar(settings) {
6867
7038
  ...TIPJAR_DEFAULT_MODULE.content.display,
6868
7039
  showEventMessages: settings.text?.show !== false
6869
7040
  };
6870
- const layer = {
6871
- id,
6872
- state: { visible: true, locked: false },
6873
- bounds: { x: 100, y: 100, width: 600, height: 700, scale: [1, 1] }
6874
- };
6875
- const module = {
6876
- id,
7041
+ const { layer, module } = buildModuleEnvelope({
7042
+ type: "tipjar",
7043
+ title: "Streamlabs Tip Jar",
7044
+ bounds: { x: 100, y: 100, width: 600, height: 700 },
6877
7045
  version: TIPJAR_DEFAULT_MODULE.version,
6878
- loaded: true,
6879
- settings: { type: "tipjar", title: "Streamlabs Tip Jar", locked: false },
6880
- lights: [],
6881
7046
  css: {
6882
7047
  fontFamily,
6883
7048
  fontSize,
@@ -6898,12 +7063,7 @@ function buildSLTipJar(settings) {
6898
7063
  source: "streamlabs",
6899
7064
  widgetType: "tip_jar",
6900
7065
  importedAt: (/* @__PURE__ */ new Date()).toISOString(),
6901
- // SL jar type doesn't have a 1:1 mapping to Lumia's preset
6902
- // vocabulary (cup-01, cup-02, …). Stashed for review.
6903
7066
  slJarType: settings.jar?.type ?? null,
6904
- // SL tier coin tiers — Lumia's sprite tiers are per-event and
6905
- // configured in the sprite editor; the user re-creates these
6906
- // manually. Stored here so they're not lost.
6907
7067
  slTipTiers: Array.isArray(tipsConfig.tiers) ? tipsConfig.tiers.map((t) => ({
6908
7068
  minimumAmount: t.minimum_amount,
6909
7069
  imageSrc: t.image_src ?? null
@@ -6911,15 +7071,12 @@ function buildSLTipJar(settings) {
6911
7071
  slMinimumTipAmount: tipsConfig.minimum_amount ?? null,
6912
7072
  slHadCustomHtml: settings.custom_html_enabled === true
6913
7073
  }
6914
- },
6915
- variables: {},
6916
- events: {}
6917
- };
7074
+ }
7075
+ });
6918
7076
  return { layer, module };
6919
7077
  }
6920
7078
 
6921
7079
  // src/sl-import/mappers/goal.ts
6922
- import { nanoid as nanoid6 } from "nanoid";
6923
7080
  var GOAL_SOURCE_MAP = {
6924
7081
  follower_goal: "follower",
6925
7082
  sub_goal: "subscriber",
@@ -6932,69 +7089,262 @@ var GOAL_TITLE_MAP = {
6932
7089
  donation_goal: "Donation Goal",
6933
7090
  bit_goal: "Bits Goal"
6934
7091
  };
6935
- function pxToNumber3(raw, fallback) {
6936
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
6937
- if (typeof raw !== "string") return fallback;
6938
- const trimmed = raw.trim();
6939
- if (!trimmed) return fallback;
6940
- if (trimmed.endsWith("px")) {
6941
- const n2 = parseFloat(trimmed.slice(0, -2));
6942
- return Number.isFinite(n2) ? n2 : fallback;
6943
- }
6944
- const n = parseFloat(trimmed);
6945
- return Number.isFinite(n) ? n : fallback;
6946
- }
7092
+ var GOAL_CURRENT_TEMPLATE = {
7093
+ follower_goal: "{{twitch_total_follower_count}}",
7094
+ sub_goal: "{{twitch_total_subscriber_count}}",
7095
+ donation_goal: "{{total_donation_amount}}",
7096
+ bit_goal: "{{twitch_total_bits_count}}"
7097
+ };
6947
7098
  function buildSLGoal(settings, widgetType) {
6948
- const id = nanoid6();
6949
7099
  const goalSource = GOAL_SOURCE_MAP[widgetType] ?? "follower";
6950
7100
  const defaultTitle = GOAL_TITLE_MAP[widgetType] ?? "Goal";
7101
+ const currentTemplate = GOAL_CURRENT_TEMPLATE[widgetType] ?? "";
7102
+ const requiresReview = settings.custom_enabled === true;
6951
7103
  const title = typeof settings.title === "string" ? settings.title : defaultTitle;
6952
7104
  const goalAmount = typeof settings.goal === "number" ? settings.goal : 100;
6953
7105
  const currentAmount = typeof settings.current === "number" ? settings.current : 0;
6954
7106
  const startingAmount = typeof settings.starting_amount === "number" ? settings.starting_amount : 0;
6955
- const endsAt = typeof settings.ends_at === "string" ? settings.ends_at : null;
7107
+ const endsAt = typeof settings.ends_at === "string" ? settings.ends_at : "";
6956
7108
  const barColor = settings.bar_color ?? "#32C3A6";
6957
- const barBackgroundColor = settings.bar_background_color ?? "#181818";
7109
+ const barBgColor = settings.bar_bg_color ?? "#181818";
7110
+ const widgetBgColor = settings.background_color ?? "transparent";
6958
7111
  const textColor = settings.text_color ?? "#FFFFFF";
6959
7112
  const barTextColor = settings.bar_text_color ?? textColor;
6960
7113
  const fontFamily = settings.font ?? "Open Sans";
6961
- const layer = {
6962
- id,
6963
- state: { visible: true, locked: false },
6964
- bounds: { x: 100, y: 100, width: 740, height: 90, scale: [1, 1] }
6965
- };
6966
- const module = {
6967
- id,
6968
- version: 1,
6969
- loaded: true,
6970
- settings: { type: "goal", title, locked: false },
6971
- lights: [],
7114
+ const fontSize = pxToNumber(settings.font_size, 24);
7115
+ const barThickness = pxToNumber(settings.bar_thickness, 48);
7116
+ const layerHeight = Math.max(60, barThickness + 32);
7117
+ const { layer, module } = buildModuleEnvelope({
7118
+ type: "goal",
7119
+ title,
7120
+ bounds: { x: 100, y: 100, width: 740, height: layerHeight },
6972
7121
  css: {
6973
7122
  fontFamily,
6974
7123
  color: textColor,
6975
- background: barBackgroundColor
7124
+ background: widgetBgColor,
7125
+ fontSize
6976
7126
  },
7127
+ // Use Lumia's canonical goal field names — `targetGoal` (string),
7128
+ // `current` (template string), `display`, `filledColor` /
7129
+ // `unfilledColor`. The prior schema used invented field names
7130
+ // (`goalAmount`, `barColor`) that the renderer ignored.
6977
7131
  content: {
6978
- title,
6979
- goalSource,
6980
- goalAmount,
6981
- currentAmount,
6982
- startingAmount,
6983
- endsAt,
6984
- barColor,
6985
- barBackgroundColor,
6986
- barTextColor,
6987
- fontFamily,
6988
- fontSize: pxToNumber3(settings.font_size, 24)
7132
+ version: 1,
7133
+ type: "bar",
7134
+ label: title,
7135
+ subLabel: "",
7136
+ display: "{{current}} / {{goal}}",
7137
+ current: currentAmount > 0 ? String(currentAmount) : currentTemplate,
7138
+ targetGoal: String(goalAmount),
7139
+ image: "",
7140
+ audio: {
7141
+ decrement: "",
7142
+ increment: "",
7143
+ goal: "",
7144
+ goalFailed: "",
7145
+ volume: 1
7146
+ },
7147
+ endDate: endsAt,
7148
+ goalAnimation: "",
7149
+ goalAnimationLoop: true,
7150
+ unfilledColor: barBgColor,
7151
+ filledColor: barColor,
7152
+ alignItemsToEdges: false,
7153
+ border: "solid 1px transparent",
7154
+ borderRadius: "40px",
7155
+ highlightColor: "inherit",
7156
+ importMeta: {
7157
+ source: "streamlabs",
7158
+ widgetType,
7159
+ importedAt: (/* @__PURE__ */ new Date()).toISOString(),
7160
+ goalSource,
7161
+ slLayout: settings.layout ?? null,
7162
+ slBarTextColor: barTextColor,
7163
+ slBarThickness: barThickness,
7164
+ slStartingAmount: startingAmount,
7165
+ slHadCustomHtml: settings.custom_enabled === true
7166
+ }
7167
+ }
7168
+ });
7169
+ return { layer, module, requiresReview };
7170
+ }
7171
+
7172
+ // src/sl-import/mappers/chat-box.ts
7173
+ function buildSitesArray(settings) {
7174
+ const defaultSites = [...CHATBOX_DEFAULT_MODULE.content.sites];
7175
+ const drop = /* @__PURE__ */ new Set();
7176
+ if (settings.show_bttv_emotes === false) drop.add("bttv");
7177
+ if (settings.show_franker_emotes === false) drop.add("ffz");
7178
+ if (settings.show_7tv_emotes === false) drop.add("seventv");
7179
+ if (settings.twitch_enabled === false) drop.add("twitch");
7180
+ if (settings.youtube_enabled === false) drop.add("youtube");
7181
+ if (settings.facebook_enabled === false) drop.add("facebook");
7182
+ if (settings.tiktok_enabled === false) drop.add("tiktok");
7183
+ if (settings.kick_enabled === false) drop.add("kick");
7184
+ if (drop.size === 0) return defaultSites;
7185
+ return defaultSites.filter((s) => !drop.has(s));
7186
+ }
7187
+ function buildSLChatBox(settings) {
7188
+ const requiresReview = settings.custom_enabled === true;
7189
+ const backgroundColor = settings.background_color ?? "transparent";
7190
+ const textColor = settings.text_color ?? "#FFFFFF";
7191
+ const textSize = pxToNumber(settings.text_size, 16);
7192
+ const fadeOutAfterDelay = settings.always_show_messages !== true;
7193
+ const fadeOutDelayTime = fadeOutAfterDelay ? Math.max(1, Math.round((settings.message_hide_delay ?? 15e3) / 1e3)) : 0;
7194
+ const sites = buildSitesArray(settings);
7195
+ const { layer, module } = buildModuleEnvelope({
7196
+ type: "chatbox",
7197
+ title: "Streamlabs Chat Box",
7198
+ bounds: { x: 100, y: 100, width: 375, height: 470 },
7199
+ version: CHATBOX_DEFAULT_MODULE.version,
7200
+ css: {
7201
+ background: backgroundColor,
7202
+ color: textColor,
7203
+ fontSize: textSize
6989
7204
  },
6990
- variables: {},
6991
- events: {}
6992
- };
6993
- return { layer, module };
7205
+ content: {
7206
+ // Spread canonical defaults FIRST so the renderer's dereferences
7207
+ // (chatboxStreamingSite.<platform>.themeConfig, animations,
7208
+ // chatboxStreamingSite.<platform>.message, etc.) all exist.
7209
+ ...CHATBOX_DEFAULT_MODULE.content,
7210
+ // Then override the bits SL controls.
7211
+ sites,
7212
+ fadeOutAfterDelay,
7213
+ fadeOutDelayTime,
7214
+ // SL keeps history forever by default — `always_show_messages`
7215
+ // implies the streamer wants a permanent scrollback. Lumia
7216
+ // exposes that as `permanent: true`.
7217
+ permanent: settings.always_show_messages === true,
7218
+ // Preserve the SL-specific provenance for the review step.
7219
+ importMeta: {
7220
+ source: "streamlabs",
7221
+ widgetType: "chat_box",
7222
+ importedAt: (/* @__PURE__ */ new Date()).toISOString(),
7223
+ slTheme: settings.theme ?? null,
7224
+ slHadCustomHtml: settings.custom_enabled === true,
7225
+ slHidesCommonBots: settings.hide_common_chat_bots === true,
7226
+ slHidesCommands: settings.hide_commands === true,
7227
+ slMutedChatters: settings.muted_chatters ?? "",
7228
+ slMessageShowDelayMs: settings.message_show_delay ?? 0
7229
+ }
7230
+ }
7231
+ });
7232
+ return { layer, module, requiresReview };
7233
+ }
7234
+
7235
+ // src/sl-import/mappers/event-list.ts
7236
+ var SL_SHOW_FLAG_TO_LUMIA_FILTER = {
7237
+ show_follows: "follower",
7238
+ show_subscriptions: "subscribers",
7239
+ show_resubs: "subscribers",
7240
+ show_sub_gifts: "gifts",
7241
+ show_subscribers: "subscribers",
7242
+ show_sponsors: "subscribers",
7243
+ // YouTube channel members
7244
+ show_sponsor_gifts: "gifts",
7245
+ show_bits: "bits",
7246
+ show_donations: "donation",
7247
+ show_pledges: "donation",
7248
+ // Patreon
7249
+ show_eldonations: "donation",
7250
+ show_tiltifydonations: "donation",
7251
+ show_donordrivedonations: "donation",
7252
+ show_justgivingdonations: "donation",
7253
+ show_streamlabscharitydonations: "donation",
7254
+ show_twitchcharitydonations: "donation",
7255
+ show_twitch_charity: "donation",
7256
+ show_treats: "donation",
7257
+ // TreatStream
7258
+ show_merch: "purchases",
7259
+ show_raids: "raids",
7260
+ show_hosts: "raids",
7261
+ show_redemptions: "redemption",
7262
+ show_fanfundings: "superchats",
7263
+ // YouTube Super Chat
7264
+ show_super_stickers: "superstickers",
7265
+ show_jewel_gifts: "stars"
7266
+ // TikTok-flavored, closest analogue
7267
+ };
7268
+ function buildFilters(settings) {
7269
+ const drop = /* @__PURE__ */ new Set();
7270
+ for (const [slFlag, lumiaCategory] of Object.entries(SL_SHOW_FLAG_TO_LUMIA_FILTER)) {
7271
+ if (settings[slFlag] === false) {
7272
+ drop.add(lumiaCategory);
7273
+ }
7274
+ }
7275
+ return Array.from(drop);
7276
+ }
7277
+ function buildSLEventList(settings) {
7278
+ const requiresReview = settings.custom_enabled === true;
7279
+ const backgroundColor = settings.background_color ?? "transparent";
7280
+ const textColor = settings.text_color ?? "#FFFFFF";
7281
+ const textSize = pxToNumber(settings.text_size, 16);
7282
+ const fontFamily = settings.font_family ?? "Roboto";
7283
+ const maxEvents = typeof settings.max_events === "number" && settings.max_events > 0 ? settings.max_events : EVENTLIST_DEFAULT_MODULE.content.maxItemsToShow;
7284
+ const enterAnimation = settings.show_animation ?? "fadeIn";
7285
+ const exitAnimation = settings.hide_animation ?? "fadeOut";
7286
+ const animationDuration = typeof settings.animation_speed === "number" ? settings.animation_speed : 1e3;
7287
+ const filters = buildFilters(settings);
7288
+ const { layer, module } = buildModuleEnvelope({
7289
+ type: "eventlist",
7290
+ title: "Streamlabs Event List",
7291
+ bounds: { x: 100, y: 100, width: 375, height: 470 },
7292
+ version: EVENTLIST_DEFAULT_MODULE.version,
7293
+ css: {
7294
+ ...EVENTLIST_DEFAULT_MODULE.css,
7295
+ background: backgroundColor,
7296
+ color: textColor,
7297
+ fontSize: textSize,
7298
+ fontFamily
7299
+ },
7300
+ content: {
7301
+ // Spread canonical defaults FIRST so the renderer's dereferences
7302
+ // don't crash on missing nested objects.
7303
+ ...EVENTLIST_DEFAULT_MODULE.content,
7304
+ maxItemsToShow: maxEvents,
7305
+ // SL's `flip_y` corresponds to Lumia's `reverse` (newest item
7306
+ // at the bottom vs the top). SL's `flip_x` flips the alignment
7307
+ // to right; Lumia covers that via css.textAlign but doesn't
7308
+ // have a per-module horizontal flip — stash as importMeta for
7309
+ // the review step.
7310
+ reverse: settings.flip_y === true,
7311
+ // SL's `keep_history: false` means events disappear after the
7312
+ // fade time; map to `fadeOutAfterDelay: true` with the fade
7313
+ // time SL configured.
7314
+ fadeOutAfterDelay: settings.keep_history === false,
7315
+ fadeOutDelayTime: typeof settings.fade_time === "number" && settings.fade_time > 0 ? Math.max(1, Math.round(settings.fade_time / 1e3)) : EVENTLIST_DEFAULT_MODULE.content.fadeOutDelayTime,
7316
+ animations: {
7317
+ ...EVENTLIST_DEFAULT_MODULE.content.animations,
7318
+ enterAnimation,
7319
+ exitAnimation,
7320
+ enterAnimationDuration: animationDuration,
7321
+ exitAnimationDuration: animationDuration
7322
+ },
7323
+ filters,
7324
+ importMeta: {
7325
+ source: "streamlabs",
7326
+ widgetType: "event_list",
7327
+ importedAt: (/* @__PURE__ */ new Date()).toISOString(),
7328
+ slTheme: settings.theme ?? null,
7329
+ slThemeColor: settings.theme_color ?? null,
7330
+ slFlipX: settings.flip_x === true,
7331
+ slHadCustomHtml: settings.custom_enabled === true,
7332
+ // Per-event-type minimums SL filters by — Lumia's eventlist
7333
+ // has no equivalent thresholding; preserve so the streamer
7334
+ // can recreate via the alert module's variation conditions.
7335
+ slHostViewerMinimum: settings.host_viewer_minimum ?? 0,
7336
+ slBitsMinimum: settings.bits_minimum ?? 0,
7337
+ slRaidRaiderMinimum: settings.raid_raider_minimum ?? 0
7338
+ }
7339
+ }
7340
+ });
7341
+ return { layer, module, requiresReview };
6994
7342
  }
6995
7343
 
6996
7344
  // src/sl-import/sl-dispatch.ts
6997
- var IMPORT_META_KEY = "importMeta";
7345
+ function buildSLDescription(widgetType, sourceUrl) {
7346
+ return buildImportDescription(`Imported from Streamlabs ${widgetType}`, sourceUrl);
7347
+ }
6998
7348
  var GOAL_WIDGET_TYPES = /* @__PURE__ */ new Set([
6999
7349
  "follower_goal",
7000
7350
  "sub_goal",
@@ -7002,8 +7352,6 @@ var GOAL_WIDGET_TYPES = /* @__PURE__ */ new Set([
7002
7352
  "bit_goal"
7003
7353
  ]);
7004
7354
  var PLACEHOLDER_WIDGET_TYPES = /* @__PURE__ */ new Set([
7005
- "chat_box",
7006
- "event_list",
7007
7355
  "donation_ticker",
7008
7356
  "stream_boss",
7009
7357
  "credits",
@@ -7013,8 +7361,6 @@ var PLACEHOLDER_WIDGET_TYPES = /* @__PURE__ */ new Set([
7013
7361
  "streamlabels"
7014
7362
  ]);
7015
7363
  var PLACEHOLDER_REASONS = {
7016
- chat_box: "Lumia has a native chatbox module \u2014 set it up directly rather than importing the SL config.",
7017
- event_list: "Lumia has a native eventlist module \u2014 set it up directly rather than importing the SL config.",
7018
7364
  donation_ticker: "No native Lumia equivalent yet \u2014 imported as a placeholder text layer.",
7019
7365
  stream_boss: "Lumia has a native streamboss module \u2014 set it up directly rather than importing the SL config.",
7020
7366
  credits: "Lumia has a native credits module \u2014 set it up directly rather than importing the SL config.",
@@ -7023,19 +7369,14 @@ var PLACEHOLDER_REASONS = {
7023
7369
  sub_train: "No native Lumia equivalent yet \u2014 imported as a placeholder text layer.",
7024
7370
  streamlabels: "Use Lumia template variables (e.g. {{twitch_last_follower}}) inside a Text module instead of importing the SL streamlabels config."
7025
7371
  };
7372
+ var CUSTOM_HTML_REVIEW_REASON = "The Streamlabs widget used custom HTML/CSS/JS that Lumia cannot host. The native Lumia module is the right starting point \u2014 recreate the styling there. The original SL settings are stashed under module.content.importMeta for reference.";
7026
7373
  function stampImportMeta(module, widgetType, role, extra = {}) {
7027
- const existing = module.content?.[IMPORT_META_KEY] ?? {};
7028
- module.content = {
7029
- ...module.content ?? {},
7030
- [IMPORT_META_KEY]: {
7031
- source: "streamlabs",
7032
- widgetType,
7033
- role,
7034
- importedAt: (/* @__PURE__ */ new Date()).toISOString(),
7035
- ...existing,
7036
- ...extra
7037
- }
7038
- };
7374
+ stampImportMetaMerge(module, {
7375
+ source: "streamlabs",
7376
+ widgetType,
7377
+ role,
7378
+ ...extra
7379
+ });
7039
7380
  }
7040
7381
  function buildPlaceholder(widgetType) {
7041
7382
  const id = nanoid7();
@@ -7067,7 +7408,7 @@ function buildPlaceholder(widgetType) {
7067
7408
  };
7068
7409
  return { layer, module };
7069
7410
  }
7070
- function importStreamlabsWidget(response, widgetType) {
7411
+ function importStreamlabsWidget(response, widgetType, options = {}) {
7071
7412
  if (!isSLWidgetConfigResponse(response)) {
7072
7413
  throw new Error(
7073
7414
  "Streamlabs response is missing the expected `settings` envelope. The widget may be private or the response format may have changed."
@@ -7117,9 +7458,61 @@ function importStreamlabsWidget(response, widgetType) {
7117
7458
  coverage.mappings.push({
7118
7459
  seType: `streamlabs:${widgetType}`,
7119
7460
  lumiaType: "goal",
7120
- status: "direct",
7461
+ status: built.requiresReview ? "partial" : "direct",
7121
7462
  count: 1
7122
7463
  });
7464
+ if (built.requiresReview) {
7465
+ reviewItems.push(
7466
+ customHtmlReviewItem(
7467
+ built.module.id,
7468
+ `streamlabs-${widgetType}`,
7469
+ response.settings,
7470
+ CUSTOM_HTML_REVIEW_REASON
7471
+ )
7472
+ );
7473
+ }
7474
+ } else if (widgetType === "chat_box") {
7475
+ const built = buildSLChatBox(response.settings);
7476
+ stampImportMeta(built.module, widgetType, "chatbox");
7477
+ layers.push(built.layer);
7478
+ modules[built.layer.id] = built.module;
7479
+ coverage.mappings.push({
7480
+ seType: "streamlabs:chat_box",
7481
+ lumiaType: "chatbox",
7482
+ status: built.requiresReview ? "partial" : "direct",
7483
+ count: 1
7484
+ });
7485
+ if (built.requiresReview) {
7486
+ reviewItems.push(
7487
+ customHtmlReviewItem(
7488
+ built.module.id,
7489
+ "streamlabs-chat_box",
7490
+ response.settings,
7491
+ CUSTOM_HTML_REVIEW_REASON
7492
+ )
7493
+ );
7494
+ }
7495
+ } else if (widgetType === "event_list") {
7496
+ const built = buildSLEventList(response.settings);
7497
+ stampImportMeta(built.module, widgetType, "eventlist");
7498
+ layers.push(built.layer);
7499
+ modules[built.layer.id] = built.module;
7500
+ coverage.mappings.push({
7501
+ seType: "streamlabs:event_list",
7502
+ lumiaType: "eventlist",
7503
+ status: built.requiresReview ? "partial" : "direct",
7504
+ count: 1
7505
+ });
7506
+ if (built.requiresReview) {
7507
+ reviewItems.push(
7508
+ customHtmlReviewItem(
7509
+ built.module.id,
7510
+ "streamlabs-event_list",
7511
+ response.settings,
7512
+ CUSTOM_HTML_REVIEW_REASON
7513
+ )
7514
+ );
7515
+ }
7123
7516
  } else if (PLACEHOLDER_WIDGET_TYPES.has(widgetType)) {
7124
7517
  const placeholder = buildPlaceholder(widgetType);
7125
7518
  const reason = PLACEHOLDER_REASONS[widgetType] ?? "No native Lumia mapping yet.";
@@ -7168,7 +7561,7 @@ function importStreamlabsWidget(response, widgetType) {
7168
7561
  uuid: "",
7169
7562
  listen_id: "",
7170
7563
  name: overlayName,
7171
- description: `Imported from Streamlabs ${widgetType}`,
7564
+ description: buildSLDescription(widgetType, options.sourceUrl),
7172
7565
  settings: {
7173
7566
  metadata: { width: 1920, height: 1080 },
7174
7567
  layers,
@@ -8752,7 +9145,12 @@ function SEImportWizard({
8752
9145
  );
8753
9146
  }
8754
9147
  importedOverlays.push({
8755
- result: importSEBootstrap(bootstrap),
9148
+ result: importSEBootstrap(bootstrap, {
9149
+ // JWT-flow imports don't come from a pasted URL; synthesize
9150
+ // the SE dashboard URL so the description still carries a
9151
+ // useful traceback. Same shape SE editor links use.
9152
+ sourceUrl: `https://streamelements.com/dashboard/overlays/${overlayId}/editor`
9153
+ }),
8756
9154
  summary
8757
9155
  });
8758
9156
  }
@@ -8797,7 +9195,9 @@ function SEImportWizard({
8797
9195
  "StreamElements returned a payload that doesn't look like an overlay bootstrap."
8798
9196
  );
8799
9197
  }
8800
- const imported = importSEBootstrap(bootstrap);
9198
+ const imported = importSEBootstrap(bootstrap, {
9199
+ sourceUrl: `https://streamelements.com/dashboard/overlays/${overlayId}/editor`
9200
+ });
8801
9201
  setBatchImports([{ result: imported, summary }]);
8802
9202
  setResult(imported);
8803
9203
  setAssetUrls(findSEAssetURLs(imported.overlay));
@@ -8885,7 +9285,9 @@ function SEImportWizard({
8885
9285
  "Streamlabs returned a response that does not look like a widget config payload."
8886
9286
  );
8887
9287
  }
8888
- const imported = importStreamlabsWidget(raw, slParts.widgetType);
9288
+ const imported = importStreamlabsWidget(raw, slParts.widgetType, {
9289
+ sourceUrl: url
9290
+ });
8889
9291
  setBatchImports([]);
8890
9292
  setResult(imported);
8891
9293
  setAssetUrls(findSEAssetURLs(imported.overlay));
@@ -8926,7 +9328,7 @@ function SEImportWizard({
8926
9328
  "StreamElements returned a response that does not look like an Element widget payload."
8927
9329
  );
8928
9330
  }
8929
- const imported = importElementOverlay(raw);
9331
+ const imported = importElementOverlay(raw, { sourceUrl: url });
8930
9332
  setBatchImports([]);
8931
9333
  setResult(imported);
8932
9334
  setAssetUrls(findSEAssetURLs(imported.overlay));
@@ -8974,7 +9376,7 @@ function SEImportWizard({
8974
9376
  ...parts,
8975
9377
  proxyFetch: bindings.proxyFetch
8976
9378
  });
8977
- const imported = importSEBootstrap(bootstrap);
9379
+ const imported = importSEBootstrap(bootstrap, { sourceUrl: url });
8978
9380
  setBatchImports([]);
8979
9381
  setResult(imported);
8980
9382
  setAssetUrls(findSEAssetURLs(imported.overlay));
@@ -9633,7 +10035,7 @@ function extractErrorMessage(err) {
9633
10035
  }
9634
10036
 
9635
10037
  // src/se-import/element-dispatch.ts
9636
- import { nanoid as nanoid9 } from "nanoid";
10038
+ import { LumiaAlertValues as LumiaAlertValues3 } from "@lumiastream/lumia-types";
9637
10039
 
9638
10040
  // src/se-import/element-types.ts
9639
10041
  function isSEElementResponse(value) {
@@ -9877,34 +10279,8 @@ function translateAnimationName(name, fallback) {
9877
10279
  if (PINNED_ANIMATION_MAP[name]) return PINNED_ANIMATION_MAP[name];
9878
10280
  return name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
9879
10281
  }
9880
- function parseAnimationDurationMs(raw, fallbackMs) {
9881
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
9882
- if (typeof raw !== "string") return fallbackMs;
9883
- const trimmed = raw.trim();
9884
- if (!trimmed) return fallbackMs;
9885
- if (trimmed.endsWith("ms")) {
9886
- const n2 = parseFloat(trimmed.slice(0, -2));
9887
- return Number.isFinite(n2) ? n2 : fallbackMs;
9888
- }
9889
- if (trimmed.endsWith("s")) {
9890
- const n2 = parseFloat(trimmed.slice(0, -1));
9891
- return Number.isFinite(n2) ? n2 * 1e3 : fallbackMs;
9892
- }
9893
- const n = parseFloat(trimmed);
9894
- return Number.isFinite(n) ? n : fallbackMs;
9895
- }
9896
- function parsePx2(raw) {
9897
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
9898
- if (typeof raw !== "string") return NaN;
9899
- const trimmed = raw.trim();
9900
- if (!trimmed) return NaN;
9901
- if (trimmed.endsWith("px")) {
9902
- const n2 = parseFloat(trimmed.slice(0, -2));
9903
- return Number.isFinite(n2) ? n2 : NaN;
9904
- }
9905
- const n = parseFloat(trimmed);
9906
- return Number.isFinite(n) ? n : NaN;
9907
- }
10282
+ var parseAnimationDurationMs = parseDurationMs;
10283
+ var parsePx2 = (raw) => pxToNumber(raw, NaN);
9908
10284
  function findFieldByType(fields, type) {
9909
10285
  if (!fields) return void 0;
9910
10286
  for (const f of Object.values(fields)) {
@@ -9980,6 +10356,47 @@ function buildAnimations(title) {
9980
10356
  const visibleDurationMs = enterDelay + enterDuration + staticDuration + exitDuration;
9981
10357
  return { animations, visibleDurationMs: visibleDurationMs || DEFAULT_DURATION_MS };
9982
10358
  }
10359
+ function resolveFieldBoundsInCanvas(style, canvas, textContent) {
10360
+ if (!style) return null;
10361
+ const x = parsePx2(style.left);
10362
+ const y = parsePx2(style.top);
10363
+ const hasXY = Number.isFinite(x) || Number.isFinite(y);
10364
+ if (!hasXY && style.width == null && style.height == null) return null;
10365
+ const sx = Number.isFinite(x) ? x : 0;
10366
+ const sy = Number.isFinite(y) ? y : 0;
10367
+ let width = parsePx2(style.width);
10368
+ let height = parsePx2(style.height);
10369
+ if (!Number.isFinite(width)) width = Math.max(1, canvas.width - sx);
10370
+ if (!Number.isFinite(height)) {
10371
+ if (typeof textContent === "string") {
10372
+ const lineCount = (textContent.match(/\n/g)?.length ?? 0) + 1;
10373
+ const lhPx = parsePx2(style.lineHeight) || parsePx2(style["line-height"]);
10374
+ const fsPx = parsePx2(style.fontSize);
10375
+ let effectiveLh = lhPx;
10376
+ if (!Number.isFinite(effectiveLh) || effectiveLh < 8) {
10377
+ effectiveLh = Number.isFinite(fsPx) ? fsPx * 1.3 : 24 * 1.3;
10378
+ }
10379
+ height = Math.max(1, lineCount * effectiveLh);
10380
+ } else {
10381
+ height = Math.max(1, canvas.height - sy);
10382
+ }
10383
+ }
10384
+ return { x: sx, y: sy, width, height };
10385
+ }
10386
+ function unionBounds(boxes) {
10387
+ if (boxes.length === 0) return null;
10388
+ let minX = Infinity;
10389
+ let minY = Infinity;
10390
+ let maxX = -Infinity;
10391
+ let maxY = -Infinity;
10392
+ for (const b of boxes) {
10393
+ minX = Math.min(minX, b.x);
10394
+ minY = Math.min(minY, b.y);
10395
+ maxX = Math.max(maxX, b.x + b.width);
10396
+ maxY = Math.max(maxY, b.y + b.height);
10397
+ }
10398
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
10399
+ }
9983
10400
  function buildElementAlertEvent(state, options = {}) {
9984
10401
  const compositeFields = state.settings?.compositeFields;
9985
10402
  const titleField = findFieldByType(compositeFields, "text");
@@ -9997,6 +10414,31 @@ function buildElementAlertEvent(state, options = {}) {
9997
10414
  const titleStyle = titleField?.style;
9998
10415
  const highlightColor = titleField?.highlightedStyle?.color ?? null;
9999
10416
  const { animations, visibleDurationMs } = buildAnimations(titleField);
10417
+ const canvas = options.canvas ?? { width: 1920, height: 1080 };
10418
+ const mediaBounds = resolveFieldBoundsInCanvas(mainAsset?.style, canvas);
10419
+ const textBounds = resolveFieldBoundsInCanvas(titleStyle, canvas, rawMessage);
10420
+ const collected = [];
10421
+ if (mediaBounds) collected.push(mediaBounds);
10422
+ if (textBounds) collected.push(textBounds);
10423
+ let layerBounds = unionBounds(collected);
10424
+ if (!layerBounds) {
10425
+ layerBounds = {
10426
+ x: Math.round(canvas.width * 0.6),
10427
+ y: Math.round(canvas.height * 0.15),
10428
+ width: 400,
10429
+ height: 300
10430
+ };
10431
+ }
10432
+ let pushTextUp = 0;
10433
+ let pushTextLeft = 0;
10434
+ if (textBounds) {
10435
+ const desiredTextTopInLayer = textBounds.y - layerBounds.y;
10436
+ const naturalTextTopInLayer = Math.max(0, (layerBounds.height - textBounds.height) / 2);
10437
+ pushTextUp = Math.round(desiredTextTopInLayer - naturalTextTopInLayer);
10438
+ const desiredTextLeftInLayer = textBounds.x - layerBounds.x;
10439
+ const naturalTextLeftInLayer = Math.max(0, (layerBounds.width - textBounds.width) / 2);
10440
+ pushTextLeft = Math.round(desiredTextLeftInLayer - naturalTextLeftInLayer);
10441
+ }
10000
10442
  const event = {
10001
10443
  on: true,
10002
10444
  // `'custom'` lowercase is load-bearing in Lumia's AlertBoxHandler —
@@ -10015,7 +10457,11 @@ function buildElementAlertEvent(state, options = {}) {
10015
10457
  showBrandIcon: true
10016
10458
  },
10017
10459
  settings: { duration: visibleDurationMs },
10018
- layout: { value: "column", css: {}, customCss: "", pushTextUp: 0, pushTextLeft: 0 },
10460
+ // `textOver` matches SE Element's text-on-video render pattern.
10461
+ // `imageOver` / `column` / `row` would all stack the text outside
10462
+ // the media, which is the bug this layout fixes (text rendered
10463
+ // below the alert graphic instead of overlaid on it).
10464
+ layout: { value: "textOver", css: {}, customCss: "", pushTextUp, pushTextLeft },
10019
10465
  text: {
10020
10466
  css: titleCss(titleStyle),
10021
10467
  messageCss: {},
@@ -10037,44 +10483,13 @@ function buildElementAlertEvent(state, options = {}) {
10037
10483
  },
10038
10484
  variations: []
10039
10485
  };
10040
- const style = mainAsset?.style;
10041
- let bounds;
10042
- if (style) {
10043
- const x = parsePx2(style.left);
10044
- const y = parsePx2(style.top);
10045
- const width = parsePx2(style.width);
10046
- const height = parsePx2(style.height);
10047
- if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(width)) {
10048
- bounds = {
10049
- x,
10050
- y,
10051
- width,
10052
- // Height isn't always present on video Main Assets (the video
10053
- // aspect-ratios itself from the source). Fall back to a
10054
- // reasonable default that matches SE's preview row.
10055
- height: Number.isFinite(height) ? height : Math.round(width * 0.75)
10056
- };
10057
- }
10058
- }
10059
- return { event, bounds };
10486
+ return { event, bounds: layerBounds };
10060
10487
  }
10061
10488
 
10062
10489
  // src/se-import/mappers/element-scene.ts
10063
- import { nanoid as nanoid8 } from "nanoid";
10064
10490
  var TEXT_FALLBACK = { width: 400, height: 50 };
10065
10491
  var IMAGE_FALLBACK = { width: 300, height: 100 };
10066
- function parsePx3(raw) {
10067
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
10068
- if (typeof raw !== "string") return NaN;
10069
- const trimmed = raw.trim();
10070
- if (!trimmed) return NaN;
10071
- if (trimmed.endsWith("px")) {
10072
- const n2 = parseFloat(trimmed.slice(0, -2));
10073
- return Number.isFinite(n2) ? n2 : NaN;
10074
- }
10075
- const n = parseFloat(trimmed);
10076
- return Number.isFinite(n) ? n : NaN;
10077
- }
10492
+ var parsePx3 = (raw) => pxToNumber(raw, NaN);
10078
10493
  function styleToBounds(style, type, canvas) {
10079
10494
  const x = Number.isFinite(parsePx3(style?.left)) ? parsePx3(style?.left) : 0;
10080
10495
  const y = Number.isFinite(parsePx3(style?.top)) ? parsePx3(style?.top) : 0;
@@ -10110,33 +10525,17 @@ function styleToBounds(style, type, canvas) {
10110
10525
  }
10111
10526
  return { x, y, width, height };
10112
10527
  }
10113
- function buildLayer(id, groupId, bounds, zIndex) {
10114
- return {
10115
- id,
10116
- group: groupId,
10117
- state: { visible: true, locked: false },
10118
- bounds: {
10119
- x: bounds.x,
10120
- y: bounds.y,
10121
- width: bounds.width,
10122
- height: bounds.height,
10123
- scale: [1, 1],
10124
- zIndex
10125
- }
10126
- };
10127
- }
10128
- function buildModule(id, type, title, content, css = {}) {
10129
- return {
10130
- id,
10131
- version: 1,
10132
- loaded: true,
10133
- settings: { type, title, locked: false },
10134
- lights: [],
10135
- css,
10528
+ function buildChild(groupId, zIndex, bounds, type, title, content, css = {}) {
10529
+ const { layer, module } = buildModuleEnvelope({
10530
+ type,
10531
+ title,
10532
+ bounds,
10136
10533
  content,
10137
- variables: {},
10138
- events: {}
10139
- };
10534
+ css,
10535
+ parentGroupId: groupId,
10536
+ zIndex
10537
+ });
10538
+ return { layer, module };
10140
10539
  }
10141
10540
  function textStyleToCss(style) {
10142
10541
  const s = style ?? {};
@@ -10151,80 +10550,69 @@ function textStyleToCss(style) {
10151
10550
  return out;
10152
10551
  }
10153
10552
  function mapCompositeField(name, field, groupId, zIndex, provider, canvas) {
10154
- const id = nanoid8();
10155
10553
  switch (field.type) {
10156
10554
  case "video": {
10157
- const bounds = styleToBounds(field.style, "video", canvas);
10158
- const layer = buildLayer(id, groupId, bounds, zIndex);
10159
10555
  const src = pickFieldMediaHref(field.media, "video/");
10160
10556
  const volume = typeof field.volume === "number" ? field.volume : 1;
10161
- const module = buildModule(id, "video", name, {
10162
- src,
10163
- volume,
10164
- loop: true,
10165
- autoplay: true,
10166
- muted: volume === 0
10167
- });
10168
- return { layer, module };
10557
+ return buildChild(
10558
+ groupId,
10559
+ zIndex,
10560
+ styleToBounds(field.style, "video", canvas),
10561
+ "video",
10562
+ name,
10563
+ { src, volume, loop: true, autoplay: true, muted: volume === 0 }
10564
+ );
10169
10565
  }
10170
10566
  case "image": {
10171
- const bounds = styleToBounds(field.style, "image", canvas);
10172
- const layer = buildLayer(id, groupId, bounds, zIndex);
10173
10567
  const src = pickFieldMediaHref(field.media, "image/");
10174
- const module = buildModule(id, "image", name, { src });
10175
- return { layer, module };
10568
+ return buildChild(
10569
+ groupId,
10570
+ zIndex,
10571
+ styleToBounds(field.style, "image", canvas),
10572
+ "image",
10573
+ name,
10574
+ { src }
10575
+ );
10176
10576
  }
10177
10577
  case "text": {
10178
- const bounds = styleToBounds(field.style, "text", canvas);
10179
- const layer = buildLayer(id, groupId, bounds, zIndex);
10180
10578
  const raw = field.text?.[0]?.content ?? "";
10181
10579
  const value = substituteElementTokens(raw, { provider });
10182
- const module = buildModule(
10183
- id,
10580
+ return buildChild(
10581
+ groupId,
10582
+ zIndex,
10583
+ styleToBounds(field.style, "text", canvas),
10184
10584
  "text",
10185
10585
  name,
10186
10586
  { value, highlightColor: "inherit" },
10187
10587
  textStyleToCss(field.style)
10188
10588
  );
10189
- return { layer, module };
10190
10589
  }
10191
10590
  case "audio": {
10192
- const layer = buildLayer(id, groupId, { x: 0, y: 0, width: 1, height: 1 }, zIndex);
10193
10591
  const src = pickFieldMediaHref(field.media, "audio/");
10194
10592
  const volume = typeof field.volume === "number" ? field.volume : 0.5;
10195
- const module = buildModule(id, "audio", name, { src, volume, loop: false });
10196
- return { layer, module };
10593
+ return buildChild(
10594
+ groupId,
10595
+ zIndex,
10596
+ { x: 0, y: 0, width: 1, height: 1 },
10597
+ "audio",
10598
+ name,
10599
+ { src, volume, loop: false }
10600
+ );
10197
10601
  }
10198
10602
  default:
10199
10603
  return null;
10200
10604
  }
10201
10605
  }
10202
10606
  function buildElementScene(state, options) {
10203
- const groupId = nanoid8();
10204
10607
  const groupTitle = typeof state.displayName === "string" && state.displayName || state.referenceId || "Scene";
10205
- const groupLayer = {
10206
- id: groupId,
10608
+ const { id: groupId, layer: groupLayer, module: groupModule } = buildModuleEnvelope({
10207
10609
  type: "group",
10208
- group: null,
10209
- bounds: {
10210
- x: 0,
10211
- y: 0,
10212
- width: options.canvas.width,
10213
- height: options.canvas.height,
10214
- scale: [1, 1]
10215
- },
10216
- state: { visible: true, locked: false, expanded: false }
10217
- };
10218
- const groupModule = {
10219
- id: groupId,
10220
- loaded: true,
10221
- settings: { type: "group", title: groupTitle },
10222
- lights: [],
10223
- css: {},
10224
- content: {},
10225
- variables: {},
10226
- events: {}
10227
- };
10610
+ title: groupTitle,
10611
+ bounds: { x: 0, y: 0, width: options.canvas.width, height: options.canvas.height },
10612
+ type_layer: "group",
10613
+ parentGroupId: null,
10614
+ expanded: false
10615
+ });
10228
10616
  const children = [];
10229
10617
  const fields = state.settings?.compositeFields ?? {};
10230
10618
  let zIndex = 1;
@@ -10243,7 +10631,12 @@ function buildElementScene(state, options) {
10243
10631
  }
10244
10632
 
10245
10633
  // src/se-import/element-dispatch.ts
10246
- var IMPORT_META_KEY2 = "importMeta";
10634
+ function buildElementDescription(widgetInstanceId, sourceUrl) {
10635
+ return buildImportDescription(
10636
+ `Imported from StreamElements Element widget ${widgetInstanceId}`,
10637
+ sourceUrl
10638
+ );
10639
+ }
10247
10640
  function elementAlertKindToLumiaKeys(kind, provider) {
10248
10641
  switch (kind) {
10249
10642
  case "follower":
@@ -10262,9 +10655,9 @@ function elementAlertKindToLumiaKeys(kind, provider) {
10262
10655
  return alertBoxKeysFor("charityCampaignDonation", provider);
10263
10656
  case "subscriberGift":
10264
10657
  case "communityGiftPurchase": {
10265
- if (provider === "twitch") return ["twitch-giftSubscription"];
10266
- if (provider === "kick") return ["kick-subscriptionGift"];
10267
- if (provider === "youtube") return ["youtube-giftMembers"];
10658
+ if (provider === "twitch") return [LumiaAlertValues3.TWITCH_GIFT_SUBSCRIPTION];
10659
+ if (provider === "kick") return [LumiaAlertValues3.KICK_GIFT_SUBSCRIPTION];
10660
+ if (provider === "youtube") return [LumiaAlertValues3.YOUTUBE_GIFT_MEMBERS];
10268
10661
  return alertBoxKeysFor("subscriber", provider);
10269
10662
  }
10270
10663
  default:
@@ -10279,7 +10672,7 @@ function buildCombinedAlertModule(alertStates, provider, canvas) {
10279
10672
  for (const state of alertStates) {
10280
10673
  const cls = classifyConfigState(state);
10281
10674
  if (cls.kind !== "alert") continue;
10282
- const { event, bounds: bounds2 } = buildElementAlertEvent(state, { provider });
10675
+ const { event, bounds: bounds2 } = buildElementAlertEvent(state, { provider, canvas });
10283
10676
  const keys = elementAlertKindToLumiaKeys(cls.alertKind, provider);
10284
10677
  if (keys.length === 0) continue;
10285
10678
  for (const key of keys) {
@@ -10291,61 +10684,42 @@ function buildCombinedAlertModule(alertStates, provider, canvas) {
10291
10684
  }
10292
10685
  }
10293
10686
  if (Object.keys(events).length === 0) return null;
10294
- const id = nanoid9();
10295
10687
  const bounds = firstBounds ?? {
10296
10688
  x: Math.round(canvas.width * 0.6),
10297
10689
  y: Math.round(canvas.height * 0.15),
10298
10690
  width: 400,
10299
10691
  height: 300
10300
10692
  };
10301
- const layer = {
10302
- id,
10303
- state: { visible: true, locked: false },
10304
- bounds: {
10305
- x: bounds.x,
10306
- y: bounds.y,
10307
- width: bounds.width,
10308
- height: bounds.height,
10309
- scale: [1, 1]
10310
- }
10311
- };
10312
10693
  const baseAlert = firstEvent ? stripEventOnlyFields2(firstEvent) : {};
10313
- const module = {
10314
- id,
10315
- version: 1,
10316
- loaded: true,
10317
- settings: { type: "alert", title: "Alerts (Element import)", locked: false },
10318
- lights: [],
10319
- css: {},
10320
- content: {},
10321
- variables: {},
10322
- events: {},
10694
+ const { layer, module } = buildModuleEnvelope({
10695
+ type: "alert",
10696
+ title: "Alerts (Element import)",
10697
+ bounds,
10323
10698
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
10324
10699
  alert: { ...baseAlert, events }
10325
- };
10700
+ });
10326
10701
  return { layer, module };
10327
10702
  }
10328
10703
  function stripEventOnlyFields2(event) {
10329
- const { on: _on, variations: _variations, matchEmptyCondition: _mec, ...base } = event;
10704
+ const {
10705
+ on: _on,
10706
+ variations: _variations,
10707
+ matchEmptyCondition: _mec,
10708
+ ...base
10709
+ } = event;
10330
10710
  return base;
10331
10711
  }
10332
10712
  function stampImportMeta2(module, state, role) {
10333
- module.content = {
10334
- ...module.content ?? {},
10335
- [IMPORT_META_KEY2]: {
10336
- source: "streamelements-element",
10337
- role,
10338
- referenceId: state?.referenceId,
10339
- categoryId: state?.categoryId,
10340
- displayName: state?.displayName,
10341
- importedAt: (/* @__PURE__ */ new Date()).toISOString()
10342
- }
10343
- };
10713
+ stampImportMetaMerge(module, {
10714
+ source: "streamelements-element",
10715
+ role,
10716
+ referenceId: state?.referenceId,
10717
+ categoryId: state?.categoryId,
10718
+ displayName: state?.displayName
10719
+ });
10344
10720
  }
10345
- function importElementOverlay(response) {
10346
- const configData = parseElementConfigData(
10347
- response.widgetInstanceConfigVersion.configData
10348
- );
10721
+ function importElementOverlay(response, options = {}) {
10722
+ const configData = parseElementConfigData(response.widgetInstanceConfigVersion.configData);
10349
10723
  if (!configData) {
10350
10724
  throw new Error(
10351
10725
  "StreamElements Element response carries an invalid configData payload. The widget may be corrupted or the response format may have changed."
@@ -10415,7 +10789,10 @@ function importElementOverlay(response) {
10415
10789
  uuid: "",
10416
10790
  listen_id: "",
10417
10791
  name: response.widgetInstance.displayName ? `[SE Element] ${response.widgetInstance.displayName}` : `Imported from StreamElements Element (${response.widgetInstance.widgetInstanceId})`,
10418
- description: `Imported from StreamElements Element widget ${response.widgetInstance.widgetInstanceId}`,
10792
+ description: buildElementDescription(
10793
+ response.widgetInstance.widgetInstanceId,
10794
+ options.sourceUrl
10795
+ ),
10419
10796
  settings: {
10420
10797
  metadata: { width: canvas.width, height: canvas.height },
10421
10798
  layers,
@@ -10429,7 +10806,7 @@ function importElementOverlay(response) {
10429
10806
  }
10430
10807
 
10431
10808
  // src/se-import/index.ts
10432
- var IMPORT_META_KEY3 = "importMeta";
10809
+ var IMPORT_META_KEY2 = "importMeta";
10433
10810
  var REVIEW_REASONS = {
10434
10811
  "se-widget-bit-boss": "No native Lumia equivalent \u2014 bit boss fight bar with HP/damage states.",
10435
10812
  // `se-widget-hype-cup` intentionally omitted — it now routes to the native
@@ -10459,6 +10836,10 @@ function reasonFor(seType, status, flaggedOff) {
10459
10836
  if (flaggedOff && FLAG_OFF_REASONS[seType]) return FLAG_OFF_REASONS[seType];
10460
10837
  return REVIEW_REASONS[seType] ?? (status === "template" ? "Imported as a generic template stub." : "No native mapping yet \u2014 imported as a labelled text placeholder.");
10461
10838
  }
10839
+ function buildSEBootstrapDescription(overlayId, sourceUrl) {
10840
+ const head = overlayId ? `Imported from StreamElements overlay ${overlayId}` : "Imported from StreamElements";
10841
+ return buildImportDescription(head, sourceUrl);
10842
+ }
10462
10843
  function extractSEOverlayId(input) {
10463
10844
  const trimmed = input.trim();
10464
10845
  const m = trimmed.match(/[0-9a-f]{24}/i);
@@ -10513,7 +10894,7 @@ function isSEBootstrap(value) {
10513
10894
  const o = v.overlay;
10514
10895
  return Array.isArray(o.widgets);
10515
10896
  }
10516
- function importSEBootstrap(bootstrap) {
10897
+ function importSEBootstrap(bootstrap, options = {}) {
10517
10898
  const seOverlay = bootstrap.overlay;
10518
10899
  const widgets = seOverlay?.widgets ?? [];
10519
10900
  const coverage = {
@@ -10536,7 +10917,7 @@ function importSEBootstrap(bootstrap) {
10536
10917
  if (w.type !== "se-widget-group") continue;
10537
10918
  const uid = w.variables?.uid;
10538
10919
  if (typeof uid !== "string") continue;
10539
- const lumiaGroupId = nanoid10();
10920
+ const lumiaGroupId = nanoid8();
10540
10921
  seUidToLumiaGroupId[uid] = lumiaGroupId;
10541
10922
  groupWidgetByUid[uid] = w;
10542
10923
  }
@@ -10575,7 +10956,7 @@ function importSEBootstrap(bootstrap) {
10575
10956
  lights: [],
10576
10957
  css: {},
10577
10958
  content: {
10578
- [IMPORT_META_KEY3]: {
10959
+ [IMPORT_META_KEY2]: {
10579
10960
  source: "streamelements",
10580
10961
  widgetType: w.type,
10581
10962
  importedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -10613,7 +10994,7 @@ function importSEBootstrap(bootstrap) {
10613
10994
  if (result.status === "placeholder" || result.status === "template") {
10614
10995
  module.content = {
10615
10996
  ...module.content ?? {},
10616
- [IMPORT_META_KEY3]: {
10997
+ [IMPORT_META_KEY2]: {
10617
10998
  source: "streamelements",
10618
10999
  widgetType: w.type,
10619
11000
  importedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -10653,7 +11034,7 @@ function importSEBootstrap(bootstrap) {
10653
11034
  uuid: "",
10654
11035
  listen_id: "",
10655
11036
  name: seOverlay?.name ? `[SE] ${seOverlay.name}` : `Imported from StreamElements (${seOverlay?._id ?? ""})`,
10656
- description: seOverlay?._id ? `Imported from StreamElements overlay ${seOverlay._id}` : "Imported from StreamElements",
11037
+ description: buildSEBootstrapDescription(seOverlay?._id, options.sourceUrl),
10657
11038
  settings: {
10658
11039
  metadata: {
10659
11040
  width: seOverlay?.settings?.width ?? 1920,
@@ -10701,7 +11082,7 @@ function applyReviewAction(result, moduleId, action, payload) {
10701
11082
  bounds: existingLayer.bounds,
10702
11083
  state: existingLayer.state
10703
11084
  };
10704
- const importMeta2 = existing2.content?.[IMPORT_META_KEY3];
11085
+ const importMeta2 = existing2.content?.[IMPORT_META_KEY2];
10705
11086
  const mergedLights = (existing2.lights?.length ?? 0) > 0 ? existing2.lights : transplant.module.lights;
10706
11087
  const mergedEvents = existing2.events && Object.keys(existing2.events).length > 0 ? { ...transplant.module.events, ...existing2.events } : transplant.module.events;
10707
11088
  const mergedVariables = existing2.variables && Object.keys(existing2.variables).length > 0 ? { ...transplant.module.variables, ...existing2.variables } : transplant.module.variables;
@@ -10713,7 +11094,7 @@ function applyReviewAction(result, moduleId, action, payload) {
10713
11094
  variables: mergedVariables,
10714
11095
  content: {
10715
11096
  ...transplant.module.content ?? {},
10716
- [IMPORT_META_KEY3]: importMeta2
11097
+ [IMPORT_META_KEY2]: importMeta2
10717
11098
  }
10718
11099
  };
10719
11100
  const rootOldNewId = transplant.layer.id;
@@ -10741,7 +11122,7 @@ function applyReviewAction(result, moduleId, action, payload) {
10741
11122
  const existing = modules[moduleId];
10742
11123
  const generated = payload;
10743
11124
  if (!existing || !generated) return result;
10744
- const importMeta = existing.content?.[IMPORT_META_KEY3];
11125
+ const importMeta = existing.content?.[IMPORT_META_KEY2];
10745
11126
  const replaced = {
10746
11127
  ...existing,
10747
11128
  settings: { ...existing.settings ?? {}, type: "custom" },
@@ -10755,7 +11136,7 @@ function applyReviewAction(result, moduleId, action, payload) {
10755
11136
  data: parseLooseJson(generated.data) ?? {},
10756
11137
  configs: parseLooseJson(generated.configs) ?? [],
10757
11138
  flavor: "ai-generated",
10758
- [IMPORT_META_KEY3]: importMeta
11139
+ [IMPORT_META_KEY2]: importMeta
10759
11140
  }
10760
11141
  };
10761
11142
  modules[moduleId] = replaced;