@lumiastream/ui 0.2.8-alpha.29 → 0.2.8-alpha.30

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
@@ -1133,15 +1133,53 @@ function mapCustom(widget, ctx) {
1133
1133
  }
1134
1134
 
1135
1135
  // src/se-import/mappers/misc.ts
1136
+ import {
1137
+ LumiaEventListTypes,
1138
+ LumiaMapAlertTypeToEventListType
1139
+ } from "@lumiastream/lumia-types";
1136
1140
  function mapChatboxTheme(seTheme) {
1137
1141
  const t = (seTheme ?? "").toString().toLowerCase();
1138
1142
  if (!t || t === "default") return "simple";
1139
- if (t.includes("flipin") || t.includes("bubble") || t.includes("bubbly")) return "bubbly";
1143
+ if (t.includes("flipin") || t.includes("bubble") || t.includes("bubbly"))
1144
+ return "bubbly";
1140
1145
  if (t.includes("glass") || t.includes("transparent")) return "glass";
1141
1146
  if (t.includes("boxy") || t.includes("checker")) return "boxy";
1142
1147
  if (t.includes("basic")) return "basic";
1143
1148
  return "simple";
1144
1149
  }
1150
+ var KAPPAGEN_EVENT_CATEGORIES = {
1151
+ tip: [LumiaEventListTypes.DONATION],
1152
+ cheer: [
1153
+ LumiaEventListTypes.BITS,
1154
+ LumiaEventListTypes.KICKS,
1155
+ LumiaEventListTypes.SUPERCHATS,
1156
+ LumiaEventListTypes.STARS
1157
+ ],
1158
+ embers: [LumiaEventListTypes.BITS],
1159
+ follower: [LumiaEventListTypes.FOLLOWER],
1160
+ host: [LumiaEventListTypes.HOSTS],
1161
+ raid: [LumiaEventListTypes.RAIDS],
1162
+ subscriber: [LumiaEventListTypes.SUBSCRIBERS],
1163
+ purchase: [LumiaEventListTypes.PURCHASES]
1164
+ };
1165
+ var alertKeysByEventListType = Object.entries(
1166
+ LumiaMapAlertTypeToEventListType
1167
+ ).reduce(
1168
+ (acc, [alertKey, eventListType]) => {
1169
+ acc[eventListType] = acc[eventListType] ?? [];
1170
+ acc[eventListType].push(alertKey);
1171
+ return acc;
1172
+ },
1173
+ {}
1174
+ );
1175
+ function kappagenAlertTriggersFor(seEvent, provider) {
1176
+ const categoryKeys = (KAPPAGEN_EVENT_CATEGORIES[seEvent] ?? []).flatMap(
1177
+ (eventListType) => alertKeysByEventListType[eventListType] ?? []
1178
+ );
1179
+ if (categoryKeys.length > 0) return categoryKeys;
1180
+ const fallback = kappagenKeyFor(seEvent, provider);
1181
+ return fallback ? [fallback] : [];
1182
+ }
1145
1183
  function paddingShorthand(message) {
1146
1184
  const pick = (key) => {
1147
1185
  const raw = message?.[key];
@@ -1165,13 +1203,18 @@ function mapChatbox(widget, ctx) {
1165
1203
  const messagePadding = paddingShorthand(messageCss);
1166
1204
  const DEFAULT_CHATBOX_FONT_SIZE = 14;
1167
1205
  const moduleCss = {};
1168
- if (textCss["font-family"] != null) moduleCss.fontFamily = textCss["font-family"];
1206
+ if (textCss["font-family"] != null)
1207
+ moduleCss.fontFamily = textCss["font-family"];
1169
1208
  moduleCss.fontSize = textCss["font-size"] != null ? textCss["font-size"] : DEFAULT_CHATBOX_FONT_SIZE;
1170
1209
  if (textCss["color"] != null) moduleCss.color = textCss["color"];
1171
- if (textCss["font-weight"] != null) moduleCss.fontWeight = textCss["font-weight"];
1172
- if (textCss["text-shadow"] != null) moduleCss.textShadow = textCss["text-shadow"];
1173
- if (textCss["text-align"] != null) moduleCss.textAlign = textCss["text-align"];
1174
- if (textCss["line-height"] != null) moduleCss.lineHeight = textCss["line-height"];
1210
+ if (textCss["font-weight"] != null)
1211
+ moduleCss.fontWeight = textCss["font-weight"];
1212
+ if (textCss["text-shadow"] != null)
1213
+ moduleCss.textShadow = textCss["text-shadow"];
1214
+ if (textCss["text-align"] != null)
1215
+ moduleCss.textAlign = textCss["text-align"];
1216
+ if (textCss["line-height"] != null)
1217
+ moduleCss.lineHeight = textCss["line-height"];
1175
1218
  if (messagePadding) moduleCss.padding = messagePadding;
1176
1219
  const DEFAULT_CHATBOX_BG = "#1e1e1e";
1177
1220
  const messageBackground = typeof messageCss["background"] === "string" && messageCss["background"].length > 0 ? messageCss["background"] : DEFAULT_CHATBOX_BG;
@@ -1179,72 +1222,82 @@ function mapChatbox(widget, ctx) {
1179
1222
  const messageFontFamily = typeof messageCss["font-family"] === "string" && messageCss["font-family"].length > 0 ? messageCss["font-family"] : void 0;
1180
1223
  const highlightBackground = v.highlight && typeof v.highlight === "object" && typeof v.highlight["background"] === "string" ? v.highlight["background"] : void 0;
1181
1224
  const accentColor = highlightBackground && highlightBackground.length > 0 ? highlightBackground : void 0;
1182
- return buildUnit(widget, "chatbox", {
1183
- // Visual styling (font / color / padding etc) lives on module.css, not
1184
- // content. Lumia's TextStyle settings panel + the chatbox renderer both
1185
- // read from here.
1186
- css: moduleCss,
1187
- content: {
1188
- theme: mapChatboxTheme(v.theme),
1189
- fadeOutAfterDelay: fadeOutSeconds > 0,
1190
- fadeOutDelayTime: fadeOutSeconds,
1191
- // Defaults Lumia's chatbox renderer + UI expect on every module.
1192
- // Without these the chatbox silently dropped messages (the
1193
- // `ignoredStreamingSites` truthy gate), rendered in the wrong
1194
- // theme (no theme blank list), or showed missing controls in
1195
- // settings. `ignoredList` carries SE's per-user ignore list across.
1196
- ignoredList: Array.isArray(v.ignored) ? v.ignored : [],
1197
- ignoredStreamingSites: [],
1198
- horizontal: false,
1199
- reverseFlow: false,
1200
- hideAlerts: false,
1201
- hideAlertMessage: false,
1202
- showAvatar: true,
1203
- showBadges: true,
1204
- showEmotes: true,
1205
- showSiteIcon: true,
1206
- showTimestamp: false,
1207
- breakLine: false,
1208
- hyperClickableLinks: true,
1209
- clickableChatterProfiles: true,
1210
- previewMediaInChat: true,
1211
- maxItemsToShow: 20,
1212
- removeAfter: 3e4,
1213
- itemGap: 8,
1214
- borderRadius: "10px",
1215
- bgOpacity: 0.4,
1216
- animations: {
1217
- enterAnimation: "fadeIn",
1218
- exitAnimation: "fadeOut",
1219
- enterAnimationDuration: 1e3,
1220
- exitAnimationDuration: 1e3,
1221
- enterAnimationDelay: 0,
1222
- exitAnimationDelay: 0
1223
- },
1224
- // Carry the SE message-level font override and the streamer's
1225
- // accent color across so the imported chatbox looks right at
1226
- // first paint. Background lives on `module.css.background`
1227
- // above `content.background` was a dead write the renderer
1228
- // never read.
1229
- ...messageFontFamily ? { messageFontFamily } : {},
1230
- ...accentColor ? {
1231
- chatboxStreamingSite: {
1232
- twitch: { themeConfig: { primaryColor: accentColor, showUsernameColors: true } }
1233
- }
1234
- } : {},
1235
- // Source-side knobs preserved under the generic importMeta envelope —
1236
- // no native Lumia equivalent today. `messageDelay` is per-message
1237
- // hold, `initialDelay` is the show-after-connect delay, and we
1238
- // stash the raw highlight block + text css for any future feature
1239
- // that wants the fine-grained styling.
1240
- importMeta: buildImportMeta(widget, {
1241
- messageDelay: v.messageDelay,
1242
- initialDelay: v.delay,
1243
- highlightUsers: v.highlight,
1244
- textCss
1245
- })
1246
- }
1247
- }, ctx);
1225
+ return buildUnit(
1226
+ widget,
1227
+ "chatbox",
1228
+ {
1229
+ // Visual styling (font / color / padding etc) lives on module.css, not
1230
+ // content. Lumia's TextStyle settings panel + the chatbox renderer both
1231
+ // read from here.
1232
+ css: moduleCss,
1233
+ content: {
1234
+ theme: mapChatboxTheme(v.theme),
1235
+ fadeOutAfterDelay: fadeOutSeconds > 0,
1236
+ fadeOutDelayTime: fadeOutSeconds,
1237
+ // Defaults Lumia's chatbox renderer + UI expect on every module.
1238
+ // Without these the chatbox silently dropped messages (the
1239
+ // `ignoredStreamingSites` truthy gate), rendered in the wrong
1240
+ // theme (no theme → blank list), or showed missing controls in
1241
+ // settings. `ignoredList` carries SE's per-user ignore list across.
1242
+ ignoredList: Array.isArray(v.ignored) ? v.ignored : [],
1243
+ ignoredStreamingSites: [],
1244
+ horizontal: false,
1245
+ reverseFlow: false,
1246
+ hideAlerts: false,
1247
+ hideAlertMessage: false,
1248
+ showAvatar: true,
1249
+ showBadges: true,
1250
+ showEmotes: true,
1251
+ showSiteIcon: true,
1252
+ showTimestamp: false,
1253
+ breakLine: false,
1254
+ hyperClickableLinks: true,
1255
+ clickableChatterProfiles: true,
1256
+ previewMediaInChat: true,
1257
+ maxItemsToShow: 20,
1258
+ removeAfter: 3e4,
1259
+ itemGap: 8,
1260
+ borderRadius: "10px",
1261
+ bgOpacity: 0.4,
1262
+ animations: {
1263
+ enterAnimation: "fadeIn",
1264
+ exitAnimation: "fadeOut",
1265
+ enterAnimationDuration: 1e3,
1266
+ exitAnimationDuration: 1e3,
1267
+ enterAnimationDelay: 0,
1268
+ exitAnimationDelay: 0
1269
+ },
1270
+ // Carry the SE message-level font override and the streamer's
1271
+ // accent color across so the imported chatbox looks right at
1272
+ // first paint. Background lives on `module.css.background`
1273
+ // above — `content.background` was a dead write the renderer
1274
+ // never read.
1275
+ ...messageFontFamily ? { messageFontFamily } : {},
1276
+ ...accentColor ? {
1277
+ chatboxStreamingSite: {
1278
+ twitch: {
1279
+ themeConfig: {
1280
+ primaryColor: accentColor,
1281
+ showUsernameColors: true
1282
+ }
1283
+ }
1284
+ }
1285
+ } : {},
1286
+ // Source-side knobs preserved under the generic importMeta envelope —
1287
+ // no native Lumia equivalent today. `messageDelay` is per-message
1288
+ // hold, `initialDelay` is the show-after-connect delay, and we
1289
+ // stash the raw highlight block + text css for any future feature
1290
+ // that wants the fine-grained styling.
1291
+ importMeta: buildImportMeta(widget, {
1292
+ messageDelay: v.messageDelay,
1293
+ initialDelay: v.delay,
1294
+ highlightUsers: v.highlight,
1295
+ textCss
1296
+ })
1297
+ }
1298
+ },
1299
+ ctx
1300
+ );
1248
1301
  }
1249
1302
  var SE_LISTENER_TO_LUMIA_CATEGORY = {
1250
1303
  "follower-latest": "follower",
@@ -1292,7 +1345,9 @@ var ALL_LUMIA_EVENTLIST_CATEGORIES = [
1292
1345
  function computeListenerFilters(seListeners, explicitListener) {
1293
1346
  const enabled = [];
1294
1347
  if (seListeners && typeof seListeners === "object" && !Array.isArray(seListeners)) {
1295
- for (const [k, v] of Object.entries(seListeners)) {
1348
+ for (const [k, v] of Object.entries(
1349
+ seListeners
1350
+ )) {
1296
1351
  if (v) enabled.push(k);
1297
1352
  }
1298
1353
  } else if (Array.isArray(seListeners)) {
@@ -1314,35 +1369,40 @@ function mapEventList(widget, ctx) {
1314
1369
  const v = widget.variables ?? {};
1315
1370
  const filters = computeListenerFilters(widget.listeners, widget.listener);
1316
1371
  const maxItems = v.visibleEvents ?? v.visibleItems ?? 5;
1317
- return buildUnit(widget, "eventlist", {
1318
- // SE event-list widgets style per-event lines via widget.text.css; the
1319
- // Lumia eventlist reads font/size/color from module.css.
1320
- css: mapSeTextCssToModuleCss(widget, { fontSize: 18 }),
1321
- content: {
1322
- theme: v.theme ?? "simple",
1323
- direction: v.direction ?? "top",
1324
- maxItemsToShow: maxItems,
1325
- visibleEvents: maxItems,
1326
- // legacy field name kept for back-compat
1327
- themeBgOpacity: v.themeBgOpacity ?? 0,
1328
- highlight: v.highlight,
1329
- // `filters` is a blacklist: every category here is HIDDEN. Empty = show all.
1330
- filters,
1331
- // `sites` is a platform blacklist (same shape). Default to "no platforms hidden".
1332
- sites: [],
1333
- // Eventlist Manager assumes these objects exist; provide an empty stub so
1334
- // imported overlays render without throwing.
1335
- themeConfig: {},
1336
- eventListType: {},
1337
- condensedText: false,
1338
- horizontal: false,
1339
- showAlertIcon: true,
1340
- showSiteIcon: true,
1341
- showUserAvatar: true,
1342
- showDetailedText: false,
1343
- hideAlertMessage: false
1344
- }
1345
- }, ctx);
1372
+ return buildUnit(
1373
+ widget,
1374
+ "eventlist",
1375
+ {
1376
+ // SE event-list widgets style per-event lines via widget.text.css; the
1377
+ // Lumia eventlist reads font/size/color from module.css.
1378
+ css: mapSeTextCssToModuleCss(widget, { fontSize: 18 }),
1379
+ content: {
1380
+ theme: v.theme ?? "simple",
1381
+ direction: v.direction ?? "top",
1382
+ maxItemsToShow: maxItems,
1383
+ visibleEvents: maxItems,
1384
+ // legacy field name kept for back-compat
1385
+ themeBgOpacity: v.themeBgOpacity ?? 0,
1386
+ highlight: v.highlight,
1387
+ // `filters` is a blacklist: every category here is HIDDEN. Empty = show all.
1388
+ filters,
1389
+ // `sites` is a platform blacklist (same shape). Default to "no platforms hidden".
1390
+ sites: [],
1391
+ // Eventlist Manager assumes these objects exist; provide an empty stub so
1392
+ // imported overlays render without throwing.
1393
+ themeConfig: {},
1394
+ eventListType: {},
1395
+ condensedText: false,
1396
+ horizontal: false,
1397
+ showAlertIcon: true,
1398
+ showSiteIcon: true,
1399
+ showUserAvatar: true,
1400
+ showDetailedText: false,
1401
+ hideAlertMessage: false
1402
+ }
1403
+ },
1404
+ ctx
1405
+ );
1346
1406
  }
1347
1407
  var SE_CREDITS_EVENTS_TO_SHOW = {
1348
1408
  mods: false,
@@ -1486,55 +1546,70 @@ function mapCredits(widget, ctx) {
1486
1546
  const v = widget.variables ?? {};
1487
1547
  const itemGap = typeof v.padding === "number" ? v.padding : 8;
1488
1548
  const reverseFlow = v.direction === "bottom";
1489
- return buildUnit(widget, "credits", {
1490
- content: {
1491
- version: 1,
1492
- type: "basic",
1493
- title: "Stream Credits",
1494
- titleColor: "#ffffff",
1495
- subtitle: "A massive thanks to everyone below",
1496
- subtitleColor: "#ffffff",
1497
- headerColor: "#ffffff",
1498
- endMessage: "See you next time!",
1499
- endMessageColor: "#ffffff",
1500
- eventColor: "#ffffff",
1501
- loop: false,
1502
- speed: 1,
1503
- sectionOrder: [...CREDITS_DEFAULT_SECTION_ORDER],
1504
- // SE only ships subscribers + gift-subs, so import with just those
1505
- // enabled. The Manager can re-enable additional sections at any time;
1506
- // the underlying activity feed is fetched session-wide regardless.
1507
- eventsToShow: { ...SE_CREDITS_EVENTS_TO_SHOW },
1508
- topLimits: { ...CREDITS_DEFAULT_TOP_LIMITS },
1509
- eventNames: { ...CREDITS_DEFAULT_EVENT_NAMES },
1510
- prefixes: { ...CREDITS_DEFAULT_PREFIXES },
1511
- templates: { ...CREDITS_DEFAULT_TEMPLATES },
1512
- daysToShow: "session",
1513
- itemGap,
1514
- reverseFlow,
1515
- media: {
1516
- type: "image",
1517
- content: { src: [], name: [], loop: true, playAudio: true, volume: 1 }
1518
- },
1519
- audio: {
1520
- content: { src: [], name: [], loop: false, volume: 1 }
1549
+ return buildUnit(
1550
+ widget,
1551
+ "credits",
1552
+ {
1553
+ content: {
1554
+ version: 1,
1555
+ type: "basic",
1556
+ title: "Stream Credits",
1557
+ titleColor: "#ffffff",
1558
+ subtitle: "A massive thanks to everyone below",
1559
+ subtitleColor: "#ffffff",
1560
+ headerColor: "#ffffff",
1561
+ endMessage: "See you next time!",
1562
+ endMessageColor: "#ffffff",
1563
+ eventColor: "#ffffff",
1564
+ loop: false,
1565
+ speed: 1,
1566
+ sectionOrder: [...CREDITS_DEFAULT_SECTION_ORDER],
1567
+ // SE only ships subscribers + gift-subs, so import with just those
1568
+ // enabled. The Manager can re-enable additional sections at any time;
1569
+ // the underlying activity feed is fetched session-wide regardless.
1570
+ eventsToShow: { ...SE_CREDITS_EVENTS_TO_SHOW },
1571
+ topLimits: { ...CREDITS_DEFAULT_TOP_LIMITS },
1572
+ eventNames: { ...CREDITS_DEFAULT_EVENT_NAMES },
1573
+ prefixes: { ...CREDITS_DEFAULT_PREFIXES },
1574
+ templates: { ...CREDITS_DEFAULT_TEMPLATES },
1575
+ daysToShow: "session",
1576
+ itemGap,
1577
+ reverseFlow,
1578
+ media: {
1579
+ type: "image",
1580
+ content: {
1581
+ src: [],
1582
+ name: [],
1583
+ loop: true,
1584
+ playAudio: true,
1585
+ volume: 1
1586
+ }
1587
+ },
1588
+ audio: {
1589
+ content: { src: [], name: [], loop: false, volume: 1 }
1590
+ },
1591
+ // Source-side knobs with no direct Lumia equivalent — preserved
1592
+ // under the generic importMeta envelope. `chosenFilter` would
1593
+ // narrow eventsToShow if we had a clean mapping; `separator` would
1594
+ // join entries but Lumia credits uses sectioned cards instead.
1595
+ importMeta: buildImportMeta(widget, {
1596
+ chosenFilter: v.chosenFilter,
1597
+ separator: v.separator,
1598
+ amount: v.amount
1599
+ })
1521
1600
  },
1522
- // Source-side knobs with no direct Lumia equivalent preserved
1523
- // under the generic importMeta envelope. `chosenFilter` would
1524
- // narrow eventsToShow if we had a clean mapping; `separator` would
1525
- // join entries but Lumia credits uses sectioned cards instead.
1526
- importMeta: buildImportMeta(widget, {
1527
- chosenFilter: v.chosenFilter,
1528
- separator: v.separator,
1529
- amount: v.amount
1601
+ // Credits renders scrolling section headers + per-event lines; the
1602
+ // renderer reads font/size/weight/color from module.css. Pull from SE's
1603
+ // text.css so an imported credits widget keeps the streamer's chosen
1604
+ // font and weight defaults match the prior hardcoded fallbacks.
1605
+ css: mapSeTextCssToModuleCss(widget, {
1606
+ fontSize: 30,
1607
+ fontWeight: "normal",
1608
+ textShadow: ""
1530
1609
  })
1531
1610
  },
1532
- // Credits renders scrolling section headers + per-event lines; the
1533
- // renderer reads font/size/weight/color from module.css. Pull from SE's
1534
- // text.css so an imported credits widget keeps the streamer's chosen
1535
- // font and weight — defaults match the prior hardcoded fallbacks.
1536
- css: mapSeTextCssToModuleCss(widget, { fontSize: 30, fontWeight: "normal", textShadow: "" })
1537
- }, ctx);
1611
+ ctx
1612
+ );
1538
1613
  }
1539
1614
  function inferSlideshowMediaType(mime, url) {
1540
1615
  if (typeof mime === "string") {
@@ -1559,49 +1634,61 @@ function mapSlideshow(widget, ctx) {
1559
1634
  const url = typeof raw === "string" ? raw : raw.url ?? raw.src ?? "";
1560
1635
  if (!url) return null;
1561
1636
  return {
1562
- type: inferSlideshowMediaType(typeof raw === "object" ? raw?.type : void 0, url),
1637
+ type: inferSlideshowMediaType(
1638
+ typeof raw === "object" ? raw?.type : void 0,
1639
+ url
1640
+ ),
1563
1641
  src: url,
1564
1642
  name: nameFromUrl(url)
1565
1643
  };
1566
- }).filter((item) => item !== null);
1644
+ }).filter(
1645
+ (item) => item !== null
1646
+ );
1567
1647
  const seAnim = typeof v.animation === "object" && v.animation !== null ? v.animation : {};
1568
- return buildUnit(widget, "slideshow", {
1569
- content: {
1570
- src: items,
1571
- // `interval` of 0 = "rapid cycle" in SE (no delay between images); Lumia's
1572
- // `imageDuration` is the total ms per image. Distinguish three cases:
1573
- // - undefined → use 5000ms default
1574
- // - 0 minimum sane cycle of 100ms (browsers can't render faster than ~60fps anyway)
1575
- // - >0 SE seconds * 1000
1576
- imageDuration: typeof v.interval === "number" ? v.interval === 0 ? 100 : v.interval * 1e3 : 5e3,
1577
- loop: v.loop !== false,
1578
- loopDelay: 0,
1579
- delay: 0,
1580
- playAudio: !!v.playAudio,
1581
- // Lumia's renderer reads `content.shuffle` (Slideshow/index.tsx:21);
1582
- // mirroring SE's `random` to that name keeps "play in random order"
1583
- // working out of the box.
1584
- shuffle: !!v.random,
1585
- animation: {
1586
- enterAnimation: typeof v.animation === "string" ? v.animation : seAnim.in ?? "fadeIn",
1587
- exitAnimation: typeof v.animation === "string" ? v.animation : seAnim.out ?? "fadeOut",
1588
- enterAnimationDuration: (seAnim.inDuration ?? 1) * 1e3,
1589
- exitAnimationDuration: (seAnim.outDuration ?? 1) * 1e3,
1590
- enterAnimationDelay: (seAnim.inDelay ?? 0) * 1e3,
1591
- exitAnimationDelay: (seAnim.outDelay ?? 0) * 1e3
1592
- },
1593
- importMeta: buildImportMeta(widget, {
1594
- interval: v.interval,
1595
- rawDuration: v.duration
1596
- })
1597
- }
1598
- }, ctx);
1648
+ return buildUnit(
1649
+ widget,
1650
+ "slideshow",
1651
+ {
1652
+ content: {
1653
+ src: items,
1654
+ // `interval` of 0 = "rapid cycle" in SE (no delay between images); Lumia's
1655
+ // `imageDuration` is the total ms per image. Distinguish three cases:
1656
+ // - undefined use 5000ms default
1657
+ // - 0 minimum sane cycle of 100ms (browsers can't render faster than ~60fps anyway)
1658
+ // - >0 → SE seconds * 1000
1659
+ imageDuration: typeof v.interval === "number" ? v.interval === 0 ? 100 : v.interval * 1e3 : 5e3,
1660
+ loop: v.loop !== false,
1661
+ loopDelay: 0,
1662
+ delay: 0,
1663
+ playAudio: !!v.playAudio,
1664
+ // Lumia's renderer reads `content.shuffle` (Slideshow/index.tsx:21);
1665
+ // mirroring SE's `random` to that name keeps "play in random order"
1666
+ // working out of the box.
1667
+ shuffle: !!v.random,
1668
+ animation: {
1669
+ enterAnimation: typeof v.animation === "string" ? v.animation : seAnim.in ?? "fadeIn",
1670
+ exitAnimation: typeof v.animation === "string" ? v.animation : seAnim.out ?? "fadeOut",
1671
+ enterAnimationDuration: (seAnim.inDuration ?? 1) * 1e3,
1672
+ exitAnimationDuration: (seAnim.outDuration ?? 1) * 1e3,
1673
+ enterAnimationDelay: (seAnim.inDelay ?? 0) * 1e3,
1674
+ exitAnimationDelay: (seAnim.outDelay ?? 0) * 1e3
1675
+ },
1676
+ importMeta: buildImportMeta(widget, {
1677
+ interval: v.interval,
1678
+ rawDuration: v.duration
1679
+ })
1680
+ }
1681
+ },
1682
+ ctx
1683
+ );
1599
1684
  }
1600
1685
  function mapTimer(widget, ctx) {
1601
1686
  const v = widget.variables ?? {};
1602
1687
  const textValue = typeof widget.text?.value === "string" ? widget.text.value : void 0;
1603
1688
  const TIMER_PASSTHROUGH = /* @__PURE__ */ new Set(["days", "hours", "minutes", "seconds"]);
1604
- const mapTemplate = (template) => translateSeText(template, null, TIMER_PASSTHROUGH, void 0, { context: "static" });
1689
+ const mapTemplate = (template) => translateSeText(template, null, TIMER_PASSTHROUGH, void 0, {
1690
+ context: "static"
1691
+ });
1605
1692
  const absoluteDate = v.date ?? v.endsAt ?? v.endDate;
1606
1693
  const target = absoluteDate ? Date.parse(absoluteDate) : NaN;
1607
1694
  const hasValidAbsoluteDate = Number.isFinite(target);
@@ -1621,7 +1708,9 @@ function mapTimer(widget, ctx) {
1621
1708
  resolved = true;
1622
1709
  }
1623
1710
  }
1624
- if (!resolved && [v.days, v.hours, v.minutes, v.seconds].some((part) => typeof part === "number")) {
1711
+ if (!resolved && [v.days, v.hours, v.minutes, v.seconds].some(
1712
+ (part) => typeof part === "number"
1713
+ )) {
1625
1714
  days = typeof v.days === "number" ? v.days : 0;
1626
1715
  hours = typeof v.hours === "number" ? v.hours : 0;
1627
1716
  minutes = typeof v.minutes === "number" ? v.minutes : 0;
@@ -1660,102 +1749,123 @@ function mapTimer(widget, ctx) {
1660
1749
  if (v.seconds !== false) clockBits.push("{{seconds}}");
1661
1750
  if (clockBits.length) parts.push(clockBits.join(":"));
1662
1751
  }
1663
- return buildUnit(widget, "timer", {
1664
- // Carry the streamer's SE text styling (font/size/color/shadow) so an
1665
- // imported countdown doesn't render in Lumia's default Roboto/24px.
1666
- css: mapSeTextCssToModuleCss(widget),
1667
- content: {
1668
- version: 1,
1669
- type: "countdown",
1670
- value: { days, hours, minutes, seconds },
1671
- display: textValue ? mapTemplate(textValue) : parts.join(" ") || "{{minutes}}:{{seconds}}",
1672
- showMilliseconds: false,
1673
- endAnimation: "",
1674
- hideAfter: 0,
1675
- highlightColor: "inherit",
1676
- messageAfterCountdown: translateSeText(v.messageAfterCountdown ?? "", null, void 0, void 0, { context: "static" }),
1677
- changeMessageAfter: !!v.changeMessageAfterCountdown,
1678
- countDate: hasValidAbsoluteDate && (!!v.exactTime || !!v.datePicker || !!absoluteDate),
1679
- date: hasValidAbsoluteDate ? new Date(target).toISOString() : "",
1680
- importMeta: buildImportMeta(widget, {
1681
- rawDate: absoluteDate,
1682
- datePicker: v.datePicker,
1683
- exactTime: v.exactTime
1684
- })
1685
- }
1686
- }, ctx);
1752
+ return buildUnit(
1753
+ widget,
1754
+ "timer",
1755
+ {
1756
+ // Carry the streamer's SE text styling (font/size/color/shadow) so an
1757
+ // imported countdown doesn't render in Lumia's default Roboto/24px.
1758
+ css: mapSeTextCssToModuleCss(widget),
1759
+ content: {
1760
+ version: 1,
1761
+ type: "countdown",
1762
+ value: { days, hours, minutes, seconds },
1763
+ display: textValue ? mapTemplate(textValue) : parts.join(" ") || "{{minutes}}:{{seconds}}",
1764
+ showMilliseconds: false,
1765
+ endAnimation: "",
1766
+ hideAfter: 0,
1767
+ highlightColor: "inherit",
1768
+ messageAfterCountdown: translateSeText(
1769
+ v.messageAfterCountdown ?? "",
1770
+ null,
1771
+ void 0,
1772
+ void 0,
1773
+ { context: "static" }
1774
+ ),
1775
+ changeMessageAfter: !!v.changeMessageAfterCountdown,
1776
+ countDate: hasValidAbsoluteDate && (!!v.exactTime || !!v.datePicker || !!absoluteDate),
1777
+ date: hasValidAbsoluteDate ? new Date(target).toISOString() : "",
1778
+ importMeta: buildImportMeta(widget, {
1779
+ rawDate: absoluteDate,
1780
+ datePicker: v.datePicker,
1781
+ exactTime: v.exactTime
1782
+ })
1783
+ }
1784
+ },
1785
+ ctx
1786
+ );
1687
1787
  }
1688
1788
  function mapNowPlaying(widget, ctx) {
1689
1789
  const v = widget.variables ?? {};
1690
1790
  const source = v.provider === "youtube" ? "youtubemusic" : "spotify";
1691
- return buildUnit(widget, "nowplaying", {
1692
- content: {
1693
- version: 1,
1694
- type: "nowplaying",
1695
- source,
1696
- theme: "basic",
1697
- borderRadius: "30px",
1698
- songTitle: `{{${source}_now_playing_song=Lumia Dream}}`,
1699
- songArtist: `{{${source}_now_playing_artist}}`,
1700
- songImg: `{{${source}_now_playing_image}}`,
1701
- showImg: true,
1702
- oneLineTitle: false,
1703
- highlightColor: "inherit",
1704
- importMeta: buildImportMeta(widget, { simpleDesign: !!v.simpleDesign })
1791
+ return buildUnit(
1792
+ widget,
1793
+ "nowplaying",
1794
+ {
1795
+ content: {
1796
+ version: 1,
1797
+ type: "nowplaying",
1798
+ source,
1799
+ theme: "basic",
1800
+ borderRadius: "30px",
1801
+ songTitle: `{{${source}_now_playing_song=Lumia Dream}}`,
1802
+ songArtist: `{{${source}_now_playing_artist}}`,
1803
+ songImg: `{{${source}_now_playing_image}}`,
1804
+ showImg: true,
1805
+ oneLineTitle: false,
1806
+ highlightColor: "inherit",
1807
+ importMeta: buildImportMeta(widget, { simpleDesign: !!v.simpleDesign })
1808
+ },
1809
+ css: {
1810
+ borderRadius: "0px",
1811
+ borderStyle: "solid",
1812
+ borderWidth: "0px",
1813
+ borderColor: "transparent",
1814
+ lineHeight: 1,
1815
+ textAlign: "left",
1816
+ fontFamily: "Be Vietnam Pro",
1817
+ color: "#ffffff",
1818
+ textShadow: "rgb(0, 0, 0) 1px 1px 1px",
1819
+ background: "transparent"
1820
+ }
1705
1821
  },
1706
- css: {
1707
- borderRadius: "0px",
1708
- borderStyle: "solid",
1709
- borderWidth: "0px",
1710
- borderColor: "transparent",
1711
- lineHeight: 1,
1712
- textAlign: "left",
1713
- fontFamily: "Be Vietnam Pro",
1714
- color: "#ffffff",
1715
- textShadow: "rgb(0, 0, 0) 1px 1px 1px",
1716
- background: "transparent"
1717
- }
1718
- }, ctx);
1822
+ ctx
1823
+ );
1719
1824
  }
1720
1825
  function mapMediaShare(widget, ctx) {
1721
1826
  const v = widget.variables ?? {};
1722
- return buildUnit(widget, "songrequest", {
1723
- content: {
1724
- version: 1,
1725
- // SE always shows the now-playing card; honour its cosmetic toggles
1726
- // for the rest. `showTitle` defaults true; the others default to
1727
- // SE's documented behavior (tipper visible, stats hidden).
1728
- showNowPlayingCard: true,
1729
- showQueueList: true,
1730
- showSkipButton: true,
1731
- showPauseButton: true,
1732
- showTitle: v.showTitle !== false,
1733
- showTipper: v.showTipper !== false,
1734
- showAmount: v.showAmount === true,
1735
- showStats: v.showStats === true,
1736
- // SE doesn't have a queue-size knob — pick a sensible default
1737
- // matching what fits in the SE widget's ~292px tall footprint.
1738
- maxQueueItemsVisible: 5,
1739
- // SE color knobs translate directly. Fall back to Lumia's defaults
1740
- // (purple accent, near-black card background) if SE values are
1741
- // missing — matches what the module ships with for native creates.
1742
- accentColor: v.accentColor ?? "#9146ff",
1743
- primaryColor: v.primaryColor ?? "#1a1a1a",
1744
- // SE's widget hides the YT player by default; mirror that for
1745
- // audio-only overlay use.
1746
- playerVisible: false,
1747
- defaultVolume: 70,
1748
- autoAdvanceOnEnd: true,
1749
- // Source-side knob preserved for round-trip provenance; Lumia
1750
- // ignores it at runtime (auto-advance fires on
1751
- // `durationSeconds + grace`).
1752
- importMeta: buildImportMeta(widget, { timeLeft: v.timeLeft === true })
1827
+ return buildUnit(
1828
+ widget,
1829
+ "songrequest",
1830
+ {
1831
+ content: {
1832
+ version: 1,
1833
+ // SE always shows the now-playing card; honour its cosmetic toggles
1834
+ // for the rest. `showTitle` defaults true; the others default to
1835
+ // SE's documented behavior (tipper visible, stats hidden).
1836
+ showNowPlayingCard: true,
1837
+ showQueueList: true,
1838
+ showSkipButton: true,
1839
+ showPauseButton: true,
1840
+ showTitle: v.showTitle !== false,
1841
+ showTipper: v.showTipper !== false,
1842
+ showAmount: v.showAmount === true,
1843
+ showStats: v.showStats === true,
1844
+ // SE doesn't have a queue-size knob pick a sensible default
1845
+ // matching what fits in the SE widget's ~292px tall footprint.
1846
+ maxQueueItemsVisible: 5,
1847
+ // SE color knobs translate directly. Fall back to Lumia's defaults
1848
+ // (purple accent, near-black card background) if SE values are
1849
+ // missing matches what the module ships with for native creates.
1850
+ accentColor: v.accentColor ?? "#9146ff",
1851
+ primaryColor: v.primaryColor ?? "#1a1a1a",
1852
+ // SE's widget hides the YT player by default; mirror that for
1853
+ // audio-only overlay use.
1854
+ playerVisible: false,
1855
+ defaultVolume: 70,
1856
+ autoAdvanceOnEnd: true,
1857
+ // Source-side knob preserved for round-trip provenance; Lumia
1858
+ // ignores it at runtime (auto-advance fires on
1859
+ // `durationSeconds + grace`).
1860
+ importMeta: buildImportMeta(widget, { timeLeft: v.timeLeft === true })
1861
+ },
1862
+ // Lumia songrequest renders the queue list as styled text; pull SE's
1863
+ // widget.text.css so font/size/color carry across (defaults match the
1864
+ // previous hardcoded fallbacks for this widget specifically).
1865
+ css: mapSeTextCssToModuleCss(widget, { fontSize: 18 })
1753
1866
  },
1754
- // Lumia songrequest renders the queue list as styled text; pull SE's
1755
- // widget.text.css so font/size/color carry across (defaults match the
1756
- // previous hardcoded fallbacks for this widget specifically).
1757
- css: mapSeTextCssToModuleCss(widget, { fontSize: 18 })
1758
- }, ctx);
1867
+ ctx
1868
+ );
1759
1869
  }
1760
1870
  function mapKappagen(widget, ctx) {
1761
1871
  const v = widget.variables ?? {};
@@ -1766,10 +1876,58 @@ function mapKappagen(widget, ctx) {
1766
1876
  raid: v.eventsConfig.raid?.enabled ?? true,
1767
1877
  tip: v.eventsConfig.tip?.enabled ?? true
1768
1878
  } : null;
1769
- const events = v.events ?? eventsFromNested ?? { subscriber: true, follower: true, cheer: true, raid: true, tip: true };
1770
- const alertTriggers = Object.entries(events).filter(([, enabled]) => !!enabled).map(([seKey]) => kappagenKeyFor(seKey, ctx?.provider)).filter((x) => !!x);
1771
- const alertEmotes = ["Kappa", "<3", "imGlitch"];
1772
- return buildUnit(
1879
+ const events = v.events ?? eventsFromNested ?? {
1880
+ subscriber: true,
1881
+ follower: true,
1882
+ cheer: true,
1883
+ raid: true,
1884
+ tip: true
1885
+ };
1886
+ const alertTriggers = Array.from(
1887
+ new Set(
1888
+ Object.entries(events).filter(([, enabled]) => !!enabled).flatMap(([seKey]) => kappagenAlertTriggersFor(seKey, ctx?.provider))
1889
+ )
1890
+ );
1891
+ const pickEmoteUrl = (urls) => {
1892
+ if (!urls) return void 0;
1893
+ return urls["4"] ?? urls["3"] ?? urls["2"] ?? urls["1"] ?? Object.values(urls).find(Boolean);
1894
+ };
1895
+ const importedChosenEmotes = Array.isArray(v.chosenEmotes) ? v.chosenEmotes.map((emote) => {
1896
+ const name = typeof emote?.name === "string" ? emote.name.trim() : "";
1897
+ const src = pickEmoteUrl(emote?.urls);
1898
+ if (!name || !src) return null;
1899
+ return { name, src };
1900
+ }).filter((emote) => !!emote) : [];
1901
+ const importedEmoteUrls = importedChosenEmotes.reduce(
1902
+ (acc, emote) => {
1903
+ acc[emote.name] = emote.src;
1904
+ return acc;
1905
+ },
1906
+ {}
1907
+ );
1908
+ const importedCustomEmotes = Array.isArray(v.customEmotes) ? v.customEmotes.map((emote) => {
1909
+ const name = typeof emote?.name === "string" ? emote.name.trim() : "";
1910
+ const src = typeof emote?.src === "string" ? emote.src.trim() : "";
1911
+ if (!name || !src) return null;
1912
+ return {
1913
+ name,
1914
+ src,
1915
+ source: "custom",
1916
+ isRegex: !!emote.isRegex,
1917
+ animated: !!emote.isGif
1918
+ };
1919
+ }).filter(
1920
+ (emote) => !!emote
1921
+ ) : [];
1922
+ const importedAlertEmoteNames = [
1923
+ ...importedChosenEmotes.map((emote) => emote.name),
1924
+ ...importedCustomEmotes.map((emote) => emote.name)
1925
+ ];
1926
+ const alertEmotes = importedAlertEmoteNames.length > 0 ? importedAlertEmoteNames : ["Kappa", "<3", "imGlitch"];
1927
+ const chatEmoteMode = typeof v.mode === "string" ? v.mode.toLowerCase() : "off";
1928
+ const animationTime = typeof v.emotesplosionduration === "number" && v.emotesplosionduration > 0 ? v.emotesplosionduration : typeof v.duration === "number" && v.duration > 0 ? v.duration : 5;
1929
+ const alertNumberEmotes = typeof v.emotesplosion === "number" && v.emotesplosion > 0 ? v.emotesplosion : typeof v.limit === "number" && v.limit > 0 ? v.limit : 30;
1930
+ const unit = buildUnit(
1773
1931
  widget,
1774
1932
  "emotealert",
1775
1933
  {
@@ -1786,12 +1944,17 @@ function mapKappagen(widget, ctx) {
1786
1944
  background: "transparent",
1787
1945
  theme: "dark",
1788
1946
  imageSize: typeof v.size === "number" ? v.size : 30,
1789
- animationTime: typeof v.duration === "number" ? v.duration : 5,
1790
- duration: typeof v.duration === "number" ? v.duration : 5,
1947
+ animationTime,
1948
+ duration: animationTime,
1791
1949
  useAnimatedEmotes: true,
1792
- alertNumberEmotes: typeof v.limit === "number" && v.limit > 0 ? v.limit : 30,
1950
+ alertNumberEmotes,
1793
1951
  alertEmotes,
1952
+ customEmotes: importedCustomEmotes,
1953
+ importedEmoteUrls,
1794
1954
  alertTriggers,
1955
+ chatEmotesOn: chatEmoteMode !== "off",
1956
+ comboMinimum: typeof v.combominimum === "number" ? v.combominimum : 1,
1957
+ comboTime: typeof v.combotime === "number" ? v.combotime : 20,
1795
1958
  // Lumia BTTV/FFZ/7TV per-source toggles — read by Emotealert/index.tsx:48.
1796
1959
  emoteSources: {
1797
1960
  bttv: v.bttv !== false,
@@ -1815,6 +1978,15 @@ function mapKappagen(widget, ctx) {
1815
1978
  },
1816
1979
  ctx
1817
1980
  );
1981
+ if (v.fullscreen && ctx?.canvas) {
1982
+ unit.layer.bounds.x = 0;
1983
+ unit.layer.bounds.y = 0;
1984
+ unit.layer.bounds.width = ctx.canvas.width;
1985
+ unit.layer.bounds.height = ctx.canvas.height;
1986
+ delete unit.layer.bounds.autoWidth;
1987
+ delete unit.layer.bounds.autoHeight;
1988
+ }
1989
+ return unit;
1818
1990
  }
1819
1991
  var TOP_LIST_SORT_BY = {
1820
1992
  tips: "higherDonations",
@@ -1829,100 +2001,110 @@ var TOP_LIST_TITLE = {
1829
2001
  function mapViewerProfilesLeaderboard(widget, currency, ctx) {
1830
2002
  const v = widget.variables ?? {};
1831
2003
  const count = v.limit ?? v.visibleItems ?? 10;
1832
- return buildUnit(widget, "viewerprofiles", {
1833
- // Carry SE text styling so the leaderboard keeps the streamer's
1834
- // chosen font / size / color.
1835
- css: mapSeTextCssToModuleCss(widget, { fontSize: 18 }),
1836
- content: {
1837
- sortBy: TOP_LIST_SORT_BY[currency],
1838
- count,
1839
- title: TOP_LIST_TITLE[currency],
1840
- showTitle: true,
1841
- showAvatar: true,
1842
- showPlatform: true,
1843
- // Hide noisy columns that aren't relevant for a single-metric
1844
- // top-X list — the streamer picked "top by gifts/tips/cheers"
1845
- // for a reason. They can re-enable in Settings if they want.
1846
- showCommands: false,
1847
- showAlerts: false,
1848
- showBits: currency === "cheers",
1849
- showLastSeen: false,
1850
- showTopActivity: false,
1851
- showAchievements: false,
1852
- showAchievementProgress: false,
1853
- refreshIntervalSeconds: 30,
1854
- itemGap: 8,
1855
- autoScroll: false,
1856
- // Source-side knobs that don't map cleanly survive under the
1857
- // generic importMeta envelope so the user can re-introspect later.
1858
- // `period` is interesting because viewerprofiles doesn't yet have
1859
- // a windowed mode if/when it does, we can promote this.
1860
- importMeta: buildImportMeta(widget, {
1861
- period: v.period,
1862
- offset: v.offset,
1863
- layout: v.layout
1864
- })
1865
- }
1866
- }, ctx);
2004
+ return buildUnit(
2005
+ widget,
2006
+ "viewerprofiles",
2007
+ {
2008
+ // Carry SE text styling so the leaderboard keeps the streamer's
2009
+ // chosen font / size / color.
2010
+ css: mapSeTextCssToModuleCss(widget, { fontSize: 18 }),
2011
+ content: {
2012
+ sortBy: TOP_LIST_SORT_BY[currency],
2013
+ count,
2014
+ title: TOP_LIST_TITLE[currency],
2015
+ showTitle: true,
2016
+ showAvatar: true,
2017
+ showPlatform: true,
2018
+ // Hide noisy columns that aren't relevant for a single-metric
2019
+ // top-X list — the streamer picked "top by gifts/tips/cheers"
2020
+ // for a reason. They can re-enable in Settings if they want.
2021
+ showCommands: false,
2022
+ showAlerts: false,
2023
+ showBits: currency === "cheers",
2024
+ showLastSeen: false,
2025
+ showTopActivity: false,
2026
+ showAchievements: false,
2027
+ showAchievementProgress: false,
2028
+ refreshIntervalSeconds: 30,
2029
+ itemGap: 8,
2030
+ autoScroll: false,
2031
+ // Source-side knobs that don't map cleanly survive under the
2032
+ // generic importMeta envelope so the user can re-introspect later.
2033
+ // `period` is interesting because viewerprofiles doesn't yet have
2034
+ // a windowed mode — if/when it does, we can promote this.
2035
+ importMeta: buildImportMeta(widget, {
2036
+ period: v.period,
2037
+ offset: v.offset,
2038
+ layout: v.layout
2039
+ })
2040
+ }
2041
+ },
2042
+ ctx
2043
+ );
1867
2044
  }
1868
2045
  function mapGiveaway(widget, ctx) {
1869
2046
  const v = widget.variables ?? {};
1870
2047
  const primary = v.colors?.primary ?? "#393853";
1871
2048
  const accent = v.colors?.accent ?? "#FF4076";
1872
- return buildUnit(widget, "raffle", {
1873
- // Raffle renders entrants/winner text via module.css.
1874
- css: mapSeTextCssToModuleCss(widget),
1875
- content: {
1876
- version: 1,
1877
- type: "basic",
1878
- title: "Play to win",
1879
- subtitle: "",
1880
- wonTitle: "{{winner}} just won",
1881
- showTitle: true,
1882
- showSubTitle: false,
1883
- // SE `displayRunningTime` toggles the running-timer label. Lumia's equivalent
1884
- // is `showTimeSince`. SE `enableTimeOut` enables auto-end which Lumia honours
1885
- // via raffle.auto_end on the raffle state itself, not the module — best we can
1886
- // do is leave Lumia's `showTimeSince` on so the countdown is visible.
1887
- showTimeSince: v.displayRunningTime ?? true,
1888
- // SE `displayEntrants: false` means "never show the entrant list" (persistent).
1889
- // Lumia's `hideParticipantOnStart` is "hide until spin starts, then show" (transient).
1890
- // Mapping one to the other reverses semantics: SE-hidden entrants would briefly
1891
- // flash on screen the moment the spin starts. Since Lumia has no permanent
1892
- // hide-entrants knob, keep Lumia at its default (show) and surface the SE
1893
- // preference via provenance a future renderer can read it without us
1894
- // having lied about the toggle. Same for `displayTicketsPurchased`.
1895
- hideParticipantOnStart: false,
1896
- removeWhenRedeemed: false,
1897
- showWinningModal: true,
1898
- playSoundFx: true,
1899
- spinSound: "https://storage.lumiastream.com/overlays/lumia/audio/Wheel_of_Fortune.mp3",
1900
- winSound: "https://storage.lumiastream.com/overlays/lumia/audio/crowdClap.mp3",
1901
- spinDuration: 4,
1902
- transparent: false,
1903
- backgroundColor: primary,
1904
- borderColor: accent,
1905
- border: "solid 1px transparent",
1906
- borderRadius: "10px",
1907
- flashOnNewEntry: true,
1908
- flashColor: accent,
1909
- raffleEndedText: "The raffle has ended, and the selection of the winner will take place shortly.",
1910
- raffleTimerText: "Time since raffle started",
1911
- raffleCountdownText: "Raffle will end after",
1912
- // Source-side knobs with no Lumia equivalent preserved under
1913
- // the generic importMeta envelope. `displayEntrants` / `displayTicketsPurchased`
1914
- // are SE's persistent "never show" toggles; Lumia has no equivalent
1915
- // today (its `hideParticipantOnStart` is transient), so we preserve
1916
- // the preference for a future renderer.
1917
- importMeta: buildImportMeta(widget, {
1918
- timeout: v.timeout,
1919
- enableTimeout: v.enableTimeOut,
1920
- displayEntrants: v.displayEntrants,
1921
- displayTicketsPurchased: v.displayTicketsPurchased,
1922
- imageStyleWidth: v.imageStyle?.width
1923
- })
1924
- }
1925
- }, ctx);
2049
+ return buildUnit(
2050
+ widget,
2051
+ "raffle",
2052
+ {
2053
+ // Raffle renders entrants/winner text via module.css.
2054
+ css: mapSeTextCssToModuleCss(widget),
2055
+ content: {
2056
+ version: 1,
2057
+ type: "basic",
2058
+ title: "Play to win",
2059
+ subtitle: "",
2060
+ wonTitle: "{{winner}} just won",
2061
+ showTitle: true,
2062
+ showSubTitle: false,
2063
+ // SE `displayRunningTime` toggles the running-timer label. Lumia's equivalent
2064
+ // is `showTimeSince`. SE `enableTimeOut` enables auto-end which Lumia honours
2065
+ // via raffle.auto_end on the raffle state itself, not the module best we can
2066
+ // do is leave Lumia's `showTimeSince` on so the countdown is visible.
2067
+ showTimeSince: v.displayRunningTime ?? true,
2068
+ // SE `displayEntrants: false` means "never show the entrant list" (persistent).
2069
+ // Lumia's `hideParticipantOnStart` is "hide until spin starts, then show" (transient).
2070
+ // Mapping one to the other reverses semantics: SE-hidden entrants would briefly
2071
+ // flash on screen the moment the spin starts. Since Lumia has no permanent
2072
+ // hide-entrants knob, keep Lumia at its default (show) and surface the SE
2073
+ // preference via provenance — a future renderer can read it without us
2074
+ // having lied about the toggle. Same for `displayTicketsPurchased`.
2075
+ hideParticipantOnStart: false,
2076
+ removeWhenRedeemed: false,
2077
+ showWinningModal: true,
2078
+ playSoundFx: true,
2079
+ spinSound: "https://storage.lumiastream.com/overlays/lumia/audio/Wheel_of_Fortune.mp3",
2080
+ winSound: "https://storage.lumiastream.com/overlays/lumia/audio/crowdClap.mp3",
2081
+ spinDuration: 4,
2082
+ transparent: false,
2083
+ backgroundColor: primary,
2084
+ borderColor: accent,
2085
+ border: "solid 1px transparent",
2086
+ borderRadius: "10px",
2087
+ flashOnNewEntry: true,
2088
+ flashColor: accent,
2089
+ raffleEndedText: "The raffle has ended, and the selection of the winner will take place shortly.",
2090
+ raffleTimerText: "Time since raffle started",
2091
+ raffleCountdownText: "Raffle will end after",
2092
+ // Source-side knobs with no Lumia equivalent preserved under
2093
+ // the generic importMeta envelope. `displayEntrants` / `displayTicketsPurchased`
2094
+ // are SE's persistent "never show" toggles; Lumia has no equivalent
2095
+ // today (its `hideParticipantOnStart` is transient), so we preserve
2096
+ // the preference for a future renderer.
2097
+ importMeta: buildImportMeta(widget, {
2098
+ timeout: v.timeout,
2099
+ enableTimeout: v.enableTimeOut,
2100
+ displayEntrants: v.displayEntrants,
2101
+ displayTicketsPurchased: v.displayTicketsPurchased,
2102
+ imageStyleWidth: v.imageStyle?.width
2103
+ })
2104
+ }
2105
+ },
2106
+ ctx
2107
+ );
1926
2108
  }
1927
2109
  var HYPETRAIN_LISTENER_KEYS = /* @__PURE__ */ new Set([
1928
2110
  "hypetrain-latest",
@@ -1955,52 +2137,57 @@ function isHypetrainCustomWidget(widget) {
1955
2137
  }
1956
2138
  function mapHypetrain(widget, ctx) {
1957
2139
  const v = widget.variables ?? {};
1958
- return buildUnit(widget, "hypetrain", {
1959
- // Hypetrain renders level / progress / contributor text — carry SE
1960
- // styling so an imported hype-train widget keeps its font + accent.
1961
- css: mapSeTextCssToModuleCss(widget),
1962
- content: {
1963
- // Twitch-native is the right default — SE hype-train widgets always
1964
- // reflected the Twitch native train. Users can flip to 'synthetic'
1965
- // or 'hybrid' from Settings if they want cross-platform pooling.
1966
- sourceMode: "twitch-native",
1967
- sources: null,
1968
- // default money-only whitelist; ignored in twitch-native anyway
1969
- customValues: {},
1970
- pretrainThreshold: 5,
1971
- pretrainDurationMs: 5 * 60 * 1e3,
1972
- levelDurationMs: 5 * 60 * 1e3,
1973
- levelStep: 5,
1974
- levelThresholds: [],
1975
- // Pick a moderately energetic default theme — SE hype-train widgets
1976
- // were never subtle. Users can switch in Settings.
1977
- theme: "train",
1978
- primaryColor: typeof v.primaryColor === "string" ? v.primaryColor : "#ff6b00",
1979
- accentColor: typeof v.accentColor === "string" ? v.accentColor : "#ffd84d",
1980
- textColor: typeof v.textColor === "string" ? v.textColor : "#ffffff",
1981
- showLevel: true,
1982
- showTotal: true,
1983
- showTimer: true,
1984
- showTopContributor: true,
1985
- showLeaderboard: true,
1986
- leaderboardLimit: 5,
1987
- leaderboardDurationMs: 30 * 1e3,
1988
- levelUpSoundUrl: "https://storage.lumiastream.com/overlays/lumia/audio/crowdClap.mp3",
1989
- trainEndSoundUrl: "https://storage.lumiastream.com/overlays/lumia/audio/crowdClap.mp3",
1990
- audioVolume: 0.5,
1991
- // Match native defaults so SE-imported trains get the same polished
1992
- // sprite pack the native module ships with.
1993
- trainLocoImageUrl: "https://storage.lumiastream.com/overlays/global/hypetrain/train_loco_default.png",
1994
- rocketImageUrl: "https://storage.lumiastream.com/overlays/global/hypetrain/rocket_default.png",
1995
- infernoImageUrl: "https://storage.lumiastream.com/overlays/global/hypetrain/inferno_default.png",
1996
- // Source config preserved verbatim under the generic importMeta
1997
- // envelope — no Lumia equivalent for most of these knobs
1998
- // (per-widget animation tunables, sound URLs the streamer might
1999
- // want to migrate manually, etc.). Users can inspect via the
2000
- // overlay JSON if they want to recreate a specific look.
2001
- importMeta: buildImportMeta(widget, { variables: v })
2002
- }
2003
- }, ctx);
2140
+ return buildUnit(
2141
+ widget,
2142
+ "hypetrain",
2143
+ {
2144
+ // Hypetrain renders level / progress / contributor text — carry SE
2145
+ // styling so an imported hype-train widget keeps its font + accent.
2146
+ css: mapSeTextCssToModuleCss(widget),
2147
+ content: {
2148
+ // Twitch-native is the right default — SE hype-train widgets always
2149
+ // reflected the Twitch native train. Users can flip to 'synthetic'
2150
+ // or 'hybrid' from Settings if they want cross-platform pooling.
2151
+ sourceMode: "twitch-native",
2152
+ sources: null,
2153
+ // default money-only whitelist; ignored in twitch-native anyway
2154
+ customValues: {},
2155
+ pretrainThreshold: 5,
2156
+ pretrainDurationMs: 5 * 60 * 1e3,
2157
+ levelDurationMs: 5 * 60 * 1e3,
2158
+ levelStep: 5,
2159
+ levelThresholds: [],
2160
+ // Pick a moderately energetic default theme SE hype-train widgets
2161
+ // were never subtle. Users can switch in Settings.
2162
+ theme: "train",
2163
+ primaryColor: typeof v.primaryColor === "string" ? v.primaryColor : "#ff6b00",
2164
+ accentColor: typeof v.accentColor === "string" ? v.accentColor : "#ffd84d",
2165
+ textColor: typeof v.textColor === "string" ? v.textColor : "#ffffff",
2166
+ showLevel: true,
2167
+ showTotal: true,
2168
+ showTimer: true,
2169
+ showTopContributor: true,
2170
+ showLeaderboard: true,
2171
+ leaderboardLimit: 5,
2172
+ leaderboardDurationMs: 30 * 1e3,
2173
+ levelUpSoundUrl: "https://storage.lumiastream.com/overlays/lumia/audio/crowdClap.mp3",
2174
+ trainEndSoundUrl: "https://storage.lumiastream.com/overlays/lumia/audio/crowdClap.mp3",
2175
+ audioVolume: 0.5,
2176
+ // Match native defaults so SE-imported trains get the same polished
2177
+ // sprite pack the native module ships with.
2178
+ trainLocoImageUrl: "https://storage.lumiastream.com/overlays/global/hypetrain/train_loco_default.png",
2179
+ rocketImageUrl: "https://storage.lumiastream.com/overlays/global/hypetrain/rocket_default.png",
2180
+ infernoImageUrl: "https://storage.lumiastream.com/overlays/global/hypetrain/inferno_default.png",
2181
+ // Source config preserved verbatim under the generic importMeta
2182
+ // envelope no Lumia equivalent for most of these knobs
2183
+ // (per-widget animation tunables, sound URLs the streamer might
2184
+ // want to migrate manually, etc.). Users can inspect via the
2185
+ // overlay JSON if they want to recreate a specific look.
2186
+ importMeta: buildImportMeta(widget, { variables: v })
2187
+ }
2188
+ },
2189
+ ctx
2190
+ );
2004
2191
  }
2005
2192
  function mapEmoteWall(widget, ctx) {
2006
2193
  const v = widget.variables ?? {};
@@ -2013,51 +2200,61 @@ function mapEmoteWall(widget, ctx) {
2013
2200
  if (v.enableFfz !== false && v.ffz !== false) sites.push("ffz");
2014
2201
  if (v.enable7tv !== false && v.sevenTv !== false) sites.push("seventv");
2015
2202
  const animationType = v.direction === "up" ? "rise" : v.direction === "down" ? "fall" : v.direction === "left" || v.direction === "right" ? "slide" : "random";
2016
- return buildUnit(widget, "emotebox", {
2017
- content: {
2018
- version: 1,
2019
- type: animationType,
2020
- imageSize,
2021
- animationTime: duration,
2022
- duration,
2023
- maxItemsToShow,
2024
- delay: typeof v.delay === "number" ? v.delay : 0,
2025
- sites,
2026
- background: "transparent",
2027
- theme: "dark",
2028
- userLevels: [1, 2, 3, 4, 5, 6, 7, 8, 9],
2029
- ignoredList: [],
2030
- excludedAnimations: [],
2031
- // Emote-wall is chat-driven, not alert-driven leave alert toggles
2032
- // off so the widget doesn't double-fire on subscribe/follower events.
2033
- alertOn: false,
2034
- alertTriggers: [],
2035
- alertEmotes: [],
2036
- alertNumberEmotes: 30,
2037
- useAnimatedEmotes: true,
2038
- importMeta: buildImportMeta(widget, {
2039
- fullscreen: !!v.fullscreen,
2040
- direction: v.direction
2041
- })
2203
+ return buildUnit(
2204
+ widget,
2205
+ "emotebox",
2206
+ {
2207
+ content: {
2208
+ version: 1,
2209
+ type: animationType,
2210
+ imageSize,
2211
+ animationTime: duration,
2212
+ duration,
2213
+ maxItemsToShow,
2214
+ delay: typeof v.delay === "number" ? v.delay : 0,
2215
+ sites,
2216
+ background: "transparent",
2217
+ theme: "dark",
2218
+ userLevels: [1, 2, 3, 4, 5, 6, 7, 8, 9],
2219
+ ignoredList: [],
2220
+ excludedAnimations: [],
2221
+ // Emote-wall is chat-driven, not alert-driven — leave alert toggles
2222
+ // off so the widget doesn't double-fire on subscribe/follower events.
2223
+ alertOn: false,
2224
+ alertTriggers: [],
2225
+ alertEmotes: [],
2226
+ alertNumberEmotes: 30,
2227
+ useAnimatedEmotes: true,
2228
+ importMeta: buildImportMeta(widget, {
2229
+ fullscreen: !!v.fullscreen,
2230
+ direction: v.direction
2231
+ })
2232
+ },
2233
+ css: { background: "transparent" }
2042
2234
  },
2043
- css: { background: "transparent" }
2044
- }, ctx);
2235
+ ctx
2236
+ );
2045
2237
  }
2046
2238
  function mapUnsupportedAsText(widget, ctx) {
2047
- return buildUnit(widget, "text", {
2048
- content: {
2049
- value: `[Unsupported SE widget: ${widget.type}]`,
2050
- highlightColor: "inherit"
2239
+ return buildUnit(
2240
+ widget,
2241
+ "text",
2242
+ {
2243
+ content: {
2244
+ value: `[Unsupported SE widget: ${widget.type}]`,
2245
+ highlightColor: "inherit"
2246
+ },
2247
+ css: {
2248
+ color: "#ff8c00",
2249
+ fontSize: 18,
2250
+ textAlign: "center",
2251
+ fontFamily: "Roboto",
2252
+ fontWeight: "bold",
2253
+ background: "rgba(0,0,0,0.4)"
2254
+ }
2051
2255
  },
2052
- css: {
2053
- color: "#ff8c00",
2054
- fontSize: 18,
2055
- textAlign: "center",
2056
- fontFamily: "Roboto",
2057
- fontWeight: "bold",
2058
- background: "rgba(0,0,0,0.4)"
2059
- }
2060
- }, ctx);
2256
+ ctx
2257
+ );
2061
2258
  }
2062
2259
 
2063
2260
  // src/se-import/mappers/hypecup.ts
@@ -5120,6 +5317,65 @@ function getJwtInstructionSteps({ pasteHint = "above" } = {}) {
5120
5317
 
5121
5318
  // src/se-import/ui/SEImportWizard.tsx
5122
5319
  import { Fragment as Fragment5, jsx as jsx14, jsxs as jsxs7 } from "react/jsx-runtime";
5320
+ function buildOverlaySaveBody(result, name, preview) {
5321
+ return {
5322
+ name: name.trim() || result.overlay.name,
5323
+ description: result.overlay.description,
5324
+ ...preview ? { image: preview, images: [preview] } : {},
5325
+ settings: result.overlay.settings
5326
+ };
5327
+ }
5328
+ function mergeCoverage(results) {
5329
+ const byKey = /* @__PURE__ */ new Map();
5330
+ const notes = /* @__PURE__ */ new Set();
5331
+ let totalWidgets = 0;
5332
+ for (const result of results) {
5333
+ totalWidgets += result.coverage.totalWidgets;
5334
+ for (const note of result.coverage.notes) notes.add(note);
5335
+ for (const mapping of result.coverage.mappings) {
5336
+ const key = `${mapping.seType}::${mapping.lumiaType}::${mapping.status}`;
5337
+ const existing = byKey.get(key);
5338
+ if (existing) {
5339
+ existing.count += mapping.count;
5340
+ } else {
5341
+ byKey.set(key, { ...mapping });
5342
+ }
5343
+ }
5344
+ }
5345
+ return {
5346
+ totalWidgets,
5347
+ mappings: Array.from(byKey.values()),
5348
+ notes: Array.from(notes)
5349
+ };
5350
+ }
5351
+ function combineImportResults(results) {
5352
+ if (results.length === 1) return results[0].result;
5353
+ const first = results[0].result.overlay;
5354
+ const importedResults = results.map((item) => item.result);
5355
+ const layers = importedResults.flatMap(
5356
+ (result) => result.overlay.settings.layers ?? []
5357
+ );
5358
+ const modules = importedResults.reduce(
5359
+ (acc, result) => ({ ...acc, ...result.overlay.settings.modules ?? {} }),
5360
+ {}
5361
+ );
5362
+ const names = results.map((item) => item.summary?.name ?? item.result.overlay.name).filter(Boolean);
5363
+ return {
5364
+ overlay: {
5365
+ ...first,
5366
+ name: `[SE] ${results.length} StreamElements overlays`,
5367
+ description: `Imported from ${results.length} StreamElements overlays${names.length ? `: ${names.join(", ")}` : ""}`,
5368
+ settings: {
5369
+ ...first.settings,
5370
+ layers,
5371
+ modules
5372
+ },
5373
+ layers_count: layers.length
5374
+ },
5375
+ coverage: mergeCoverage(importedResults),
5376
+ reviewItems: importedResults.flatMap((result) => result.reviewItems)
5377
+ };
5378
+ }
5123
5379
  var CORE_STEPS = [
5124
5380
  { key: "connect", label: "Connect" },
5125
5381
  { key: "pick", label: "Overlays" },
@@ -5251,7 +5507,9 @@ function OverlayBatchProgress({
5251
5507
  queuedCount
5252
5508
  }) {
5253
5509
  const selectedOverlays = useMemo5(() => {
5254
- const byId = new Map((overlays ?? []).map((overlay) => [overlay._id, overlay]));
5510
+ const byId = new Map(
5511
+ (overlays ?? []).map((overlay) => [overlay._id, overlay])
5512
+ );
5255
5513
  return Array.from(selectedIds).map((id) => byId.get(id)).filter(Boolean);
5256
5514
  }, [overlays, selectedIds]);
5257
5515
  if (!active || selectedOverlays.length <= 1) return null;
@@ -5260,7 +5518,10 @@ function OverlayBatchProgress({
5260
5518
  const queued = selectedOverlays.slice(currentIndex);
5261
5519
  const visibleCompleted = completed.slice(-3);
5262
5520
  const visibleQueued = queued.slice(0, 3);
5263
- const hiddenCompleted = Math.max(0, completed.length - visibleCompleted.length);
5521
+ const hiddenCompleted = Math.max(
5522
+ 0,
5523
+ completed.length - visibleCompleted.length
5524
+ );
5264
5525
  const hiddenQueued = Math.max(0, queued.length - visibleQueued.length);
5265
5526
  const renderThumb = (overlay, state) => /* @__PURE__ */ jsxs7(
5266
5527
  "div",
@@ -5274,29 +5535,36 @@ function OverlayBatchProgress({
5274
5535
  },
5275
5536
  overlay._id
5276
5537
  );
5277
- return /* @__PURE__ */ jsxs7("div", { className: "se-import-overlay-progress", "aria-label": "Overlay import progress", children: [
5278
- /* @__PURE__ */ jsxs7("div", { className: "se-import-overlay-progress__copy", children: [
5279
- "Importing overlay ",
5280
- currentIndex,
5281
- " of ",
5282
- total,
5283
- currentName ? ` \xB7 ${currentName}` : "",
5284
- queuedCount > 0 ? ` \xB7 ${queuedCount} queued` : ""
5285
- ] }),
5286
- /* @__PURE__ */ jsxs7("div", { className: "se-import-overlay-progress__strip", children: [
5287
- hiddenCompleted > 0 && /* @__PURE__ */ jsxs7("span", { className: "se-import-overlay-progress__more", children: [
5288
- "+",
5289
- hiddenCompleted
5290
- ] }),
5291
- /* @__PURE__ */ jsx14("div", { className: "se-import-overlay-progress__stack se-import-overlay-progress__stack--done", children: visibleCompleted.map((overlay) => renderThumb(overlay, "done")) }),
5292
- current && renderThumb(current, "current"),
5293
- /* @__PURE__ */ jsx14("div", { className: "se-import-overlay-progress__stack se-import-overlay-progress__stack--queued", children: visibleQueued.map((overlay) => renderThumb(overlay, "queued")) }),
5294
- hiddenQueued > 0 && /* @__PURE__ */ jsxs7("span", { className: "se-import-overlay-progress__more", children: [
5295
- "+",
5296
- hiddenQueued
5297
- ] })
5298
- ] })
5299
- ] });
5538
+ return /* @__PURE__ */ jsxs7(
5539
+ "div",
5540
+ {
5541
+ className: "se-import-overlay-progress",
5542
+ "aria-label": "Overlay import progress",
5543
+ children: [
5544
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-overlay-progress__copy", children: [
5545
+ "Importing overlay ",
5546
+ currentIndex,
5547
+ " of ",
5548
+ total,
5549
+ currentName ? ` \xB7 ${currentName}` : "",
5550
+ queuedCount > 0 ? ` \xB7 ${queuedCount} queued` : ""
5551
+ ] }),
5552
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-overlay-progress__strip", children: [
5553
+ hiddenCompleted > 0 && /* @__PURE__ */ jsxs7("span", { className: "se-import-overlay-progress__more", children: [
5554
+ "+",
5555
+ hiddenCompleted
5556
+ ] }),
5557
+ /* @__PURE__ */ jsx14("div", { className: "se-import-overlay-progress__stack se-import-overlay-progress__stack--done", children: visibleCompleted.map((overlay) => renderThumb(overlay, "done")) }),
5558
+ current && renderThumb(current, "current"),
5559
+ /* @__PURE__ */ jsx14("div", { className: "se-import-overlay-progress__stack se-import-overlay-progress__stack--queued", children: visibleQueued.map((overlay) => renderThumb(overlay, "queued")) }),
5560
+ hiddenQueued > 0 && /* @__PURE__ */ jsxs7("span", { className: "se-import-overlay-progress__more", children: [
5561
+ "+",
5562
+ hiddenQueued
5563
+ ] })
5564
+ ] })
5565
+ ]
5566
+ }
5567
+ );
5300
5568
  }
5301
5569
  function CustomOverlayPreview({
5302
5570
  html,
@@ -5651,7 +5919,7 @@ function StepPickOverlay({
5651
5919
  {
5652
5920
  number: 2,
5653
5921
  title: "Pick the overlays to import",
5654
- subtitle: `Select one or more StreamElements overlays from your account. We'll run each one through the discovery \u2192 review \u2192 import pipeline in sequence. ${total > 0 ? `${total} overlay${total === 1 ? "" : "s"} on this channel.` : ""}`
5922
+ subtitle: `Select one or more StreamElements overlays from your account. One overlay opens the full review flow; multiple overlays are combined and reviewed once. ${total > 0 ? `${total} overlay${total === 1 ? "" : "s"} on this channel.` : ""}`
5655
5923
  }
5656
5924
  ),
5657
5925
  loading && /* @__PURE__ */ jsxs7("div", { className: "se-import-help se-import-help--info", children: [
@@ -5722,7 +5990,7 @@ function StepPickOverlay({
5722
5990
  ]
5723
5991
  }
5724
5992
  ),
5725
- /* @__PURE__ */ jsx14("span", { style: { fontSize: 13, color: "var(--se-muted)" }, children: hasAnySelected ? `${selectedCount} overlay${selectedCount === 1 ? "" : "s"} queued. Each runs through the full import flow in sequence.` : "Click overlays below to select them." })
5993
+ /* @__PURE__ */ jsx14("span", { style: { fontSize: 13, color: "var(--se-muted)" }, children: hasAnySelected ? selectedCount > 1 ? `${selectedCount} overlays selected. They will be combined and reviewed once.` : "1 overlay selected. It will open the full review flow." : "Click overlays below to select them." })
5726
5994
  ]
5727
5995
  }
5728
5996
  ),
@@ -5758,7 +6026,12 @@ function StepPickOverlay({
5758
6026
  checked: selected,
5759
6027
  onChange: () => toggleId(o._id),
5760
6028
  size: "small",
5761
- style: { position: "absolute", top: 6, right: 6, zIndex: 1 }
6029
+ style: {
6030
+ position: "absolute",
6031
+ top: 6,
6032
+ right: 6,
6033
+ zIndex: 1
6034
+ }
5762
6035
  }
5763
6036
  ),
5764
6037
  o.preview ? /* @__PURE__ */ jsx14(
@@ -5902,21 +6175,37 @@ function StepURL({
5902
6175
  }
5903
6176
  function StepDiscovery({
5904
6177
  result,
5905
- assetCount
6178
+ assetCount,
6179
+ overlayCount = 1
5906
6180
  }) {
5907
6181
  const widgets = result.coverage.totalWidgets;
5908
6182
  const reviewCount = result.reviewItems.length;
6183
+ const layerCount = result.overlay.settings.layers?.length ?? 0;
5909
6184
  const customCount = result.coverage.mappings.find(
5910
6185
  (item) => item.seType === "se-widget-custom-event-list"
5911
6186
  )?.count ?? 0;
5912
6187
  const alertCount = result.coverage.mappings.filter((item) => item.lumiaType === "alert").reduce((sum, item) => sum + item.count, 0);
5913
6188
  const topCards = [
6189
+ {
6190
+ key: "overlays",
6191
+ label: "Overlays selected",
6192
+ value: overlayCount,
6193
+ icon: source_se_default,
6194
+ description: overlayCount > 1 ? "Selected StreamElements overlays combined into this import." : "Selected StreamElements overlay."
6195
+ },
6196
+ {
6197
+ key: "layers",
6198
+ label: "Layers imported",
6199
+ value: layerCount,
6200
+ icon: source_lumia_default,
6201
+ description: "Lumia layers prepared from the selected overlay content."
6202
+ },
5914
6203
  {
5915
6204
  key: "widgets",
5916
6205
  label: "Widgets found",
5917
6206
  value: widgets,
5918
6207
  icon: source_se_default,
5919
- description: "StreamElements layers detected in this overlay."
6208
+ description: overlayCount > 1 ? "StreamElements widgets detected across the selected overlays." : "StreamElements widgets detected in this overlay."
5920
6209
  },
5921
6210
  {
5922
6211
  key: "alerts",
@@ -5941,7 +6230,13 @@ function StepDiscovery({
5941
6230
  }
5942
6231
  ].filter((card) => card.value > 0);
5943
6232
  return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--wide"), children: [
5944
- /* @__PURE__ */ jsx14(StepHeader, { number: 2, title: "Here's what we found in your overlay" }),
6233
+ /* @__PURE__ */ jsx14(
6234
+ StepHeader,
6235
+ {
6236
+ number: 2,
6237
+ title: overlayCount > 1 ? "Here's what we found in your overlays" : "Here's what we found in your overlay"
6238
+ }
6239
+ ),
5945
6240
  topCards.length > 0 && /* @__PURE__ */ jsx14("div", { className: "se-import-grid se-import-grid--stats", children: topCards.map((card) => /* @__PURE__ */ jsx14(
5946
6241
  StatusCard,
5947
6242
  {
@@ -5968,7 +6263,8 @@ function StepDiscovery({
5968
6263
  function StepOptions({
5969
6264
  options,
5970
6265
  setOptions,
5971
- assetCount
6266
+ assetCount,
6267
+ hideName = false
5972
6268
  }) {
5973
6269
  return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--medium"), children: [
5974
6270
  /* @__PURE__ */ jsx14(
@@ -6006,7 +6302,7 @@ function StepOptions({
6006
6302
  ]
6007
6303
  }
6008
6304
  ),
6009
- /* @__PURE__ */ jsx14(
6305
+ !hideName && /* @__PURE__ */ jsx14(
6010
6306
  LSInput,
6011
6307
  {
6012
6308
  label: "Overlay name",
@@ -6320,9 +6616,11 @@ function StepConfirm({
6320
6616
  result,
6321
6617
  options,
6322
6618
  mirrorRows,
6323
- rowStates
6619
+ rowStates,
6620
+ overlayCount = 1
6324
6621
  }) {
6325
6622
  const widgets = result.coverage.totalWidgets;
6623
+ const layerCount = result.overlay.settings.layers?.length ?? 0;
6326
6624
  const alertCount = result.coverage.mappings.filter((item) => item.lumiaType === "alert").reduce((sum, item) => sum + item.count, 0);
6327
6625
  const mirrored = mirrorRows.filter(
6328
6626
  (row) => row.state === "done" || row.state === "reused" || row.state === "kept-on-cdn"
@@ -6345,7 +6643,15 @@ function StepConfirm({
6345
6643
  ),
6346
6644
  /* @__PURE__ */ jsxs7("div", { className: "se-import-overview", children: [
6347
6645
  /* @__PURE__ */ jsx14("h3", { children: "Overview" }),
6348
- /* @__PURE__ */ jsx14(Row, { label: "Overlay", value: options.name || result.overlay.name }),
6646
+ /* @__PURE__ */ jsx14(
6647
+ Row,
6648
+ {
6649
+ label: overlayCount > 1 ? "Combined overlay" : "Overlay",
6650
+ value: options.name || result.overlay.name
6651
+ }
6652
+ ),
6653
+ overlayCount > 1 ? /* @__PURE__ */ jsx14(Row, { label: "Source overlays", value: `${overlayCount}` }) : null,
6654
+ /* @__PURE__ */ jsx14(Row, { label: "Layers imported", value: `${layerCount}` }),
6349
6655
  /* @__PURE__ */ jsx14(Row, { label: "Widgets", value: `${widgets}` }),
6350
6656
  /* @__PURE__ */ jsx14(Row, { label: "Alerts", value: `${alertCount}` }),
6351
6657
  /* @__PURE__ */ jsx14(
@@ -6383,7 +6689,9 @@ function Row({
6383
6689
  function SEImportWizard({
6384
6690
  bindings,
6385
6691
  initialUrl = "",
6386
- initialJwt = ""
6692
+ initialJwt = "",
6693
+ initialOverlayIds,
6694
+ initialOverlayList
6387
6695
  }) {
6388
6696
  const {
6389
6697
  fetchMarketplaceOverlay,
@@ -6412,7 +6720,7 @@ function SEImportWizard({
6412
6720
  const [connectError, setConnectError] = useState6(null);
6413
6721
  const [connecting, setConnecting] = useState6(false);
6414
6722
  const [overlayList, setOverlayList] = useState6(
6415
- null
6723
+ initialOverlayList?.length ? initialOverlayList : null
6416
6724
  );
6417
6725
  const [overlaysLoading, setOverlaysLoading] = useState6(false);
6418
6726
  const [overlaysError, setOverlaysError] = useState6(null);
@@ -6422,6 +6730,8 @@ function SEImportWizard({
6422
6730
  const [overlayQueue, setOverlayQueue] = useState6([]);
6423
6731
  const [currentOverlayIndex, setCurrentOverlayIndex] = useState6(0);
6424
6732
  const [totalOverlaysToImport, setTotalOverlaysToImport] = useState6(0);
6733
+ const [activeOverlaySummaries, setActiveOverlaySummaries] = useState6([]);
6734
+ const [activeOverlayPreview, setActiveOverlayPreview] = useState6();
6425
6735
  const [url, setUrl] = useState6(initialUrl);
6426
6736
  const [loadError, setLoadError] = useState6(null);
6427
6737
  const [loading, setLoading] = useState6(false);
@@ -6441,6 +6751,7 @@ function SEImportWizard({
6441
6751
  const [marketplacePickerOpen, setMarketplacePickerOpen] = useState6(false);
6442
6752
  const mirrorStartedRef = useRef4(false);
6443
6753
  const mirrorAbortRef = useRef4(null);
6754
+ const initialOverlayImportStartedRef = useRef4(false);
6444
6755
  const filenameCacheRef = useRef4(/* @__PURE__ */ new Map());
6445
6756
  const [assetUrls, setAssetUrls] = useState6([]);
6446
6757
  const currentItem = originalReviewItems[reviewIndex];
@@ -6490,7 +6801,8 @@ function SEImportWizard({
6490
6801
  }
6491
6802
  }, [bindings, initialJwt, jwt]);
6492
6803
  useEffect6(() => {
6493
- if (initialJwt.trim() && step === "connect" && !seClient && !connecting && !connectError) void handleConnect();
6804
+ if (initialJwt.trim() && step === "connect" && !seClient && !connecting && !connectError)
6805
+ void handleConnect();
6494
6806
  }, [connectError, connecting, handleConnect, initialJwt, seClient, step]);
6495
6807
  const loadOverlayList = useCallback3(
6496
6808
  async (client) => {
@@ -6531,8 +6843,58 @@ function SEImportWizard({
6531
6843
  const clearAllOverlays = useCallback3(() => {
6532
6844
  setSelectedOverlayIds(/* @__PURE__ */ new Set());
6533
6845
  }, []);
6846
+ const loadSelectedOverlaysAndKickoff = useCallback3(
6847
+ async (client, ids) => {
6848
+ setLoadError(null);
6849
+ setLoading(true);
6850
+ try {
6851
+ const importedOverlays = [];
6852
+ for (let index = 0; index < ids.length; index += 1) {
6853
+ const overlayId = ids[index];
6854
+ const summary = (overlayList ?? []).find((o) => o._id === overlayId);
6855
+ const bootstrap = await fetchSEBootstrapForOverlay(client, overlayId);
6856
+ if (!isSEBootstrap(bootstrap)) {
6857
+ throw new Error(
6858
+ `StreamElements returned an invalid bootstrap for ${summary?.name ?? overlayId}.`
6859
+ );
6860
+ }
6861
+ const imported = importSEBootstrap(bootstrap);
6862
+ importedOverlays.push({ result: imported, summary });
6863
+ }
6864
+ const combined = combineImportResults(importedOverlays);
6865
+ setResult(combined);
6866
+ setAssetUrls(findSEAssetURLs(combined.overlay));
6867
+ setOptions((o) => ({ ...o, name: combined.overlay.name }));
6868
+ setOriginalReviewItems(combined.reviewItems);
6869
+ setRowStates(
6870
+ Object.fromEntries(
6871
+ combined.reviewItems.map((item) => [
6872
+ item.moduleId,
6873
+ { state: "pending" }
6874
+ ])
6875
+ )
6876
+ );
6877
+ setReviewIndex(0);
6878
+ setMirrorRows([]);
6879
+ mirrorStartedRef.current = false;
6880
+ const summaries = importedOverlays.map((item) => item.summary).filter(Boolean);
6881
+ setActiveOverlaySummaries(summaries);
6882
+ setActiveOverlayPreview(summaries[0]?.preview);
6883
+ setStep("discovery");
6884
+ } catch (e) {
6885
+ const detail = e?.message ?? String(e);
6886
+ setLoadError({
6887
+ message: detail || "Could not load the selected overlays.",
6888
+ hint: detail ? "If this looks transient, retry in a moment. Otherwise check the token and try again." : void 0
6889
+ });
6890
+ } finally {
6891
+ setLoading(false);
6892
+ }
6893
+ },
6894
+ [overlayList]
6895
+ );
6534
6896
  const loadBootstrapAndKickoff = useCallback3(
6535
- async (client, overlayId) => {
6897
+ async (client, overlayId, summary) => {
6536
6898
  setLoadError(null);
6537
6899
  setLoading(true);
6538
6900
  try {
@@ -6558,6 +6920,9 @@ function SEImportWizard({
6558
6920
  setReviewIndex(0);
6559
6921
  setMirrorRows([]);
6560
6922
  mirrorStartedRef.current = false;
6923
+ const summaries = summary ? [summary] : [];
6924
+ setActiveOverlaySummaries(summaries);
6925
+ setActiveOverlayPreview(summary?.preview ?? bootstrap.overlay.preview);
6561
6926
  setStep("discovery");
6562
6927
  } catch (e) {
6563
6928
  const detail = e?.message ?? String(e);
@@ -6575,12 +6940,55 @@ function SEImportWizard({
6575
6940
  if (!seClient) return;
6576
6941
  const ids = Array.from(selectedOverlayIds);
6577
6942
  if (ids.length === 0) return;
6943
+ if (ids.length > 1) {
6944
+ setOverlayQueue([]);
6945
+ setCurrentOverlayIndex(0);
6946
+ setTotalOverlaysToImport(0);
6947
+ await loadSelectedOverlaysAndKickoff(seClient, ids);
6948
+ return;
6949
+ }
6578
6950
  const [first, ...rest] = ids;
6951
+ const summary = (overlayList ?? []).find((o) => o._id === first);
6952
+ const summaries = summary ? [summary] : [];
6953
+ setActiveOverlaySummaries(summaries);
6579
6954
  setOverlayQueue(rest);
6580
6955
  setTotalOverlaysToImport(ids.length);
6581
6956
  setCurrentOverlayIndex(1);
6582
- await loadBootstrapAndKickoff(seClient, first);
6583
- }, [seClient, selectedOverlayIds, loadBootstrapAndKickoff]);
6957
+ await loadBootstrapAndKickoff(seClient, first, summary);
6958
+ }, [
6959
+ seClient,
6960
+ selectedOverlayIds,
6961
+ overlayList,
6962
+ loadSelectedOverlaysAndKickoff,
6963
+ loadBootstrapAndKickoff
6964
+ ]);
6965
+ useEffect6(() => {
6966
+ if (initialOverlayImportStartedRef.current || !seClient || !initialOverlayIds?.length || !overlayList || overlaysLoading || loading)
6967
+ return;
6968
+ const availableIds = new Set(overlayList.map((overlay) => overlay._id));
6969
+ const ids = initialOverlayIds.filter((id) => availableIds.has(id));
6970
+ if (ids.length === 0) return;
6971
+ initialOverlayImportStartedRef.current = true;
6972
+ setSelectedOverlayIds(new Set(ids));
6973
+ setOverlayQueue([]);
6974
+ setCurrentOverlayIndex(ids.length === 1 ? 1 : 0);
6975
+ setTotalOverlaysToImport(ids.length === 1 ? 1 : 0);
6976
+ if (ids.length > 1) {
6977
+ void loadSelectedOverlaysAndKickoff(seClient, ids);
6978
+ return;
6979
+ }
6980
+ const first = ids[0];
6981
+ const summary = overlayList.find((overlay) => overlay._id === first);
6982
+ void loadBootstrapAndKickoff(seClient, first, summary);
6983
+ }, [
6984
+ initialOverlayIds,
6985
+ loadBootstrapAndKickoff,
6986
+ loadSelectedOverlaysAndKickoff,
6987
+ loading,
6988
+ overlayList,
6989
+ overlaysLoading,
6990
+ seClient
6991
+ ]);
6584
6992
  const handleLoad = useCallback3(async () => {
6585
6993
  setLoadError(null);
6586
6994
  const parts = extractSEPreviewParts(url);
@@ -6616,6 +7024,8 @@ function SEImportWizard({
6616
7024
  )
6617
7025
  );
6618
7026
  setReviewIndex(0);
7027
+ setActiveOverlaySummaries([]);
7028
+ setActiveOverlayPreview(bootstrap.overlay.preview);
6619
7029
  setStep("discovery");
6620
7030
  } catch (error) {
6621
7031
  const detail = error?.message ?? String(error);
@@ -6658,6 +7068,25 @@ function SEImportWizard({
6658
7068
  },
6659
7069
  [uploadAsset]
6660
7070
  );
7071
+ const resolveOverlayThumbnailUrl = useCallback3(
7072
+ async (preview) => {
7073
+ if (!preview) return void 0;
7074
+ const filename = filenameFromURL(preview);
7075
+ const existing = existingAssets.find(
7076
+ (asset) => asset.file_name === filename || asset.name === filename
7077
+ );
7078
+ if (existing?.url) return existing.url;
7079
+ try {
7080
+ return await mirrorOneAsset(preview, (file) => uploadAsset(file));
7081
+ } catch (error) {
7082
+ notify.warning(
7083
+ `Could not import the StreamElements thumbnail: ${extractErrorMessage(error)}`
7084
+ );
7085
+ return preview;
7086
+ }
7087
+ },
7088
+ [existingAssets, notify, uploadAsset]
7089
+ );
6661
7090
  const applyRewrite = useCallback3((mapping) => {
6662
7091
  if (Object.keys(mapping).length === 0) return;
6663
7092
  setResult(
@@ -6699,7 +7128,9 @@ function SEImportWizard({
6699
7128
  rewriteMap[initialRows[i].url] = outcome.newUrl;
6700
7129
  }
6701
7130
  setMirrorRows(
6702
- (previous) => previous.map((row, idx) => idx === i ? { ...row, ...outcome } : row)
7131
+ (previous) => previous.map(
7132
+ (row, idx) => idx === i ? { ...row, ...outcome } : row
7133
+ )
6703
7134
  );
6704
7135
  }
6705
7136
  };
@@ -6882,11 +7313,8 @@ function SEImportWizard({
6882
7313
  if (!result) return;
6883
7314
  setImporting(true);
6884
7315
  try {
6885
- const body = {
6886
- name: options.name.trim() || result.overlay.name,
6887
- description: result.overlay.description,
6888
- settings: result.overlay.settings
6889
- };
7316
+ const thumbnailUrl = await resolveOverlayThumbnailUrl(activeOverlayPreview);
7317
+ const body = buildOverlaySaveBody(result, options.name, thumbnailUrl);
6890
7318
  const { uuid } = await saveOverlay(body);
6891
7319
  notify.success("Imported overlay from StreamElements.");
6892
7320
  mirrorAbortRef.current?.abort();
@@ -6909,38 +7337,35 @@ function SEImportWizard({
6909
7337
  }
6910
7338
  setImporting(true);
6911
7339
  try {
6912
- const body = {
6913
- name: options.name.trim() || result.overlay.name,
6914
- description: result.overlay.description,
6915
- settings: result.overlay.settings
6916
- };
6917
- const { uuid } = await saveOverlay(body);
6918
7340
  const orderedIds = Array.from(selectedOverlayIds);
6919
7341
  const sourceSEOverlayId = orderedIds[currentOverlayIndex - 1];
6920
- const sourceSummary = sourceSEOverlayId ? (overlayList ?? []).find((o) => o._id === sourceSEOverlayId) : void 0;
7342
+ const fallbackSourceSummary = sourceSEOverlayId ? (overlayList ?? []).find((o) => o._id === sourceSEOverlayId) : void 0;
7343
+ const sourceSummaries = activeOverlaySummaries.length > 0 ? activeOverlaySummaries : fallbackSourceSummary ? [fallbackSourceSummary] : [];
7344
+ const thumbnailUrl = await resolveOverlayThumbnailUrl(activeOverlayPreview);
7345
+ const body = buildOverlaySaveBody(result, options.name, thumbnailUrl);
7346
+ const { uuid } = await saveOverlay(body);
6921
7347
  if (uuid) {
6922
- onOverlayImported?.({
6923
- overlayId: uuid,
6924
- sourceSEOverlayId,
6925
- sourceSEOverlayName: sourceSummary?.name ?? sourceSummary?.settings?.name ?? result.overlay.name ?? void 0,
6926
- width: sourceSummary?.settings?.width ?? result.overlay.settings.metadata.width ?? void 0,
6927
- height: sourceSummary?.settings?.height ?? result.overlay.settings.metadata.height ?? void 0
6928
- });
6929
- }
6930
- const hasMore = overlayQueue.length > 0;
6931
- if (hasMore) {
6932
- notify.success(
6933
- `Imported ${currentOverlayIndex} of ${totalOverlaysToImport} overlays from StreamElements.`
6934
- );
6935
- const [nextId, ...rest] = overlayQueue;
6936
- setOverlayQueue(rest);
6937
- setCurrentOverlayIndex(currentOverlayIndex + 1);
6938
- setImporting(false);
6939
- await loadBootstrapAndKickoff(seClient, nextId);
6940
- return;
7348
+ if (sourceSummaries.length > 0) {
7349
+ for (const sourceSummary of sourceSummaries) {
7350
+ onOverlayImported?.({
7351
+ overlayId: uuid,
7352
+ sourceSEOverlayId: sourceSummary._id,
7353
+ sourceSEOverlayName: sourceSummary.name ?? sourceSummary.settings?.name ?? result.overlay.name ?? void 0,
7354
+ width: sourceSummary.settings?.width ?? result.overlay.settings.metadata.width ?? void 0,
7355
+ height: sourceSummary.settings?.height ?? result.overlay.settings.metadata.height ?? void 0
7356
+ });
7357
+ }
7358
+ } else {
7359
+ onOverlayImported?.({
7360
+ overlayId: uuid,
7361
+ sourceSEOverlayName: result.overlay.name ?? void 0,
7362
+ width: result.overlay.settings.metadata.width ?? void 0,
7363
+ height: result.overlay.settings.metadata.height ?? void 0
7364
+ });
7365
+ }
6941
7366
  }
6942
7367
  notify.success(
6943
- totalOverlaysToImport > 1 ? `Imported all ${totalOverlaysToImport} overlays from StreamElements.` : "Imported overlay from StreamElements."
7368
+ sourceSummaries.length > 1 ? `Imported ${sourceSummaries.length} StreamElements overlays as one Lumia overlay.` : "Imported overlay from StreamElements."
6944
7369
  );
6945
7370
  mirrorAbortRef.current?.abort();
6946
7371
  onClose();
@@ -6959,12 +7384,12 @@ function SEImportWizard({
6959
7384
  result,
6960
7385
  saveOverlay,
6961
7386
  seClient,
6962
- overlayQueue,
6963
7387
  currentOverlayIndex,
6964
- totalOverlaysToImport,
6965
- loadBootstrapAndKickoff,
6966
7388
  selectedOverlayIds,
6967
- overlayList
7389
+ overlayList,
7390
+ activeOverlaySummaries,
7391
+ activeOverlayPreview,
7392
+ resolveOverlayThumbnailUrl
6968
7393
  ]);
6969
7394
  const visibleSteps = useMemo5(() => {
6970
7395
  const showMirrorStep = options.mirrorAssets && (assetUrls.length > 0 || mirrorRows.length > 0 || step === "mirror");
@@ -7039,7 +7464,9 @@ function SEImportWizard({
7039
7464
  if (step === "connect")
7040
7465
  return connecting ? "Connecting..." : seClient ? "Continue" : "Connect";
7041
7466
  if (step === "pick") {
7042
- if (loading) return "Loading overlay...";
7467
+ if (importing) return "Importing...";
7468
+ if (loading)
7469
+ return selectedOverlayIds.size > 1 ? "Loading overlays..." : "Loading overlay...";
7043
7470
  const n = selectedOverlayIds.size;
7044
7471
  return n > 1 ? `Import ${n} overlays` : "Import overlay";
7045
7472
  }
@@ -7054,7 +7481,7 @@ function SEImportWizard({
7054
7481
  const nextDisabled = (() => {
7055
7482
  if (step === "connect") return connecting || !seClient && !jwt.trim();
7056
7483
  if (step === "pick")
7057
- return loading || overlaysLoading || selectedOverlayIds.size === 0;
7484
+ return importing || loading || overlaysLoading || selectedOverlayIds.size === 0;
7058
7485
  if (step === "url") return loading || !url.trim();
7059
7486
  if (step === "mirror") return mirrorRunning;
7060
7487
  if (step === "review")
@@ -7157,13 +7584,21 @@ function SEImportWizard({
7157
7584
  }
7158
7585
  }
7159
7586
  ),
7160
- step === "discovery" && result && /* @__PURE__ */ jsx14(StepDiscovery, { result, assetCount: assetUrls.length }),
7587
+ step === "discovery" && result && /* @__PURE__ */ jsx14(
7588
+ StepDiscovery,
7589
+ {
7590
+ result,
7591
+ assetCount: assetUrls.length,
7592
+ overlayCount: Math.max(1, activeOverlaySummaries.length)
7593
+ }
7594
+ ),
7161
7595
  step === "options" && /* @__PURE__ */ jsx14(
7162
7596
  StepOptions,
7163
7597
  {
7164
7598
  options,
7165
7599
  setOptions,
7166
- assetCount: assetUrls.length
7600
+ assetCount: assetUrls.length,
7601
+ hideName: activeOverlaySummaries.length > 1
7167
7602
  }
7168
7603
  ),
7169
7604
  step === "mirror" && /* @__PURE__ */ jsx14(
@@ -7219,7 +7654,8 @@ function SEImportWizard({
7219
7654
  result,
7220
7655
  options,
7221
7656
  mirrorRows,
7222
- rowStates
7657
+ rowStates,
7658
+ overlayCount: Math.max(1, activeOverlaySummaries.length)
7223
7659
  }
7224
7660
  )
7225
7661
  ] }),
@@ -7293,7 +7729,9 @@ function extractSEOverlayId(input) {
7293
7729
  }
7294
7730
  function extractSEPreviewParts(input) {
7295
7731
  const trimmed = input.trim();
7296
- const m = trimmed.match(/streamelements\.com\/overlay\/([0-9a-f]{24})(?:\/([A-Za-z0-9_-]+))?/i);
7732
+ const m = trimmed.match(
7733
+ /streamelements\.com\/overlay\/([0-9a-f]{24})(?:\/([A-Za-z0-9_-]+))?/i
7734
+ );
7297
7735
  if (!m) return null;
7298
7736
  const overlayId = m[1];
7299
7737
  const second = m[2];
@@ -7303,9 +7741,14 @@ function extractSEPreviewParts(input) {
7303
7741
  function buildBootstrapUrl(overlayId) {
7304
7742
  return `https://api.streamelements.com/kappa/v2/overlays/${overlayId}/bootstrap?isEditor=false&isMobile=false&isObs=false&isObsLive=false&isXsplit=false`;
7305
7743
  }
7306
- async function fetchSEBootstrap({ overlayId, apikey }) {
7744
+ async function fetchSEBootstrap({
7745
+ overlayId,
7746
+ apikey
7747
+ }) {
7307
7748
  if (!apikey) {
7308
- throw new Error("Missing access token. Use the public preview URL (Overlay menu \u2192 Get URL \u2192 Public link) \u2014 editor URLs do not include a token.");
7749
+ throw new Error(
7750
+ "Missing access token. Use the public preview URL (Overlay menu \u2192 Get URL \u2192 Public link) \u2014 editor URLs do not include a token."
7751
+ );
7309
7752
  }
7310
7753
  const res = await fetch(buildBootstrapUrl(overlayId), {
7311
7754
  method: "GET",
@@ -7323,7 +7766,9 @@ async function fetchSEBootstrap({ overlayId, apikey }) {
7323
7766
  }
7324
7767
  const json = await res.json();
7325
7768
  if (!isSEBootstrap(json)) {
7326
- throw new Error("Server returned a response that does not look like an SE bootstrap payload.");
7769
+ throw new Error(
7770
+ "Server returned a response that does not look like an SE bootstrap payload."
7771
+ );
7327
7772
  }
7328
7773
  return json;
7329
7774
  }
@@ -7346,7 +7791,10 @@ function importSEBootstrap(bootstrap) {
7346
7791
  console.log("[SE-IMPORT] bootstrap received", {
7347
7792
  overlayId: seOverlay?._id,
7348
7793
  name: seOverlay?.name,
7349
- canvas: { width: seOverlay?.settings?.width, height: seOverlay?.settings?.height },
7794
+ canvas: {
7795
+ width: seOverlay?.settings?.width,
7796
+ height: seOverlay?.settings?.height
7797
+ },
7350
7798
  widgetCount: widgets.length,
7351
7799
  widgetTypes: widgets.map((w) => w.type),
7352
7800
  provider
@@ -7374,7 +7822,11 @@ function importSEBootstrap(bootstrap) {
7374
7822
  const lumiaGroupId = seUidToLumiaGroupId[uid];
7375
7823
  const parentUid = typeof w.group === "string" ? w.group : null;
7376
7824
  const parentLumiaGroupId = parentUid ? seUidToLumiaGroupId[parentUid] ?? null : null;
7377
- const bounds = seCssToBounds(w.css, { width: 200, height: 200 }, importCanvas);
7825
+ const bounds = seCssToBounds(
7826
+ w.css,
7827
+ { width: 200, height: 200 },
7828
+ importCanvas
7829
+ );
7378
7830
  const layer = {
7379
7831
  id: lumiaGroupId,
7380
7832
  type: "group",
@@ -7418,7 +7870,15 @@ function importSEBootstrap(bootstrap) {
7418
7870
  };
7419
7871
  layers.push(layer);
7420
7872
  modules[lumiaGroupId] = module;
7421
- recordCoverage(coverage, { unit: { layer, module }, status: "direct", lumiaType: "group" }, w.type);
7873
+ recordCoverage(
7874
+ coverage,
7875
+ {
7876
+ unit: { layer, module },
7877
+ status: "direct",
7878
+ lumiaType: "group"
7879
+ },
7880
+ w.type
7881
+ );
7422
7882
  }
7423
7883
  for (const w of widgets) {
7424
7884
  if (w.type === "se-widget-group") continue;
@@ -7456,13 +7916,19 @@ function importSEBootstrap(bootstrap) {
7456
7916
  recordCoverage(coverage, result, w.type);
7457
7917
  }
7458
7918
  if (coverage.mappings.some((m) => m.status === "placeholder")) {
7459
- coverage.notes.push("Some widgets imported as placeholder text layers \u2014 see SE/05-widget-to-module-mapping.md for status.");
7919
+ coverage.notes.push(
7920
+ "Some widgets imported as placeholder text layers \u2014 see SE/05-widget-to-module-mapping.md for status."
7921
+ );
7460
7922
  }
7461
7923
  if (coverage.mappings.some((m) => m.seType === "se-widget-merch-goal")) {
7462
- coverage.notes.push("Merch-goal widgets imported as generic goals \u2014 see SE/03-missing-variables.md \xA78 for the missing data source.");
7924
+ coverage.notes.push(
7925
+ "Merch-goal widgets imported as generic goals \u2014 see SE/03-missing-variables.md \xA78 for the missing data source."
7926
+ );
7463
7927
  }
7464
7928
  if (coverage.mappings.some((m) => m.seType === "se-widget-custom-event-list")) {
7465
- coverage.notes.push("\u26A0 Custom widgets are experimental \u2014 they run under the SE compatibility shim (Embed.tsx) which handles most SE APIs but may not cover every edge case. Verify each custom layer renders correctly in the editor.");
7929
+ coverage.notes.push(
7930
+ "\u26A0 Custom widgets are experimental \u2014 they run under the SE compatibility shim (Embed.tsx) which handles most SE APIs but may not cover every edge case. Verify each custom layer renders correctly in the editor."
7931
+ );
7466
7932
  }
7467
7933
  const overlay = {
7468
7934
  uuid: "",
@@ -7499,10 +7965,16 @@ function applyReviewAction(result, moduleId, action, payload) {
7499
7965
  if (action === "skip") {
7500
7966
  delete modules[moduleId];
7501
7967
  const nextLayers = layers.filter((l) => l.id !== moduleId);
7502
- const reviewItems2 = result.reviewItems.filter((r) => r.moduleId !== moduleId);
7968
+ const reviewItems2 = result.reviewItems.filter(
7969
+ (r) => r.moduleId !== moduleId
7970
+ );
7503
7971
  return {
7504
7972
  ...result,
7505
- overlay: { ...result.overlay, settings: { ...result.overlay.settings, layers: nextLayers, modules }, layers_count: nextLayers.length },
7973
+ overlay: {
7974
+ ...result.overlay,
7975
+ settings: { ...result.overlay.settings, layers: nextLayers, modules },
7976
+ layers_count: nextLayers.length
7977
+ },
7506
7978
  reviewItems: reviewItems2
7507
7979
  };
7508
7980
  }
@@ -7528,7 +8000,10 @@ function applyReviewAction(result, moduleId, action, payload) {
7528
8000
  lights: mergedLights,
7529
8001
  events: mergedEvents,
7530
8002
  variables: mergedVariables,
7531
- content: { ...transplant.module.content ?? {}, [IMPORT_META_KEY]: importMeta2 }
8003
+ content: {
8004
+ ...transplant.module.content ?? {},
8005
+ [IMPORT_META_KEY]: importMeta2
8006
+ }
7532
8007
  };
7533
8008
  const rootOldNewId = transplant.layer.id;
7534
8009
  const remappedExtras = (transplant.extras ?? []).map((e) => ({
@@ -7541,7 +8016,9 @@ function applyReviewAction(result, moduleId, action, payload) {
7541
8016
  modules[extra.module.id] = extra.module;
7542
8017
  }
7543
8018
  modules[moduleId] = newModule;
7544
- const reviewItems2 = result.reviewItems.filter((r) => r.moduleId !== moduleId);
8019
+ const reviewItems2 = result.reviewItems.filter(
8020
+ (r) => r.moduleId !== moduleId
8021
+ );
7545
8022
  return {
7546
8023
  ...result,
7547
8024
  overlay: {
@@ -7576,7 +8053,10 @@ function applyReviewAction(result, moduleId, action, payload) {
7576
8053
  const reviewItems = result.reviewItems.filter((r) => r.moduleId !== moduleId);
7577
8054
  return {
7578
8055
  ...result,
7579
- overlay: { ...result.overlay, settings: { ...result.overlay.settings, modules } },
8056
+ overlay: {
8057
+ ...result.overlay,
8058
+ settings: { ...result.overlay.settings, modules }
8059
+ },
7580
8060
  reviewItems
7581
8061
  };
7582
8062
  }
@@ -7644,6 +8124,7 @@ export {
7644
8124
  extractSEPreviewParts,
7645
8125
  fetchSEBootstrap,
7646
8126
  fetchSEFiltersRaw,
8127
+ fetchSEOverlays,
7647
8128
  fetchUsableChannels,
7648
8129
  filenameFromURL,
7649
8130
  findSEAssetURLs,