@lumiastream/ui 0.5.3 → 0.5.5

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 = {
@@ -1050,26 +1050,55 @@ function mapText(widget, ctx) {
1050
1050
  ctx
1051
1051
  );
1052
1052
  }
1053
+ function seInnerAutoFit(widget, innerCss, mode) {
1054
+ const isInnerAutoWidth = innerCss?.width === "auto";
1055
+ const isInnerAutoHeight = innerCss?.height === "auto";
1056
+ if (mode === "width-drives-both") {
1057
+ if (!isInnerAutoWidth) return {};
1058
+ return { objectFit: "contain", autoWidth: true, autoHeight: true };
1059
+ }
1060
+ if (!isInnerAutoWidth && !isInnerAutoHeight) return {};
1061
+ const result = { objectFit: "contain" };
1062
+ if (isInnerAutoWidth && !isExplicitPxSize(widget.css?.width))
1063
+ result.autoWidth = true;
1064
+ if (isInnerAutoHeight && !isExplicitPxSize(widget.css?.height))
1065
+ result.autoHeight = true;
1066
+ return result;
1067
+ }
1068
+ function applySeAutoFitBounds(unit, fit) {
1069
+ const bounds = unit.layer.bounds;
1070
+ if (fit.autoWidth) bounds.autoWidth = true;
1071
+ if (fit.autoHeight) bounds.autoHeight = true;
1072
+ }
1053
1073
  function mapImage(widget, ctx) {
1074
+ const fit = seInnerAutoFit(widget, widget.image?.css, "inner-axis-gated");
1054
1075
  const unit = buildUnit(
1055
1076
  widget,
1056
1077
  "image",
1057
1078
  {
1058
1079
  content: {
1059
1080
  src: widget.image?.src ?? "",
1060
- loop: true
1081
+ loop: true,
1082
+ ...fit.objectFit ? { objectFit: fit.objectFit } : {}
1061
1083
  }
1062
1084
  },
1063
1085
  ctx
1064
1086
  );
1065
- const innerWidth = widget.image?.css?.width;
1066
- const innerHeight = widget.image?.css?.height;
1067
- const bounds = unit.layer.bounds;
1068
- if (innerWidth === "auto") bounds.autoWidth = true;
1069
- if (innerHeight === "auto") bounds.autoHeight = true;
1087
+ applySeAutoFitBounds(unit, fit);
1070
1088
  return unit;
1071
1089
  }
1090
+ function isExplicitPxSize(value) {
1091
+ if (value == null) return false;
1092
+ if (typeof value === "number") return Number.isFinite(value) && value > 0;
1093
+ if (typeof value !== "string") return false;
1094
+ const s = value.trim().toLowerCase();
1095
+ if (s === "" || s === "auto") return false;
1096
+ if (s.endsWith("%")) return false;
1097
+ const parsed = parseFloat(s);
1098
+ return Number.isFinite(parsed) && parsed > 0;
1099
+ }
1072
1100
  function mapVideo(widget, ctx) {
1101
+ const fit = seInnerAutoFit(widget, widget.video?.css, "width-drives-both");
1073
1102
  const unit = buildUnit(
1074
1103
  widget,
1075
1104
  "video",
@@ -1083,6 +1112,7 @@ function mapVideo(widget, ctx) {
1083
1112
  },
1084
1113
  ctx
1085
1114
  );
1115
+ applySeAutoFitBounds(unit, fit);
1086
1116
  unit.layer.bounds.autoHeight = true;
1087
1117
  return unit;
1088
1118
  }
@@ -1142,118 +1172,157 @@ function mapReadout(widget, fallbackVar, ctx) {
1142
1172
 
1143
1173
  // src/se-import/mappers/alert.ts
1144
1174
  import { LumiaAlertConfigs } from "@lumiastream/lumia-types";
1175
+ import { nanoid as nanoid2 } from "nanoid";
1176
+
1177
+ // src/se-import/import-utils.ts
1178
+ function pxToNumber(raw, fallback) {
1179
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw;
1180
+ if (typeof raw !== "string") return fallback;
1181
+ const trimmed = raw.trim();
1182
+ if (!trimmed) return fallback;
1183
+ if (trimmed.endsWith("px")) {
1184
+ const n2 = parseFloat(trimmed.slice(0, -2));
1185
+ return Number.isFinite(n2) ? n2 : fallback;
1186
+ }
1187
+ const n = parseFloat(trimmed);
1188
+ return Number.isFinite(n) ? n : fallback;
1189
+ }
1190
+ function parseDurationMs(raw, fallbackMs) {
1191
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw;
1192
+ if (typeof raw !== "string") return fallbackMs;
1193
+ const trimmed = raw.trim();
1194
+ if (!trimmed) return fallbackMs;
1195
+ if (trimmed.endsWith("ms")) {
1196
+ const n2 = parseFloat(trimmed.slice(0, -2));
1197
+ return Number.isFinite(n2) ? n2 : fallbackMs;
1198
+ }
1199
+ if (trimmed.endsWith("s")) {
1200
+ const n2 = parseFloat(trimmed.slice(0, -1));
1201
+ return Number.isFinite(n2) ? n2 * 1e3 : fallbackMs;
1202
+ }
1203
+ const n = parseFloat(trimmed);
1204
+ return Number.isFinite(n) ? n : fallbackMs;
1205
+ }
1206
+ function buildImportDescription(head, sourceUrl) {
1207
+ if (typeof sourceUrl === "string" && sourceUrl.trim().length > 0) {
1208
+ return `${head}
1209
+ Source: ${sourceUrl.trim()}`;
1210
+ }
1211
+ return head;
1212
+ }
1145
1213
 
1146
1214
  // src/se-import/mappers/provider-alerts.ts
1215
+ import { LumiaAlertValues } from "@lumiastream/lumia-types";
1147
1216
  var ALERT_BOX_KEYS = {
1148
1217
  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"]
1218
+ follower: [LumiaAlertValues.TWITCH_FOLLOWER],
1219
+ subscriber: [LumiaAlertValues.TWITCH_SUBSCRIBER],
1220
+ tip: [LumiaAlertValues.LUMIASTREAM_DONATION, LumiaAlertValues.STREAMELEMENTS_DONATION],
1221
+ cheer: [LumiaAlertValues.TWITCH_BITS],
1222
+ raid: [LumiaAlertValues.TWITCH_RAID],
1223
+ merch: [LumiaAlertValues.FOURTHWALL_SHOPORDER],
1224
+ purchase: [LumiaAlertValues.FOURTHWALL_SHOPORDER],
1225
+ charityCampaignDonation: [LumiaAlertValues.TWITCH_CHARITY_DONATION]
1157
1226
  },
1158
1227
  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"]
1228
+ follower: [LumiaAlertValues.KICK_FOLLOWER],
1229
+ subscriber: [LumiaAlertValues.KICK_SUBSCRIBER],
1230
+ tip: [LumiaAlertValues.LUMIASTREAM_DONATION, LumiaAlertValues.STREAMELEMENTS_DONATION],
1231
+ cheer: [LumiaAlertValues.KICK_KICKS],
1232
+ raid: [LumiaAlertValues.KICK_HOST],
1233
+ host: [LumiaAlertValues.KICK_HOST],
1234
+ merch: [LumiaAlertValues.FOURTHWALL_SHOPORDER],
1235
+ purchase: [LumiaAlertValues.FOURTHWALL_SHOPORDER]
1167
1236
  },
1168
1237
  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"]
1238
+ follower: [LumiaAlertValues.YOUTUBE_SUBSCRIBER],
1239
+ subscriber: [LumiaAlertValues.YOUTUBE_MEMBER],
1240
+ tip: [LumiaAlertValues.LUMIASTREAM_DONATION, LumiaAlertValues.STREAMELEMENTS_DONATION],
1241
+ cheer: [LumiaAlertValues.YOUTUBE_SUPERCHAT],
1242
+ merch: [LumiaAlertValues.FOURTHWALL_SHOPORDER],
1243
+ purchase: [LumiaAlertValues.FOURTHWALL_SHOPORDER]
1175
1244
  }
1176
1245
  };
1177
1246
  var KAPPAGEN_KEYS = {
1178
1247
  twitch: {
1179
- follower: "twitch-follower",
1180
- subscriber: "twitch-subscriber",
1181
- cheer: "twitch-bits",
1182
- raid: "twitch-raid",
1183
- tip: "lumiastream-donation"
1248
+ follower: LumiaAlertValues.TWITCH_FOLLOWER,
1249
+ subscriber: LumiaAlertValues.TWITCH_SUBSCRIBER,
1250
+ cheer: LumiaAlertValues.TWITCH_BITS,
1251
+ raid: LumiaAlertValues.TWITCH_RAID,
1252
+ tip: LumiaAlertValues.LUMIASTREAM_DONATION
1184
1253
  },
1185
1254
  kick: {
1186
- follower: "kick-follower",
1187
- subscriber: "kick-subscriber",
1188
- cheer: "kick-kicks",
1189
- raid: "kick-host",
1190
- tip: "lumiastream-donation"
1255
+ follower: LumiaAlertValues.KICK_FOLLOWER,
1256
+ subscriber: LumiaAlertValues.KICK_SUBSCRIBER,
1257
+ cheer: LumiaAlertValues.KICK_KICKS,
1258
+ raid: LumiaAlertValues.KICK_HOST,
1259
+ tip: LumiaAlertValues.LUMIASTREAM_DONATION
1191
1260
  },
1192
1261
  youtube: {
1193
- follower: "youtube-subscriber",
1194
- subscriber: "youtube-member",
1195
- cheer: "youtube-superchat",
1196
- tip: "lumiastream-donation"
1262
+ follower: LumiaAlertValues.YOUTUBE_SUBSCRIBER,
1263
+ subscriber: LumiaAlertValues.YOUTUBE_MEMBER,
1264
+ cheer: LumiaAlertValues.YOUTUBE_SUPERCHAT,
1265
+ tip: LumiaAlertValues.LUMIASTREAM_DONATION
1197
1266
  }
1198
1267
  };
1199
1268
  var STREAMBOSS_LISTENER_KEYS = {
1200
1269
  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"]
1270
+ "follower-latest": [LumiaAlertValues.TWITCH_FOLLOWER],
1271
+ "subscriber-latest": [LumiaAlertValues.TWITCH_SUBSCRIBER],
1272
+ "tip-latest": [LumiaAlertValues.LUMIASTREAM_DONATION, LumiaAlertValues.STREAMELEMENTS_DONATION],
1273
+ "cheer-latest": [LumiaAlertValues.TWITCH_BITS],
1274
+ "raid-latest": [LumiaAlertValues.TWITCH_RAID]
1206
1275
  },
1207
1276
  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"]
1277
+ "follower-latest": [LumiaAlertValues.KICK_FOLLOWER],
1278
+ "subscriber-latest": [LumiaAlertValues.KICK_SUBSCRIBER],
1279
+ "tip-latest": [LumiaAlertValues.LUMIASTREAM_DONATION, LumiaAlertValues.STREAMELEMENTS_DONATION],
1280
+ "cheer-latest": [LumiaAlertValues.KICK_KICKS],
1281
+ "raid-latest": [LumiaAlertValues.KICK_HOST]
1213
1282
  },
1214
1283
  youtube: {
1215
- "follower-latest": ["youtube-subscriber"],
1216
- "subscriber-latest": ["youtube-member"],
1217
- "tip-latest": ["lumiastream-donation", "streamelements-donation"],
1218
- "cheer-latest": ["youtube-superchat"]
1284
+ "follower-latest": [LumiaAlertValues.YOUTUBE_SUBSCRIBER],
1285
+ "subscriber-latest": [LumiaAlertValues.YOUTUBE_MEMBER],
1286
+ "tip-latest": [LumiaAlertValues.LUMIASTREAM_DONATION, LumiaAlertValues.STREAMELEMENTS_DONATION],
1287
+ "cheer-latest": [LumiaAlertValues.YOUTUBE_SUPERCHAT]
1219
1288
  }
1220
1289
  };
1221
1290
  var CURATED_ALERTS = {
1222
1291
  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"
1292
+ LumiaAlertValues.TWITCH_FOLLOWER,
1293
+ LumiaAlertValues.TWITCH_SUBSCRIBER,
1294
+ LumiaAlertValues.TWITCH_GIFT_SUBSCRIPTION,
1295
+ LumiaAlertValues.TWITCH_BITS,
1296
+ LumiaAlertValues.TWITCH_BITS_COMBO,
1297
+ LumiaAlertValues.TWITCH_RAID,
1298
+ LumiaAlertValues.TWITCH_POINTS,
1299
+ LumiaAlertValues.TWITCH_REDEMPTION,
1300
+ LumiaAlertValues.TWITCH_CHARITY_DONATION,
1301
+ LumiaAlertValues.TWITCH_HYPETRAIN_STARTED,
1302
+ LumiaAlertValues.TWITCH_CLIP,
1303
+ LumiaAlertValues.TWITCH_SHOUTOUT_RECEIVE,
1304
+ LumiaAlertValues.TWITCH_WATCH_STREAK,
1305
+ LumiaAlertValues.TWITCH_FIRST_CHATTER,
1306
+ LumiaAlertValues.TWITCH_EXTENSION
1238
1307
  ],
1239
1308
  kick: [
1240
- "kick-follower",
1241
- "kick-subscriber",
1242
- "kick-subscriptionGift",
1243
- "kick-kicks",
1244
- "kick-host",
1245
- "kick-points",
1246
- "kick-firstChatter"
1309
+ LumiaAlertValues.KICK_FOLLOWER,
1310
+ LumiaAlertValues.KICK_SUBSCRIBER,
1311
+ LumiaAlertValues.KICK_GIFT_SUBSCRIPTION,
1312
+ LumiaAlertValues.KICK_KICKS,
1313
+ LumiaAlertValues.KICK_HOST,
1314
+ LumiaAlertValues.KICK_POINTS,
1315
+ LumiaAlertValues.KICK_FIRST_CHATTER
1247
1316
  ],
1248
1317
  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"
1318
+ LumiaAlertValues.YOUTUBE_SUBSCRIBER,
1319
+ LumiaAlertValues.YOUTUBE_MEMBER,
1320
+ LumiaAlertValues.YOUTUBE_GIFT_MEMBERS,
1321
+ LumiaAlertValues.YOUTUBE_SUPERCHAT,
1322
+ LumiaAlertValues.YOUTUBE_SUPERSTICKER,
1323
+ LumiaAlertValues.YOUTUBE_GIFTS,
1324
+ LumiaAlertValues.YOUTUBE_LIKE,
1325
+ LumiaAlertValues.YOUTUBE_FIRST_CHATTER
1257
1326
  ]
1258
1327
  };
1259
1328
  function curatedAlertsFor(provider = "twitch") {
@@ -1321,8 +1390,12 @@ function buildAlertEvent(seEvent, seEventType) {
1321
1390
  const shadowEnabled = text.enableShadow !== false;
1322
1391
  const importedTextShadow = css["text-shadow"] ?? "rgba(0, 0, 0, 0.8) 1px 1px 1px";
1323
1392
  const textShadow = shadowEnabled ? importedTextShadow : "none";
1324
- const marginTopRaw = Number(css["margin-top"]);
1325
- const pushTextUp = seLayoutUsesPushText(seEvent.layout) && Number.isFinite(marginTopRaw) ? -marginTopRaw : 0;
1393
+ const allowPush = seLayoutUsesPushText(seEvent.layout);
1394
+ const pushTextUp = allowPush ? pxToNumber(css["margin-top"], 0) : 0;
1395
+ const pushTextLeft = allowPush ? pxToNumber(css["margin-left"], 0) : 0;
1396
+ const usesColumnLayout = !seEvent.layout || seEvent.layout === "column";
1397
+ const usesRowLayout = seEvent.layout === "row";
1398
+ const tightLayout = allowPush && (usesColumnLayout || usesRowLayout);
1326
1399
  const variations = (seEvent.variations ?? []).map((variation) => buildAlertVariation(variation, seEventType, seEvent)).filter((v) => v != null);
1327
1400
  const defaultDuration = (seEventType && SE_DEFAULT_DURATION_BY_EVENT[seEventType]) ?? 8;
1328
1401
  return {
@@ -1349,7 +1422,7 @@ function buildAlertEvent(seEvent, seEventType) {
1349
1422
  showBrandIcon: true
1350
1423
  },
1351
1424
  settings: { duration: (seEvent.duration ?? defaultDuration) * 1e3 },
1352
- layout: { value: translateLayout(seEvent.layout), css: {}, customCss: "", pushTextUp, pushTextLeft: 0 },
1425
+ layout: { value: translateLayout(seEvent.layout), css: {}, customCss: "", pushTextUp, pushTextLeft, tight: tightLayout },
1353
1426
  text: {
1354
1427
  css: {
1355
1428
  fontFamily: css["font-family"] ?? "Roboto",
@@ -1358,6 +1431,10 @@ function buildAlertEvent(seEvent, seEventType) {
1358
1431
  fontWeight: css["font-weight"] ?? "bold",
1359
1432
  textShadow,
1360
1433
  textAlign: css["text-align"] ?? "center"
1434
+ // SE's per-event `margin-top` / `margin-left` are routed to
1435
+ // `layout.pushTextUp` / `layout.pushTextLeft` above (not
1436
+ // stamped on `text.css`) so they land in the alert editor's
1437
+ // dedicated sliders. See the long comment near `pushTextUp`.
1361
1438
  },
1362
1439
  messageCss,
1363
1440
  content: { value: translateSeText(text.message ?? "") }
@@ -1427,6 +1504,15 @@ function buildAlertVariation(v, seEventType, parentSettings) {
1427
1504
  const mergedSettings = deepMergeSeSettings(parentSettings ? parentInheritable : void 0, v.settings);
1428
1505
  const base = buildAlertEvent(mergedSettings, seEventType);
1429
1506
  return {
1507
+ // Stable per-variation ID. Lumia's alert editor (Settings.tsx) auto-
1508
+ // stamps a nanoid on first open for any variation missing an id, but
1509
+ // that leaves a window where the imported overlay's variations have
1510
+ // no stable key — list components rendering them key on
1511
+ // `variation.id || variation.name || idx` and adjacent variations
1512
+ // with the same name collide. Stamping at import time gives every
1513
+ // variation a stable identity from save #1 without waiting for the
1514
+ // editor's first mount.
1515
+ id: nanoid2(),
1430
1516
  name: v.name ?? "Variation",
1431
1517
  on: v.enabled !== false,
1432
1518
  randomChance: v.chance ?? 100,
@@ -1510,7 +1596,7 @@ function mapSingleAlert(widget, lumiaAlertKey, ctx) {
1510
1596
  css: { ...topCss, ...innerCss }
1511
1597
  };
1512
1598
  }
1513
- const built = buildAlertEvent(merged);
1599
+ const built = buildAlertEvent(merged, void 0);
1514
1600
  const events = {
1515
1601
  [lumiaAlertKey]: built
1516
1602
  };
@@ -1656,7 +1742,7 @@ function mapGoal(widget, ctx) {
1656
1742
  }
1657
1743
 
1658
1744
  // src/se-import/mappers/custom.ts
1659
- import { nanoid as nanoid2 } from "nanoid";
1745
+ import { nanoid as nanoid3 } from "nanoid";
1660
1746
  var SOURCE_TO_LUMIA_FIELD_TYPE = {
1661
1747
  text: "input",
1662
1748
  number: "number",
@@ -1746,7 +1832,7 @@ function mapCustom(widget, ctx) {
1746
1832
  // imports — codeId is the scoping key for variables, storage, and
1747
1833
  // event listeners. Mint a fresh nanoid so every imported widget
1748
1834
  // has its own scope.
1749
- codeId: nanoid2(),
1835
+ codeId: nanoid3(),
1750
1836
  html: v.html ?? "",
1751
1837
  css: v.css ?? "",
1752
1838
  js: v.js ?? "",
@@ -1883,6 +1969,63 @@ var TIPJAR_DEFAULT_MODULE = {
1883
1969
  version: 1,
1884
1970
  content: TIPJAR_DEFAULT_CONTENT_FIELDS
1885
1971
  };
1972
+ var EVENTLIST_DEFAULT_CONTENT_FIELDS = {
1973
+ version: 2,
1974
+ eventListType: {},
1975
+ theme: "basic",
1976
+ themeConfig: {},
1977
+ borderRadius: "10px",
1978
+ condensedText: true,
1979
+ src: "",
1980
+ // Category blacklist — events whose Lumia type appears here are
1981
+ // hidden. SL `show_<type>: false` maps to inserting the equivalent
1982
+ // Lumia category here.
1983
+ filters: [],
1984
+ sites: [],
1985
+ maxItemsToShow: 20,
1986
+ removeAfter: 0,
1987
+ reverse: false,
1988
+ horizontal: false,
1989
+ animations: {
1990
+ enterAnimation: "fadeIn",
1991
+ exitAnimation: "fadeOut",
1992
+ enterAnimationDuration: 1e3,
1993
+ exitAnimationDuration: 1e3,
1994
+ enterAnimationDelay: 0,
1995
+ exitAnimationDelay: 0
1996
+ },
1997
+ showDetailedText: true,
1998
+ hideAlertMessage: false,
1999
+ showAlertIcon: true,
2000
+ showSiteIcon: false,
2001
+ showUserAvatar: false,
2002
+ fadeOutAfterDelay: false,
2003
+ fadeOutDelayTime: 5,
2004
+ image: "",
2005
+ itemGap: 16,
2006
+ audio: {
2007
+ enter: "",
2008
+ volume: 1
2009
+ }
2010
+ };
2011
+ var EVENTLIST_DEFAULT_MODULE = {
2012
+ version: 2,
2013
+ content: EVENTLIST_DEFAULT_CONTENT_FIELDS,
2014
+ css: {
2015
+ borderRadius: "0px",
2016
+ borderStyle: "solid",
2017
+ borderWidth: "0px",
2018
+ borderColor: "transparent",
2019
+ fontSize: 16,
2020
+ lineHeight: 1.2,
2021
+ textAlign: "left",
2022
+ fontFamily: "Roboto",
2023
+ fontWeight: "normal",
2024
+ color: "#ffffff",
2025
+ textShadow: "",
2026
+ background: "transparent"
2027
+ }
2028
+ };
1886
2029
 
1887
2030
  // src/se-import/mappers/misc.ts
1888
2031
  function mapChatboxTheme(seTheme) {
@@ -2169,7 +2312,11 @@ function mapEventList(widget, ctx) {
2169
2312
  ...isMarquee ? { marqueeItemGap: 24 } : {},
2170
2313
  showAlertIcon: !isMarquee,
2171
2314
  showSiteIcon: !isMarquee,
2172
- showUserAvatar: true,
2315
+ // SE event lists are text-only (`{name}: {amount}` rows for tip-recent,
2316
+ // follower-recent, etc.) — no avatar column. Mirror that on import so
2317
+ // the streamer's layout matches what they had in SE; they can re-enable
2318
+ // avatars from the Eventlist settings panel if they want.
2319
+ showUserAvatar: false,
2173
2320
  showDetailedText: false,
2174
2321
  hideAlertMessage: false
2175
2322
  }
@@ -3832,11 +3979,6 @@ function rewriteAssetURLs(overlay, mapping) {
3832
3979
  };
3833
3980
  }
3834
3981
  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
3982
  async function mirrorOneAsset(url, upload, signal, proxyAssetFetch) {
3841
3983
  const blob = await fetchAssetBlob(url, signal, proxyAssetFetch);
3842
3984
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
@@ -3868,7 +4010,7 @@ async function fetchAssetBlob(url, signal, proxyAssetFetch) {
3868
4010
  return res.blob();
3869
4011
  }
3870
4012
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
3871
- if (proxyAssetFetch && looksLikeCorsError2(directErr)) {
4013
+ if (proxyAssetFetch && looksLikeCorsError(directErr)) {
3872
4014
  try {
3873
4015
  return await proxyAssetFetch(url);
3874
4016
  } catch (proxyErr) {
@@ -3919,7 +4061,7 @@ function guessMime(name) {
3919
4061
  }
3920
4062
 
3921
4063
  // src/se-import/transplant.ts
3922
- import { nanoid as nanoid3 } from "nanoid";
4064
+ import { nanoid as nanoid4 } from "nanoid";
3923
4065
  function cloneModuleForTransplant(srcModule, newId) {
3924
4066
  const clonedContent = srcModule.content ? structuredClone(srcModule.content) : srcModule.content;
3925
4067
  if (srcModule.settings?.type === "custom" && clonedContent) {
@@ -3937,7 +4079,7 @@ function transplantLayer(source, sourceLayerId) {
3937
4079
  const srcLayer = layers.find((l) => l.id === sourceLayerId);
3938
4080
  const srcModule = modules[sourceLayerId];
3939
4081
  if (!srcLayer || !srcModule) return null;
3940
- const rootNewId = nanoid3();
4082
+ const rootNewId = nanoid4();
3941
4083
  if (srcLayer.type !== "group") {
3942
4084
  const layer = { ...srcLayer, id: rootNewId };
3943
4085
  const module = cloneModuleForTransplant(srcModule, rootNewId);
@@ -3955,7 +4097,7 @@ function transplantLayer(source, sourceLayerId) {
3955
4097
  if (seen.has(child.id)) continue;
3956
4098
  seen.add(child.id);
3957
4099
  descendants.push(child);
3958
- idRemap.set(child.id, nanoid3());
4100
+ idRemap.set(child.id, nanoid4());
3959
4101
  if (child.type === "group") queue.push(child.id);
3960
4102
  }
3961
4103
  }
@@ -4373,9 +4515,6 @@ var LSInput = forwardRef(
4373
4515
  inputAfterText,
4374
4516
  maxWidth,
4375
4517
  className = "",
4376
- InputProps: inputPropsProp,
4377
- InputLabelProps: inputLabelPropsProp,
4378
- inputProps: htmlInputPropsProp,
4379
4518
  slotProps: slotPropsProp,
4380
4519
  onChange,
4381
4520
  onChangeStart,
@@ -4410,25 +4549,11 @@ var LSInput = forwardRef(
4410
4549
  onChange?.(event, sanitizedValue);
4411
4550
  onChangeEnd?.(event, sanitizedValue);
4412
4551
  };
4413
- const resolvedStartAdornment = inputPropsProp?.startAdornment ?? (startAdornment ? /* @__PURE__ */ jsx2(InputAdornment, { position: "start", children: startAdornment }) : inputBeforeText ? /* @__PURE__ */ jsx2(InputAdornment, { position: "start", children: /* @__PURE__ */ jsx2("span", { className: "mui-ls-input-affix", children: inputBeforeText }) }) : void 0);
4414
- const resolvedEndAdornment = inputPropsProp?.endAdornment ?? (endAdornment ? /* @__PURE__ */ jsx2(InputAdornment, { position: "end", children: endAdornment }) : inputAfterText ? /* @__PURE__ */ jsx2(InputAdornment, { position: "end", children: /* @__PURE__ */ jsx2("span", { className: "mui-ls-input-affix", children: inputAfterText }) }) : void 0);
4415
- const inputSlotProps = {
4416
- ...inputPropsProp,
4417
- startAdornment: resolvedStartAdornment,
4418
- endAdornment: resolvedEndAdornment,
4419
- readOnly: rest.readOnly ?? inputPropsProp?.readOnly,
4420
- sx: {
4421
- ...inputPropsProp?.sx ?? {},
4422
- "& input, & textarea": {
4423
- textAlign: centerText ? "center" : void 0,
4424
- color: textColor ?? void 0
4425
- }
4426
- }
4427
- };
4428
- const inputLabelProps = {
4429
- shrink: true,
4430
- ...inputLabelPropsProp
4431
- };
4552
+ const callerInputSlot = slotPropsProp?.input ?? {};
4553
+ const callerHtmlInputSlot = slotPropsProp?.htmlInput ?? {};
4554
+ const callerInputLabelSlot = slotPropsProp?.inputLabel ?? {};
4555
+ const resolvedStartAdornment = callerInputSlot.startAdornment ?? (startAdornment ? /* @__PURE__ */ jsx2(InputAdornment, { position: "start", children: startAdornment }) : inputBeforeText ? /* @__PURE__ */ jsx2(InputAdornment, { position: "start", children: /* @__PURE__ */ jsx2("span", { className: "mui-ls-input-affix", children: inputBeforeText }) }) : void 0);
4556
+ const resolvedEndAdornment = callerInputSlot.endAdornment ?? (endAdornment ? /* @__PURE__ */ jsx2(InputAdornment, { position: "end", children: endAdornment }) : inputAfterText ? /* @__PURE__ */ jsx2(InputAdornment, { position: "end", children: /* @__PURE__ */ jsx2("span", { className: "mui-ls-input-affix", children: inputAfterText }) }) : void 0);
4432
4557
  const helperContent = helperText ?? (typeof error === "string" ? error : "");
4433
4558
  const hasError = typeof error === "string" ? true : Boolean(error);
4434
4559
  const resolvedMaxWidth = maxWidth ?? style?.maxWidth;
@@ -4436,20 +4561,24 @@ var LSInput = forwardRef(
4436
4561
  const slotProps = {
4437
4562
  ...slotPropsProp ?? {},
4438
4563
  input: {
4439
- ...slotPropsProp?.input ?? {},
4440
- ...inputSlotProps,
4564
+ ...callerInputSlot,
4565
+ startAdornment: resolvedStartAdornment,
4566
+ endAdornment: resolvedEndAdornment,
4567
+ readOnly: rest.readOnly ?? callerInputSlot.readOnly,
4441
4568
  sx: {
4442
- ...slotPropsProp?.input?.sx ?? {},
4443
- ...inputSlotProps.sx
4569
+ ...callerInputSlot.sx ?? {},
4570
+ "& input, & textarea": {
4571
+ textAlign: centerText ? "center" : void 0,
4572
+ color: textColor ?? void 0
4573
+ }
4444
4574
  }
4445
4575
  },
4446
4576
  inputLabel: {
4447
- ...slotPropsProp?.inputLabel ?? {},
4448
- ...inputLabelProps
4577
+ shrink: true,
4578
+ ...callerInputLabelSlot
4449
4579
  },
4450
4580
  htmlInput: {
4451
- ...slotPropsProp?.htmlInput ?? {},
4452
- ...htmlInputPropsProp ?? {}
4581
+ ...callerHtmlInputSlot
4453
4582
  }
4454
4583
  };
4455
4584
  const TextFieldComponent = TextField;
@@ -4639,15 +4768,15 @@ var LSSliderInput = ({
4639
4768
  onChange: (_event, nextValue) => handleInputChange({ target: { value: nextValue ?? "" } }),
4640
4769
  value: displayValue ?? "",
4641
4770
  inputAfterText,
4642
- InputProps: {
4643
- ...inputProps?.InputProps ?? {},
4644
- name,
4645
- type: "number",
4646
- value: displayValue,
4647
- min: displayMinimum,
4648
- max: displayMaximum,
4649
- step: displayStep,
4650
- inputMode: stepPrecision > 0 ? "decimal" : "numeric"
4771
+ slotProps: {
4772
+ ...inputProps?.slotProps ?? {},
4773
+ htmlInput: {
4774
+ ...inputProps?.slotProps?.htmlInput ?? {},
4775
+ min: displayMinimum,
4776
+ max: displayMaximum,
4777
+ step: displayStep,
4778
+ inputMode: stepPrecision > 0 ? "decimal" : "numeric"
4779
+ }
4651
4780
  },
4652
4781
  fullWidth: hideSlider ? fullWidth : false,
4653
4782
  $noMinHeight: false,
@@ -4711,6 +4840,16 @@ var LSSelect = ({
4711
4840
  border: "1px solid var(--neutralDark4)",
4712
4841
  borderRadius: "var(--radius, 1rem)",
4713
4842
  boxShadow: "0 16px 32px rgba(0, 0, 0, 0.32)",
4843
+ "& .MuiList-root": {
4844
+ backgroundColor: "var(--neutralDark2)"
4845
+ },
4846
+ "& .MuiListSubheader-root": {
4847
+ backgroundColor: "var(--neutralDark1)",
4848
+ color: "var(--neutralLight2)"
4849
+ },
4850
+ "& .MuiMenuItem-root": {
4851
+ color: "var(--neutralLight1)"
4852
+ },
4714
4853
  ...MenuProps?.slotProps?.paper?.sx
4715
4854
  }
4716
4855
  },
@@ -4722,28 +4861,6 @@ var LSSelect = ({
4722
4861
  ...MenuProps?.slotProps?.list?.sx
4723
4862
  }
4724
4863
  }
4725
- },
4726
- PaperProps: {
4727
- ...MenuProps?.PaperProps ?? {},
4728
- sx: {
4729
- background: "var(--neutralDark2) !important",
4730
- backgroundColor: "var(--neutralDark2)",
4731
- color: "var(--neutralLight1)",
4732
- border: "1px solid var(--neutralDark4)",
4733
- borderRadius: "var(--radius, 1rem)",
4734
- boxShadow: "0 16px 32px rgba(0, 0, 0, 0.32)",
4735
- "& .MuiList-root": {
4736
- backgroundColor: "var(--neutralDark2)"
4737
- },
4738
- "& .MuiListSubheader-root": {
4739
- backgroundColor: "var(--neutralDark1)",
4740
- color: "var(--neutralLight2)"
4741
- },
4742
- "& .MuiMenuItem-root": {
4743
- color: "var(--neutralLight1)"
4744
- },
4745
- ...MenuProps?.PaperProps?.sx
4746
- }
4747
4864
  }
4748
4865
  };
4749
4866
  return /* @__PURE__ */ jsxs2(
@@ -4856,14 +4973,7 @@ LSDatePicker.displayName = "LSDatePicker";
4856
4973
  import { KeyboardArrowDown } from "@mui/icons-material";
4857
4974
  import Autocomplete from "@mui/material/Autocomplete";
4858
4975
  import TextField2 from "@mui/material/TextField";
4859
- import {
4860
- memo,
4861
- startTransition,
4862
- useEffect as useEffect3,
4863
- useMemo as useMemo2,
4864
- useRef as useRef2,
4865
- useState as useState3
4866
- } from "react";
4976
+ import { memo, startTransition, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef2, useState as useState3 } from "react";
4867
4977
  import { jsx as jsx8 } from "react/jsx-runtime";
4868
4978
  var LSFontPicker = memo(function LSFontPicker2({
4869
4979
  value,
@@ -4921,9 +5031,7 @@ var LSFontPicker = memo(function LSFontPicker2({
4921
5031
  popupIcon: /* @__PURE__ */ jsx8(KeyboardArrowDown, {}),
4922
5032
  isOptionEqualToValue: (option, selectedValue) => option === selectedValue,
4923
5033
  onChange: (_event, newValue) => {
4924
- handleValueChange(
4925
- typeof newValue === "string" ? newValue : `${newValue ?? ""}`
4926
- );
5034
+ handleValueChange(typeof newValue === "string" ? newValue : `${newValue ?? ""}`);
4927
5035
  },
4928
5036
  onInputChange: (_event, newInputValue, reason) => {
4929
5037
  if (reason === "input" || reason === "clear") {
@@ -4980,11 +5088,7 @@ var LSFontPicker = memo(function LSFontPicker2({
4980
5088
  );
4981
5089
  },
4982
5090
  renderInput: (params) => {
4983
- const {
4984
- InputLabelProps: paramsInputLabelProps,
4985
- InputProps: paramsInputProps,
4986
- ...restParams
4987
- } = params;
5091
+ const { slotProps: paramsSlotProps = {}, ...restParams } = params;
4988
5092
  return /* @__PURE__ */ jsx8(
4989
5093
  TextFieldComponent,
4990
5094
  {
@@ -5001,12 +5105,16 @@ var LSFontPicker = memo(function LSFontPicker2({
5001
5105
  }
5002
5106
  },
5003
5107
  slotProps: {
5108
+ ...paramsSlotProps,
5004
5109
  inputLabel: {
5005
- shrink: true,
5006
- ...paramsInputLabelProps ?? {}
5110
+ ...paramsSlotProps.inputLabel ?? {},
5111
+ shrink: true
5007
5112
  },
5008
5113
  input: {
5009
- ...paramsInputProps ?? {}
5114
+ ...paramsSlotProps.input ?? {}
5115
+ },
5116
+ htmlInput: {
5117
+ ...paramsSlotProps.htmlInput ?? {}
5010
5118
  }
5011
5119
  }
5012
5120
  }
@@ -5050,7 +5158,8 @@ import { forwardRef as forwardRef4 } from "react";
5050
5158
  import { jsx as jsx11 } from "react/jsx-runtime";
5051
5159
  var LSTextField = forwardRef4(
5052
5160
  (props, ref) => {
5053
- const { InputLabelProps, InputProps, slotProps, ...rest } = props;
5161
+ const { slotProps, ...rest } = props;
5162
+ const paramsInputLabel = slotProps?.inputLabel ?? {};
5054
5163
  return /* @__PURE__ */ jsx11(
5055
5164
  TextField3,
5056
5165
  {
@@ -5059,18 +5168,9 @@ var LSTextField = forwardRef4(
5059
5168
  className: "mui-ls-input",
5060
5169
  slotProps: {
5061
5170
  ...slotProps ?? {},
5062
- input: {
5063
- ...slotProps?.input ?? {},
5064
- ...InputProps ?? {},
5065
- sx: {
5066
- ...slotProps?.input?.sx ?? {},
5067
- ...InputProps?.sx ?? {}
5068
- }
5069
- },
5070
5171
  inputLabel: {
5071
- ...slotProps?.inputLabel ?? {},
5072
- shrink: true,
5073
- ...InputLabelProps ?? {}
5172
+ ...paramsInputLabel,
5173
+ shrink: true
5074
5174
  }
5075
5175
  },
5076
5176
  ref
@@ -6223,14 +6323,12 @@ var VariableInputTextField = forwardRef5(
6223
6323
  containerRef,
6224
6324
  showVariableIcon
6225
6325
  }, ref) => {
6226
- const inputPropsInputProps = inputProps?.InputProps ?? {};
6227
- const paramsInputProps = params?.InputProps ?? {};
6228
- const inputPropsSlotInputProps = inputProps?.slotProps?.input ?? {};
6229
- const paramsSlotInputProps = params?.slotProps?.input ?? {};
6326
+ const inputPropsSlotInput = inputProps?.slotProps?.input ?? {};
6327
+ const paramsSlotInput = params?.slotProps?.input ?? {};
6230
6328
  const explicitStartAdornment = inputProps?.startAdornment;
6231
6329
  const resolvedExplicitStartAdornment = explicitStartAdornment ? /* @__PURE__ */ jsx12(InputAdornment2, { position: "start", children: explicitStartAdornment }) : void 0;
6232
- const startAdornment = paramsInputProps.startAdornment ?? paramsSlotInputProps.startAdornment ?? inputPropsInputProps.startAdornment ?? inputPropsSlotInputProps.startAdornment ?? resolvedExplicitStartAdornment;
6233
- const endAdornment = paramsInputProps.endAdornment ?? paramsSlotInputProps.endAdornment ?? inputPropsInputProps.endAdornment ?? inputPropsSlotInputProps.endAdornment;
6330
+ const startAdornment = paramsSlotInput.startAdornment ?? inputPropsSlotInput.startAdornment ?? resolvedExplicitStartAdornment;
6331
+ const endAdornment = paramsSlotInput.endAdornment ?? inputPropsSlotInput.endAdornment;
6234
6332
  return /* @__PURE__ */ jsx12(
6235
6333
  LSTextField,
6236
6334
  {
@@ -6252,31 +6350,27 @@ var VariableInputTextField = forwardRef5(
6252
6350
  onChange: onChange ? (e) => onChange(e.target.value) : void 0,
6253
6351
  ...inputProps,
6254
6352
  ...params,
6255
- InputProps: {
6256
- ...inputPropsSlotInputProps,
6257
- ...inputPropsInputProps,
6258
- ...paramsSlotInputProps,
6259
- ...paramsInputProps,
6260
- startAdornment,
6261
- endAdornment: /* @__PURE__ */ jsxs5(Fragment3, { children: [
6262
- endAdornment ?? null,
6263
- showVariableIcon ? /* @__PURE__ */ jsx12(
6264
- Tooltip,
6265
- {
6266
- title: t("chatbot.allowed-variables", "Allowed Variables"),
6267
- children: /* @__PURE__ */ jsx12(
6268
- InputAdornment2,
6269
- {
6270
- position: "end",
6271
- onClick: clickedVariableIcon,
6272
- ref: containerRef,
6273
- className: "ls-variable-input-adornment",
6274
- children: "{}"
6275
- }
6276
- )
6277
- }
6278
- ) : null
6279
- ] })
6353
+ slotProps: {
6354
+ ...inputProps?.slotProps ?? {},
6355
+ ...params?.slotProps ?? {},
6356
+ input: {
6357
+ ...inputPropsSlotInput,
6358
+ ...paramsSlotInput,
6359
+ startAdornment,
6360
+ endAdornment: /* @__PURE__ */ jsxs5(Fragment3, { children: [
6361
+ endAdornment ?? null,
6362
+ showVariableIcon ? /* @__PURE__ */ jsx12(Tooltip, { title: t("chatbot.allowed-variables", "Allowed Variables"), children: /* @__PURE__ */ jsx12(
6363
+ InputAdornment2,
6364
+ {
6365
+ position: "end",
6366
+ onClick: clickedVariableIcon,
6367
+ ref: containerRef,
6368
+ className: "ls-variable-input-adornment",
6369
+ children: "{}"
6370
+ }
6371
+ ) }) : null
6372
+ ] })
6373
+ }
6280
6374
  },
6281
6375
  inputRef: ref
6282
6376
  }
@@ -6441,7 +6535,74 @@ function readSLProvider(response) {
6441
6535
  }
6442
6536
 
6443
6537
  // src/sl-import/mappers/alert-box.ts
6444
- import { nanoid as nanoid4 } from "nanoid";
6538
+ import { nanoid as nanoid6 } from "nanoid";
6539
+ import { LumiaAlertValues as LumiaAlertValues2 } from "@lumiastream/lumia-types";
6540
+
6541
+ // src/se-import/import-envelope.ts
6542
+ import { nanoid as nanoid5 } from "nanoid";
6543
+ function buildModuleEnvelope(opts) {
6544
+ const id = opts.id ?? nanoid5();
6545
+ const locked = opts.locked ?? false;
6546
+ const visible = opts.visible ?? true;
6547
+ const layer = {
6548
+ id,
6549
+ ...opts.parentGroupId !== void 0 ? { group: opts.parentGroupId } : {},
6550
+ ...opts.type_layer ? { type: opts.type_layer } : {},
6551
+ state: {
6552
+ visible,
6553
+ locked,
6554
+ ...opts.expanded !== void 0 ? { expanded: opts.expanded } : {}
6555
+ },
6556
+ bounds: {
6557
+ x: opts.bounds.x,
6558
+ y: opts.bounds.y,
6559
+ width: opts.bounds.width,
6560
+ height: opts.bounds.height,
6561
+ scale: [1, 1],
6562
+ ...opts.zIndex !== void 0 ? { zIndex: opts.zIndex } : {}
6563
+ }
6564
+ };
6565
+ const module = {
6566
+ id,
6567
+ version: opts.version ?? 1,
6568
+ loaded: true,
6569
+ settings: { type: opts.type, title: opts.title, locked },
6570
+ lights: opts.lights ?? [],
6571
+ css: opts.css ?? {},
6572
+ content: opts.content ?? {},
6573
+ variables: opts.variables ?? {},
6574
+ events: opts.events ?? {},
6575
+ ...opts.alert !== void 0 ? { alert: opts.alert } : {}
6576
+ };
6577
+ return { id, layer, module };
6578
+ }
6579
+ var IMPORT_META_KEY = "importMeta";
6580
+ function stampImportMetaMerge(module, meta) {
6581
+ const existing = module.content?.[IMPORT_META_KEY] ?? {};
6582
+ module.content = {
6583
+ ...module.content ?? {},
6584
+ [IMPORT_META_KEY]: {
6585
+ importedAt: (/* @__PURE__ */ new Date()).toISOString(),
6586
+ ...existing,
6587
+ ...meta
6588
+ }
6589
+ };
6590
+ }
6591
+ function customHtmlReviewItem(moduleId, importSourceType, settings, reason) {
6592
+ return {
6593
+ moduleId,
6594
+ // ReviewItem's `seWidget` is typed against SEWidget but the review
6595
+ // UI just reads `.type` and `.settings`. Cast through unknown is
6596
+ // the only place we touch the shape mismatch; widening the
6597
+ // ReviewItem type is a future cleanup.
6598
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6599
+ seWidget: { type: importSourceType, settings },
6600
+ status: "template",
6601
+ reason
6602
+ };
6603
+ }
6604
+
6605
+ // src/sl-import/mappers/alert-box.ts
6445
6606
  var SL_EVENT_ROUTING = {
6446
6607
  // Platform-native — provider routes (Twitch / Kick / YouTube).
6447
6608
  follow: { pickProviderKeys: "follower" },
@@ -6453,47 +6614,35 @@ var SL_EVENT_ROUTING = {
6453
6614
  // `subscriber` here is YouTube subscribe (free); `sponsor` is YouTube
6454
6615
  // channel membership; `fanfunding` is YouTube superchat. SL ships these
6455
6616
  // 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"] },
6617
+ subscriber: { keys: [LumiaAlertValues2.YOUTUBE_SUBSCRIBER] },
6618
+ sponsor: { keys: [LumiaAlertValues2.YOUTUBE_MEMBER] },
6619
+ fanfunding: { keys: [LumiaAlertValues2.YOUTUBE_SUPERCHAT] },
6620
+ super_sticker: { keys: [LumiaAlertValues2.YOUTUBE_SUPERSTICKER] },
6460
6621
  // Donation / tip channels — Lumia ships dedicated alert keys for each
6461
6622
  // integration. `donation` itself fans out to `streamlabs-donation` AND
6462
6623
  // `lumiastream-donation` so the imported alert fires regardless of which
6463
6624
  // 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"] },
6625
+ donation: { keys: [LumiaAlertValues2.STREAMLABS_DONATION, LumiaAlertValues2.LUMIASTREAM_DONATION] },
6626
+ eldonation: { keys: [LumiaAlertValues2.EXTRALIFE_DONATION] },
6627
+ tiltifydonation: { keys: [LumiaAlertValues2.TILTIFY_DONATION] },
6628
+ donordrivedonation: { keys: [LumiaAlertValues2.DONORDRIVE_DONATION] },
6629
+ justgivingdonation: { keys: [LumiaAlertValues2.STREAMLABS_DONATION] },
6630
+ pledge: { keys: [LumiaAlertValues2.PATREON_PLEDGE] },
6470
6631
  // Charity / merch / loyalty / prime gifting — SL-flavored channels with
6471
6632
  // 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"] },
6633
+ streamlabscharitydonation: { keys: [LumiaAlertValues2.STREAMLABS_CHARITY] },
6634
+ twitchcharitydonation: { keys: [LumiaAlertValues2.TWITCH_CHARITY_DONATION] },
6635
+ merch: { keys: [LumiaAlertValues2.STREAMLABS_MERCH] },
6636
+ loyalty_store_redemption: { keys: [LumiaAlertValues2.STREAMLABS_REDEMPTION] },
6637
+ prime_sub_gift: { keys: [LumiaAlertValues2.STREAMLABS_PRIMEGIFT] },
6638
+ treat: { keys: [LumiaAlertValues2.TREATSTREAM_TREAT] },
6478
6639
  // TikTok-flavored (jewel gift is TikTok-ish in concept).
6479
- jewel_gift: { keys: ["tiktok-gift"] }
6640
+ jewel_gift: { keys: [LumiaAlertValues2.TIKTOK_GIFT] }
6480
6641
  };
6481
6642
  function getField(settings, prefix, field) {
6482
6643
  const key = `${prefix}_${field}`;
6483
6644
  return settings[key];
6484
6645
  }
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
6646
  function translateLayout2(slLayout) {
6498
6647
  switch (slLayout) {
6499
6648
  case "above":
@@ -6603,9 +6752,7 @@ function buildBaseAlertEvent(settings, prefix, defaultMessage) {
6603
6752
  tts: {
6604
6753
  enabled: getField(settings, prefix, "tts_enabled") ?? false,
6605
6754
  voice: getField(settings, prefix, "tts_language") ?? "Brian",
6606
- volume: clampVolume(
6607
- getField(settings, prefix, "tts_volume") ?? 75
6608
- ),
6755
+ volume: clampVolume(getField(settings, prefix, "tts_volume") ?? 75),
6609
6756
  minAmount: getField(settings, prefix, "tts_min_amount") ?? 0
6610
6757
  },
6611
6758
  variations: []
@@ -6620,10 +6767,7 @@ function readSecondaryMessageConfig(settings, prefix, primaryFontFamily) {
6620
6767
  const enabled = settings[showKey];
6621
6768
  if (enabled !== true) return null;
6622
6769
  const fontFamily = getField(settings, secondaryPrefix, "message_font") ?? primaryFontFamily;
6623
- const fontSize = pxToNumber(
6624
- getField(settings, secondaryPrefix, "message_font_size"),
6625
- 20
6626
- );
6770
+ const fontSize = pxToNumber(getField(settings, secondaryPrefix, "message_font_size"), 20);
6627
6771
  const fontWeight = getField(settings, secondaryPrefix, "message_font_weight") ?? 400;
6628
6772
  const fontColor = getField(settings, secondaryPrefix, "message_font_color") ?? "#FFFFFF";
6629
6773
  return {
@@ -6650,11 +6794,20 @@ function translateSLVariationCondition(v) {
6650
6794
  const numericData = typeof data === "number" ? data : Number(data);
6651
6795
  switch (v.condition) {
6652
6796
  case "RANDOM":
6653
- return { conditionType: "RANDOM", condition: Number.isFinite(numericData) ? numericData : 100 };
6797
+ return {
6798
+ conditionType: "RANDOM",
6799
+ condition: Number.isFinite(numericData) ? numericData : 100
6800
+ };
6654
6801
  case "MIN_BITS_USED":
6655
- return { conditionType: "GREATER_NUMBER", condition: Number.isFinite(numericData) ? numericData : 0 };
6802
+ return {
6803
+ conditionType: "GREATER_NUMBER",
6804
+ condition: Number.isFinite(numericData) ? numericData : 0
6805
+ };
6656
6806
  case "EXACT_BITS_USED":
6657
- return { conditionType: "EQUAL_NUMBER", condition: Number.isFinite(numericData) ? numericData : 0 };
6807
+ return {
6808
+ conditionType: "EQUAL_NUMBER",
6809
+ condition: Number.isFinite(numericData) ? numericData : 0
6810
+ };
6658
6811
  case "MIN_MONTHS_SUBSCRIBED":
6659
6812
  return {
6660
6813
  conditionType: "SUBSCRIBED_MONTHS_GREATER",
@@ -6719,6 +6872,14 @@ function buildVariationEvent(v, parentBase) {
6719
6872
  if (s.hideAnimation) inherited.animations.alertExitAnimation = s.hideAnimation;
6720
6873
  if (s.layout) inherited.layout.value = translateLayout2(s.layout);
6721
6874
  return {
6875
+ // Reuse SL's own `uuid` when present so re-importing the same SL
6876
+ // alert-box keeps the variation identity stable across import
6877
+ // runs. SL always emits a uuid in their config (every fixture we've
6878
+ // seen has one); fall back to nanoid for the edge case of a
6879
+ // hand-crafted / older payload that omits it. Matches the SE
6880
+ // alert mapper's variation-id pattern — see Settings.tsx auto-fill
6881
+ // note there for why a stable id matters from save #1.
6882
+ id: typeof v.uuid === "string" && v.uuid.length > 0 ? v.uuid : nanoid6(),
6722
6883
  name: v.name ?? "Variation",
6723
6884
  on: v.paused !== true,
6724
6885
  randomChance: 100,
@@ -6784,11 +6945,7 @@ function buildSLAlertBox(settings, options = {}) {
6784
6945
  for (const [prefix, routing] of Object.entries(SL_EVENT_ROUTING)) {
6785
6946
  const lumiaKeys = routing.pickProviderKeys ? alertBoxKeysFor(routing.pickProviderKeys, provider) : routing.keys ?? [];
6786
6947
  if (lumiaKeys.length === 0) continue;
6787
- const built = buildBaseAlertEvent(
6788
- settings,
6789
- prefix,
6790
- DEFAULT_MESSAGES[prefix] ?? "{name}"
6791
- );
6948
+ const built = buildBaseAlertEvent(settings, prefix, DEFAULT_MESSAGES[prefix] ?? "{name}");
6792
6949
  if (!built) continue;
6793
6950
  const variationKey = VARIATION_KEY_FOR_PREFIX[prefix];
6794
6951
  const variations = variationKey ? settings[variationKey] : void 0;
@@ -6805,53 +6962,32 @@ function buildSLAlertBox(settings, options = {}) {
6805
6962
  channelsImported.push(prefix);
6806
6963
  if (firstEvent == null) firstEvent = built;
6807
6964
  }
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
6965
  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: {},
6966
+ const { layer, module } = buildModuleEnvelope({
6967
+ type: "alert",
6968
+ title: "Streamlabs Alerts",
6969
+ bounds: { x: 100, y: 100, width: 700, height: 600 },
6825
6970
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
6826
6971
  alert: { ...baseAlert, events }
6827
- };
6972
+ });
6828
6973
  return { layer, module, channelsImported };
6829
6974
  }
6830
6975
  function stripEventOnlyFields(event) {
6831
- const { on: _on, variations: _variations, matchEmptyCondition: _mec, ...base } = event;
6976
+ const {
6977
+ on: _on,
6978
+ variations: _variations,
6979
+ matchEmptyCondition: _mec,
6980
+ ...base
6981
+ } = event;
6832
6982
  return base;
6833
6983
  }
6834
6984
 
6835
6985
  // 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
6986
  function buildSLTipJar(settings) {
6850
- const id = nanoid5();
6851
6987
  const backgroundColor = settings.background?.color ?? "transparent";
6852
6988
  const textColor = settings.text?.color ?? "#FFFFFF";
6853
6989
  const fontFamily = settings.text?.font ?? "Open Sans";
6854
- const fontSize = pxToNumber2(settings.text?.size, 32);
6990
+ const fontSize = pxToNumber(settings.text?.size, 32);
6855
6991
  const types = settings.types ?? {};
6856
6992
  const tipsConfig = types.tips ?? {};
6857
6993
  const slEvents = {
@@ -6867,17 +7003,11 @@ function buildSLTipJar(settings) {
6867
7003
  ...TIPJAR_DEFAULT_MODULE.content.display,
6868
7004
  showEventMessages: settings.text?.show !== false
6869
7005
  };
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,
7006
+ const { layer, module } = buildModuleEnvelope({
7007
+ type: "tipjar",
7008
+ title: "Streamlabs Tip Jar",
7009
+ bounds: { x: 100, y: 100, width: 600, height: 700 },
6877
7010
  version: TIPJAR_DEFAULT_MODULE.version,
6878
- loaded: true,
6879
- settings: { type: "tipjar", title: "Streamlabs Tip Jar", locked: false },
6880
- lights: [],
6881
7011
  css: {
6882
7012
  fontFamily,
6883
7013
  fontSize,
@@ -6898,12 +7028,7 @@ function buildSLTipJar(settings) {
6898
7028
  source: "streamlabs",
6899
7029
  widgetType: "tip_jar",
6900
7030
  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
7031
  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
7032
  slTipTiers: Array.isArray(tipsConfig.tiers) ? tipsConfig.tiers.map((t) => ({
6908
7033
  minimumAmount: t.minimum_amount,
6909
7034
  imageSrc: t.image_src ?? null
@@ -6911,15 +7036,12 @@ function buildSLTipJar(settings) {
6911
7036
  slMinimumTipAmount: tipsConfig.minimum_amount ?? null,
6912
7037
  slHadCustomHtml: settings.custom_html_enabled === true
6913
7038
  }
6914
- },
6915
- variables: {},
6916
- events: {}
6917
- };
7039
+ }
7040
+ });
6918
7041
  return { layer, module };
6919
7042
  }
6920
7043
 
6921
7044
  // src/sl-import/mappers/goal.ts
6922
- import { nanoid as nanoid6 } from "nanoid";
6923
7045
  var GOAL_SOURCE_MAP = {
6924
7046
  follower_goal: "follower",
6925
7047
  sub_goal: "subscriber",
@@ -6932,69 +7054,262 @@ var GOAL_TITLE_MAP = {
6932
7054
  donation_goal: "Donation Goal",
6933
7055
  bit_goal: "Bits Goal"
6934
7056
  };
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
- }
7057
+ var GOAL_CURRENT_TEMPLATE = {
7058
+ follower_goal: "{{twitch_total_follower_count}}",
7059
+ sub_goal: "{{twitch_total_subscriber_count}}",
7060
+ donation_goal: "{{total_donation_amount}}",
7061
+ bit_goal: "{{twitch_total_bits_count}}"
7062
+ };
6947
7063
  function buildSLGoal(settings, widgetType) {
6948
- const id = nanoid6();
6949
7064
  const goalSource = GOAL_SOURCE_MAP[widgetType] ?? "follower";
6950
7065
  const defaultTitle = GOAL_TITLE_MAP[widgetType] ?? "Goal";
7066
+ const currentTemplate = GOAL_CURRENT_TEMPLATE[widgetType] ?? "";
7067
+ const requiresReview = settings.custom_enabled === true;
6951
7068
  const title = typeof settings.title === "string" ? settings.title : defaultTitle;
6952
7069
  const goalAmount = typeof settings.goal === "number" ? settings.goal : 100;
6953
7070
  const currentAmount = typeof settings.current === "number" ? settings.current : 0;
6954
7071
  const startingAmount = typeof settings.starting_amount === "number" ? settings.starting_amount : 0;
6955
- const endsAt = typeof settings.ends_at === "string" ? settings.ends_at : null;
7072
+ const endsAt = typeof settings.ends_at === "string" ? settings.ends_at : "";
6956
7073
  const barColor = settings.bar_color ?? "#32C3A6";
6957
- const barBackgroundColor = settings.bar_background_color ?? "#181818";
7074
+ const barBgColor = settings.bar_bg_color ?? "#181818";
7075
+ const widgetBgColor = settings.background_color ?? "transparent";
6958
7076
  const textColor = settings.text_color ?? "#FFFFFF";
6959
7077
  const barTextColor = settings.bar_text_color ?? textColor;
6960
7078
  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: [],
7079
+ const fontSize = pxToNumber(settings.font_size, 24);
7080
+ const barThickness = pxToNumber(settings.bar_thickness, 48);
7081
+ const layerHeight = Math.max(60, barThickness + 32);
7082
+ const { layer, module } = buildModuleEnvelope({
7083
+ type: "goal",
7084
+ title,
7085
+ bounds: { x: 100, y: 100, width: 740, height: layerHeight },
6972
7086
  css: {
6973
7087
  fontFamily,
6974
7088
  color: textColor,
6975
- background: barBackgroundColor
7089
+ background: widgetBgColor,
7090
+ fontSize
6976
7091
  },
7092
+ // Use Lumia's canonical goal field names — `targetGoal` (string),
7093
+ // `current` (template string), `display`, `filledColor` /
7094
+ // `unfilledColor`. The prior schema used invented field names
7095
+ // (`goalAmount`, `barColor`) that the renderer ignored.
6977
7096
  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)
7097
+ version: 1,
7098
+ type: "bar",
7099
+ label: title,
7100
+ subLabel: "",
7101
+ display: "{{current}} / {{goal}}",
7102
+ current: currentAmount > 0 ? String(currentAmount) : currentTemplate,
7103
+ targetGoal: String(goalAmount),
7104
+ image: "",
7105
+ audio: {
7106
+ decrement: "",
7107
+ increment: "",
7108
+ goal: "",
7109
+ goalFailed: "",
7110
+ volume: 1
7111
+ },
7112
+ endDate: endsAt,
7113
+ goalAnimation: "",
7114
+ goalAnimationLoop: true,
7115
+ unfilledColor: barBgColor,
7116
+ filledColor: barColor,
7117
+ alignItemsToEdges: false,
7118
+ border: "solid 1px transparent",
7119
+ borderRadius: "40px",
7120
+ highlightColor: "inherit",
7121
+ importMeta: {
7122
+ source: "streamlabs",
7123
+ widgetType,
7124
+ importedAt: (/* @__PURE__ */ new Date()).toISOString(),
7125
+ goalSource,
7126
+ slLayout: settings.layout ?? null,
7127
+ slBarTextColor: barTextColor,
7128
+ slBarThickness: barThickness,
7129
+ slStartingAmount: startingAmount,
7130
+ slHadCustomHtml: settings.custom_enabled === true
7131
+ }
7132
+ }
7133
+ });
7134
+ return { layer, module, requiresReview };
7135
+ }
7136
+
7137
+ // src/sl-import/mappers/chat-box.ts
7138
+ function buildSitesArray(settings) {
7139
+ const defaultSites = [...CHATBOX_DEFAULT_MODULE.content.sites];
7140
+ const drop = /* @__PURE__ */ new Set();
7141
+ if (settings.show_bttv_emotes === false) drop.add("bttv");
7142
+ if (settings.show_franker_emotes === false) drop.add("ffz");
7143
+ if (settings.show_7tv_emotes === false) drop.add("seventv");
7144
+ if (settings.twitch_enabled === false) drop.add("twitch");
7145
+ if (settings.youtube_enabled === false) drop.add("youtube");
7146
+ if (settings.facebook_enabled === false) drop.add("facebook");
7147
+ if (settings.tiktok_enabled === false) drop.add("tiktok");
7148
+ if (settings.kick_enabled === false) drop.add("kick");
7149
+ if (drop.size === 0) return defaultSites;
7150
+ return defaultSites.filter((s) => !drop.has(s));
7151
+ }
7152
+ function buildSLChatBox(settings) {
7153
+ const requiresReview = settings.custom_enabled === true;
7154
+ const backgroundColor = settings.background_color ?? "transparent";
7155
+ const textColor = settings.text_color ?? "#FFFFFF";
7156
+ const textSize = pxToNumber(settings.text_size, 16);
7157
+ const fadeOutAfterDelay = settings.always_show_messages !== true;
7158
+ const fadeOutDelayTime = fadeOutAfterDelay ? Math.max(1, Math.round((settings.message_hide_delay ?? 15e3) / 1e3)) : 0;
7159
+ const sites = buildSitesArray(settings);
7160
+ const { layer, module } = buildModuleEnvelope({
7161
+ type: "chatbox",
7162
+ title: "Streamlabs Chat Box",
7163
+ bounds: { x: 100, y: 100, width: 375, height: 470 },
7164
+ version: CHATBOX_DEFAULT_MODULE.version,
7165
+ css: {
7166
+ background: backgroundColor,
7167
+ color: textColor,
7168
+ fontSize: textSize
6989
7169
  },
6990
- variables: {},
6991
- events: {}
6992
- };
6993
- return { layer, module };
7170
+ content: {
7171
+ // Spread canonical defaults FIRST so the renderer's dereferences
7172
+ // (chatboxStreamingSite.<platform>.themeConfig, animations,
7173
+ // chatboxStreamingSite.<platform>.message, etc.) all exist.
7174
+ ...CHATBOX_DEFAULT_MODULE.content,
7175
+ // Then override the bits SL controls.
7176
+ sites,
7177
+ fadeOutAfterDelay,
7178
+ fadeOutDelayTime,
7179
+ // SL keeps history forever by default — `always_show_messages`
7180
+ // implies the streamer wants a permanent scrollback. Lumia
7181
+ // exposes that as `permanent: true`.
7182
+ permanent: settings.always_show_messages === true,
7183
+ // Preserve the SL-specific provenance for the review step.
7184
+ importMeta: {
7185
+ source: "streamlabs",
7186
+ widgetType: "chat_box",
7187
+ importedAt: (/* @__PURE__ */ new Date()).toISOString(),
7188
+ slTheme: settings.theme ?? null,
7189
+ slHadCustomHtml: settings.custom_enabled === true,
7190
+ slHidesCommonBots: settings.hide_common_chat_bots === true,
7191
+ slHidesCommands: settings.hide_commands === true,
7192
+ slMutedChatters: settings.muted_chatters ?? "",
7193
+ slMessageShowDelayMs: settings.message_show_delay ?? 0
7194
+ }
7195
+ }
7196
+ });
7197
+ return { layer, module, requiresReview };
7198
+ }
7199
+
7200
+ // src/sl-import/mappers/event-list.ts
7201
+ var SL_SHOW_FLAG_TO_LUMIA_FILTER = {
7202
+ show_follows: "follower",
7203
+ show_subscriptions: "subscribers",
7204
+ show_resubs: "subscribers",
7205
+ show_sub_gifts: "gifts",
7206
+ show_subscribers: "subscribers",
7207
+ show_sponsors: "subscribers",
7208
+ // YouTube channel members
7209
+ show_sponsor_gifts: "gifts",
7210
+ show_bits: "bits",
7211
+ show_donations: "donation",
7212
+ show_pledges: "donation",
7213
+ // Patreon
7214
+ show_eldonations: "donation",
7215
+ show_tiltifydonations: "donation",
7216
+ show_donordrivedonations: "donation",
7217
+ show_justgivingdonations: "donation",
7218
+ show_streamlabscharitydonations: "donation",
7219
+ show_twitchcharitydonations: "donation",
7220
+ show_twitch_charity: "donation",
7221
+ show_treats: "donation",
7222
+ // TreatStream
7223
+ show_merch: "purchases",
7224
+ show_raids: "raids",
7225
+ show_hosts: "raids",
7226
+ show_redemptions: "redemption",
7227
+ show_fanfundings: "superchats",
7228
+ // YouTube Super Chat
7229
+ show_super_stickers: "superstickers",
7230
+ show_jewel_gifts: "stars"
7231
+ // TikTok-flavored, closest analogue
7232
+ };
7233
+ function buildFilters(settings) {
7234
+ const drop = /* @__PURE__ */ new Set();
7235
+ for (const [slFlag, lumiaCategory] of Object.entries(SL_SHOW_FLAG_TO_LUMIA_FILTER)) {
7236
+ if (settings[slFlag] === false) {
7237
+ drop.add(lumiaCategory);
7238
+ }
7239
+ }
7240
+ return Array.from(drop);
7241
+ }
7242
+ function buildSLEventList(settings) {
7243
+ const requiresReview = settings.custom_enabled === true;
7244
+ const backgroundColor = settings.background_color ?? "transparent";
7245
+ const textColor = settings.text_color ?? "#FFFFFF";
7246
+ const textSize = pxToNumber(settings.text_size, 16);
7247
+ const fontFamily = settings.font_family ?? "Roboto";
7248
+ const maxEvents = typeof settings.max_events === "number" && settings.max_events > 0 ? settings.max_events : EVENTLIST_DEFAULT_MODULE.content.maxItemsToShow;
7249
+ const enterAnimation = settings.show_animation ?? "fadeIn";
7250
+ const exitAnimation = settings.hide_animation ?? "fadeOut";
7251
+ const animationDuration = typeof settings.animation_speed === "number" ? settings.animation_speed : 1e3;
7252
+ const filters = buildFilters(settings);
7253
+ const { layer, module } = buildModuleEnvelope({
7254
+ type: "eventlist",
7255
+ title: "Streamlabs Event List",
7256
+ bounds: { x: 100, y: 100, width: 375, height: 470 },
7257
+ version: EVENTLIST_DEFAULT_MODULE.version,
7258
+ css: {
7259
+ ...EVENTLIST_DEFAULT_MODULE.css,
7260
+ background: backgroundColor,
7261
+ color: textColor,
7262
+ fontSize: textSize,
7263
+ fontFamily
7264
+ },
7265
+ content: {
7266
+ // Spread canonical defaults FIRST so the renderer's dereferences
7267
+ // don't crash on missing nested objects.
7268
+ ...EVENTLIST_DEFAULT_MODULE.content,
7269
+ maxItemsToShow: maxEvents,
7270
+ // SL's `flip_y` corresponds to Lumia's `reverse` (newest item
7271
+ // at the bottom vs the top). SL's `flip_x` flips the alignment
7272
+ // to right; Lumia covers that via css.textAlign but doesn't
7273
+ // have a per-module horizontal flip — stash as importMeta for
7274
+ // the review step.
7275
+ reverse: settings.flip_y === true,
7276
+ // SL's `keep_history: false` means events disappear after the
7277
+ // fade time; map to `fadeOutAfterDelay: true` with the fade
7278
+ // time SL configured.
7279
+ fadeOutAfterDelay: settings.keep_history === false,
7280
+ fadeOutDelayTime: typeof settings.fade_time === "number" && settings.fade_time > 0 ? Math.max(1, Math.round(settings.fade_time / 1e3)) : EVENTLIST_DEFAULT_MODULE.content.fadeOutDelayTime,
7281
+ animations: {
7282
+ ...EVENTLIST_DEFAULT_MODULE.content.animations,
7283
+ enterAnimation,
7284
+ exitAnimation,
7285
+ enterAnimationDuration: animationDuration,
7286
+ exitAnimationDuration: animationDuration
7287
+ },
7288
+ filters,
7289
+ importMeta: {
7290
+ source: "streamlabs",
7291
+ widgetType: "event_list",
7292
+ importedAt: (/* @__PURE__ */ new Date()).toISOString(),
7293
+ slTheme: settings.theme ?? null,
7294
+ slThemeColor: settings.theme_color ?? null,
7295
+ slFlipX: settings.flip_x === true,
7296
+ slHadCustomHtml: settings.custom_enabled === true,
7297
+ // Per-event-type minimums SL filters by — Lumia's eventlist
7298
+ // has no equivalent thresholding; preserve so the streamer
7299
+ // can recreate via the alert module's variation conditions.
7300
+ slHostViewerMinimum: settings.host_viewer_minimum ?? 0,
7301
+ slBitsMinimum: settings.bits_minimum ?? 0,
7302
+ slRaidRaiderMinimum: settings.raid_raider_minimum ?? 0
7303
+ }
7304
+ }
7305
+ });
7306
+ return { layer, module, requiresReview };
6994
7307
  }
6995
7308
 
6996
7309
  // src/sl-import/sl-dispatch.ts
6997
- var IMPORT_META_KEY = "importMeta";
7310
+ function buildSLDescription(widgetType, sourceUrl) {
7311
+ return buildImportDescription(`Imported from Streamlabs ${widgetType}`, sourceUrl);
7312
+ }
6998
7313
  var GOAL_WIDGET_TYPES = /* @__PURE__ */ new Set([
6999
7314
  "follower_goal",
7000
7315
  "sub_goal",
@@ -7002,8 +7317,6 @@ var GOAL_WIDGET_TYPES = /* @__PURE__ */ new Set([
7002
7317
  "bit_goal"
7003
7318
  ]);
7004
7319
  var PLACEHOLDER_WIDGET_TYPES = /* @__PURE__ */ new Set([
7005
- "chat_box",
7006
- "event_list",
7007
7320
  "donation_ticker",
7008
7321
  "stream_boss",
7009
7322
  "credits",
@@ -7013,8 +7326,6 @@ var PLACEHOLDER_WIDGET_TYPES = /* @__PURE__ */ new Set([
7013
7326
  "streamlabels"
7014
7327
  ]);
7015
7328
  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
7329
  donation_ticker: "No native Lumia equivalent yet \u2014 imported as a placeholder text layer.",
7019
7330
  stream_boss: "Lumia has a native streamboss module \u2014 set it up directly rather than importing the SL config.",
7020
7331
  credits: "Lumia has a native credits module \u2014 set it up directly rather than importing the SL config.",
@@ -7023,19 +7334,14 @@ var PLACEHOLDER_REASONS = {
7023
7334
  sub_train: "No native Lumia equivalent yet \u2014 imported as a placeholder text layer.",
7024
7335
  streamlabels: "Use Lumia template variables (e.g. {{twitch_last_follower}}) inside a Text module instead of importing the SL streamlabels config."
7025
7336
  };
7337
+ 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
7338
  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
- };
7339
+ stampImportMetaMerge(module, {
7340
+ source: "streamlabs",
7341
+ widgetType,
7342
+ role,
7343
+ ...extra
7344
+ });
7039
7345
  }
7040
7346
  function buildPlaceholder(widgetType) {
7041
7347
  const id = nanoid7();
@@ -7067,7 +7373,7 @@ function buildPlaceholder(widgetType) {
7067
7373
  };
7068
7374
  return { layer, module };
7069
7375
  }
7070
- function importStreamlabsWidget(response, widgetType) {
7376
+ function importStreamlabsWidget(response, widgetType, options = {}) {
7071
7377
  if (!isSLWidgetConfigResponse(response)) {
7072
7378
  throw new Error(
7073
7379
  "Streamlabs response is missing the expected `settings` envelope. The widget may be private or the response format may have changed."
@@ -7117,9 +7423,61 @@ function importStreamlabsWidget(response, widgetType) {
7117
7423
  coverage.mappings.push({
7118
7424
  seType: `streamlabs:${widgetType}`,
7119
7425
  lumiaType: "goal",
7120
- status: "direct",
7426
+ status: built.requiresReview ? "partial" : "direct",
7121
7427
  count: 1
7122
7428
  });
7429
+ if (built.requiresReview) {
7430
+ reviewItems.push(
7431
+ customHtmlReviewItem(
7432
+ built.module.id,
7433
+ `streamlabs-${widgetType}`,
7434
+ response.settings,
7435
+ CUSTOM_HTML_REVIEW_REASON
7436
+ )
7437
+ );
7438
+ }
7439
+ } else if (widgetType === "chat_box") {
7440
+ const built = buildSLChatBox(response.settings);
7441
+ stampImportMeta(built.module, widgetType, "chatbox");
7442
+ layers.push(built.layer);
7443
+ modules[built.layer.id] = built.module;
7444
+ coverage.mappings.push({
7445
+ seType: "streamlabs:chat_box",
7446
+ lumiaType: "chatbox",
7447
+ status: built.requiresReview ? "partial" : "direct",
7448
+ count: 1
7449
+ });
7450
+ if (built.requiresReview) {
7451
+ reviewItems.push(
7452
+ customHtmlReviewItem(
7453
+ built.module.id,
7454
+ "streamlabs-chat_box",
7455
+ response.settings,
7456
+ CUSTOM_HTML_REVIEW_REASON
7457
+ )
7458
+ );
7459
+ }
7460
+ } else if (widgetType === "event_list") {
7461
+ const built = buildSLEventList(response.settings);
7462
+ stampImportMeta(built.module, widgetType, "eventlist");
7463
+ layers.push(built.layer);
7464
+ modules[built.layer.id] = built.module;
7465
+ coverage.mappings.push({
7466
+ seType: "streamlabs:event_list",
7467
+ lumiaType: "eventlist",
7468
+ status: built.requiresReview ? "partial" : "direct",
7469
+ count: 1
7470
+ });
7471
+ if (built.requiresReview) {
7472
+ reviewItems.push(
7473
+ customHtmlReviewItem(
7474
+ built.module.id,
7475
+ "streamlabs-event_list",
7476
+ response.settings,
7477
+ CUSTOM_HTML_REVIEW_REASON
7478
+ )
7479
+ );
7480
+ }
7123
7481
  } else if (PLACEHOLDER_WIDGET_TYPES.has(widgetType)) {
7124
7482
  const placeholder = buildPlaceholder(widgetType);
7125
7483
  const reason = PLACEHOLDER_REASONS[widgetType] ?? "No native Lumia mapping yet.";
@@ -7168,7 +7526,7 @@ function importStreamlabsWidget(response, widgetType) {
7168
7526
  uuid: "",
7169
7527
  listen_id: "",
7170
7528
  name: overlayName,
7171
- description: `Imported from Streamlabs ${widgetType}`,
7529
+ description: buildSLDescription(widgetType, options.sourceUrl),
7172
7530
  settings: {
7173
7531
  metadata: { width: 1920, height: 1080 },
7174
7532
  layers,
@@ -8752,7 +9110,12 @@ function SEImportWizard({
8752
9110
  );
8753
9111
  }
8754
9112
  importedOverlays.push({
8755
- result: importSEBootstrap(bootstrap),
9113
+ result: importSEBootstrap(bootstrap, {
9114
+ // JWT-flow imports don't come from a pasted URL; synthesize
9115
+ // the SE dashboard URL so the description still carries a
9116
+ // useful traceback. Same shape SE editor links use.
9117
+ sourceUrl: `https://streamelements.com/dashboard/overlays/${overlayId}/editor`
9118
+ }),
8756
9119
  summary
8757
9120
  });
8758
9121
  }
@@ -8797,7 +9160,9 @@ function SEImportWizard({
8797
9160
  "StreamElements returned a payload that doesn't look like an overlay bootstrap."
8798
9161
  );
8799
9162
  }
8800
- const imported = importSEBootstrap(bootstrap);
9163
+ const imported = importSEBootstrap(bootstrap, {
9164
+ sourceUrl: `https://streamelements.com/dashboard/overlays/${overlayId}/editor`
9165
+ });
8801
9166
  setBatchImports([{ result: imported, summary }]);
8802
9167
  setResult(imported);
8803
9168
  setAssetUrls(findSEAssetURLs(imported.overlay));
@@ -8885,7 +9250,9 @@ function SEImportWizard({
8885
9250
  "Streamlabs returned a response that does not look like a widget config payload."
8886
9251
  );
8887
9252
  }
8888
- const imported = importStreamlabsWidget(raw, slParts.widgetType);
9253
+ const imported = importStreamlabsWidget(raw, slParts.widgetType, {
9254
+ sourceUrl: url
9255
+ });
8889
9256
  setBatchImports([]);
8890
9257
  setResult(imported);
8891
9258
  setAssetUrls(findSEAssetURLs(imported.overlay));
@@ -8926,7 +9293,7 @@ function SEImportWizard({
8926
9293
  "StreamElements returned a response that does not look like an Element widget payload."
8927
9294
  );
8928
9295
  }
8929
- const imported = importElementOverlay(raw);
9296
+ const imported = importElementOverlay(raw, { sourceUrl: url });
8930
9297
  setBatchImports([]);
8931
9298
  setResult(imported);
8932
9299
  setAssetUrls(findSEAssetURLs(imported.overlay));
@@ -8974,7 +9341,7 @@ function SEImportWizard({
8974
9341
  ...parts,
8975
9342
  proxyFetch: bindings.proxyFetch
8976
9343
  });
8977
- const imported = importSEBootstrap(bootstrap);
9344
+ const imported = importSEBootstrap(bootstrap, { sourceUrl: url });
8978
9345
  setBatchImports([]);
8979
9346
  setResult(imported);
8980
9347
  setAssetUrls(findSEAssetURLs(imported.overlay));
@@ -9633,7 +10000,7 @@ function extractErrorMessage(err) {
9633
10000
  }
9634
10001
 
9635
10002
  // src/se-import/element-dispatch.ts
9636
- import { nanoid as nanoid9 } from "nanoid";
10003
+ import { LumiaAlertValues as LumiaAlertValues3 } from "@lumiastream/lumia-types";
9637
10004
 
9638
10005
  // src/se-import/element-types.ts
9639
10006
  function isSEElementResponse(value) {
@@ -9877,34 +10244,8 @@ function translateAnimationName(name, fallback) {
9877
10244
  if (PINNED_ANIMATION_MAP[name]) return PINNED_ANIMATION_MAP[name];
9878
10245
  return name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
9879
10246
  }
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
- }
10247
+ var parseAnimationDurationMs = parseDurationMs;
10248
+ var parsePx2 = (raw) => pxToNumber(raw, NaN);
9908
10249
  function findFieldByType(fields, type) {
9909
10250
  if (!fields) return void 0;
9910
10251
  for (const f of Object.values(fields)) {
@@ -9980,6 +10321,47 @@ function buildAnimations(title) {
9980
10321
  const visibleDurationMs = enterDelay + enterDuration + staticDuration + exitDuration;
9981
10322
  return { animations, visibleDurationMs: visibleDurationMs || DEFAULT_DURATION_MS };
9982
10323
  }
10324
+ function resolveFieldBoundsInCanvas(style, canvas, textContent) {
10325
+ if (!style) return null;
10326
+ const x = parsePx2(style.left);
10327
+ const y = parsePx2(style.top);
10328
+ const hasXY = Number.isFinite(x) || Number.isFinite(y);
10329
+ if (!hasXY && style.width == null && style.height == null) return null;
10330
+ const sx = Number.isFinite(x) ? x : 0;
10331
+ const sy = Number.isFinite(y) ? y : 0;
10332
+ let width = parsePx2(style.width);
10333
+ let height = parsePx2(style.height);
10334
+ if (!Number.isFinite(width)) width = Math.max(1, canvas.width - sx);
10335
+ if (!Number.isFinite(height)) {
10336
+ if (typeof textContent === "string") {
10337
+ const lineCount = (textContent.match(/\n/g)?.length ?? 0) + 1;
10338
+ const lhPx = parsePx2(style.lineHeight) || parsePx2(style["line-height"]);
10339
+ const fsPx = parsePx2(style.fontSize);
10340
+ let effectiveLh = lhPx;
10341
+ if (!Number.isFinite(effectiveLh) || effectiveLh < 8) {
10342
+ effectiveLh = Number.isFinite(fsPx) ? fsPx * 1.3 : 24 * 1.3;
10343
+ }
10344
+ height = Math.max(1, lineCount * effectiveLh);
10345
+ } else {
10346
+ height = Math.max(1, canvas.height - sy);
10347
+ }
10348
+ }
10349
+ return { x: sx, y: sy, width, height };
10350
+ }
10351
+ function unionBounds(boxes) {
10352
+ if (boxes.length === 0) return null;
10353
+ let minX = Infinity;
10354
+ let minY = Infinity;
10355
+ let maxX = -Infinity;
10356
+ let maxY = -Infinity;
10357
+ for (const b of boxes) {
10358
+ minX = Math.min(minX, b.x);
10359
+ minY = Math.min(minY, b.y);
10360
+ maxX = Math.max(maxX, b.x + b.width);
10361
+ maxY = Math.max(maxY, b.y + b.height);
10362
+ }
10363
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
10364
+ }
9983
10365
  function buildElementAlertEvent(state, options = {}) {
9984
10366
  const compositeFields = state.settings?.compositeFields;
9985
10367
  const titleField = findFieldByType(compositeFields, "text");
@@ -9997,6 +10379,31 @@ function buildElementAlertEvent(state, options = {}) {
9997
10379
  const titleStyle = titleField?.style;
9998
10380
  const highlightColor = titleField?.highlightedStyle?.color ?? null;
9999
10381
  const { animations, visibleDurationMs } = buildAnimations(titleField);
10382
+ const canvas = options.canvas ?? { width: 1920, height: 1080 };
10383
+ const mediaBounds = resolveFieldBoundsInCanvas(mainAsset?.style, canvas);
10384
+ const textBounds = resolveFieldBoundsInCanvas(titleStyle, canvas, rawMessage);
10385
+ const collected = [];
10386
+ if (mediaBounds) collected.push(mediaBounds);
10387
+ if (textBounds) collected.push(textBounds);
10388
+ let layerBounds = unionBounds(collected);
10389
+ if (!layerBounds) {
10390
+ layerBounds = {
10391
+ x: Math.round(canvas.width * 0.6),
10392
+ y: Math.round(canvas.height * 0.15),
10393
+ width: 400,
10394
+ height: 300
10395
+ };
10396
+ }
10397
+ let pushTextUp = 0;
10398
+ let pushTextLeft = 0;
10399
+ if (textBounds) {
10400
+ const desiredTextTopInLayer = textBounds.y - layerBounds.y;
10401
+ const naturalTextTopInLayer = Math.max(0, (layerBounds.height - textBounds.height) / 2);
10402
+ pushTextUp = Math.round(desiredTextTopInLayer - naturalTextTopInLayer);
10403
+ const desiredTextLeftInLayer = textBounds.x - layerBounds.x;
10404
+ const naturalTextLeftInLayer = Math.max(0, (layerBounds.width - textBounds.width) / 2);
10405
+ pushTextLeft = Math.round(desiredTextLeftInLayer - naturalTextLeftInLayer);
10406
+ }
10000
10407
  const event = {
10001
10408
  on: true,
10002
10409
  // `'custom'` lowercase is load-bearing in Lumia's AlertBoxHandler —
@@ -10015,7 +10422,11 @@ function buildElementAlertEvent(state, options = {}) {
10015
10422
  showBrandIcon: true
10016
10423
  },
10017
10424
  settings: { duration: visibleDurationMs },
10018
- layout: { value: "column", css: {}, customCss: "", pushTextUp: 0, pushTextLeft: 0 },
10425
+ // `textOver` matches SE Element's text-on-video render pattern.
10426
+ // `imageOver` / `column` / `row` would all stack the text outside
10427
+ // the media, which is the bug this layout fixes (text rendered
10428
+ // below the alert graphic instead of overlaid on it).
10429
+ layout: { value: "textOver", css: {}, customCss: "", pushTextUp, pushTextLeft },
10019
10430
  text: {
10020
10431
  css: titleCss(titleStyle),
10021
10432
  messageCss: {},
@@ -10037,44 +10448,13 @@ function buildElementAlertEvent(state, options = {}) {
10037
10448
  },
10038
10449
  variations: []
10039
10450
  };
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 };
10451
+ return { event, bounds: layerBounds };
10060
10452
  }
10061
10453
 
10062
10454
  // src/se-import/mappers/element-scene.ts
10063
- import { nanoid as nanoid8 } from "nanoid";
10064
10455
  var TEXT_FALLBACK = { width: 400, height: 50 };
10065
10456
  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
- }
10457
+ var parsePx3 = (raw) => pxToNumber(raw, NaN);
10078
10458
  function styleToBounds(style, type, canvas) {
10079
10459
  const x = Number.isFinite(parsePx3(style?.left)) ? parsePx3(style?.left) : 0;
10080
10460
  const y = Number.isFinite(parsePx3(style?.top)) ? parsePx3(style?.top) : 0;
@@ -10110,33 +10490,17 @@ function styleToBounds(style, type, canvas) {
10110
10490
  }
10111
10491
  return { x, y, width, height };
10112
10492
  }
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,
10493
+ function buildChild(groupId, zIndex, bounds, type, title, content, css = {}) {
10494
+ const { layer, module } = buildModuleEnvelope({
10495
+ type,
10496
+ title,
10497
+ bounds,
10136
10498
  content,
10137
- variables: {},
10138
- events: {}
10139
- };
10499
+ css,
10500
+ parentGroupId: groupId,
10501
+ zIndex
10502
+ });
10503
+ return { layer, module };
10140
10504
  }
10141
10505
  function textStyleToCss(style) {
10142
10506
  const s = style ?? {};
@@ -10151,80 +10515,69 @@ function textStyleToCss(style) {
10151
10515
  return out;
10152
10516
  }
10153
10517
  function mapCompositeField(name, field, groupId, zIndex, provider, canvas) {
10154
- const id = nanoid8();
10155
10518
  switch (field.type) {
10156
10519
  case "video": {
10157
- const bounds = styleToBounds(field.style, "video", canvas);
10158
- const layer = buildLayer(id, groupId, bounds, zIndex);
10159
10520
  const src = pickFieldMediaHref(field.media, "video/");
10160
10521
  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 };
10522
+ return buildChild(
10523
+ groupId,
10524
+ zIndex,
10525
+ styleToBounds(field.style, "video", canvas),
10526
+ "video",
10527
+ name,
10528
+ { src, volume, loop: true, autoplay: true, muted: volume === 0 }
10529
+ );
10169
10530
  }
10170
10531
  case "image": {
10171
- const bounds = styleToBounds(field.style, "image", canvas);
10172
- const layer = buildLayer(id, groupId, bounds, zIndex);
10173
10532
  const src = pickFieldMediaHref(field.media, "image/");
10174
- const module = buildModule(id, "image", name, { src });
10175
- return { layer, module };
10533
+ return buildChild(
10534
+ groupId,
10535
+ zIndex,
10536
+ styleToBounds(field.style, "image", canvas),
10537
+ "image",
10538
+ name,
10539
+ { src }
10540
+ );
10176
10541
  }
10177
10542
  case "text": {
10178
- const bounds = styleToBounds(field.style, "text", canvas);
10179
- const layer = buildLayer(id, groupId, bounds, zIndex);
10180
10543
  const raw = field.text?.[0]?.content ?? "";
10181
10544
  const value = substituteElementTokens(raw, { provider });
10182
- const module = buildModule(
10183
- id,
10545
+ return buildChild(
10546
+ groupId,
10547
+ zIndex,
10548
+ styleToBounds(field.style, "text", canvas),
10184
10549
  "text",
10185
10550
  name,
10186
10551
  { value, highlightColor: "inherit" },
10187
10552
  textStyleToCss(field.style)
10188
10553
  );
10189
- return { layer, module };
10190
10554
  }
10191
10555
  case "audio": {
10192
- const layer = buildLayer(id, groupId, { x: 0, y: 0, width: 1, height: 1 }, zIndex);
10193
10556
  const src = pickFieldMediaHref(field.media, "audio/");
10194
10557
  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 };
10558
+ return buildChild(
10559
+ groupId,
10560
+ zIndex,
10561
+ { x: 0, y: 0, width: 1, height: 1 },
10562
+ "audio",
10563
+ name,
10564
+ { src, volume, loop: false }
10565
+ );
10197
10566
  }
10198
10567
  default:
10199
10568
  return null;
10200
10569
  }
10201
10570
  }
10202
10571
  function buildElementScene(state, options) {
10203
- const groupId = nanoid8();
10204
10572
  const groupTitle = typeof state.displayName === "string" && state.displayName || state.referenceId || "Scene";
10205
- const groupLayer = {
10206
- id: groupId,
10573
+ const { id: groupId, layer: groupLayer, module: groupModule } = buildModuleEnvelope({
10207
10574
  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
- };
10575
+ title: groupTitle,
10576
+ bounds: { x: 0, y: 0, width: options.canvas.width, height: options.canvas.height },
10577
+ type_layer: "group",
10578
+ parentGroupId: null,
10579
+ expanded: false
10580
+ });
10228
10581
  const children = [];
10229
10582
  const fields = state.settings?.compositeFields ?? {};
10230
10583
  let zIndex = 1;
@@ -10243,7 +10596,12 @@ function buildElementScene(state, options) {
10243
10596
  }
10244
10597
 
10245
10598
  // src/se-import/element-dispatch.ts
10246
- var IMPORT_META_KEY2 = "importMeta";
10599
+ function buildElementDescription(widgetInstanceId, sourceUrl) {
10600
+ return buildImportDescription(
10601
+ `Imported from StreamElements Element widget ${widgetInstanceId}`,
10602
+ sourceUrl
10603
+ );
10604
+ }
10247
10605
  function elementAlertKindToLumiaKeys(kind, provider) {
10248
10606
  switch (kind) {
10249
10607
  case "follower":
@@ -10262,9 +10620,9 @@ function elementAlertKindToLumiaKeys(kind, provider) {
10262
10620
  return alertBoxKeysFor("charityCampaignDonation", provider);
10263
10621
  case "subscriberGift":
10264
10622
  case "communityGiftPurchase": {
10265
- if (provider === "twitch") return ["twitch-giftSubscription"];
10266
- if (provider === "kick") return ["kick-subscriptionGift"];
10267
- if (provider === "youtube") return ["youtube-giftMembers"];
10623
+ if (provider === "twitch") return [LumiaAlertValues3.TWITCH_GIFT_SUBSCRIPTION];
10624
+ if (provider === "kick") return [LumiaAlertValues3.KICK_GIFT_SUBSCRIPTION];
10625
+ if (provider === "youtube") return [LumiaAlertValues3.YOUTUBE_GIFT_MEMBERS];
10268
10626
  return alertBoxKeysFor("subscriber", provider);
10269
10627
  }
10270
10628
  default:
@@ -10279,7 +10637,7 @@ function buildCombinedAlertModule(alertStates, provider, canvas) {
10279
10637
  for (const state of alertStates) {
10280
10638
  const cls = classifyConfigState(state);
10281
10639
  if (cls.kind !== "alert") continue;
10282
- const { event, bounds: bounds2 } = buildElementAlertEvent(state, { provider });
10640
+ const { event, bounds: bounds2 } = buildElementAlertEvent(state, { provider, canvas });
10283
10641
  const keys = elementAlertKindToLumiaKeys(cls.alertKind, provider);
10284
10642
  if (keys.length === 0) continue;
10285
10643
  for (const key of keys) {
@@ -10291,61 +10649,42 @@ function buildCombinedAlertModule(alertStates, provider, canvas) {
10291
10649
  }
10292
10650
  }
10293
10651
  if (Object.keys(events).length === 0) return null;
10294
- const id = nanoid9();
10295
10652
  const bounds = firstBounds ?? {
10296
10653
  x: Math.round(canvas.width * 0.6),
10297
10654
  y: Math.round(canvas.height * 0.15),
10298
10655
  width: 400,
10299
10656
  height: 300
10300
10657
  };
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
10658
  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: {},
10659
+ const { layer, module } = buildModuleEnvelope({
10660
+ type: "alert",
10661
+ title: "Alerts (Element import)",
10662
+ bounds,
10323
10663
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
10324
10664
  alert: { ...baseAlert, events }
10325
- };
10665
+ });
10326
10666
  return { layer, module };
10327
10667
  }
10328
10668
  function stripEventOnlyFields2(event) {
10329
- const { on: _on, variations: _variations, matchEmptyCondition: _mec, ...base } = event;
10669
+ const {
10670
+ on: _on,
10671
+ variations: _variations,
10672
+ matchEmptyCondition: _mec,
10673
+ ...base
10674
+ } = event;
10330
10675
  return base;
10331
10676
  }
10332
10677
  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
- };
10678
+ stampImportMetaMerge(module, {
10679
+ source: "streamelements-element",
10680
+ role,
10681
+ referenceId: state?.referenceId,
10682
+ categoryId: state?.categoryId,
10683
+ displayName: state?.displayName
10684
+ });
10344
10685
  }
10345
- function importElementOverlay(response) {
10346
- const configData = parseElementConfigData(
10347
- response.widgetInstanceConfigVersion.configData
10348
- );
10686
+ function importElementOverlay(response, options = {}) {
10687
+ const configData = parseElementConfigData(response.widgetInstanceConfigVersion.configData);
10349
10688
  if (!configData) {
10350
10689
  throw new Error(
10351
10690
  "StreamElements Element response carries an invalid configData payload. The widget may be corrupted or the response format may have changed."
@@ -10415,7 +10754,10 @@ function importElementOverlay(response) {
10415
10754
  uuid: "",
10416
10755
  listen_id: "",
10417
10756
  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}`,
10757
+ description: buildElementDescription(
10758
+ response.widgetInstance.widgetInstanceId,
10759
+ options.sourceUrl
10760
+ ),
10419
10761
  settings: {
10420
10762
  metadata: { width: canvas.width, height: canvas.height },
10421
10763
  layers,
@@ -10429,7 +10771,7 @@ function importElementOverlay(response) {
10429
10771
  }
10430
10772
 
10431
10773
  // src/se-import/index.ts
10432
- var IMPORT_META_KEY3 = "importMeta";
10774
+ var IMPORT_META_KEY2 = "importMeta";
10433
10775
  var REVIEW_REASONS = {
10434
10776
  "se-widget-bit-boss": "No native Lumia equivalent \u2014 bit boss fight bar with HP/damage states.",
10435
10777
  // `se-widget-hype-cup` intentionally omitted — it now routes to the native
@@ -10459,6 +10801,10 @@ function reasonFor(seType, status, flaggedOff) {
10459
10801
  if (flaggedOff && FLAG_OFF_REASONS[seType]) return FLAG_OFF_REASONS[seType];
10460
10802
  return REVIEW_REASONS[seType] ?? (status === "template" ? "Imported as a generic template stub." : "No native mapping yet \u2014 imported as a labelled text placeholder.");
10461
10803
  }
10804
+ function buildSEBootstrapDescription(overlayId, sourceUrl) {
10805
+ const head = overlayId ? `Imported from StreamElements overlay ${overlayId}` : "Imported from StreamElements";
10806
+ return buildImportDescription(head, sourceUrl);
10807
+ }
10462
10808
  function extractSEOverlayId(input) {
10463
10809
  const trimmed = input.trim();
10464
10810
  const m = trimmed.match(/[0-9a-f]{24}/i);
@@ -10513,7 +10859,7 @@ function isSEBootstrap(value) {
10513
10859
  const o = v.overlay;
10514
10860
  return Array.isArray(o.widgets);
10515
10861
  }
10516
- function importSEBootstrap(bootstrap) {
10862
+ function importSEBootstrap(bootstrap, options = {}) {
10517
10863
  const seOverlay = bootstrap.overlay;
10518
10864
  const widgets = seOverlay?.widgets ?? [];
10519
10865
  const coverage = {
@@ -10536,7 +10882,7 @@ function importSEBootstrap(bootstrap) {
10536
10882
  if (w.type !== "se-widget-group") continue;
10537
10883
  const uid = w.variables?.uid;
10538
10884
  if (typeof uid !== "string") continue;
10539
- const lumiaGroupId = nanoid10();
10885
+ const lumiaGroupId = nanoid8();
10540
10886
  seUidToLumiaGroupId[uid] = lumiaGroupId;
10541
10887
  groupWidgetByUid[uid] = w;
10542
10888
  }
@@ -10575,7 +10921,7 @@ function importSEBootstrap(bootstrap) {
10575
10921
  lights: [],
10576
10922
  css: {},
10577
10923
  content: {
10578
- [IMPORT_META_KEY3]: {
10924
+ [IMPORT_META_KEY2]: {
10579
10925
  source: "streamelements",
10580
10926
  widgetType: w.type,
10581
10927
  importedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -10613,7 +10959,7 @@ function importSEBootstrap(bootstrap) {
10613
10959
  if (result.status === "placeholder" || result.status === "template") {
10614
10960
  module.content = {
10615
10961
  ...module.content ?? {},
10616
- [IMPORT_META_KEY3]: {
10962
+ [IMPORT_META_KEY2]: {
10617
10963
  source: "streamelements",
10618
10964
  widgetType: w.type,
10619
10965
  importedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -10653,7 +10999,7 @@ function importSEBootstrap(bootstrap) {
10653
10999
  uuid: "",
10654
11000
  listen_id: "",
10655
11001
  name: seOverlay?.name ? `[SE] ${seOverlay.name}` : `Imported from StreamElements (${seOverlay?._id ?? ""})`,
10656
- description: seOverlay?._id ? `Imported from StreamElements overlay ${seOverlay._id}` : "Imported from StreamElements",
11002
+ description: buildSEBootstrapDescription(seOverlay?._id, options.sourceUrl),
10657
11003
  settings: {
10658
11004
  metadata: {
10659
11005
  width: seOverlay?.settings?.width ?? 1920,
@@ -10701,7 +11047,7 @@ function applyReviewAction(result, moduleId, action, payload) {
10701
11047
  bounds: existingLayer.bounds,
10702
11048
  state: existingLayer.state
10703
11049
  };
10704
- const importMeta2 = existing2.content?.[IMPORT_META_KEY3];
11050
+ const importMeta2 = existing2.content?.[IMPORT_META_KEY2];
10705
11051
  const mergedLights = (existing2.lights?.length ?? 0) > 0 ? existing2.lights : transplant.module.lights;
10706
11052
  const mergedEvents = existing2.events && Object.keys(existing2.events).length > 0 ? { ...transplant.module.events, ...existing2.events } : transplant.module.events;
10707
11053
  const mergedVariables = existing2.variables && Object.keys(existing2.variables).length > 0 ? { ...transplant.module.variables, ...existing2.variables } : transplant.module.variables;
@@ -10713,7 +11059,7 @@ function applyReviewAction(result, moduleId, action, payload) {
10713
11059
  variables: mergedVariables,
10714
11060
  content: {
10715
11061
  ...transplant.module.content ?? {},
10716
- [IMPORT_META_KEY3]: importMeta2
11062
+ [IMPORT_META_KEY2]: importMeta2
10717
11063
  }
10718
11064
  };
10719
11065
  const rootOldNewId = transplant.layer.id;
@@ -10741,7 +11087,7 @@ function applyReviewAction(result, moduleId, action, payload) {
10741
11087
  const existing = modules[moduleId];
10742
11088
  const generated = payload;
10743
11089
  if (!existing || !generated) return result;
10744
- const importMeta = existing.content?.[IMPORT_META_KEY3];
11090
+ const importMeta = existing.content?.[IMPORT_META_KEY2];
10745
11091
  const replaced = {
10746
11092
  ...existing,
10747
11093
  settings: { ...existing.settings ?? {}, type: "custom" },
@@ -10755,7 +11101,7 @@ function applyReviewAction(result, moduleId, action, payload) {
10755
11101
  data: parseLooseJson(generated.data) ?? {},
10756
11102
  configs: parseLooseJson(generated.configs) ?? [],
10757
11103
  flavor: "ai-generated",
10758
- [IMPORT_META_KEY3]: importMeta
11104
+ [IMPORT_META_KEY2]: importMeta
10759
11105
  }
10760
11106
  };
10761
11107
  modules[moduleId] = replaced;