@lumiastream/ui 0.5.2 → 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":
@@ -6523,13 +6707,7 @@ function buildBaseAlertEvent(settings, prefix, defaultMessage) {
6523
6707
  const fontWeight = getField(settings, prefix, "font_weight") ?? 600;
6524
6708
  const fontColor = getField(settings, prefix, "font_color") ?? "#FFFFFF";
6525
6709
  const highlightColor = getField(settings, prefix, "font_color2") ?? "#32C3A6";
6526
- const messageFontSize = pxToNumber(
6527
- getField(settings, prefix, "message_font_size"),
6528
- 20
6529
- );
6530
- const messageFontFamily = getField(settings, prefix, "message_font") ?? fontFamily;
6531
- const messageFontWeight = getField(settings, prefix, "message_font_weight") ?? 400;
6532
- const messageFontColor = getField(settings, prefix, "message_font_color") ?? "#FFFFFF";
6710
+ const secondaryMessage = readSecondaryMessageConfig(settings, prefix, fontFamily);
6533
6711
  const imageHref = absolutizeSLUrl(getField(settings, prefix, "image_href"));
6534
6712
  const soundHref = absolutizeSLUrl(getField(settings, prefix, "sound_href"));
6535
6713
  const soundVolume = getField(settings, prefix, "sound_volume") ?? 50;
@@ -6548,7 +6726,7 @@ function buildBaseAlertEvent(settings, prefix, defaultMessage) {
6548
6726
  accentColor: null,
6549
6727
  highlightColor,
6550
6728
  firstMessageTemplate: translateSeText(messageTemplate),
6551
- secondMessageTemplate: "",
6729
+ secondMessageTemplate: secondaryMessage?.template ?? "",
6552
6730
  thirdMessageTemplate: "",
6553
6731
  showAvatar: false,
6554
6732
  showAmount: true,
@@ -6571,12 +6749,10 @@ function buildBaseAlertEvent(settings, prefix, defaultMessage) {
6571
6749
  textShadow: "rgba(0, 0, 0, 0.8) 1px 1px 1px",
6572
6750
  textAlign: "center"
6573
6751
  },
6574
- messageCss: {
6575
- fontFamily: messageFontFamily,
6576
- fontSize: messageFontSize,
6577
- color: messageFontColor,
6578
- fontWeight: messageFontWeight
6579
- },
6752
+ // `messageCss` only matters when secondMessageTemplate is set;
6753
+ // leave it empty when SL didn't ship a secondary-message block
6754
+ // so Lumia's renderer falls back to its own default styling.
6755
+ messageCss: secondaryMessage?.css ?? {},
6580
6756
  content: { value: translateSeText(messageTemplate) }
6581
6757
  },
6582
6758
  media: {
@@ -6611,14 +6787,39 @@ function buildBaseAlertEvent(settings, prefix, defaultMessage) {
6611
6787
  tts: {
6612
6788
  enabled: getField(settings, prefix, "tts_enabled") ?? false,
6613
6789
  voice: getField(settings, prefix, "tts_language") ?? "Brian",
6614
- volume: clampVolume(
6615
- getField(settings, prefix, "tts_volume") ?? 75
6616
- ),
6790
+ volume: clampVolume(getField(settings, prefix, "tts_volume") ?? 75),
6617
6791
  minAmount: getField(settings, prefix, "tts_min_amount") ?? 0
6618
6792
  },
6619
6793
  variations: []
6620
6794
  };
6621
6795
  }
6796
+ var SECONDARY_MESSAGE_PREFIX_OVERRIDE = {
6797
+ sub: "resub"
6798
+ };
6799
+ function readSecondaryMessageConfig(settings, prefix, primaryFontFamily) {
6800
+ const secondaryPrefix = SECONDARY_MESSAGE_PREFIX_OVERRIDE[prefix] ?? prefix;
6801
+ const showKey = `show_${secondaryPrefix}_message`;
6802
+ const enabled = settings[showKey];
6803
+ if (enabled !== true) return null;
6804
+ const fontFamily = getField(settings, secondaryPrefix, "message_font") ?? primaryFontFamily;
6805
+ const fontSize = pxToNumber(getField(settings, secondaryPrefix, "message_font_size"), 20);
6806
+ const fontWeight = getField(settings, secondaryPrefix, "message_font_weight") ?? 400;
6807
+ const fontColor = getField(settings, secondaryPrefix, "message_font_color") ?? "#FFFFFF";
6808
+ return {
6809
+ // `{message}` is the SE/SL chat-message token (mapped to Lumia's
6810
+ // `{{message}}` in translate.ts). It resolves at render time from
6811
+ // the alert payload's `message` field — populated for every event
6812
+ // where SL ships a secondary-message line (donations, resubs,
6813
+ // cheers, charity, merch, etc.).
6814
+ template: translateSeText("{message}"),
6815
+ css: {
6816
+ fontFamily,
6817
+ fontSize,
6818
+ color: fontColor,
6819
+ fontWeight
6820
+ }
6821
+ };
6822
+ }
6622
6823
  function clampVolume(slVolume) {
6623
6824
  if (typeof slVolume !== "number" || !Number.isFinite(slVolume)) return 0.5;
6624
6825
  return Math.max(0, Math.min(1, slVolume / 100));
@@ -6628,11 +6829,20 @@ function translateSLVariationCondition(v) {
6628
6829
  const numericData = typeof data === "number" ? data : Number(data);
6629
6830
  switch (v.condition) {
6630
6831
  case "RANDOM":
6631
- return { conditionType: "RANDOM", condition: Number.isFinite(numericData) ? numericData : 100 };
6832
+ return {
6833
+ conditionType: "RANDOM",
6834
+ condition: Number.isFinite(numericData) ? numericData : 100
6835
+ };
6632
6836
  case "MIN_BITS_USED":
6633
- return { conditionType: "GREATER_NUMBER", condition: Number.isFinite(numericData) ? numericData : 0 };
6837
+ return {
6838
+ conditionType: "GREATER_NUMBER",
6839
+ condition: Number.isFinite(numericData) ? numericData : 0
6840
+ };
6634
6841
  case "EXACT_BITS_USED":
6635
- return { conditionType: "EQUAL_NUMBER", condition: Number.isFinite(numericData) ? numericData : 0 };
6842
+ return {
6843
+ conditionType: "EQUAL_NUMBER",
6844
+ condition: Number.isFinite(numericData) ? numericData : 0
6845
+ };
6636
6846
  case "MIN_MONTHS_SUBSCRIBED":
6637
6847
  return {
6638
6848
  conditionType: "SUBSCRIBED_MONTHS_GREATER",
@@ -6697,6 +6907,14 @@ function buildVariationEvent(v, parentBase) {
6697
6907
  if (s.hideAnimation) inherited.animations.alertExitAnimation = s.hideAnimation;
6698
6908
  if (s.layout) inherited.layout.value = translateLayout2(s.layout);
6699
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(),
6700
6918
  name: v.name ?? "Variation",
6701
6919
  on: v.paused !== true,
6702
6920
  randomChance: 100,
@@ -6762,11 +6980,7 @@ function buildSLAlertBox(settings, options = {}) {
6762
6980
  for (const [prefix, routing] of Object.entries(SL_EVENT_ROUTING)) {
6763
6981
  const lumiaKeys = routing.pickProviderKeys ? alertBoxKeysFor(routing.pickProviderKeys, provider) : routing.keys ?? [];
6764
6982
  if (lumiaKeys.length === 0) continue;
6765
- const built = buildBaseAlertEvent(
6766
- settings,
6767
- prefix,
6768
- DEFAULT_MESSAGES[prefix] ?? "{name}"
6769
- );
6983
+ const built = buildBaseAlertEvent(settings, prefix, DEFAULT_MESSAGES[prefix] ?? "{name}");
6770
6984
  if (!built) continue;
6771
6985
  const variationKey = VARIATION_KEY_FOR_PREFIX[prefix];
6772
6986
  const variations = variationKey ? settings[variationKey] : void 0;
@@ -6783,53 +6997,32 @@ function buildSLAlertBox(settings, options = {}) {
6783
6997
  channelsImported.push(prefix);
6784
6998
  if (firstEvent == null) firstEvent = built;
6785
6999
  }
6786
- const id = nanoid4();
6787
- const layer = {
6788
- id,
6789
- state: { visible: true, locked: false },
6790
- bounds: { x: 100, y: 100, width: 700, height: 600, scale: [1, 1] }
6791
- };
6792
7000
  const baseAlert = firstEvent ? stripEventOnlyFields(firstEvent) : {};
6793
- const module = {
6794
- id,
6795
- version: 1,
6796
- loaded: true,
6797
- settings: { type: "alert", title: "Streamlabs Alerts", locked: false },
6798
- lights: [],
6799
- css: {},
6800
- content: {},
6801
- variables: {},
6802
- events: {},
7001
+ const { layer, module } = buildModuleEnvelope({
7002
+ type: "alert",
7003
+ title: "Streamlabs Alerts",
7004
+ bounds: { x: 100, y: 100, width: 700, height: 600 },
6803
7005
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
6804
7006
  alert: { ...baseAlert, events }
6805
- };
7007
+ });
6806
7008
  return { layer, module, channelsImported };
6807
7009
  }
6808
7010
  function stripEventOnlyFields(event) {
6809
- const { on: _on, variations: _variations, matchEmptyCondition: _mec, ...base } = event;
7011
+ const {
7012
+ on: _on,
7013
+ variations: _variations,
7014
+ matchEmptyCondition: _mec,
7015
+ ...base
7016
+ } = event;
6810
7017
  return base;
6811
7018
  }
6812
7019
 
6813
7020
  // src/sl-import/mappers/tip-jar.ts
6814
- import { nanoid as nanoid5 } from "nanoid";
6815
- function pxToNumber2(raw, fallback) {
6816
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
6817
- if (typeof raw !== "string") return fallback;
6818
- const trimmed = raw.trim();
6819
- if (!trimmed) return fallback;
6820
- if (trimmed.endsWith("px")) {
6821
- const n2 = parseFloat(trimmed.slice(0, -2));
6822
- return Number.isFinite(n2) ? n2 : fallback;
6823
- }
6824
- const n = parseFloat(trimmed);
6825
- return Number.isFinite(n) ? n : fallback;
6826
- }
6827
7021
  function buildSLTipJar(settings) {
6828
- const id = nanoid5();
6829
7022
  const backgroundColor = settings.background?.color ?? "transparent";
6830
7023
  const textColor = settings.text?.color ?? "#FFFFFF";
6831
7024
  const fontFamily = settings.text?.font ?? "Open Sans";
6832
- const fontSize = pxToNumber2(settings.text?.size, 32);
7025
+ const fontSize = pxToNumber(settings.text?.size, 32);
6833
7026
  const types = settings.types ?? {};
6834
7027
  const tipsConfig = types.tips ?? {};
6835
7028
  const slEvents = {
@@ -6845,17 +7038,11 @@ function buildSLTipJar(settings) {
6845
7038
  ...TIPJAR_DEFAULT_MODULE.content.display,
6846
7039
  showEventMessages: settings.text?.show !== false
6847
7040
  };
6848
- const layer = {
6849
- id,
6850
- state: { visible: true, locked: false },
6851
- bounds: { x: 100, y: 100, width: 600, height: 700, scale: [1, 1] }
6852
- };
6853
- const module = {
6854
- id,
7041
+ const { layer, module } = buildModuleEnvelope({
7042
+ type: "tipjar",
7043
+ title: "Streamlabs Tip Jar",
7044
+ bounds: { x: 100, y: 100, width: 600, height: 700 },
6855
7045
  version: TIPJAR_DEFAULT_MODULE.version,
6856
- loaded: true,
6857
- settings: { type: "tipjar", title: "Streamlabs Tip Jar", locked: false },
6858
- lights: [],
6859
7046
  css: {
6860
7047
  fontFamily,
6861
7048
  fontSize,
@@ -6876,12 +7063,7 @@ function buildSLTipJar(settings) {
6876
7063
  source: "streamlabs",
6877
7064
  widgetType: "tip_jar",
6878
7065
  importedAt: (/* @__PURE__ */ new Date()).toISOString(),
6879
- // SL jar type doesn't have a 1:1 mapping to Lumia's preset
6880
- // vocabulary (cup-01, cup-02, …). Stashed for review.
6881
7066
  slJarType: settings.jar?.type ?? null,
6882
- // SL tier coin tiers — Lumia's sprite tiers are per-event and
6883
- // configured in the sprite editor; the user re-creates these
6884
- // manually. Stored here so they're not lost.
6885
7067
  slTipTiers: Array.isArray(tipsConfig.tiers) ? tipsConfig.tiers.map((t) => ({
6886
7068
  minimumAmount: t.minimum_amount,
6887
7069
  imageSrc: t.image_src ?? null
@@ -6889,15 +7071,12 @@ function buildSLTipJar(settings) {
6889
7071
  slMinimumTipAmount: tipsConfig.minimum_amount ?? null,
6890
7072
  slHadCustomHtml: settings.custom_html_enabled === true
6891
7073
  }
6892
- },
6893
- variables: {},
6894
- events: {}
6895
- };
7074
+ }
7075
+ });
6896
7076
  return { layer, module };
6897
7077
  }
6898
7078
 
6899
7079
  // src/sl-import/mappers/goal.ts
6900
- import { nanoid as nanoid6 } from "nanoid";
6901
7080
  var GOAL_SOURCE_MAP = {
6902
7081
  follower_goal: "follower",
6903
7082
  sub_goal: "subscriber",
@@ -6910,69 +7089,262 @@ var GOAL_TITLE_MAP = {
6910
7089
  donation_goal: "Donation Goal",
6911
7090
  bit_goal: "Bits Goal"
6912
7091
  };
6913
- function pxToNumber3(raw, fallback) {
6914
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
6915
- if (typeof raw !== "string") return fallback;
6916
- const trimmed = raw.trim();
6917
- if (!trimmed) return fallback;
6918
- if (trimmed.endsWith("px")) {
6919
- const n2 = parseFloat(trimmed.slice(0, -2));
6920
- return Number.isFinite(n2) ? n2 : fallback;
6921
- }
6922
- const n = parseFloat(trimmed);
6923
- return Number.isFinite(n) ? n : fallback;
6924
- }
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
+ };
6925
7098
  function buildSLGoal(settings, widgetType) {
6926
- const id = nanoid6();
6927
7099
  const goalSource = GOAL_SOURCE_MAP[widgetType] ?? "follower";
6928
7100
  const defaultTitle = GOAL_TITLE_MAP[widgetType] ?? "Goal";
7101
+ const currentTemplate = GOAL_CURRENT_TEMPLATE[widgetType] ?? "";
7102
+ const requiresReview = settings.custom_enabled === true;
6929
7103
  const title = typeof settings.title === "string" ? settings.title : defaultTitle;
6930
7104
  const goalAmount = typeof settings.goal === "number" ? settings.goal : 100;
6931
7105
  const currentAmount = typeof settings.current === "number" ? settings.current : 0;
6932
7106
  const startingAmount = typeof settings.starting_amount === "number" ? settings.starting_amount : 0;
6933
- const endsAt = typeof settings.ends_at === "string" ? settings.ends_at : null;
7107
+ const endsAt = typeof settings.ends_at === "string" ? settings.ends_at : "";
6934
7108
  const barColor = settings.bar_color ?? "#32C3A6";
6935
- const barBackgroundColor = settings.bar_background_color ?? "#181818";
7109
+ const barBgColor = settings.bar_bg_color ?? "#181818";
7110
+ const widgetBgColor = settings.background_color ?? "transparent";
6936
7111
  const textColor = settings.text_color ?? "#FFFFFF";
6937
7112
  const barTextColor = settings.bar_text_color ?? textColor;
6938
7113
  const fontFamily = settings.font ?? "Open Sans";
6939
- const layer = {
6940
- id,
6941
- state: { visible: true, locked: false },
6942
- bounds: { x: 100, y: 100, width: 740, height: 90, scale: [1, 1] }
6943
- };
6944
- const module = {
6945
- id,
6946
- version: 1,
6947
- loaded: true,
6948
- settings: { type: "goal", title, locked: false },
6949
- 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 },
6950
7121
  css: {
6951
7122
  fontFamily,
6952
7123
  color: textColor,
6953
- background: barBackgroundColor
7124
+ background: widgetBgColor,
7125
+ fontSize
6954
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.
6955
7131
  content: {
6956
- title,
6957
- goalSource,
6958
- goalAmount,
6959
- currentAmount,
6960
- startingAmount,
6961
- endsAt,
6962
- barColor,
6963
- barBackgroundColor,
6964
- barTextColor,
6965
- fontFamily,
6966
- 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
6967
7204
  },
6968
- variables: {},
6969
- events: {}
6970
- };
6971
- 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 };
6972
7342
  }
6973
7343
 
6974
7344
  // src/sl-import/sl-dispatch.ts
6975
- var IMPORT_META_KEY = "importMeta";
7345
+ function buildSLDescription(widgetType, sourceUrl) {
7346
+ return buildImportDescription(`Imported from Streamlabs ${widgetType}`, sourceUrl);
7347
+ }
6976
7348
  var GOAL_WIDGET_TYPES = /* @__PURE__ */ new Set([
6977
7349
  "follower_goal",
6978
7350
  "sub_goal",
@@ -6980,8 +7352,6 @@ var GOAL_WIDGET_TYPES = /* @__PURE__ */ new Set([
6980
7352
  "bit_goal"
6981
7353
  ]);
6982
7354
  var PLACEHOLDER_WIDGET_TYPES = /* @__PURE__ */ new Set([
6983
- "chat_box",
6984
- "event_list",
6985
7355
  "donation_ticker",
6986
7356
  "stream_boss",
6987
7357
  "credits",
@@ -6991,8 +7361,6 @@ var PLACEHOLDER_WIDGET_TYPES = /* @__PURE__ */ new Set([
6991
7361
  "streamlabels"
6992
7362
  ]);
6993
7363
  var PLACEHOLDER_REASONS = {
6994
- chat_box: "Lumia has a native chatbox module \u2014 set it up directly rather than importing the SL config.",
6995
- event_list: "Lumia has a native eventlist module \u2014 set it up directly rather than importing the SL config.",
6996
7364
  donation_ticker: "No native Lumia equivalent yet \u2014 imported as a placeholder text layer.",
6997
7365
  stream_boss: "Lumia has a native streamboss module \u2014 set it up directly rather than importing the SL config.",
6998
7366
  credits: "Lumia has a native credits module \u2014 set it up directly rather than importing the SL config.",
@@ -7001,19 +7369,14 @@ var PLACEHOLDER_REASONS = {
7001
7369
  sub_train: "No native Lumia equivalent yet \u2014 imported as a placeholder text layer.",
7002
7370
  streamlabels: "Use Lumia template variables (e.g. {{twitch_last_follower}}) inside a Text module instead of importing the SL streamlabels config."
7003
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.";
7004
7373
  function stampImportMeta(module, widgetType, role, extra = {}) {
7005
- const existing = module.content?.[IMPORT_META_KEY] ?? {};
7006
- module.content = {
7007
- ...module.content ?? {},
7008
- [IMPORT_META_KEY]: {
7009
- source: "streamlabs",
7010
- widgetType,
7011
- role,
7012
- importedAt: (/* @__PURE__ */ new Date()).toISOString(),
7013
- ...existing,
7014
- ...extra
7015
- }
7016
- };
7374
+ stampImportMetaMerge(module, {
7375
+ source: "streamlabs",
7376
+ widgetType,
7377
+ role,
7378
+ ...extra
7379
+ });
7017
7380
  }
7018
7381
  function buildPlaceholder(widgetType) {
7019
7382
  const id = nanoid7();
@@ -7045,7 +7408,7 @@ function buildPlaceholder(widgetType) {
7045
7408
  };
7046
7409
  return { layer, module };
7047
7410
  }
7048
- function importStreamlabsWidget(response, widgetType) {
7411
+ function importStreamlabsWidget(response, widgetType, options = {}) {
7049
7412
  if (!isSLWidgetConfigResponse(response)) {
7050
7413
  throw new Error(
7051
7414
  "Streamlabs response is missing the expected `settings` envelope. The widget may be private or the response format may have changed."
@@ -7095,9 +7458,61 @@ function importStreamlabsWidget(response, widgetType) {
7095
7458
  coverage.mappings.push({
7096
7459
  seType: `streamlabs:${widgetType}`,
7097
7460
  lumiaType: "goal",
7098
- status: "direct",
7461
+ status: built.requiresReview ? "partial" : "direct",
7099
7462
  count: 1
7100
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
+ }
7101
7516
  } else if (PLACEHOLDER_WIDGET_TYPES.has(widgetType)) {
7102
7517
  const placeholder = buildPlaceholder(widgetType);
7103
7518
  const reason = PLACEHOLDER_REASONS[widgetType] ?? "No native Lumia mapping yet.";
@@ -7146,7 +7561,7 @@ function importStreamlabsWidget(response, widgetType) {
7146
7561
  uuid: "",
7147
7562
  listen_id: "",
7148
7563
  name: overlayName,
7149
- description: `Imported from Streamlabs ${widgetType}`,
7564
+ description: buildSLDescription(widgetType, options.sourceUrl),
7150
7565
  settings: {
7151
7566
  metadata: { width: 1920, height: 1080 },
7152
7567
  layers,
@@ -8730,7 +9145,12 @@ function SEImportWizard({
8730
9145
  );
8731
9146
  }
8732
9147
  importedOverlays.push({
8733
- 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
+ }),
8734
9154
  summary
8735
9155
  });
8736
9156
  }
@@ -8775,7 +9195,9 @@ function SEImportWizard({
8775
9195
  "StreamElements returned a payload that doesn't look like an overlay bootstrap."
8776
9196
  );
8777
9197
  }
8778
- const imported = importSEBootstrap(bootstrap);
9198
+ const imported = importSEBootstrap(bootstrap, {
9199
+ sourceUrl: `https://streamelements.com/dashboard/overlays/${overlayId}/editor`
9200
+ });
8779
9201
  setBatchImports([{ result: imported, summary }]);
8780
9202
  setResult(imported);
8781
9203
  setAssetUrls(findSEAssetURLs(imported.overlay));
@@ -8863,7 +9285,9 @@ function SEImportWizard({
8863
9285
  "Streamlabs returned a response that does not look like a widget config payload."
8864
9286
  );
8865
9287
  }
8866
- const imported = importStreamlabsWidget(raw, slParts.widgetType);
9288
+ const imported = importStreamlabsWidget(raw, slParts.widgetType, {
9289
+ sourceUrl: url
9290
+ });
8867
9291
  setBatchImports([]);
8868
9292
  setResult(imported);
8869
9293
  setAssetUrls(findSEAssetURLs(imported.overlay));
@@ -8904,7 +9328,7 @@ function SEImportWizard({
8904
9328
  "StreamElements returned a response that does not look like an Element widget payload."
8905
9329
  );
8906
9330
  }
8907
- const imported = importElementOverlay(raw);
9331
+ const imported = importElementOverlay(raw, { sourceUrl: url });
8908
9332
  setBatchImports([]);
8909
9333
  setResult(imported);
8910
9334
  setAssetUrls(findSEAssetURLs(imported.overlay));
@@ -8952,7 +9376,7 @@ function SEImportWizard({
8952
9376
  ...parts,
8953
9377
  proxyFetch: bindings.proxyFetch
8954
9378
  });
8955
- const imported = importSEBootstrap(bootstrap);
9379
+ const imported = importSEBootstrap(bootstrap, { sourceUrl: url });
8956
9380
  setBatchImports([]);
8957
9381
  setResult(imported);
8958
9382
  setAssetUrls(findSEAssetURLs(imported.overlay));
@@ -9611,7 +10035,7 @@ function extractErrorMessage(err) {
9611
10035
  }
9612
10036
 
9613
10037
  // src/se-import/element-dispatch.ts
9614
- import { nanoid as nanoid9 } from "nanoid";
10038
+ import { LumiaAlertValues as LumiaAlertValues3 } from "@lumiastream/lumia-types";
9615
10039
 
9616
10040
  // src/se-import/element-types.ts
9617
10041
  function isSEElementResponse(value) {
@@ -9855,34 +10279,8 @@ function translateAnimationName(name, fallback) {
9855
10279
  if (PINNED_ANIMATION_MAP[name]) return PINNED_ANIMATION_MAP[name];
9856
10280
  return name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
9857
10281
  }
9858
- function parseAnimationDurationMs(raw, fallbackMs) {
9859
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
9860
- if (typeof raw !== "string") return fallbackMs;
9861
- const trimmed = raw.trim();
9862
- if (!trimmed) return fallbackMs;
9863
- if (trimmed.endsWith("ms")) {
9864
- const n2 = parseFloat(trimmed.slice(0, -2));
9865
- return Number.isFinite(n2) ? n2 : fallbackMs;
9866
- }
9867
- if (trimmed.endsWith("s")) {
9868
- const n2 = parseFloat(trimmed.slice(0, -1));
9869
- return Number.isFinite(n2) ? n2 * 1e3 : fallbackMs;
9870
- }
9871
- const n = parseFloat(trimmed);
9872
- return Number.isFinite(n) ? n : fallbackMs;
9873
- }
9874
- function parsePx2(raw) {
9875
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
9876
- if (typeof raw !== "string") return NaN;
9877
- const trimmed = raw.trim();
9878
- if (!trimmed) return NaN;
9879
- if (trimmed.endsWith("px")) {
9880
- const n2 = parseFloat(trimmed.slice(0, -2));
9881
- return Number.isFinite(n2) ? n2 : NaN;
9882
- }
9883
- const n = parseFloat(trimmed);
9884
- return Number.isFinite(n) ? n : NaN;
9885
- }
10282
+ var parseAnimationDurationMs = parseDurationMs;
10283
+ var parsePx2 = (raw) => pxToNumber(raw, NaN);
9886
10284
  function findFieldByType(fields, type) {
9887
10285
  if (!fields) return void 0;
9888
10286
  for (const f of Object.values(fields)) {
@@ -9958,6 +10356,47 @@ function buildAnimations(title) {
9958
10356
  const visibleDurationMs = enterDelay + enterDuration + staticDuration + exitDuration;
9959
10357
  return { animations, visibleDurationMs: visibleDurationMs || DEFAULT_DURATION_MS };
9960
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
+ }
9961
10400
  function buildElementAlertEvent(state, options = {}) {
9962
10401
  const compositeFields = state.settings?.compositeFields;
9963
10402
  const titleField = findFieldByType(compositeFields, "text");
@@ -9975,6 +10414,31 @@ function buildElementAlertEvent(state, options = {}) {
9975
10414
  const titleStyle = titleField?.style;
9976
10415
  const highlightColor = titleField?.highlightedStyle?.color ?? null;
9977
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
+ }
9978
10442
  const event = {
9979
10443
  on: true,
9980
10444
  // `'custom'` lowercase is load-bearing in Lumia's AlertBoxHandler —
@@ -9993,7 +10457,11 @@ function buildElementAlertEvent(state, options = {}) {
9993
10457
  showBrandIcon: true
9994
10458
  },
9995
10459
  settings: { duration: visibleDurationMs },
9996
- 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 },
9997
10465
  text: {
9998
10466
  css: titleCss(titleStyle),
9999
10467
  messageCss: {},
@@ -10015,44 +10483,13 @@ function buildElementAlertEvent(state, options = {}) {
10015
10483
  },
10016
10484
  variations: []
10017
10485
  };
10018
- const style = mainAsset?.style;
10019
- let bounds;
10020
- if (style) {
10021
- const x = parsePx2(style.left);
10022
- const y = parsePx2(style.top);
10023
- const width = parsePx2(style.width);
10024
- const height = parsePx2(style.height);
10025
- if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(width)) {
10026
- bounds = {
10027
- x,
10028
- y,
10029
- width,
10030
- // Height isn't always present on video Main Assets (the video
10031
- // aspect-ratios itself from the source). Fall back to a
10032
- // reasonable default that matches SE's preview row.
10033
- height: Number.isFinite(height) ? height : Math.round(width * 0.75)
10034
- };
10035
- }
10036
- }
10037
- return { event, bounds };
10486
+ return { event, bounds: layerBounds };
10038
10487
  }
10039
10488
 
10040
10489
  // src/se-import/mappers/element-scene.ts
10041
- import { nanoid as nanoid8 } from "nanoid";
10042
10490
  var TEXT_FALLBACK = { width: 400, height: 50 };
10043
10491
  var IMAGE_FALLBACK = { width: 300, height: 100 };
10044
- function parsePx3(raw) {
10045
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
10046
- if (typeof raw !== "string") return NaN;
10047
- const trimmed = raw.trim();
10048
- if (!trimmed) return NaN;
10049
- if (trimmed.endsWith("px")) {
10050
- const n2 = parseFloat(trimmed.slice(0, -2));
10051
- return Number.isFinite(n2) ? n2 : NaN;
10052
- }
10053
- const n = parseFloat(trimmed);
10054
- return Number.isFinite(n) ? n : NaN;
10055
- }
10492
+ var parsePx3 = (raw) => pxToNumber(raw, NaN);
10056
10493
  function styleToBounds(style, type, canvas) {
10057
10494
  const x = Number.isFinite(parsePx3(style?.left)) ? parsePx3(style?.left) : 0;
10058
10495
  const y = Number.isFinite(parsePx3(style?.top)) ? parsePx3(style?.top) : 0;
@@ -10088,33 +10525,17 @@ function styleToBounds(style, type, canvas) {
10088
10525
  }
10089
10526
  return { x, y, width, height };
10090
10527
  }
10091
- function buildLayer(id, groupId, bounds, zIndex) {
10092
- return {
10093
- id,
10094
- group: groupId,
10095
- state: { visible: true, locked: false },
10096
- bounds: {
10097
- x: bounds.x,
10098
- y: bounds.y,
10099
- width: bounds.width,
10100
- height: bounds.height,
10101
- scale: [1, 1],
10102
- zIndex
10103
- }
10104
- };
10105
- }
10106
- function buildModule(id, type, title, content, css = {}) {
10107
- return {
10108
- id,
10109
- version: 1,
10110
- loaded: true,
10111
- settings: { type, title, locked: false },
10112
- lights: [],
10113
- css,
10528
+ function buildChild(groupId, zIndex, bounds, type, title, content, css = {}) {
10529
+ const { layer, module } = buildModuleEnvelope({
10530
+ type,
10531
+ title,
10532
+ bounds,
10114
10533
  content,
10115
- variables: {},
10116
- events: {}
10117
- };
10534
+ css,
10535
+ parentGroupId: groupId,
10536
+ zIndex
10537
+ });
10538
+ return { layer, module };
10118
10539
  }
10119
10540
  function textStyleToCss(style) {
10120
10541
  const s = style ?? {};
@@ -10129,80 +10550,69 @@ function textStyleToCss(style) {
10129
10550
  return out;
10130
10551
  }
10131
10552
  function mapCompositeField(name, field, groupId, zIndex, provider, canvas) {
10132
- const id = nanoid8();
10133
10553
  switch (field.type) {
10134
10554
  case "video": {
10135
- const bounds = styleToBounds(field.style, "video", canvas);
10136
- const layer = buildLayer(id, groupId, bounds, zIndex);
10137
10555
  const src = pickFieldMediaHref(field.media, "video/");
10138
10556
  const volume = typeof field.volume === "number" ? field.volume : 1;
10139
- const module = buildModule(id, "video", name, {
10140
- src,
10141
- volume,
10142
- loop: true,
10143
- autoplay: true,
10144
- muted: volume === 0
10145
- });
10146
- 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
+ );
10147
10565
  }
10148
10566
  case "image": {
10149
- const bounds = styleToBounds(field.style, "image", canvas);
10150
- const layer = buildLayer(id, groupId, bounds, zIndex);
10151
10567
  const src = pickFieldMediaHref(field.media, "image/");
10152
- const module = buildModule(id, "image", name, { src });
10153
- return { layer, module };
10568
+ return buildChild(
10569
+ groupId,
10570
+ zIndex,
10571
+ styleToBounds(field.style, "image", canvas),
10572
+ "image",
10573
+ name,
10574
+ { src }
10575
+ );
10154
10576
  }
10155
10577
  case "text": {
10156
- const bounds = styleToBounds(field.style, "text", canvas);
10157
- const layer = buildLayer(id, groupId, bounds, zIndex);
10158
10578
  const raw = field.text?.[0]?.content ?? "";
10159
10579
  const value = substituteElementTokens(raw, { provider });
10160
- const module = buildModule(
10161
- id,
10580
+ return buildChild(
10581
+ groupId,
10582
+ zIndex,
10583
+ styleToBounds(field.style, "text", canvas),
10162
10584
  "text",
10163
10585
  name,
10164
10586
  { value, highlightColor: "inherit" },
10165
10587
  textStyleToCss(field.style)
10166
10588
  );
10167
- return { layer, module };
10168
10589
  }
10169
10590
  case "audio": {
10170
- const layer = buildLayer(id, groupId, { x: 0, y: 0, width: 1, height: 1 }, zIndex);
10171
10591
  const src = pickFieldMediaHref(field.media, "audio/");
10172
10592
  const volume = typeof field.volume === "number" ? field.volume : 0.5;
10173
- const module = buildModule(id, "audio", name, { src, volume, loop: false });
10174
- 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
+ );
10175
10601
  }
10176
10602
  default:
10177
10603
  return null;
10178
10604
  }
10179
10605
  }
10180
10606
  function buildElementScene(state, options) {
10181
- const groupId = nanoid8();
10182
10607
  const groupTitle = typeof state.displayName === "string" && state.displayName || state.referenceId || "Scene";
10183
- const groupLayer = {
10184
- id: groupId,
10608
+ const { id: groupId, layer: groupLayer, module: groupModule } = buildModuleEnvelope({
10185
10609
  type: "group",
10186
- group: null,
10187
- bounds: {
10188
- x: 0,
10189
- y: 0,
10190
- width: options.canvas.width,
10191
- height: options.canvas.height,
10192
- scale: [1, 1]
10193
- },
10194
- state: { visible: true, locked: false, expanded: false }
10195
- };
10196
- const groupModule = {
10197
- id: groupId,
10198
- loaded: true,
10199
- settings: { type: "group", title: groupTitle },
10200
- lights: [],
10201
- css: {},
10202
- content: {},
10203
- variables: {},
10204
- events: {}
10205
- };
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
+ });
10206
10616
  const children = [];
10207
10617
  const fields = state.settings?.compositeFields ?? {};
10208
10618
  let zIndex = 1;
@@ -10221,7 +10631,12 @@ function buildElementScene(state, options) {
10221
10631
  }
10222
10632
 
10223
10633
  // src/se-import/element-dispatch.ts
10224
- var IMPORT_META_KEY2 = "importMeta";
10634
+ function buildElementDescription(widgetInstanceId, sourceUrl) {
10635
+ return buildImportDescription(
10636
+ `Imported from StreamElements Element widget ${widgetInstanceId}`,
10637
+ sourceUrl
10638
+ );
10639
+ }
10225
10640
  function elementAlertKindToLumiaKeys(kind, provider) {
10226
10641
  switch (kind) {
10227
10642
  case "follower":
@@ -10240,9 +10655,9 @@ function elementAlertKindToLumiaKeys(kind, provider) {
10240
10655
  return alertBoxKeysFor("charityCampaignDonation", provider);
10241
10656
  case "subscriberGift":
10242
10657
  case "communityGiftPurchase": {
10243
- if (provider === "twitch") return ["twitch-giftSubscription"];
10244
- if (provider === "kick") return ["kick-subscriptionGift"];
10245
- 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];
10246
10661
  return alertBoxKeysFor("subscriber", provider);
10247
10662
  }
10248
10663
  default:
@@ -10257,7 +10672,7 @@ function buildCombinedAlertModule(alertStates, provider, canvas) {
10257
10672
  for (const state of alertStates) {
10258
10673
  const cls = classifyConfigState(state);
10259
10674
  if (cls.kind !== "alert") continue;
10260
- const { event, bounds: bounds2 } = buildElementAlertEvent(state, { provider });
10675
+ const { event, bounds: bounds2 } = buildElementAlertEvent(state, { provider, canvas });
10261
10676
  const keys = elementAlertKindToLumiaKeys(cls.alertKind, provider);
10262
10677
  if (keys.length === 0) continue;
10263
10678
  for (const key of keys) {
@@ -10269,61 +10684,42 @@ function buildCombinedAlertModule(alertStates, provider, canvas) {
10269
10684
  }
10270
10685
  }
10271
10686
  if (Object.keys(events).length === 0) return null;
10272
- const id = nanoid9();
10273
10687
  const bounds = firstBounds ?? {
10274
10688
  x: Math.round(canvas.width * 0.6),
10275
10689
  y: Math.round(canvas.height * 0.15),
10276
10690
  width: 400,
10277
10691
  height: 300
10278
10692
  };
10279
- const layer = {
10280
- id,
10281
- state: { visible: true, locked: false },
10282
- bounds: {
10283
- x: bounds.x,
10284
- y: bounds.y,
10285
- width: bounds.width,
10286
- height: bounds.height,
10287
- scale: [1, 1]
10288
- }
10289
- };
10290
10693
  const baseAlert = firstEvent ? stripEventOnlyFields2(firstEvent) : {};
10291
- const module = {
10292
- id,
10293
- version: 1,
10294
- loaded: true,
10295
- settings: { type: "alert", title: "Alerts (Element import)", locked: false },
10296
- lights: [],
10297
- css: {},
10298
- content: {},
10299
- variables: {},
10300
- events: {},
10694
+ const { layer, module } = buildModuleEnvelope({
10695
+ type: "alert",
10696
+ title: "Alerts (Element import)",
10697
+ bounds,
10301
10698
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
10302
10699
  alert: { ...baseAlert, events }
10303
- };
10700
+ });
10304
10701
  return { layer, module };
10305
10702
  }
10306
10703
  function stripEventOnlyFields2(event) {
10307
- const { on: _on, variations: _variations, matchEmptyCondition: _mec, ...base } = event;
10704
+ const {
10705
+ on: _on,
10706
+ variations: _variations,
10707
+ matchEmptyCondition: _mec,
10708
+ ...base
10709
+ } = event;
10308
10710
  return base;
10309
10711
  }
10310
10712
  function stampImportMeta2(module, state, role) {
10311
- module.content = {
10312
- ...module.content ?? {},
10313
- [IMPORT_META_KEY2]: {
10314
- source: "streamelements-element",
10315
- role,
10316
- referenceId: state?.referenceId,
10317
- categoryId: state?.categoryId,
10318
- displayName: state?.displayName,
10319
- importedAt: (/* @__PURE__ */ new Date()).toISOString()
10320
- }
10321
- };
10713
+ stampImportMetaMerge(module, {
10714
+ source: "streamelements-element",
10715
+ role,
10716
+ referenceId: state?.referenceId,
10717
+ categoryId: state?.categoryId,
10718
+ displayName: state?.displayName
10719
+ });
10322
10720
  }
10323
- function importElementOverlay(response) {
10324
- const configData = parseElementConfigData(
10325
- response.widgetInstanceConfigVersion.configData
10326
- );
10721
+ function importElementOverlay(response, options = {}) {
10722
+ const configData = parseElementConfigData(response.widgetInstanceConfigVersion.configData);
10327
10723
  if (!configData) {
10328
10724
  throw new Error(
10329
10725
  "StreamElements Element response carries an invalid configData payload. The widget may be corrupted or the response format may have changed."
@@ -10393,7 +10789,10 @@ function importElementOverlay(response) {
10393
10789
  uuid: "",
10394
10790
  listen_id: "",
10395
10791
  name: response.widgetInstance.displayName ? `[SE Element] ${response.widgetInstance.displayName}` : `Imported from StreamElements Element (${response.widgetInstance.widgetInstanceId})`,
10396
- description: `Imported from StreamElements Element widget ${response.widgetInstance.widgetInstanceId}`,
10792
+ description: buildElementDescription(
10793
+ response.widgetInstance.widgetInstanceId,
10794
+ options.sourceUrl
10795
+ ),
10397
10796
  settings: {
10398
10797
  metadata: { width: canvas.width, height: canvas.height },
10399
10798
  layers,
@@ -10407,7 +10806,7 @@ function importElementOverlay(response) {
10407
10806
  }
10408
10807
 
10409
10808
  // src/se-import/index.ts
10410
- var IMPORT_META_KEY3 = "importMeta";
10809
+ var IMPORT_META_KEY2 = "importMeta";
10411
10810
  var REVIEW_REASONS = {
10412
10811
  "se-widget-bit-boss": "No native Lumia equivalent \u2014 bit boss fight bar with HP/damage states.",
10413
10812
  // `se-widget-hype-cup` intentionally omitted — it now routes to the native
@@ -10437,6 +10836,10 @@ function reasonFor(seType, status, flaggedOff) {
10437
10836
  if (flaggedOff && FLAG_OFF_REASONS[seType]) return FLAG_OFF_REASONS[seType];
10438
10837
  return REVIEW_REASONS[seType] ?? (status === "template" ? "Imported as a generic template stub." : "No native mapping yet \u2014 imported as a labelled text placeholder.");
10439
10838
  }
10839
+ function buildSEBootstrapDescription(overlayId, sourceUrl) {
10840
+ const head = overlayId ? `Imported from StreamElements overlay ${overlayId}` : "Imported from StreamElements";
10841
+ return buildImportDescription(head, sourceUrl);
10842
+ }
10440
10843
  function extractSEOverlayId(input) {
10441
10844
  const trimmed = input.trim();
10442
10845
  const m = trimmed.match(/[0-9a-f]{24}/i);
@@ -10491,7 +10894,7 @@ function isSEBootstrap(value) {
10491
10894
  const o = v.overlay;
10492
10895
  return Array.isArray(o.widgets);
10493
10896
  }
10494
- function importSEBootstrap(bootstrap) {
10897
+ function importSEBootstrap(bootstrap, options = {}) {
10495
10898
  const seOverlay = bootstrap.overlay;
10496
10899
  const widgets = seOverlay?.widgets ?? [];
10497
10900
  const coverage = {
@@ -10514,7 +10917,7 @@ function importSEBootstrap(bootstrap) {
10514
10917
  if (w.type !== "se-widget-group") continue;
10515
10918
  const uid = w.variables?.uid;
10516
10919
  if (typeof uid !== "string") continue;
10517
- const lumiaGroupId = nanoid10();
10920
+ const lumiaGroupId = nanoid8();
10518
10921
  seUidToLumiaGroupId[uid] = lumiaGroupId;
10519
10922
  groupWidgetByUid[uid] = w;
10520
10923
  }
@@ -10553,7 +10956,7 @@ function importSEBootstrap(bootstrap) {
10553
10956
  lights: [],
10554
10957
  css: {},
10555
10958
  content: {
10556
- [IMPORT_META_KEY3]: {
10959
+ [IMPORT_META_KEY2]: {
10557
10960
  source: "streamelements",
10558
10961
  widgetType: w.type,
10559
10962
  importedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -10591,7 +10994,7 @@ function importSEBootstrap(bootstrap) {
10591
10994
  if (result.status === "placeholder" || result.status === "template") {
10592
10995
  module.content = {
10593
10996
  ...module.content ?? {},
10594
- [IMPORT_META_KEY3]: {
10997
+ [IMPORT_META_KEY2]: {
10595
10998
  source: "streamelements",
10596
10999
  widgetType: w.type,
10597
11000
  importedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -10631,7 +11034,7 @@ function importSEBootstrap(bootstrap) {
10631
11034
  uuid: "",
10632
11035
  listen_id: "",
10633
11036
  name: seOverlay?.name ? `[SE] ${seOverlay.name}` : `Imported from StreamElements (${seOverlay?._id ?? ""})`,
10634
- description: seOverlay?._id ? `Imported from StreamElements overlay ${seOverlay._id}` : "Imported from StreamElements",
11037
+ description: buildSEBootstrapDescription(seOverlay?._id, options.sourceUrl),
10635
11038
  settings: {
10636
11039
  metadata: {
10637
11040
  width: seOverlay?.settings?.width ?? 1920,
@@ -10679,7 +11082,7 @@ function applyReviewAction(result, moduleId, action, payload) {
10679
11082
  bounds: existingLayer.bounds,
10680
11083
  state: existingLayer.state
10681
11084
  };
10682
- const importMeta2 = existing2.content?.[IMPORT_META_KEY3];
11085
+ const importMeta2 = existing2.content?.[IMPORT_META_KEY2];
10683
11086
  const mergedLights = (existing2.lights?.length ?? 0) > 0 ? existing2.lights : transplant.module.lights;
10684
11087
  const mergedEvents = existing2.events && Object.keys(existing2.events).length > 0 ? { ...transplant.module.events, ...existing2.events } : transplant.module.events;
10685
11088
  const mergedVariables = existing2.variables && Object.keys(existing2.variables).length > 0 ? { ...transplant.module.variables, ...existing2.variables } : transplant.module.variables;
@@ -10691,7 +11094,7 @@ function applyReviewAction(result, moduleId, action, payload) {
10691
11094
  variables: mergedVariables,
10692
11095
  content: {
10693
11096
  ...transplant.module.content ?? {},
10694
- [IMPORT_META_KEY3]: importMeta2
11097
+ [IMPORT_META_KEY2]: importMeta2
10695
11098
  }
10696
11099
  };
10697
11100
  const rootOldNewId = transplant.layer.id;
@@ -10719,7 +11122,7 @@ function applyReviewAction(result, moduleId, action, payload) {
10719
11122
  const existing = modules[moduleId];
10720
11123
  const generated = payload;
10721
11124
  if (!existing || !generated) return result;
10722
- const importMeta = existing.content?.[IMPORT_META_KEY3];
11125
+ const importMeta = existing.content?.[IMPORT_META_KEY2];
10723
11126
  const replaced = {
10724
11127
  ...existing,
10725
11128
  settings: { ...existing.settings ?? {}, type: "custom" },
@@ -10733,7 +11136,7 @@ function applyReviewAction(result, moduleId, action, payload) {
10733
11136
  data: parseLooseJson(generated.data) ?? {},
10734
11137
  configs: parseLooseJson(generated.configs) ?? [],
10735
11138
  flavor: "ai-generated",
10736
- [IMPORT_META_KEY3]: importMeta
11139
+ [IMPORT_META_KEY2]: importMeta
10737
11140
  }
10738
11141
  };
10739
11142
  modules[moduleId] = replaced;