@lumiastream/ui 0.2.8-alpha.16 → 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);
@@ -6101,7 +6123,8 @@ function SEImportWizard({
6101
6123
  if (!parts.apikey) {
6102
6124
  setLoadError({
6103
6125
  message: "That looks like an editor URL, which doesn't include an access token.",
6104
- hint: `Use the public preview URL (Overlay menu \u2192 Get URL \u2192 Public link). Overlay id: ${parts.overlayId}`
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
6105
6128
  });
6106
6129
  return;
6107
6130
  }
@@ -6110,6 +6133,7 @@ function SEImportWizard({
6110
6133
  const bootstrap = await fetchSEBootstrap(parts);
6111
6134
  const imported = importSEBootstrap(bootstrap);
6112
6135
  setResult(imported);
6136
+ setAssetUrls(findSEAssetURLs(imported.overlay));
6113
6137
  setOptions((previous) => ({ ...previous, name: imported.overlay.name }));
6114
6138
  setOriginalReviewItems(imported.reviewItems);
6115
6139
  setRowStates(
@@ -6123,27 +6147,21 @@ function SEImportWizard({
6123
6147
  setReviewIndex(0);
6124
6148
  setStep("discovery");
6125
6149
  } catch (error) {
6150
+ const detail = error?.message ?? String(error);
6126
6151
  setLoadError({
6127
- message: "Could not load the overlay.",
6128
- 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
6129
6154
  });
6130
6155
  } finally {
6131
6156
  setLoading(false);
6132
6157
  }
6133
6158
  }, [url]);
6159
+ const MIRROR_CONCURRENCY = 5;
6134
6160
  const processAssetRow = useCallback3(
6135
6161
  async (assetUrl, idx, existingByFilename, signal) => {
6136
6162
  const filename = filenameFromURL(assetUrl);
6137
6163
  const reuseUrl = existingByFilename.get(filename);
6138
6164
  if (reuseUrl) {
6139
- setResult(
6140
- (current) => current ? {
6141
- ...current,
6142
- overlay: rewriteAssetURLs(current.overlay, {
6143
- [assetUrl]: reuseUrl
6144
- })
6145
- } : current
6146
- );
6147
6165
  return { state: "reused", newUrl: reuseUrl };
6148
6166
  }
6149
6167
  if (filename.toLowerCase().endsWith(".svg"))
@@ -6160,14 +6178,6 @@ function SEImportWizard({
6160
6178
  signal
6161
6179
  );
6162
6180
  existingByFilename.set(filename, newUrl);
6163
- setResult(
6164
- (current) => current ? {
6165
- ...current,
6166
- overlay: rewriteAssetURLs(current.overlay, {
6167
- [assetUrl]: newUrl
6168
- })
6169
- } : current
6170
- );
6171
6181
  return { state: "done", newUrl };
6172
6182
  } catch (error) {
6173
6183
  if (error?.name === "AbortError" || signal?.aborted)
@@ -6177,8 +6187,15 @@ function SEImportWizard({
6177
6187
  },
6178
6188
  [uploadAsset]
6179
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
+ }, []);
6180
6196
  const runMirror = useCallback3(async () => {
6181
6197
  if (!result) return;
6198
+ mirrorAbortRef.current?.abort();
6182
6199
  mirrorAbortRef.current = new AbortController();
6183
6200
  const signal = mirrorAbortRef.current.signal;
6184
6201
  setMirrorRunning(true);
@@ -6193,21 +6210,33 @@ function SEImportWizard({
6193
6210
  if (name && asset.url) existingByFilename.set(name, asset.url);
6194
6211
  }
6195
6212
  filenameCacheRef.current = existingByFilename;
6196
- for (let i = 0; i < initialRows.length; i += 1) {
6197
- if (signal.aborted) break;
6198
- const outcome = await processAssetRow(
6199
- initialRows[i].url,
6200
- i,
6201
- existingByFilename,
6202
- signal
6203
- );
6204
- if (signal.aborted) break;
6205
- setMirrorRows(
6206
- (previous) => previous.map((row, idx) => idx === i ? { ...row, ...outcome } : row)
6207
- );
6208
- }
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);
6209
6238
  setMirrorRunning(false);
6210
- }, [assetUrls, existingAssets, processAssetRow, result]);
6239
+ }, [applyRewrite, assetUrls, existingAssets, processAssetRow, result]);
6211
6240
  const retryMirrorRow = useCallback3(
6212
6241
  async (idx) => {
6213
6242
  const rowUrl = mirrorRows[idx]?.url;
@@ -6221,8 +6250,11 @@ function SEImportWizard({
6221
6250
  setMirrorRows(
6222
6251
  (previous) => previous.map((row, i) => i === idx ? { ...row, ...outcome } : row)
6223
6252
  );
6253
+ if (outcome.newUrl && (outcome.state === "done" || outcome.state === "reused")) {
6254
+ applyRewrite({ [rowUrl]: outcome.newUrl });
6255
+ }
6224
6256
  },
6225
- [mirrorRows, processAssetRow]
6257
+ [applyRewrite, mirrorRows, processAssetRow]
6226
6258
  );
6227
6259
  useEffect6(() => {
6228
6260
  if (step === "mirror" && !mirrorStartedRef.current) {
@@ -6230,6 +6262,13 @@ function SEImportWizard({
6230
6262
  void runMirror();
6231
6263
  }
6232
6264
  }, [step, runMirror]);
6265
+ useEffect6(() => {
6266
+ return () => {
6267
+ setJwt("");
6268
+ setSeClient(null);
6269
+ mirrorAbortRef.current?.abort();
6270
+ };
6271
+ }, []);
6233
6272
  useEffect6(() => {
6234
6273
  if (step === "pick" && seClient && overlayList === null && !overlaysLoading) {
6235
6274
  void loadOverlayList(seClient);
@@ -6624,7 +6663,12 @@ function SEImportWizard({
6624
6663
  url,
6625
6664
  setUrl,
6626
6665
  error: loadError,
6627
- busy: loading
6666
+ busy: loading,
6667
+ onSwitchToJwt: () => {
6668
+ setLoadError(null);
6669
+ setEntryMode("jwt");
6670
+ setStep("connect");
6671
+ }
6628
6672
  }
6629
6673
  ),
6630
6674
  step === "discovery" && result && /* @__PURE__ */ jsx14(StepDiscovery, { result, assetCount: assetUrls.length }),
@@ -6828,145 +6872,138 @@ function importSEBootstrap(bootstrap) {
6828
6872
  name: seOverlay?.name,
6829
6873
  canvas: { width: seOverlay?.settings?.width, height: seOverlay?.settings?.height },
6830
6874
  widgetCount: widgets.length,
6831
- widgetTypes: widgets.map((w) => w.type),
6832
- fullBootstrap: bootstrap
6875
+ widgetTypes: widgets.map((w) => w.type)
6833
6876
  });
6834
6877
  const importCanvas = {
6835
6878
  width: seOverlay?.settings?.width ?? 1920,
6836
6879
  height: seOverlay?.settings?.height ?? 1080
6837
6880
  };
6838
- setImportCanvas(importCanvas);
6881
+ const ctx = { canvas: importCanvas };
6839
6882
  const layers = [];
6840
6883
  const modules = {};
6841
6884
  const reviewItems = [];
6842
- try {
6843
- const seUidToLumiaGroupId = {};
6844
- const groupWidgetByUid = {};
6845
- for (const w of widgets) {
6846
- if (w.type !== "se-widget-group") continue;
6847
- const uid = w.variables?.uid;
6848
- if (typeof uid !== "string") continue;
6849
- const lumiaGroupId = nanoid3();
6850
- seUidToLumiaGroupId[uid] = lumiaGroupId;
6851
- groupWidgetByUid[uid] = w;
6852
- }
6853
- for (const uid of Object.keys(groupWidgetByUid)) {
6854
- const w = groupWidgetByUid[uid];
6855
- const lumiaGroupId = seUidToLumiaGroupId[uid];
6856
- const parentUid = typeof w.group === "string" ? w.group : null;
6857
- const parentLumiaGroupId = parentUid ? seUidToLumiaGroupId[parentUid] ?? null : null;
6858
- const bounds = seCssToBounds(w.css, { width: 200, height: 200 }, importCanvas);
6859
- const layer = {
6860
- id: lumiaGroupId,
6861
- type: "group",
6862
- group: parentLumiaGroupId,
6863
- bounds: {
6864
- x: bounds.x,
6865
- y: bounds.y,
6866
- width: bounds.width,
6867
- height: bounds.height,
6868
- opacity: bounds.opacity,
6869
- zIndex: bounds.zIndex,
6870
- rotate: 0,
6871
- scale: [1, 1],
6872
- matrix: ""
6873
- },
6874
- state: {
6875
- locked: w.locked ?? false,
6876
- visible: w.visible ?? true,
6877
- // SE's `variables.expanded` is the editor's collapsed/open state.
6878
- expanded: w.variables?.expanded ?? true
6879
- }
6880
- };
6881
- const module = {
6882
- id: lumiaGroupId,
6883
- loaded: true,
6884
- settings: { title: defaultLabelForSEWidget(w), type: "group" },
6885
- lights: [],
6886
- css: {},
6887
- content: {
6888
- se_provenance: { widget: w, reason: "Imported as native group", status: "group" }
6889
- },
6890
- events: {},
6891
- variables: {}
6892
- };
6893
- layers.push(layer);
6894
- modules[lumiaGroupId] = module;
6895
- recordCoverage(coverage, { unit: { layer, module }, status: "direct", lumiaType: "group" }, w.type);
6896
- }
6897
- for (const w of widgets) {
6898
- if (w.type === "se-widget-group") continue;
6899
- const result = dispatch(w);
6900
- if (typeof w.group === "string") {
6901
- const parentLumiaGroupId = seUidToLumiaGroupId[w.group];
6902
- if (parentLumiaGroupId) {
6903
- result.unit.layer.group = parentLumiaGroupId;
6904
- }
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
6905
6921
  }
6906
- layers.push(result.unit.layer);
6907
- const module = result.unit.module;
6908
- if (result.status === "placeholder" || result.status === "template") {
6909
- module.content = {
6910
- ...module.content ?? {},
6911
- se_provenance: {
6912
- widget: w,
6913
- reason: reasonFor(w.type, result.status, result.flaggedOff),
6914
- status: result.status,
6915
- flaggedOff: result.flaggedOff
6916
- }
6917
- };
6918
- reviewItems.push({
6919
- moduleId: module.id,
6920
- seWidget: w,
6921
- status: result.status,
6922
- reason: reasonFor(w.type, result.status, result.flaggedOff),
6923
- flaggedOff: result.flaggedOff
6924
- });
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;
6925
6946
  }
6926
- modules[module.id] = module;
6927
- recordCoverage(coverage, result, w.type);
6928
- }
6929
- if (coverage.mappings.some((m) => m.status === "placeholder")) {
6930
- coverage.notes.push("Some widgets imported as placeholder text layers \u2014 see SE/05-widget-to-module-mapping.md for status.");
6931
- }
6932
- if (coverage.mappings.some((m) => m.seType === "se-widget-merch-goal")) {
6933
- coverage.notes.push("Merch-goal widgets imported as generic goals \u2014 see SE/03-missing-variables.md \xA78 for the missing data source.");
6934
6947
  }
6935
- if (coverage.mappings.some((m) => m.seType === "se-widget-custom-event-list")) {
6936
- 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
+ });
6937
6967
  }
6938
- const overlay = {
6939
- uuid: "",
6940
- listen_id: "",
6941
- name: seOverlay?.name ? `[SE] ${seOverlay.name}` : `Imported from StreamElements (${seOverlay?._id ?? ""})`,
6942
- description: seOverlay?._id ? `Imported from StreamElements overlay ${seOverlay._id}` : "Imported from StreamElements",
6943
- settings: {
6944
- metadata: {
6945
- width: seOverlay?.settings?.width ?? 1920,
6946
- height: seOverlay?.settings?.height ?? 1080
6947
- },
6948
- layers,
6949
- modules
6950
- },
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: {
6951
6986
  metadata: {
6952
- width: seOverlay?.settings?.width ?? null,
6953
- height: seOverlay?.settings?.height ?? null
6987
+ width: seOverlay?.settings?.width ?? 1920,
6988
+ height: seOverlay?.settings?.height ?? 1080
6954
6989
  },
6955
- layers_count: layers.length,
6956
- hasFullSettings: true
6957
- };
6958
- console.log("[SE-IMPORT] import complete", {
6959
- overlayName: overlay.name,
6960
- layerCount: overlay.layers_count,
6961
- coverage,
6962
- reviewItemCount: reviewItems.length,
6963
- reviewItems,
6964
- modules: overlay.settings.modules
6965
- });
6966
- return { overlay, coverage, reviewItems };
6967
- } finally {
6968
- setImportCanvas(null);
6969
- }
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 };
6970
7007
  }
6971
7008
  function applyReviewAction(result, moduleId, action, payload) {
6972
7009
  if (action === "keep") return result;
@@ -6995,9 +7032,15 @@ function applyReviewAction(result, moduleId, action, payload) {
6995
7032
  state: existingLayer.state
6996
7033
  };
6997
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;
6998
7038
  const newModule = {
6999
7039
  ...transplant.module,
7000
7040
  id: moduleId,
7041
+ lights: mergedLights,
7042
+ events: mergedEvents,
7043
+ variables: mergedVariables,
7001
7044
  content: { ...transplant.module.content ?? {}, se_provenance: provenance2 }
7002
7045
  };
7003
7046
  const rootOldNewId = transplant.layer.id;
@@ -7068,6 +7111,11 @@ function buildAIPromptForSEWidget(widget, canvas) {
7068
7111
  const listener = typeof widget.listener === "string" ? widget.listener : null;
7069
7112
  const listeners = widget.listeners && typeof widget.listeners === "object" && !Array.isArray(widget.listeners) ? Object.keys(widget.listeners) : null;
7070
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;
7071
7119
  return [
7072
7120
  "Recreate a StreamElements overlay widget as a Lumia Stream custom overlay (HTML + CSS + JS).",
7073
7121
  "",
@@ -7075,10 +7123,11 @@ function buildAIPromptForSEWidget(widget, canvas) {
7075
7123
  listener ? `Primary event listener: ${listener}` : "",
7076
7124
  listeners ? `Subscribes to events: ${listeners.join(", ")}` : "",
7077
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." : "",
7078
7127
  "",
7079
7128
  "Widget configuration (JSON):",
7080
7129
  "```json",
7081
- JSON.stringify(variables, null, 2).slice(0, 6e3),
7130
+ variablesJson,
7082
7131
  "```",
7083
7132
  "",
7084
7133
  "Constraints \u2014 your output must run inside a Lumia Custom Overlay sandboxed iframe:",
@@ -7098,7 +7147,6 @@ export {
7098
7147
  SEAuthError,
7099
7148
  SEClient,
7100
7149
  SEImportWizard,
7101
- SE_IMPORT_FLAGS,
7102
7150
  SE_WIDGET_TO_MARKETPLACE_CANDIDATES,
7103
7151
  applyReviewAction,
7104
7152
  buildAIPromptForSEWidget,