@lumiastream/ui 0.2.8-alpha.15 → 0.2.8-alpha.17

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
@@ -90,10 +90,11 @@ function defaultLabelForSEWidget(widget) {
90
90
  const tail = words.slice(1).join(" ");
91
91
  return tail ? `${head} ${tail}` : head;
92
92
  }
93
- function translateSeText(input, listener) {
93
+ function translateSeText(input, listener, passthrough) {
94
94
  if (!input) return "";
95
95
  const ctx = listener ? listenerPlaceholderContext(listener) : {};
96
96
  return input.replace(/\{([a-zA-Z][a-zA-Z0-9_.]*)\}/g, (_match, raw) => {
97
+ if (passthrough?.has(raw)) return `{{${raw}}}`;
97
98
  const mapped = ctx[raw] ?? SE_TO_LUMIA_PLACEHOLDER[raw] ?? raw;
98
99
  return `{{${mapped}}}`;
99
100
  });
@@ -287,14 +288,10 @@ function seLayoutUsesPushText(seLayout) {
287
288
 
288
289
  // src/se-import/mappers/build.ts
289
290
  import { nanoid } from "nanoid";
290
- var currentImportCanvas = null;
291
- function setImportCanvas(canvas) {
292
- currentImportCanvas = canvas;
293
- }
294
- function buildUnit(widget, lumiaType, moduleExtras) {
291
+ function buildUnit(widget, lumiaType, moduleExtras, ctx) {
295
292
  const id = nanoid();
296
293
  const defaults = LUMIA_DEFAULT_SIZES[lumiaType] ?? { width: 400, height: 400 };
297
- const bounds = seCssToBounds(widget.css, defaults, currentImportCanvas ?? void 0);
294
+ const bounds = seCssToBounds(widget.css, defaults, ctx?.canvas);
298
295
  const layer = {
299
296
  id,
300
297
  state: { visible: widget.visible ?? true, locked: widget.locked ?? false },
@@ -346,7 +343,7 @@ function seExtraTextCss(seCss, scrolling) {
346
343
  }
347
344
  return extra;
348
345
  }
349
- function mapText(widget) {
346
+ function mapText(widget, ctx) {
350
347
  const value = translateSeText(widget.text?.value ?? "");
351
348
  const seCss = widget.text?.css ?? {};
352
349
  return buildUnit(widget, "text", {
@@ -365,17 +362,17 @@ function mapText(widget) {
365
362
  background: "transparent",
366
363
  ...seExtraTextCss(seCss, widget.text?.scrolling)
367
364
  }
368
- });
365
+ }, ctx);
369
366
  }
370
- function mapImage(widget) {
367
+ function mapImage(widget, ctx) {
371
368
  return buildUnit(widget, "image", {
372
369
  content: {
373
370
  src: widget.image?.src ?? "",
374
371
  loop: true
375
372
  }
376
- });
373
+ }, ctx);
377
374
  }
378
- function mapVideo(widget) {
375
+ function mapVideo(widget, ctx) {
379
376
  return buildUnit(widget, "video", {
380
377
  content: {
381
378
  src: widget.video?.src ?? "",
@@ -383,10 +380,10 @@ function mapVideo(widget) {
383
380
  loop: true,
384
381
  muted: false
385
382
  }
386
- });
383
+ }, ctx);
387
384
  }
388
385
  var LUMIA_SNOW_WEBM = "https://storage.lumiastream.com/overlays/global/seasonal/snow.webm";
389
- function mapSnow(widget) {
386
+ function mapSnow(widget, ctx) {
390
387
  return buildUnit(widget, "video", {
391
388
  content: {
392
389
  src: LUMIA_SNOW_WEBM,
@@ -394,9 +391,9 @@ function mapSnow(widget) {
394
391
  loop: true,
395
392
  muted: true
396
393
  }
397
- });
394
+ }, ctx);
398
395
  }
399
- function mapReadout(widget, fallbackVar) {
396
+ function mapReadout(widget, fallbackVar, ctx) {
400
397
  const explicitListener = widget.variables?.listener;
401
398
  const explicit = typeof explicitListener === "string" ? explicitListener : null;
402
399
  const widgetListener = typeof widget.listener === "string" ? widget.listener : null;
@@ -419,7 +416,7 @@ function mapReadout(widget, fallbackVar) {
419
416
  background: "transparent",
420
417
  ...seExtraTextCss(seCss, widget.text?.scrolling)
421
418
  }
422
- });
419
+ }, ctx);
423
420
  }
424
421
 
425
422
  // src/se-import/mappers/alert.ts
@@ -451,13 +448,14 @@ var SE_EVENT_TO_LUMIA_ALERT = {
451
448
  // mapping prevents misleading "channel-point redemption" alerts.
452
449
  };
453
450
  function pickGraphics(g) {
454
- if (!g) return { type: "image", content: { src: "", loop: false, volume: 1 } };
451
+ const DEFAULT_GRAPHIC_VOLUME = 0.5;
452
+ if (!g) return { type: "image", content: { src: "", loop: false, volume: DEFAULT_GRAPHIC_VOLUME } };
455
453
  return {
456
454
  type: g.type === "video" ? "video" : "image",
457
455
  content: {
458
456
  src: g.src ?? "",
459
457
  loop: false,
460
- volume: g.volume ?? 0.5
458
+ volume: g.volume ?? DEFAULT_GRAPHIC_VOLUME
461
459
  }
462
460
  };
463
461
  }
@@ -628,7 +626,7 @@ function pickBaseAlertFields(event) {
628
626
  const { on: _on, variations: _variations, matchEmptyCondition: _mec, ...base } = event;
629
627
  return base;
630
628
  }
631
- function mapAlertBox(widget, opts = {}) {
629
+ function mapAlertBox(widget, opts = {}, ctx) {
632
630
  const v = widget.variables ?? {};
633
631
  const events = {};
634
632
  const eventKeys = opts.onlyEvents ?? ["follower", "subscriber", "tip", "cheer", "raid", "merch", "purchase", "charityCampaignDonation"];
@@ -643,9 +641,9 @@ function mapAlertBox(widget, opts = {}) {
643
641
  if (firstEvent == null) firstEvent = built;
644
642
  }
645
643
  const baseAlert = firstEvent ? pickBaseAlertFields(firstEvent) : {};
646
- return buildUnit(widget, "alert", { alert: { ...baseAlert, events } });
644
+ return buildUnit(widget, "alert", { alert: { ...baseAlert, events } }, ctx);
647
645
  }
648
- function mapSingleAlert(widget, lumiaAlertKey) {
646
+ function mapSingleAlert(widget, lumiaAlertKey, ctx) {
649
647
  const v = widget.variables ?? {};
650
648
  const topText = widget.text;
651
649
  const merged = { ...v };
@@ -669,7 +667,7 @@ function mapSingleAlert(widget, lumiaAlertKey) {
669
667
  [lumiaAlertKey]: built
670
668
  };
671
669
  const baseAlert = pickBaseAlertFields(built);
672
- return buildUnit(widget, "alert", { alert: { ...baseAlert, events } });
670
+ return buildUnit(widget, "alert", { alert: { ...baseAlert, events } }, ctx);
673
671
  }
674
672
 
675
673
  // src/se-import/mappers/goal.ts
@@ -707,7 +705,7 @@ function sourceFromListener(listener) {
707
705
  if (listener.startsWith("merch-")) return "merch_orders";
708
706
  return "custom";
709
707
  }
710
- function mapGoal(widget) {
708
+ function mapGoal(widget, ctx) {
711
709
  const v = widget.variables ?? {};
712
710
  const listener = v.goalListener ?? (typeof widget.listener === "string" ? widget.listener : null);
713
711
  const charity = charityGoalVariables(widget.type);
@@ -768,7 +766,7 @@ function mapGoal(widget) {
768
766
  textShadow: seTextCss["text-shadow"] ?? "rgb(0, 0, 0) 1px 1px 1px",
769
767
  background: "transparent"
770
768
  }
771
- });
769
+ }, ctx);
772
770
  }
773
771
 
774
772
  // src/se-import/mappers/custom.ts
@@ -780,19 +778,46 @@ var SE_FIELD_TYPE_TO_LUMIA = {
780
778
  slider: "slider",
781
779
  colorpicker: "colorpicker",
782
780
  fontpicker: "fontpicker",
783
- image: "input"
781
+ // SE's font picker variant — same control surface as `fontpicker` for our
782
+ // purposes; the SE renderer pulls Google Fonts at runtime which Lumia's
783
+ // fontpicker also does.
784
+ googleFont: "fontpicker",
785
+ // SE asset uploads — the asset URL lands in the `value` as a CDN string,
786
+ // which our `input` renders fine as-is. Once Lumia ships dedicated asset/
787
+ // audio pickers wired through the asset library, flip these mappings.
788
+ image: "input",
789
+ "image-input": "input",
790
+ "sound-input": "input",
791
+ "video-input": "input",
792
+ // Multi-line text — `input` accepts strings of any length; the editor
793
+ // surfaces a textarea by inspecting `value.length` heuristically.
794
+ textarea: "input",
795
+ hidden: "input",
796
+ // SE's button is a "fire a JS action" trigger with no persisted value.
797
+ // Lumia has no equivalent; surface as a non-editable label.
798
+ button: "input"
784
799
  };
800
+ var SE_RESERVED_FIELD_KEYS = /* @__PURE__ */ new Set(["widgetName", "widgetAuthor", "widgetDuration"]);
785
801
  function translateConfigs(seFields) {
786
802
  if (!seFields) return [];
787
- return Object.entries(seFields).map(([key, f]) => ({
788
- key,
789
- label: f.label ?? key,
790
- type: SE_FIELD_TYPE_TO_LUMIA[f.type ?? "text"] ?? "input",
791
- value: f.value,
792
- options: f.options
793
- }));
803
+ return Object.entries(seFields).filter(([key]) => !SE_RESERVED_FIELD_KEYS.has(key)).map(([key, f]) => {
804
+ const seType = f.type ?? "text";
805
+ return {
806
+ key,
807
+ label: f.label ?? key,
808
+ type: SE_FIELD_TYPE_TO_LUMIA[seType] ?? "input",
809
+ value: f.value,
810
+ options: f.options,
811
+ // Preserve the original SE type + group so a future editor UX
812
+ // upgrade can render the right control (e.g. an actual asset
813
+ // picker for `image-input`) without re-importing. Mentioned in
814
+ // docs/SE_IMPORT_GAPS.md under "Other widgets > custom-event-list".
815
+ seType,
816
+ ...f.group ? { seGroup: f.group } : {}
817
+ };
818
+ });
794
819
  }
795
- function mapCustom(widget) {
820
+ function mapCustom(widget, ctx) {
796
821
  const v = widget.variables ?? {};
797
822
  const parse = (x) => {
798
823
  if (x == null) return void 0;
@@ -815,7 +840,7 @@ function mapCustom(widget) {
815
840
  data: fieldData ?? {},
816
841
  flavor: "streamelements"
817
842
  }
818
- });
843
+ }, ctx);
819
844
  }
820
845
 
821
846
  // src/se-import/mappers/misc.ts
@@ -843,7 +868,7 @@ function paddingShorthand(message) {
843
868
  const px = (n) => `${n ?? 0}px`;
844
869
  return `${px(t)} ${px(r)} ${px(b)} ${px(l)}`;
845
870
  }
846
- function mapChatbox(widget) {
871
+ function mapChatbox(widget, ctx) {
847
872
  const v = widget.variables ?? {};
848
873
  const textCss = widget.text?.css ?? {};
849
874
  const messageCss = textCss.message ?? {};
@@ -927,7 +952,7 @@ function mapChatbox(widget) {
927
952
  se_highlight_users: v.highlight,
928
953
  se_text_css: textCss
929
954
  }
930
- });
955
+ }, ctx);
931
956
  }
932
957
  var SE_LISTENER_TO_LUMIA_CATEGORY = {
933
958
  "follower-latest": "follower",
@@ -993,7 +1018,7 @@ function computeListenerFilters(seListeners, explicitListener) {
993
1018
  if (allowed.size === 0) return [];
994
1019
  return ALL_LUMIA_EVENTLIST_CATEGORIES.filter((c) => !allowed.has(c));
995
1020
  }
996
- function mapEventList(widget) {
1021
+ function mapEventList(widget, ctx) {
997
1022
  const v = widget.variables ?? {};
998
1023
  const filters = computeListenerFilters(widget.listeners, widget.listener);
999
1024
  const maxItems = v.visibleEvents ?? v.visibleItems ?? 5;
@@ -1022,7 +1047,7 @@ function mapEventList(widget) {
1022
1047
  showDetailedText: false,
1023
1048
  hideAlertMessage: false
1024
1049
  }
1025
- });
1050
+ }, ctx);
1026
1051
  }
1027
1052
  var SE_CREDITS_EVENTS_TO_SHOW = {
1028
1053
  mods: false,
@@ -1162,7 +1187,7 @@ var CREDITS_DEFAULT_TEMPLATES = {
1162
1187
  topSuperchats: "{{username}} x{{amount}}",
1163
1188
  topRedemptions: "{{username}} x{{amount}}"
1164
1189
  };
1165
- function mapCredits(widget) {
1190
+ function mapCredits(widget, ctx) {
1166
1191
  const v = widget.variables ?? {};
1167
1192
  const itemGap = typeof v.padding === "number" ? v.padding : 8;
1168
1193
  const reverseFlow = v.direction === "bottom";
@@ -1216,7 +1241,7 @@ function mapCredits(widget) {
1216
1241
  textAlign: "center",
1217
1242
  background: "transparent"
1218
1243
  }
1219
- });
1244
+ }, ctx);
1220
1245
  }
1221
1246
  function inferSlideshowMediaType(mime, url) {
1222
1247
  if (typeof mime === "string") {
@@ -1235,7 +1260,7 @@ function nameFromUrl(url) {
1235
1260
  return url;
1236
1261
  }
1237
1262
  }
1238
- function mapSlideshow(widget) {
1263
+ function mapSlideshow(widget, ctx) {
1239
1264
  const v = widget.variables ?? {};
1240
1265
  const items = (v.images ?? []).map((raw) => {
1241
1266
  const url = typeof raw === "string" ? raw : raw.url ?? raw.src ?? "";
@@ -1275,21 +1300,13 @@ function mapSlideshow(widget) {
1275
1300
  se_interval: v.interval,
1276
1301
  se_raw_duration: v.duration
1277
1302
  }
1278
- });
1303
+ }, ctx);
1279
1304
  }
1280
- function mapTimer(widget) {
1305
+ function mapTimer(widget, ctx) {
1281
1306
  const v = widget.variables ?? {};
1282
1307
  const textValue = typeof widget.text?.value === "string" ? widget.text.value : void 0;
1283
- const mapTemplate = (template) => {
1284
- const protectedTemplate = template.replace(
1285
- /\{(days|hours|minutes|seconds)\}/g,
1286
- "__SE_COUNTDOWN_$1__"
1287
- );
1288
- return translateSeText(protectedTemplate).replace(
1289
- /__SE_COUNTDOWN_(days|hours|minutes|seconds)__/g,
1290
- "{{$1}}"
1291
- );
1292
- };
1308
+ const TIMER_PASSTHROUGH = /* @__PURE__ */ new Set(["days", "hours", "minutes", "seconds"]);
1309
+ const mapTemplate = (template) => translateSeText(template, null, TIMER_PASSTHROUGH);
1293
1310
  const absoluteDate = v.date ?? v.endsAt ?? v.endDate;
1294
1311
  const target = absoluteDate ? Date.parse(absoluteDate) : NaN;
1295
1312
  const hasValidAbsoluteDate = Number.isFinite(target);
@@ -1365,9 +1382,9 @@ function mapTimer(widget) {
1365
1382
  se_date_picker: v.datePicker,
1366
1383
  se_exact_time: v.exactTime
1367
1384
  }
1368
- });
1385
+ }, ctx);
1369
1386
  }
1370
- function mapNowPlaying(widget) {
1387
+ function mapNowPlaying(widget, ctx) {
1371
1388
  const v = widget.variables ?? {};
1372
1389
  const source = v.provider === "youtube" ? "youtubemusic" : "spotify";
1373
1390
  return buildUnit(widget, "nowplaying", {
@@ -1397,9 +1414,9 @@ function mapNowPlaying(widget) {
1397
1414
  textShadow: "rgb(0, 0, 0) 1px 1px 1px",
1398
1415
  background: "transparent"
1399
1416
  }
1400
- });
1417
+ }, ctx);
1401
1418
  }
1402
- function mapMediaShare(widget) {
1419
+ function mapMediaShare(widget, ctx) {
1403
1420
  const v = widget.variables ?? {};
1404
1421
  return buildUnit(widget, "songrequest", {
1405
1422
  content: {
@@ -1440,7 +1457,7 @@ function mapMediaShare(widget) {
1440
1457
  background: "transparent",
1441
1458
  fontSize: 18
1442
1459
  }
1443
- });
1460
+ }, ctx);
1444
1461
  }
1445
1462
  var SE_KAPPAGEN_EVENT_TO_LUMIA_ALERT = {
1446
1463
  follower: "twitch-follower",
@@ -1463,7 +1480,7 @@ var SE_KAPPAGEN_EVENT_TO_LUMIA_ALERT = {
1463
1480
  // All three drop silently; the rest of the widget (size, spawn zone,
1464
1481
  // per-event flags for the remaining events) still imports.
1465
1482
  };
1466
- function mapKappagen(widget) {
1483
+ function mapKappagen(widget, ctx) {
1467
1484
  const v = widget.variables ?? {};
1468
1485
  const eventsFromNested = v.eventsConfig ? {
1469
1486
  subscriber: v.eventsConfig.subscriber?.enabled ?? true,
@@ -1515,10 +1532,11 @@ function mapKappagen(widget) {
1515
1532
  se_direction: v.direction
1516
1533
  },
1517
1534
  css: { background: "transparent" }
1518
- }
1535
+ },
1536
+ ctx
1519
1537
  );
1520
1538
  }
1521
- function mapLeaderboardFromVariables(widget, currency) {
1539
+ function mapLeaderboardFromVariables(widget, currency, ctx) {
1522
1540
  const v = widget.variables ?? {};
1523
1541
  const count = v.limit ?? v.visibleItems ?? 10;
1524
1542
  const window = v.period === "weekly" ? "week" : v.period === "monthly" ? "month" : "alltime";
@@ -1541,9 +1559,9 @@ function mapLeaderboardFromVariables(widget, currency) {
1541
1559
  se_offset: v.offset,
1542
1560
  se_layout: v.layout
1543
1561
  }
1544
- });
1562
+ }, ctx);
1545
1563
  }
1546
- function mapGiveaway(widget) {
1564
+ function mapGiveaway(widget, ctx) {
1547
1565
  const v = widget.variables ?? {};
1548
1566
  const primary = v.colors?.primary ?? "#393853";
1549
1567
  const accent = v.colors?.accent ?? "#FF4076";
@@ -1594,7 +1612,7 @@ function mapGiveaway(widget) {
1594
1612
  se_display_tickets_purchased: v.displayTicketsPurchased,
1595
1613
  se_image_style_width: v.imageStyle?.width
1596
1614
  }
1597
- });
1615
+ }, ctx);
1598
1616
  }
1599
1617
  var HYPETRAIN_LISTENER_KEYS = /* @__PURE__ */ new Set([
1600
1618
  "hypetrain-latest",
@@ -1625,7 +1643,7 @@ function isHypetrainCustomWidget(widget) {
1625
1643
  if (enabled.length === 0) return false;
1626
1644
  return enabled.every((listener) => HYPETRAIN_LISTENER_KEYS.has(listener));
1627
1645
  }
1628
- function mapHypetrain(widget) {
1646
+ function mapHypetrain(widget, ctx) {
1629
1647
  const v = widget.variables ?? {};
1630
1648
  return buildUnit(widget, "hypetrain", {
1631
1649
  content: {
@@ -1668,9 +1686,9 @@ function mapHypetrain(widget) {
1668
1686
  // these via the overlay JSON if they want to recreate a specific look.
1669
1687
  se_variables: v
1670
1688
  }
1671
- });
1689
+ }, ctx);
1672
1690
  }
1673
- function mapUnsupportedAsText(widget) {
1691
+ function mapUnsupportedAsText(widget, ctx) {
1674
1692
  return buildUnit(widget, "text", {
1675
1693
  content: {
1676
1694
  value: `[Unsupported SE widget: ${widget.type}]`,
@@ -1684,7 +1702,7 @@ function mapUnsupportedAsText(widget) {
1684
1702
  fontWeight: "bold",
1685
1703
  background: "rgba(0,0,0,0.4)"
1686
1704
  }
1687
- });
1705
+ }, ctx);
1688
1706
  }
1689
1707
 
1690
1708
  // src/se-import/mappers/hypecup.ts
@@ -1754,7 +1772,7 @@ function categoriesFromListeners(listeners) {
1754
1772
  if (events.subs) events.resubs = true;
1755
1773
  return events;
1756
1774
  }
1757
- function mapHypeCup(widget) {
1775
+ function mapHypeCup(widget, ctx) {
1758
1776
  const v = widget.variables ?? {};
1759
1777
  const widgetWidth = typeof v.width === "number" && v.width > 0 ? v.width : parsePx(widget.css?.width);
1760
1778
  const widgetHeight = typeof v.height === "number" && v.height > 0 ? v.height : parsePx(widget.css?.height);
@@ -1812,7 +1830,7 @@ function mapHypeCup(widget) {
1812
1830
  css: {
1813
1831
  background: "transparent"
1814
1832
  }
1815
- });
1833
+ }, ctx);
1816
1834
  }
1817
1835
  function clamp01(n) {
1818
1836
  if (!Number.isFinite(n)) return 0.5;
@@ -1859,7 +1877,7 @@ function listenerIsOn(listeners, key) {
1859
1877
  if (Array.isArray(listeners)) return listeners.includes(key);
1860
1878
  return listeners[key] === true;
1861
1879
  }
1862
- function mapStreamBoss(widget) {
1880
+ function mapStreamBoss(widget, ctx) {
1863
1881
  const v = widget.variables ?? {};
1864
1882
  const baseHp = typeof v.baseHP === "number" && v.baseHP > 0 ? Math.round(v.baseHP) : 1e3;
1865
1883
  const seDamage = v.damage ?? {};
@@ -1941,7 +1959,7 @@ function mapStreamBoss(widget) {
1941
1959
  css: {
1942
1960
  background: "transparent"
1943
1961
  }
1944
- });
1962
+ }, ctx);
1945
1963
  if (theme === "card") {
1946
1964
  const MIN_CARD_WIDTH = 480;
1947
1965
  const MIN_CARD_HEIGHT = 110;
@@ -1952,25 +1970,6 @@ function mapStreamBoss(widget) {
1952
1970
  }
1953
1971
 
1954
1972
  // src/se-import/feature-flags.ts
1955
- var SE_IMPORT_FLAGS = {
1956
- // (Removed) IMPORT_CUSTOM_EMBED: custom-event-list now routes to native eventlist
1957
- // by default with a one-click "switch to custom HTML" option in the review step.
1958
- // (Removed) IMPORT_MERCH_GOAL: Fourthwall integration provides session/total order
1959
- // counters now (see fourthwall.manager.ts ORDER_PLACED handler); merch goal maps
1960
- // to {{fourthwall_total_*}} via the goal mapper (per-listener: orders → order_count,
1961
- // items → items_count, total → order_amount).
1962
- // (Removed) IMPORT_SEASONAL: seasonal widgets now route per-widget into existing
1963
- // modules (image / slideshow / video) rather than a single approximate canvas-
1964
- // particle template. See dispatcher.ts SEASONAL_IMAGE_TYPES comment for the
1965
- // rationale and /SE/05-widget-to-module-mapping.md §D for the current mapping.
1966
- // (Removed) IMPORT_MERCH_PRODUCTS: the merch-products-rotator now imports as an
1967
- // empty slideshow with a friendly layer title prompting the user to add product
1968
- // images manually. This is a degraded but honest UX — better than a placeholder
1969
- // text layer for the (rare) streamer who uses SE's merch rotator. A Fourthwall
1970
- // catalog fetcher would unlock fully-automatic import, but that's a multi-day
1971
- // project gated on a new Fourthwall API auth scope, an endpoint, and a refresh
1972
- // cadence.
1973
- };
1974
1973
  function isWidgetFlaggedOff(_seType) {
1975
1974
  return false;
1976
1975
  }
@@ -2025,7 +2024,7 @@ var GOAL_TYPES = /* @__PURE__ */ new Set([
2025
2024
  "se-widget-merch-goal"
2026
2025
  ]);
2027
2026
  var EVENTLIST_TYPES = /* @__PURE__ */ new Set(["se-widget-event-list", "se-widget-follower-recent", "se-widget-subscriber-recent", "se-widget-cheer-recent", "se-widget-tip-recent"]);
2028
- function dispatch(widget) {
2027
+ function dispatch(widget, ctx) {
2029
2028
  const t = widget.type;
2030
2029
  console.log("[SE-IMPORT] dispatch widget", {
2031
2030
  seType: t,
@@ -2036,46 +2035,46 @@ function dispatch(widget) {
2036
2035
  hasCss: !!widget.css
2037
2036
  });
2038
2037
  if (isWidgetFlaggedOff(t)) {
2039
- return { unit: mapUnsupportedAsText(widget), status: "placeholder", lumiaType: "text", flaggedOff: true };
2038
+ return { unit: mapUnsupportedAsText(widget, ctx), status: "placeholder", lumiaType: "text", flaggedOff: true };
2040
2039
  }
2041
- if (t === "text") return { unit: mapText(widget), status: "direct", lumiaType: "text" };
2042
- if (t === "image") return { unit: mapImage(widget), status: "direct", lumiaType: "image" };
2043
- if (t === "video") return { unit: mapVideo(widget), status: "direct", lumiaType: "video" };
2040
+ if (t === "text") return { unit: mapText(widget, ctx), status: "direct", lumiaType: "text" };
2041
+ if (t === "image") return { unit: mapImage(widget, ctx), status: "direct", lumiaType: "image" };
2042
+ if (t === "video") return { unit: mapVideo(widget, ctx), status: "direct", lumiaType: "video" };
2044
2043
  if (t in READOUT_TYPES) {
2045
- return { unit: mapReadout(widget, READOUT_TYPES[t]), status: "direct", lumiaType: "text" };
2044
+ return { unit: mapReadout(widget, READOUT_TYPES[t], ctx), status: "direct", lumiaType: "text" };
2046
2045
  }
2047
- if (GOAL_TYPES.has(t)) return { unit: mapGoal(widget), status: "direct", lumiaType: "goal" };
2046
+ if (GOAL_TYPES.has(t)) return { unit: mapGoal(widget, ctx), status: "direct", lumiaType: "goal" };
2048
2047
  if ((t === "se-widget-alert-box" || t === "se-widget-alert-box-merch") && hasSECustomCode(widget)) {
2049
- return { unit: mapUnsupportedAsText(widget), status: "placeholder", lumiaType: "text", flaggedOff: true };
2048
+ return { unit: mapUnsupportedAsText(widget, ctx), status: "placeholder", lumiaType: "text", flaggedOff: true };
2050
2049
  }
2051
- if (t === "se-widget-alert-box") return { unit: mapAlertBox(widget), status: "direct", lumiaType: "alert" };
2052
- if (t === "se-widget-alert-box-merch") return { unit: mapAlertBox(widget, { onlyEvents: ["merch"] }), status: "direct", lumiaType: "alert" };
2053
- if (t === "se-widget-donor-drive-alert") return { unit: mapSingleAlert(widget, "donordrive-donation"), status: "direct", lumiaType: "alert" };
2054
- if (t === "se-widget-extralife-alert") return { unit: mapSingleAlert(widget, "extralife-donation"), status: "direct", lumiaType: "alert" };
2055
- if (t === "se-widget-tiltify-alert") return { unit: mapSingleAlert(widget, "tiltify-campaignDonation"), status: "direct", lumiaType: "alert" };
2056
- if (EVENTLIST_TYPES.has(t)) return { unit: mapEventList(widget), status: "direct", lumiaType: "eventlist" };
2057
- if (t === "se-widget-top-tippers-list") return { unit: mapLeaderboardFromVariables(widget, "tips"), status: "direct", lumiaType: "loyaltyleaderboard" };
2058
- if (t === "se-widget-top-cheerers-list") return { unit: mapLeaderboardFromVariables(widget, "cheers"), status: "direct", lumiaType: "loyaltyleaderboard" };
2059
- if (t === "se-widget-top-gifters-list") return { unit: mapLeaderboardFromVariables(widget, "gifts"), status: "direct", lumiaType: "loyaltyleaderboard" };
2060
- if (t === "se-widget-credit-roll") return { unit: mapCredits(widget), status: "partial", lumiaType: "credits" };
2061
- if (t === "se-widget-twitch-chat") return { unit: mapChatbox(widget), status: "partial", lumiaType: "chatbox" };
2062
- if (t === "se-widget-image-rotator" || t === "se-widget-merch-products-rotator") return { unit: mapSlideshow(widget), status: t === "se-widget-merch-products-rotator" ? "partial" : "direct", lumiaType: "slideshow" };
2063
- if (t === "se-widget-countdown") return { unit: mapTimer(widget), status: "partial", lumiaType: "timer" };
2064
- if (t === "se-widget-current-song") return { unit: mapNowPlaying(widget), status: "partial", lumiaType: "nowplaying" };
2065
- if (t === "se-widget-kappagen") return { unit: mapKappagen(widget), status: "partial", lumiaType: "emotealert" };
2066
- if (t === "se-widget-giveaway") return { unit: mapGiveaway(widget), status: "direct", lumiaType: "raffle" };
2067
- if (t === "se-widget-media-share") return { unit: mapMediaShare(widget), status: "partial", lumiaType: "songrequest" };
2068
- if (t === "se-widget-hype-cup") return { unit: mapHypeCup(widget), status: "partial", lumiaType: "tipjar" };
2069
- if (t === "se-widget-bit-boss") return { unit: mapStreamBoss(widget), status: "partial", lumiaType: "streamboss" };
2070
- if (t === "se-widget-hype-train") return { unit: mapHypetrain(widget), status: "partial", lumiaType: "hypetrain" };
2050
+ if (t === "se-widget-alert-box") return { unit: mapAlertBox(widget, {}, ctx), status: "direct", lumiaType: "alert" };
2051
+ if (t === "se-widget-alert-box-merch") return { unit: mapAlertBox(widget, { onlyEvents: ["merch"] }, ctx), status: "direct", lumiaType: "alert" };
2052
+ if (t === "se-widget-donor-drive-alert") return { unit: mapSingleAlert(widget, "donordrive-donation", ctx), status: "direct", lumiaType: "alert" };
2053
+ if (t === "se-widget-extralife-alert") return { unit: mapSingleAlert(widget, "extralife-donation", ctx), status: "direct", lumiaType: "alert" };
2054
+ if (t === "se-widget-tiltify-alert") return { unit: mapSingleAlert(widget, "tiltify-campaignDonation", ctx), status: "direct", lumiaType: "alert" };
2055
+ if (EVENTLIST_TYPES.has(t)) return { unit: mapEventList(widget, ctx), status: "direct", lumiaType: "eventlist" };
2056
+ if (t === "se-widget-top-tippers-list") return { unit: mapLeaderboardFromVariables(widget, "tips", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
2057
+ if (t === "se-widget-top-cheerers-list") return { unit: mapLeaderboardFromVariables(widget, "cheers", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
2058
+ if (t === "se-widget-top-gifters-list") return { unit: mapLeaderboardFromVariables(widget, "gifts", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
2059
+ if (t === "se-widget-credit-roll") return { unit: mapCredits(widget, ctx), status: "partial", lumiaType: "credits" };
2060
+ if (t === "se-widget-twitch-chat") return { unit: mapChatbox(widget, ctx), status: "partial", lumiaType: "chatbox" };
2061
+ if (t === "se-widget-image-rotator" || t === "se-widget-merch-products-rotator") return { unit: mapSlideshow(widget, ctx), status: t === "se-widget-merch-products-rotator" ? "partial" : "direct", lumiaType: "slideshow" };
2062
+ if (t === "se-widget-countdown") return { unit: mapTimer(widget, ctx), status: "partial", lumiaType: "timer" };
2063
+ if (t === "se-widget-current-song") return { unit: mapNowPlaying(widget, ctx), status: "partial", lumiaType: "nowplaying" };
2064
+ if (t === "se-widget-kappagen") return { unit: mapKappagen(widget, ctx), status: "partial", lumiaType: "emotealert" };
2065
+ if (t === "se-widget-giveaway") return { unit: mapGiveaway(widget, ctx), status: "direct", lumiaType: "raffle" };
2066
+ if (t === "se-widget-media-share") return { unit: mapMediaShare(widget, ctx), status: "partial", lumiaType: "songrequest" };
2067
+ if (t === "se-widget-hype-cup") return { unit: mapHypeCup(widget, ctx), status: "partial", lumiaType: "tipjar" };
2068
+ if (t === "se-widget-bit-boss") return { unit: mapStreamBoss(widget, ctx), status: "partial", lumiaType: "streamboss" };
2069
+ if (t === "se-widget-hype-train") return { unit: mapHypetrain(widget, ctx), status: "partial", lumiaType: "hypetrain" };
2071
2070
  if (t === "se-widget-custom-event-list" && isHypetrainCustomWidget(widget)) {
2072
- return { unit: mapHypetrain(widget), status: "partial", lumiaType: "hypetrain" };
2071
+ return { unit: mapHypetrain(widget, ctx), status: "partial", lumiaType: "hypetrain" };
2073
2072
  }
2074
- if (t === "se-widget-custom-event-list") return { unit: mapCustom(widget), status: "partial", lumiaType: "custom" };
2075
- if (t === "se-widget-snow") return { unit: mapSnow(widget), status: "direct", lumiaType: "video" };
2076
- if (t === "se-widget-halloween-2019") return { unit: mapSlideshow(widget), status: "direct", lumiaType: "slideshow" };
2077
- if (SEASONAL_IMAGE_TYPES.has(t)) return { unit: mapImage(widget), status: "direct", lumiaType: "image" };
2078
- return { unit: mapUnsupportedAsText(widget), status: "placeholder", lumiaType: "text" };
2073
+ if (t === "se-widget-custom-event-list") return { unit: mapCustom(widget, ctx), status: "partial", lumiaType: "custom" };
2074
+ if (t === "se-widget-snow") return { unit: mapSnow(widget, ctx), status: "direct", lumiaType: "video" };
2075
+ if (t === "se-widget-halloween-2019") return { unit: mapSlideshow(widget, ctx), status: "direct", lumiaType: "slideshow" };
2076
+ if (SEASONAL_IMAGE_TYPES.has(t)) return { unit: mapImage(widget, ctx), status: "direct", lumiaType: "image" };
2077
+ return { unit: mapUnsupportedAsText(widget, ctx), status: "placeholder", lumiaType: "text" };
2079
2078
  }
2080
2079
  function recordCoverage(coverage, result, seType) {
2081
2080
  const existing = coverage.mappings.find((m) => m.seType === seType);
@@ -2133,11 +2132,19 @@ function rewriteAssetURLs(overlay, mapping) {
2133
2132
  settings: JSON.parse(rewritten)
2134
2133
  };
2135
2134
  }
2135
+ var MAX_MIRROR_ASSET_BYTES = 100 * 1024 * 1024;
2136
2136
  async function mirrorOneAsset(url, upload, signal) {
2137
2137
  const res = await fetch(url, { mode: "cors", signal });
2138
2138
  if (!res.ok) throw new Error(`Source returned HTTP ${res.status}`);
2139
+ const declared = Number(res.headers.get("content-length"));
2140
+ if (Number.isFinite(declared) && declared > MAX_MIRROR_ASSET_BYTES) {
2141
+ throw new Error(`Asset is ${(declared / (1024 * 1024)).toFixed(1)} MB \u2014 over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
2142
+ }
2139
2143
  const blob = await res.blob();
2140
2144
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
2145
+ if (blob.size > MAX_MIRROR_ASSET_BYTES) {
2146
+ throw new Error(`Asset is ${(blob.size / (1024 * 1024)).toFixed(1)} MB \u2014 over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
2147
+ }
2141
2148
  const filename = filenameFromURL(url);
2142
2149
  const type = blob.type || guessMime(filename);
2143
2150
  const file = new File([blob], filename, { type });
@@ -4835,13 +4842,19 @@ function CustomOverlayPreview({
4835
4842
  }) {
4836
4843
  const srcdoc = useMemo5(() => {
4837
4844
  const safeData = JSON.stringify(data ?? {});
4838
- const safeJs = JSON.stringify(`(async () => { ${js || ""} })()`);
4845
+ const safeJs = (js || "").replace(/<\/script>/gi, "<\\/script>");
4839
4846
  const safeHtml = (html || "").replace(/<\/script>/gi, "<\\/script>");
4840
4847
  const safeCss = (css || "").replace(/<\/style>/gi, "<\\/style>");
4841
4848
  return `<!DOCTYPE html><html><head><meta charset="utf-8"/><style>html,body{margin:0;padding:0;width:100%;height:100%;overflow:hidden;background:transparent;}${safeCss}</style></head><body>${safeHtml}<script>
4842
4849
  window.DATA = ${safeData};
4843
4850
  window.Overlay = window.Overlay || { on:()=>{}, callCommand:()=>Promise.resolve(), chatbot:()=>Promise.resolve(), setVariable:()=>Promise.resolve(), getVariable:()=>Promise.resolve(), saveStorage:()=>Promise.resolve(), getStorage:()=>Promise.resolve(), deleteStorage:()=>Promise.resolve(), addLoyaltyPoints:()=>Promise.resolve(), getLoyaltyPoints:()=>Promise.resolve() };
4844
- try { eval(${safeJs}); } catch(e) { document.body.insertAdjacentHTML('beforeend','<pre style=\\"color:#fca5a5;font:11px monospace;padding:8px;\\">' + (e && e.message ? e.message : e) + '</pre>'); }
4851
+ function __seImportShowError(e){document.body.insertAdjacentHTML('beforeend','<pre style="color:#fca5a5;font:11px monospace;padding:8px;">' + (e && e.message ? e.message : e) + '</pre>');}
4852
+ window.addEventListener('error', function(ev){__seImportShowError(ev.error || ev.message);});
4853
+ window.addEventListener('unhandledrejection', function(ev){__seImportShowError(ev.reason);});
4854
+ </script><script>
4855
+ (async function __seImportUserScript(){
4856
+ ${safeJs}
4857
+ })().catch(__seImportShowError);
4845
4858
  </script></body></html>`;
4846
4859
  }, [html, css, js, data]);
4847
4860
  return /* @__PURE__ */ jsx14(
@@ -5399,7 +5412,8 @@ function StepURL({
5399
5412
  url,
5400
5413
  setUrl,
5401
5414
  error,
5402
- busy
5415
+ busy,
5416
+ onSwitchToJwt
5403
5417
  }) {
5404
5418
  return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--narrow"), children: [
5405
5419
  /* @__PURE__ */ jsx14(
@@ -5445,7 +5459,16 @@ function StepURL({
5445
5459
  ] })
5446
5460
  ] })
5447
5461
  ] }),
5448
- error && /* @__PURE__ */ jsx14(ErrorPanel, { message: error.message, hint: error.hint })
5462
+ error && /* @__PURE__ */ jsx14(ErrorPanel, { message: error.message, hint: error.hint }),
5463
+ error?.editorUrl && onSwitchToJwt && /* @__PURE__ */ jsx14("div", { style: { marginTop: 12 }, children: /* @__PURE__ */ jsx14(
5464
+ LSButton,
5465
+ {
5466
+ type: "button",
5467
+ color: "primary",
5468
+ onClick: onSwitchToJwt,
5469
+ label: "Connect with JWT instead"
5470
+ }
5471
+ ) })
5449
5472
  ] });
5450
5473
  }
5451
5474
  function StepDiscovery({
@@ -5952,10 +5975,7 @@ function SEImportWizard({
5952
5975
  const mirrorStartedRef = useRef4(false);
5953
5976
  const mirrorAbortRef = useRef4(null);
5954
5977
  const filenameCacheRef = useRef4(/* @__PURE__ */ new Map());
5955
- const assetUrls = useMemo5(
5956
- () => result ? findSEAssetURLs(result.overlay) : [],
5957
- [result]
5958
- );
5978
+ const [assetUrls, setAssetUrls] = useState6([]);
5959
5979
  const currentItem = originalReviewItems[reviewIndex];
5960
5980
  const currentRow = currentItem ? rowStates[currentItem.moduleId] ?? { state: "pending" } : void 0;
5961
5981
  const handleConnect = useCallback3(async () => {
@@ -6053,6 +6073,7 @@ function SEImportWizard({
6053
6073
  }
6054
6074
  const imported = importSEBootstrap(bootstrap);
6055
6075
  setResult(imported);
6076
+ setAssetUrls(findSEAssetURLs(imported.overlay));
6056
6077
  setOptions((o) => ({ ...o, name: imported.overlay.name }));
6057
6078
  setOriginalReviewItems(imported.reviewItems);
6058
6079
  setRowStates(
@@ -6068,9 +6089,10 @@ function SEImportWizard({
6068
6089
  mirrorStartedRef.current = false;
6069
6090
  setStep("discovery");
6070
6091
  } catch (e) {
6092
+ const detail = e?.message ?? String(e);
6071
6093
  setLoadError({
6072
- message: "Could not load the overlay.",
6073
- hint: e?.message ?? String(e)
6094
+ message: detail || "Could not load the overlay.",
6095
+ hint: detail ? "If this looks transient, retry in a moment. Otherwise check the URL / token and try again." : void 0
6074
6096
  });
6075
6097
  } finally {
6076
6098
  setLoading(false);
@@ -6098,11 +6120,20 @@ function SEImportWizard({
6098
6120
  });
6099
6121
  return;
6100
6122
  }
6123
+ if (!parts.apikey) {
6124
+ setLoadError({
6125
+ message: "That looks like an editor URL, which doesn't include an access token.",
6126
+ hint: `Use the public preview URL (Overlay menu \u2192 Get URL \u2192 Public link), or connect your account with a JWT to skip the URL step entirely. Overlay id: ${parts.overlayId}`,
6127
+ editorUrl: true
6128
+ });
6129
+ return;
6130
+ }
6101
6131
  setLoading(true);
6102
6132
  try {
6103
6133
  const bootstrap = await fetchSEBootstrap(parts);
6104
6134
  const imported = importSEBootstrap(bootstrap);
6105
6135
  setResult(imported);
6136
+ setAssetUrls(findSEAssetURLs(imported.overlay));
6106
6137
  setOptions((previous) => ({ ...previous, name: imported.overlay.name }));
6107
6138
  setOriginalReviewItems(imported.reviewItems);
6108
6139
  setRowStates(
@@ -6116,27 +6147,21 @@ function SEImportWizard({
6116
6147
  setReviewIndex(0);
6117
6148
  setStep("discovery");
6118
6149
  } catch (error) {
6150
+ const detail = error?.message ?? String(error);
6119
6151
  setLoadError({
6120
- message: "Could not load the overlay.",
6121
- hint: error?.message
6152
+ message: detail || "Could not load the overlay.",
6153
+ hint: detail ? "If this looks transient, retry in a moment. Otherwise check the URL / token and try again." : void 0
6122
6154
  });
6123
6155
  } finally {
6124
6156
  setLoading(false);
6125
6157
  }
6126
6158
  }, [url]);
6159
+ const MIRROR_CONCURRENCY = 5;
6127
6160
  const processAssetRow = useCallback3(
6128
6161
  async (assetUrl, idx, existingByFilename, signal) => {
6129
6162
  const filename = filenameFromURL(assetUrl);
6130
6163
  const reuseUrl = existingByFilename.get(filename);
6131
6164
  if (reuseUrl) {
6132
- setResult(
6133
- (current) => current ? {
6134
- ...current,
6135
- overlay: rewriteAssetURLs(current.overlay, {
6136
- [assetUrl]: reuseUrl
6137
- })
6138
- } : current
6139
- );
6140
6165
  return { state: "reused", newUrl: reuseUrl };
6141
6166
  }
6142
6167
  if (filename.toLowerCase().endsWith(".svg"))
@@ -6153,14 +6178,6 @@ function SEImportWizard({
6153
6178
  signal
6154
6179
  );
6155
6180
  existingByFilename.set(filename, newUrl);
6156
- setResult(
6157
- (current) => current ? {
6158
- ...current,
6159
- overlay: rewriteAssetURLs(current.overlay, {
6160
- [assetUrl]: newUrl
6161
- })
6162
- } : current
6163
- );
6164
6181
  return { state: "done", newUrl };
6165
6182
  } catch (error) {
6166
6183
  if (error?.name === "AbortError" || signal?.aborted)
@@ -6170,8 +6187,15 @@ function SEImportWizard({
6170
6187
  },
6171
6188
  [uploadAsset]
6172
6189
  );
6190
+ const applyRewrite = useCallback3((mapping) => {
6191
+ if (Object.keys(mapping).length === 0) return;
6192
+ setResult(
6193
+ (current) => current ? { ...current, overlay: rewriteAssetURLs(current.overlay, mapping) } : current
6194
+ );
6195
+ }, []);
6173
6196
  const runMirror = useCallback3(async () => {
6174
6197
  if (!result) return;
6198
+ mirrorAbortRef.current?.abort();
6175
6199
  mirrorAbortRef.current = new AbortController();
6176
6200
  const signal = mirrorAbortRef.current.signal;
6177
6201
  setMirrorRunning(true);
@@ -6186,21 +6210,33 @@ function SEImportWizard({
6186
6210
  if (name && asset.url) existingByFilename.set(name, asset.url);
6187
6211
  }
6188
6212
  filenameCacheRef.current = existingByFilename;
6189
- for (let i = 0; i < initialRows.length; i += 1) {
6190
- if (signal.aborted) break;
6191
- const outcome = await processAssetRow(
6192
- initialRows[i].url,
6193
- i,
6194
- existingByFilename,
6195
- signal
6196
- );
6197
- if (signal.aborted) break;
6198
- setMirrorRows(
6199
- (previous) => previous.map((row, idx) => idx === i ? { ...row, ...outcome } : row)
6200
- );
6201
- }
6213
+ const rewriteMap = {};
6214
+ let cursor = 0;
6215
+ const worker = async () => {
6216
+ while (true) {
6217
+ if (signal.aborted) return;
6218
+ const i = cursor++;
6219
+ if (i >= initialRows.length) return;
6220
+ const outcome = await processAssetRow(
6221
+ initialRows[i].url,
6222
+ i,
6223
+ existingByFilename,
6224
+ signal
6225
+ );
6226
+ if (signal.aborted) return;
6227
+ if (outcome.newUrl && (outcome.state === "done" || outcome.state === "reused")) {
6228
+ rewriteMap[initialRows[i].url] = outcome.newUrl;
6229
+ }
6230
+ setMirrorRows(
6231
+ (previous) => previous.map((row, idx) => idx === i ? { ...row, ...outcome } : row)
6232
+ );
6233
+ }
6234
+ };
6235
+ const poolSize = Math.min(MIRROR_CONCURRENCY, initialRows.length);
6236
+ await Promise.all(Array.from({ length: poolSize }, worker));
6237
+ if (!signal.aborted) applyRewrite(rewriteMap);
6202
6238
  setMirrorRunning(false);
6203
- }, [assetUrls, existingAssets, processAssetRow, result]);
6239
+ }, [applyRewrite, assetUrls, existingAssets, processAssetRow, result]);
6204
6240
  const retryMirrorRow = useCallback3(
6205
6241
  async (idx) => {
6206
6242
  const rowUrl = mirrorRows[idx]?.url;
@@ -6214,8 +6250,11 @@ function SEImportWizard({
6214
6250
  setMirrorRows(
6215
6251
  (previous) => previous.map((row, i) => i === idx ? { ...row, ...outcome } : row)
6216
6252
  );
6253
+ if (outcome.newUrl && (outcome.state === "done" || outcome.state === "reused")) {
6254
+ applyRewrite({ [rowUrl]: outcome.newUrl });
6255
+ }
6217
6256
  },
6218
- [mirrorRows, processAssetRow]
6257
+ [applyRewrite, mirrorRows, processAssetRow]
6219
6258
  );
6220
6259
  useEffect6(() => {
6221
6260
  if (step === "mirror" && !mirrorStartedRef.current) {
@@ -6223,6 +6262,13 @@ function SEImportWizard({
6223
6262
  void runMirror();
6224
6263
  }
6225
6264
  }, [step, runMirror]);
6265
+ useEffect6(() => {
6266
+ return () => {
6267
+ setJwt("");
6268
+ setSeClient(null);
6269
+ mirrorAbortRef.current?.abort();
6270
+ };
6271
+ }, []);
6226
6272
  useEffect6(() => {
6227
6273
  if (step === "pick" && seClient && overlayList === null && !overlaysLoading) {
6228
6274
  void loadOverlayList(seClient);
@@ -6617,7 +6663,12 @@ function SEImportWizard({
6617
6663
  url,
6618
6664
  setUrl,
6619
6665
  error: loadError,
6620
- busy: loading
6666
+ busy: loading,
6667
+ onSwitchToJwt: () => {
6668
+ setLoadError(null);
6669
+ setEntryMode("jwt");
6670
+ setStep("connect");
6671
+ }
6621
6672
  }
6622
6673
  ),
6623
6674
  step === "discovery" && result && /* @__PURE__ */ jsx14(StepDiscovery, { result, assetCount: assetUrls.length }),
@@ -6767,14 +6818,20 @@ function extractSEOverlayId(input) {
6767
6818
  }
6768
6819
  function extractSEPreviewParts(input) {
6769
6820
  const trimmed = input.trim();
6770
- const m = trimmed.match(/streamelements\.com\/overlay\/([0-9a-f]{24})\/([A-Za-z0-9_-]+)/i);
6821
+ const m = trimmed.match(/streamelements\.com\/overlay\/([0-9a-f]{24})(?:\/([A-Za-z0-9_-]+))?/i);
6771
6822
  if (!m) return null;
6772
- return { overlayId: m[1], apikey: m[2] };
6823
+ const overlayId = m[1];
6824
+ const second = m[2];
6825
+ const apikey = second && second.toLowerCase() !== "editor" ? second : null;
6826
+ return { overlayId, apikey };
6773
6827
  }
6774
6828
  function buildBootstrapUrl(overlayId) {
6775
6829
  return `https://api.streamelements.com/kappa/v2/overlays/${overlayId}/bootstrap?isEditor=false&isMobile=false&isObs=false&isObsLive=false&isXsplit=false`;
6776
6830
  }
6777
6831
  async function fetchSEBootstrap({ overlayId, apikey }) {
6832
+ if (!apikey) {
6833
+ 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.");
6834
+ }
6778
6835
  const res = await fetch(buildBootstrapUrl(overlayId), {
6779
6836
  method: "GET",
6780
6837
  headers: { Accept: "application/json", Authorization: `apikey ${apikey}` }
@@ -6815,145 +6872,138 @@ function importSEBootstrap(bootstrap) {
6815
6872
  name: seOverlay?.name,
6816
6873
  canvas: { width: seOverlay?.settings?.width, height: seOverlay?.settings?.height },
6817
6874
  widgetCount: widgets.length,
6818
- widgetTypes: widgets.map((w) => w.type),
6819
- fullBootstrap: bootstrap
6875
+ widgetTypes: widgets.map((w) => w.type)
6820
6876
  });
6821
6877
  const importCanvas = {
6822
6878
  width: seOverlay?.settings?.width ?? 1920,
6823
6879
  height: seOverlay?.settings?.height ?? 1080
6824
6880
  };
6825
- setImportCanvas(importCanvas);
6881
+ const ctx = { canvas: importCanvas };
6826
6882
  const layers = [];
6827
6883
  const modules = {};
6828
6884
  const reviewItems = [];
6829
- try {
6830
- const seUidToLumiaGroupId = {};
6831
- const groupWidgetByUid = {};
6832
- for (const w of widgets) {
6833
- if (w.type !== "se-widget-group") continue;
6834
- const uid = w.variables?.uid;
6835
- if (typeof uid !== "string") continue;
6836
- const lumiaGroupId = nanoid3();
6837
- seUidToLumiaGroupId[uid] = lumiaGroupId;
6838
- groupWidgetByUid[uid] = w;
6839
- }
6840
- for (const uid of Object.keys(groupWidgetByUid)) {
6841
- const w = groupWidgetByUid[uid];
6842
- const lumiaGroupId = seUidToLumiaGroupId[uid];
6843
- const parentUid = typeof w.group === "string" ? w.group : null;
6844
- const parentLumiaGroupId = parentUid ? seUidToLumiaGroupId[parentUid] ?? null : null;
6845
- const bounds = seCssToBounds(w.css, { width: 200, height: 200 }, importCanvas);
6846
- const layer = {
6847
- id: lumiaGroupId,
6848
- type: "group",
6849
- group: parentLumiaGroupId,
6850
- bounds: {
6851
- x: bounds.x,
6852
- y: bounds.y,
6853
- width: bounds.width,
6854
- height: bounds.height,
6855
- opacity: bounds.opacity,
6856
- zIndex: bounds.zIndex,
6857
- rotate: 0,
6858
- scale: [1, 1],
6859
- matrix: ""
6860
- },
6861
- state: {
6862
- locked: w.locked ?? false,
6863
- visible: w.visible ?? true,
6864
- // SE's `variables.expanded` is the editor's collapsed/open state.
6865
- expanded: w.variables?.expanded ?? true
6866
- }
6867
- };
6868
- const module = {
6869
- id: lumiaGroupId,
6870
- loaded: true,
6871
- settings: { title: defaultLabelForSEWidget(w), type: "group" },
6872
- lights: [],
6873
- css: {},
6874
- content: {
6875
- se_provenance: { widget: w, reason: "Imported as native group", status: "group" }
6876
- },
6877
- events: {},
6878
- variables: {}
6879
- };
6880
- layers.push(layer);
6881
- modules[lumiaGroupId] = module;
6882
- recordCoverage(coverage, { unit: { layer, module }, status: "direct", lumiaType: "group" }, w.type);
6883
- }
6884
- for (const w of widgets) {
6885
- if (w.type === "se-widget-group") continue;
6886
- const result = dispatch(w);
6887
- if (typeof w.group === "string") {
6888
- const parentLumiaGroupId = seUidToLumiaGroupId[w.group];
6889
- if (parentLumiaGroupId) {
6890
- result.unit.layer.group = parentLumiaGroupId;
6891
- }
6885
+ const seUidToLumiaGroupId = {};
6886
+ const groupWidgetByUid = {};
6887
+ for (const w of widgets) {
6888
+ if (w.type !== "se-widget-group") continue;
6889
+ const uid = w.variables?.uid;
6890
+ if (typeof uid !== "string") continue;
6891
+ const lumiaGroupId = nanoid3();
6892
+ seUidToLumiaGroupId[uid] = lumiaGroupId;
6893
+ groupWidgetByUid[uid] = w;
6894
+ }
6895
+ for (const uid of Object.keys(groupWidgetByUid)) {
6896
+ const w = groupWidgetByUid[uid];
6897
+ const lumiaGroupId = seUidToLumiaGroupId[uid];
6898
+ const parentUid = typeof w.group === "string" ? w.group : null;
6899
+ const parentLumiaGroupId = parentUid ? seUidToLumiaGroupId[parentUid] ?? null : null;
6900
+ const bounds = seCssToBounds(w.css, { width: 200, height: 200 }, importCanvas);
6901
+ const layer = {
6902
+ id: lumiaGroupId,
6903
+ type: "group",
6904
+ group: parentLumiaGroupId,
6905
+ bounds: {
6906
+ x: bounds.x,
6907
+ y: bounds.y,
6908
+ width: bounds.width,
6909
+ height: bounds.height,
6910
+ opacity: bounds.opacity,
6911
+ zIndex: bounds.zIndex,
6912
+ rotate: 0,
6913
+ scale: [1, 1],
6914
+ matrix: ""
6915
+ },
6916
+ state: {
6917
+ locked: w.locked ?? false,
6918
+ visible: w.visible ?? true,
6919
+ // SE's `variables.expanded` is the editor's collapsed/open state.
6920
+ expanded: w.variables?.expanded ?? true
6892
6921
  }
6893
- layers.push(result.unit.layer);
6894
- const module = result.unit.module;
6895
- if (result.status === "placeholder" || result.status === "template") {
6896
- module.content = {
6897
- ...module.content ?? {},
6898
- se_provenance: {
6899
- widget: w,
6900
- reason: reasonFor(w.type, result.status, result.flaggedOff),
6901
- status: result.status,
6902
- flaggedOff: result.flaggedOff
6903
- }
6904
- };
6905
- reviewItems.push({
6906
- moduleId: module.id,
6907
- seWidget: w,
6908
- status: result.status,
6909
- reason: reasonFor(w.type, result.status, result.flaggedOff),
6910
- flaggedOff: result.flaggedOff
6911
- });
6922
+ };
6923
+ const module = {
6924
+ id: lumiaGroupId,
6925
+ loaded: true,
6926
+ settings: { title: defaultLabelForSEWidget(w), type: "group" },
6927
+ lights: [],
6928
+ css: {},
6929
+ content: {
6930
+ se_provenance: { widget: w, reason: "Imported as native group", status: "group" }
6931
+ },
6932
+ events: {},
6933
+ variables: {}
6934
+ };
6935
+ layers.push(layer);
6936
+ modules[lumiaGroupId] = module;
6937
+ recordCoverage(coverage, { unit: { layer, module }, status: "direct", lumiaType: "group" }, w.type);
6938
+ }
6939
+ for (const w of widgets) {
6940
+ if (w.type === "se-widget-group") continue;
6941
+ const result = dispatch(w, ctx);
6942
+ if (typeof w.group === "string") {
6943
+ const parentLumiaGroupId = seUidToLumiaGroupId[w.group];
6944
+ if (parentLumiaGroupId) {
6945
+ result.unit.layer.group = parentLumiaGroupId;
6912
6946
  }
6913
- modules[module.id] = module;
6914
- recordCoverage(coverage, result, w.type);
6915
- }
6916
- if (coverage.mappings.some((m) => m.status === "placeholder")) {
6917
- coverage.notes.push("Some widgets imported as placeholder text layers \u2014 see SE/05-widget-to-module-mapping.md for status.");
6918
6947
  }
6919
- if (coverage.mappings.some((m) => m.seType === "se-widget-merch-goal")) {
6920
- coverage.notes.push("Merch-goal widgets imported as generic goals \u2014 see SE/03-missing-variables.md \xA78 for the missing data source.");
6921
- }
6922
- if (coverage.mappings.some((m) => m.seType === "se-widget-custom-event-list")) {
6923
- 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.");
6948
+ layers.push(result.unit.layer);
6949
+ const module = result.unit.module;
6950
+ if (result.status === "placeholder" || result.status === "template") {
6951
+ module.content = {
6952
+ ...module.content ?? {},
6953
+ se_provenance: {
6954
+ widget: w,
6955
+ reason: reasonFor(w.type, result.status, result.flaggedOff),
6956
+ status: result.status,
6957
+ flaggedOff: result.flaggedOff
6958
+ }
6959
+ };
6960
+ reviewItems.push({
6961
+ moduleId: module.id,
6962
+ seWidget: w,
6963
+ status: result.status,
6964
+ reason: reasonFor(w.type, result.status, result.flaggedOff),
6965
+ flaggedOff: result.flaggedOff
6966
+ });
6924
6967
  }
6925
- const overlay = {
6926
- uuid: "",
6927
- listen_id: "",
6928
- name: seOverlay?.name ? `[SE] ${seOverlay.name}` : `Imported from StreamElements (${seOverlay?._id ?? ""})`,
6929
- description: seOverlay?._id ? `Imported from StreamElements overlay ${seOverlay._id}` : "Imported from StreamElements",
6930
- settings: {
6931
- metadata: {
6932
- width: seOverlay?.settings?.width ?? 1920,
6933
- height: seOverlay?.settings?.height ?? 1080
6934
- },
6935
- layers,
6936
- modules
6937
- },
6968
+ modules[module.id] = module;
6969
+ recordCoverage(coverage, result, w.type);
6970
+ }
6971
+ if (coverage.mappings.some((m) => m.status === "placeholder")) {
6972
+ coverage.notes.push("Some widgets imported as placeholder text layers \u2014 see SE/05-widget-to-module-mapping.md for status.");
6973
+ }
6974
+ if (coverage.mappings.some((m) => m.seType === "se-widget-merch-goal")) {
6975
+ coverage.notes.push("Merch-goal widgets imported as generic goals \u2014 see SE/03-missing-variables.md \xA78 for the missing data source.");
6976
+ }
6977
+ if (coverage.mappings.some((m) => m.seType === "se-widget-custom-event-list")) {
6978
+ 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.");
6979
+ }
6980
+ const overlay = {
6981
+ uuid: "",
6982
+ listen_id: "",
6983
+ name: seOverlay?.name ? `[SE] ${seOverlay.name}` : `Imported from StreamElements (${seOverlay?._id ?? ""})`,
6984
+ description: seOverlay?._id ? `Imported from StreamElements overlay ${seOverlay._id}` : "Imported from StreamElements",
6985
+ settings: {
6938
6986
  metadata: {
6939
- width: seOverlay?.settings?.width ?? null,
6940
- height: seOverlay?.settings?.height ?? null
6987
+ width: seOverlay?.settings?.width ?? 1920,
6988
+ height: seOverlay?.settings?.height ?? 1080
6941
6989
  },
6942
- layers_count: layers.length,
6943
- hasFullSettings: true
6944
- };
6945
- console.log("[SE-IMPORT] import complete", {
6946
- overlayName: overlay.name,
6947
- layerCount: overlay.layers_count,
6948
- coverage,
6949
- reviewItemCount: reviewItems.length,
6950
- reviewItems,
6951
- modules: overlay.settings.modules
6952
- });
6953
- return { overlay, coverage, reviewItems };
6954
- } finally {
6955
- setImportCanvas(null);
6956
- }
6990
+ layers,
6991
+ modules
6992
+ },
6993
+ metadata: {
6994
+ width: seOverlay?.settings?.width ?? null,
6995
+ height: seOverlay?.settings?.height ?? null
6996
+ },
6997
+ layers_count: layers.length,
6998
+ hasFullSettings: true
6999
+ };
7000
+ console.log("[SE-IMPORT] import complete", {
7001
+ overlayName: overlay.name,
7002
+ layerCount: overlay.layers_count,
7003
+ coverage,
7004
+ reviewItemCount: reviewItems.length
7005
+ });
7006
+ return { overlay, coverage, reviewItems };
6957
7007
  }
6958
7008
  function applyReviewAction(result, moduleId, action, payload) {
6959
7009
  if (action === "keep") return result;
@@ -6982,9 +7032,15 @@ function applyReviewAction(result, moduleId, action, payload) {
6982
7032
  state: existingLayer.state
6983
7033
  };
6984
7034
  const provenance2 = existing2.content?.se_provenance;
7035
+ const mergedLights = (existing2.lights?.length ?? 0) > 0 ? existing2.lights : transplant.module.lights;
7036
+ const mergedEvents = existing2.events && Object.keys(existing2.events).length > 0 ? { ...transplant.module.events, ...existing2.events } : transplant.module.events;
7037
+ const mergedVariables = existing2.variables && Object.keys(existing2.variables).length > 0 ? { ...transplant.module.variables, ...existing2.variables } : transplant.module.variables;
6985
7038
  const newModule = {
6986
7039
  ...transplant.module,
6987
7040
  id: moduleId,
7041
+ lights: mergedLights,
7042
+ events: mergedEvents,
7043
+ variables: mergedVariables,
6988
7044
  content: { ...transplant.module.content ?? {}, se_provenance: provenance2 }
6989
7045
  };
6990
7046
  const rootOldNewId = transplant.layer.id;
@@ -7055,6 +7111,11 @@ function buildAIPromptForSEWidget(widget, canvas) {
7055
7111
  const listener = typeof widget.listener === "string" ? widget.listener : null;
7056
7112
  const listeners = widget.listeners && typeof widget.listeners === "object" && !Array.isArray(widget.listeners) ? Object.keys(widget.listeners) : null;
7057
7113
  const landing = getAILandingBounds(widget, canvas);
7114
+ const VARIABLES_BUDGET = 6e3;
7115
+ const fullVariables = JSON.stringify(variables, null, 2);
7116
+ const truncated = fullVariables.length > VARIABLES_BUDGET;
7117
+ const variablesJson = truncated ? `${fullVariables.slice(0, VARIABLES_BUDGET)}
7118
+ \u2026[truncated \u2014 ${fullVariables.length - VARIABLES_BUDGET} more bytes elided]` : fullVariables;
7058
7119
  return [
7059
7120
  "Recreate a StreamElements overlay widget as a Lumia Stream custom overlay (HTML + CSS + JS).",
7060
7121
  "",
@@ -7062,10 +7123,11 @@ function buildAIPromptForSEWidget(widget, canvas) {
7062
7123
  listener ? `Primary event listener: ${listener}` : "",
7063
7124
  listeners ? `Subscribes to events: ${listeners.join(", ")}` : "",
7064
7125
  `Canvas: ${landing.width} \xD7 ${landing.height}`,
7126
+ truncated ? "Note: the widget config below is truncated \u2014 focus on visible/structural fields and ignore tail content you cannot see." : "",
7065
7127
  "",
7066
7128
  "Widget configuration (JSON):",
7067
7129
  "```json",
7068
- JSON.stringify(variables, null, 2).slice(0, 6e3),
7130
+ variablesJson,
7069
7131
  "```",
7070
7132
  "",
7071
7133
  "Constraints \u2014 your output must run inside a Lumia Custom Overlay sandboxed iframe:",
@@ -7085,7 +7147,6 @@ export {
7085
7147
  SEAuthError,
7086
7148
  SEClient,
7087
7149
  SEImportWizard,
7088
- SE_IMPORT_FLAGS,
7089
7150
  SE_WIDGET_TO_MARKETPLACE_CANDIDATES,
7090
7151
  applyReviewAction,
7091
7152
  buildAIPromptForSEWidget,