@lumiastream/ui 0.2.8-alpha.16 → 0.2.8-alpha.18

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,17 @@ 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);
671
+ }
672
+
673
+ // src/se-import/mappers/importMeta.ts
674
+ function buildImportMeta(widget, config) {
675
+ return {
676
+ source: "streamelements",
677
+ widgetType: widget.type,
678
+ importedAt: (/* @__PURE__ */ new Date()).toISOString(),
679
+ ...config && Object.keys(config).length > 0 ? { config } : {}
680
+ };
673
681
  }
674
682
 
675
683
  // src/se-import/mappers/goal.ts
@@ -707,7 +715,7 @@ function sourceFromListener(listener) {
707
715
  if (listener.startsWith("merch-")) return "merch_orders";
708
716
  return "custom";
709
717
  }
710
- function mapGoal(widget) {
718
+ function mapGoal(widget, ctx) {
711
719
  const v = widget.variables ?? {};
712
720
  const listener = v.goalListener ?? (typeof widget.listener === "string" ? widget.listener : null);
713
721
  const charity = charityGoalVariables(widget.type);
@@ -747,9 +755,12 @@ function mapGoal(widget) {
747
755
  border: "solid 1px transparent",
748
756
  borderRadius: "40px",
749
757
  highlightColor: "inherit",
750
- // SE-only fields preserved as provenance no native Lumia equivalent.
751
- se_source_listener: listener,
752
- se_simple_design: !!v.simpleDesign
758
+ // Source-side knobs without a native Lumia equivalent live under the
759
+ // generic `importMeta` envelope (see mappers/importMeta.ts).
760
+ importMeta: buildImportMeta(widget, {
761
+ sourceListener: listener,
762
+ simpleDesign: !!v.simpleDesign
763
+ })
753
764
  },
754
765
  css: {
755
766
  // `borderRadius` / `borderStyle` / `borderWidth` / `borderColor`
@@ -768,11 +779,11 @@ function mapGoal(widget) {
768
779
  textShadow: seTextCss["text-shadow"] ?? "rgb(0, 0, 0) 1px 1px 1px",
769
780
  background: "transparent"
770
781
  }
771
- });
782
+ }, ctx);
772
783
  }
773
784
 
774
785
  // src/se-import/mappers/custom.ts
775
- var SE_FIELD_TYPE_TO_LUMIA = {
786
+ var SOURCE_TO_LUMIA_FIELD_TYPE = {
776
787
  text: "input",
777
788
  number: "number",
778
789
  checkbox: "checkbox",
@@ -780,19 +791,45 @@ var SE_FIELD_TYPE_TO_LUMIA = {
780
791
  slider: "slider",
781
792
  colorpicker: "colorpicker",
782
793
  fontpicker: "fontpicker",
783
- image: "input"
794
+ // SE's `googleFont` picker is functionally the same as Lumia's native
795
+ // fontpicker (both load Google Fonts on demand).
796
+ googleFont: "fontpicker",
797
+ // Asset uploads land on the native upload types added to Lumia's
798
+ // ConfigsFieldType. `image` (legacy SE alias) folds into `imageupload`.
799
+ image: "imageupload",
800
+ "image-input": "imageupload",
801
+ "sound-input": "soundupload",
802
+ "video-input": "videoupload",
803
+ // Multi-line text → native textarea.
804
+ textarea: "textarea",
805
+ // Hidden fields keep their value in `Overlay.data` but don't render a
806
+ // control; we map to `input` + the BaseConfigField `hidden: true` flag
807
+ // (applied below in translateConfigs).
808
+ hidden: "input",
809
+ // SE-side trigger buttons → native action button.
810
+ button: "actionbutton"
784
811
  };
785
- function translateConfigs(seFields) {
786
- 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
- }));
812
+ var RESERVED_FIELD_KEYS = /* @__PURE__ */ new Set(["widgetName", "widgetAuthor", "widgetDuration"]);
813
+ function translateConfigs(sourceFields) {
814
+ if (!sourceFields) return [];
815
+ return Object.entries(sourceFields).filter(([key]) => !RESERVED_FIELD_KEYS.has(key)).map(([key, f]) => {
816
+ const sourceType = f.type ?? "text";
817
+ const nativeType = SOURCE_TO_LUMIA_FIELD_TYPE[sourceType] ?? "input";
818
+ const base = {
819
+ key,
820
+ label: f.label ?? key,
821
+ // The emitted `type` is a native Lumia ConfigsFieldType — no
822
+ // source-prefixed shadow field rides along. Anything that needs
823
+ // richer control behavior should land as a new native type.
824
+ type: nativeType,
825
+ value: f.value,
826
+ options: f.options
827
+ };
828
+ if (sourceType === "hidden") base.hidden = true;
829
+ return base;
830
+ });
794
831
  }
795
- function mapCustom(widget) {
832
+ function mapCustom(widget, ctx) {
796
833
  const v = widget.variables ?? {};
797
834
  const parse = (x) => {
798
835
  if (x == null) return void 0;
@@ -815,7 +852,7 @@ function mapCustom(widget) {
815
852
  data: fieldData ?? {},
816
853
  flavor: "streamelements"
817
854
  }
818
- });
855
+ }, ctx);
819
856
  }
820
857
 
821
858
  // src/se-import/mappers/misc.ts
@@ -843,7 +880,7 @@ function paddingShorthand(message) {
843
880
  const px = (n) => `${n ?? 0}px`;
844
881
  return `${px(t)} ${px(r)} ${px(b)} ${px(l)}`;
845
882
  }
846
- function mapChatbox(widget) {
883
+ function mapChatbox(widget, ctx) {
847
884
  const v = widget.variables ?? {};
848
885
  const textCss = widget.text?.css ?? {};
849
886
  const messageCss = textCss.message ?? {};
@@ -918,16 +955,19 @@ function mapChatbox(widget) {
918
955
  twitch: { themeConfig: { primaryColor: accentColor, showUsernameColors: true } }
919
956
  }
920
957
  } : {},
921
- // SE-only fields preserved as provenance no native Lumia equivalent.
922
- // `delay` / `messageDelay` (initial + per-message hold), and the
923
- // raw highlight block in case a future Lumia feature wants the
924
- // fine-grained styling.
925
- se_message_delay: v.messageDelay,
926
- se_initial_delay: v.delay,
927
- se_highlight_users: v.highlight,
928
- se_text_css: textCss
958
+ // Source-side knobs preserved under the generic importMeta envelope
959
+ // no native Lumia equivalent today. `messageDelay` is per-message
960
+ // hold, `initialDelay` is the show-after-connect delay, and we
961
+ // stash the raw highlight block + text css for any future feature
962
+ // that wants the fine-grained styling.
963
+ importMeta: buildImportMeta(widget, {
964
+ messageDelay: v.messageDelay,
965
+ initialDelay: v.delay,
966
+ highlightUsers: v.highlight,
967
+ textCss
968
+ })
929
969
  }
930
- });
970
+ }, ctx);
931
971
  }
932
972
  var SE_LISTENER_TO_LUMIA_CATEGORY = {
933
973
  "follower-latest": "follower",
@@ -993,7 +1033,7 @@ function computeListenerFilters(seListeners, explicitListener) {
993
1033
  if (allowed.size === 0) return [];
994
1034
  return ALL_LUMIA_EVENTLIST_CATEGORIES.filter((c) => !allowed.has(c));
995
1035
  }
996
- function mapEventList(widget) {
1036
+ function mapEventList(widget, ctx) {
997
1037
  const v = widget.variables ?? {};
998
1038
  const filters = computeListenerFilters(widget.listeners, widget.listener);
999
1039
  const maxItems = v.visibleEvents ?? v.visibleItems ?? 5;
@@ -1022,7 +1062,7 @@ function mapEventList(widget) {
1022
1062
  showDetailedText: false,
1023
1063
  hideAlertMessage: false
1024
1064
  }
1025
- });
1065
+ }, ctx);
1026
1066
  }
1027
1067
  var SE_CREDITS_EVENTS_TO_SHOW = {
1028
1068
  mods: false,
@@ -1162,7 +1202,7 @@ var CREDITS_DEFAULT_TEMPLATES = {
1162
1202
  topSuperchats: "{{username}} x{{amount}}",
1163
1203
  topRedemptions: "{{username}} x{{amount}}"
1164
1204
  };
1165
- function mapCredits(widget) {
1205
+ function mapCredits(widget, ctx) {
1166
1206
  const v = widget.variables ?? {};
1167
1207
  const itemGap = typeof v.padding === "number" ? v.padding : 8;
1168
1208
  const reverseFlow = v.direction === "bottom";
@@ -1199,12 +1239,15 @@ function mapCredits(widget) {
1199
1239
  audio: {
1200
1240
  content: { src: [], name: [], loop: false, volume: 1 }
1201
1241
  },
1202
- // SE-specific knobs preserved as provenance; no direct Lumia equivalent for
1203
- // these. `chosenFilter` would narrow eventsToShow if we had a clean mapping;
1204
- // `separator` would join entries but Lumia credits uses sectioned cards instead.
1205
- se_chosen_filter: v.chosenFilter,
1206
- se_separator: v.separator,
1207
- se_amount: v.amount
1242
+ // Source-side knobs with no direct Lumia equivalent — preserved
1243
+ // under the generic importMeta envelope. `chosenFilter` would
1244
+ // narrow eventsToShow if we had a clean mapping; `separator` would
1245
+ // join entries but Lumia credits uses sectioned cards instead.
1246
+ importMeta: buildImportMeta(widget, {
1247
+ chosenFilter: v.chosenFilter,
1248
+ separator: v.separator,
1249
+ amount: v.amount
1250
+ })
1208
1251
  },
1209
1252
  css: {
1210
1253
  fontSize: 30,
@@ -1216,7 +1259,7 @@ function mapCredits(widget) {
1216
1259
  textAlign: "center",
1217
1260
  background: "transparent"
1218
1261
  }
1219
- });
1262
+ }, ctx);
1220
1263
  }
1221
1264
  function inferSlideshowMediaType(mime, url) {
1222
1265
  if (typeof mime === "string") {
@@ -1235,7 +1278,7 @@ function nameFromUrl(url) {
1235
1278
  return url;
1236
1279
  }
1237
1280
  }
1238
- function mapSlideshow(widget) {
1281
+ function mapSlideshow(widget, ctx) {
1239
1282
  const v = widget.variables ?? {};
1240
1283
  const items = (v.images ?? []).map((raw) => {
1241
1284
  const url = typeof raw === "string" ? raw : raw.url ?? raw.src ?? "";
@@ -1272,24 +1315,18 @@ function mapSlideshow(widget) {
1272
1315
  enterAnimationDelay: (seAnim.inDelay ?? 0) * 1e3,
1273
1316
  exitAnimationDelay: (seAnim.outDelay ?? 0) * 1e3
1274
1317
  },
1275
- se_interval: v.interval,
1276
- se_raw_duration: v.duration
1318
+ importMeta: buildImportMeta(widget, {
1319
+ interval: v.interval,
1320
+ rawDuration: v.duration
1321
+ })
1277
1322
  }
1278
- });
1323
+ }, ctx);
1279
1324
  }
1280
- function mapTimer(widget) {
1325
+ function mapTimer(widget, ctx) {
1281
1326
  const v = widget.variables ?? {};
1282
1327
  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
- };
1328
+ const TIMER_PASSTHROUGH = /* @__PURE__ */ new Set(["days", "hours", "minutes", "seconds"]);
1329
+ const mapTemplate = (template) => translateSeText(template, null, TIMER_PASSTHROUGH);
1293
1330
  const absoluteDate = v.date ?? v.endsAt ?? v.endDate;
1294
1331
  const target = absoluteDate ? Date.parse(absoluteDate) : NaN;
1295
1332
  const hasValidAbsoluteDate = Number.isFinite(target);
@@ -1361,13 +1398,15 @@ function mapTimer(widget) {
1361
1398
  changeMessageAfter: !!v.changeMessageAfterCountdown,
1362
1399
  countDate: hasValidAbsoluteDate && (!!v.exactTime || !!v.datePicker || !!absoluteDate),
1363
1400
  date: hasValidAbsoluteDate ? new Date(target).toISOString() : "",
1364
- se_date: absoluteDate,
1365
- se_date_picker: v.datePicker,
1366
- se_exact_time: v.exactTime
1401
+ importMeta: buildImportMeta(widget, {
1402
+ rawDate: absoluteDate,
1403
+ datePicker: v.datePicker,
1404
+ exactTime: v.exactTime
1405
+ })
1367
1406
  }
1368
- });
1407
+ }, ctx);
1369
1408
  }
1370
- function mapNowPlaying(widget) {
1409
+ function mapNowPlaying(widget, ctx) {
1371
1410
  const v = widget.variables ?? {};
1372
1411
  const source = v.provider === "youtube" ? "youtubemusic" : "spotify";
1373
1412
  return buildUnit(widget, "nowplaying", {
@@ -1383,7 +1422,7 @@ function mapNowPlaying(widget) {
1383
1422
  showImg: true,
1384
1423
  oneLineTitle: false,
1385
1424
  highlightColor: "inherit",
1386
- se_simple_design: !!v.simpleDesign
1425
+ importMeta: buildImportMeta(widget, { simpleDesign: !!v.simpleDesign })
1387
1426
  },
1388
1427
  css: {
1389
1428
  borderRadius: "0px",
@@ -1397,9 +1436,9 @@ function mapNowPlaying(widget) {
1397
1436
  textShadow: "rgb(0, 0, 0) 1px 1px 1px",
1398
1437
  background: "transparent"
1399
1438
  }
1400
- });
1439
+ }, ctx);
1401
1440
  }
1402
- function mapMediaShare(widget) {
1441
+ function mapMediaShare(widget, ctx) {
1403
1442
  const v = widget.variables ?? {};
1404
1443
  return buildUnit(widget, "songrequest", {
1405
1444
  content: {
@@ -1428,9 +1467,10 @@ function mapMediaShare(widget) {
1428
1467
  playerVisible: false,
1429
1468
  defaultVolume: 70,
1430
1469
  autoAdvanceOnEnd: true,
1431
- // SE-only knob preserved for round-trip provenance; Lumia ignores
1432
- // it (LumiaStream auto-advance fires on `durationSeconds + grace`).
1433
- se_timeLeft: v.timeLeft === true
1470
+ // Source-side knob preserved for round-trip provenance; Lumia
1471
+ // ignores it at runtime (auto-advance fires on
1472
+ // `durationSeconds + grace`).
1473
+ importMeta: buildImportMeta(widget, { timeLeft: v.timeLeft === true })
1434
1474
  },
1435
1475
  css: {
1436
1476
  fontFamily: "Roboto",
@@ -1440,7 +1480,7 @@ function mapMediaShare(widget) {
1440
1480
  background: "transparent",
1441
1481
  fontSize: 18
1442
1482
  }
1443
- });
1483
+ }, ctx);
1444
1484
  }
1445
1485
  var SE_KAPPAGEN_EVENT_TO_LUMIA_ALERT = {
1446
1486
  follower: "twitch-follower",
@@ -1463,7 +1503,7 @@ var SE_KAPPAGEN_EVENT_TO_LUMIA_ALERT = {
1463
1503
  // All three drop silently; the rest of the widget (size, spawn zone,
1464
1504
  // per-event flags for the remaining events) still imports.
1465
1505
  };
1466
- function mapKappagen(widget) {
1506
+ function mapKappagen(widget, ctx) {
1467
1507
  const v = widget.variables ?? {};
1468
1508
  const eventsFromNested = v.eventsConfig ? {
1469
1509
  subscriber: v.eventsConfig.subscriber?.enabled ?? true,
@@ -1504,21 +1544,25 @@ function mapKappagen(widget) {
1504
1544
  ffz: v.ffz !== false,
1505
1545
  sevenTv: v.sevenTv !== false
1506
1546
  },
1507
- // SE-only knobs preserved as provenance; no Lumia rendering for these.
1508
- se_mode: v.mode,
1509
- se_cheersplosions: !!v.cheersplosions,
1510
- se_emotesplosion: v.emotesplosion,
1511
- se_emotesplosion_dir: v.emotesplosiondir,
1512
- se_emotesplosion_duration: v.emotesplosionduration,
1513
- se_spawn_zone: v.spawnzone,
1514
- se_fullscreen: !!v.fullscreen,
1515
- se_direction: v.direction
1547
+ // Source-side knobs preserved under the generic importMeta
1548
+ // envelope — no Lumia rendering for these today.
1549
+ importMeta: buildImportMeta(widget, {
1550
+ mode: v.mode,
1551
+ cheersplosions: !!v.cheersplosions,
1552
+ emotesplosion: v.emotesplosion,
1553
+ emotesplosionDir: v.emotesplosiondir,
1554
+ emotesplosionDuration: v.emotesplosionduration,
1555
+ spawnZone: v.spawnzone,
1556
+ fullscreen: !!v.fullscreen,
1557
+ direction: v.direction
1558
+ })
1516
1559
  },
1517
1560
  css: { background: "transparent" }
1518
- }
1561
+ },
1562
+ ctx
1519
1563
  );
1520
1564
  }
1521
- function mapLeaderboardFromVariables(widget, currency) {
1565
+ function mapLeaderboardFromVariables(widget, currency, ctx) {
1522
1566
  const v = widget.variables ?? {};
1523
1567
  const count = v.limit ?? v.visibleItems ?? 10;
1524
1568
  const window = v.period === "weekly" ? "week" : v.period === "monthly" ? "month" : "alltime";
@@ -1535,15 +1579,17 @@ function mapLeaderboardFromVariables(widget, currency) {
1535
1579
  showCurrentPoints: false,
1536
1580
  showAllTimePoints: true,
1537
1581
  itemGap: 8,
1538
- // Preserve SE knobs that don't map cleanly so they survive the round-trip
1539
- // and the user can re-introspect later if behaviour differs.
1540
- se_period: v.period,
1541
- se_offset: v.offset,
1542
- se_layout: v.layout
1582
+ // Source-side knobs that don't map cleanly survive under the
1583
+ // generic importMeta envelope so the user can re-introspect.
1584
+ importMeta: buildImportMeta(widget, {
1585
+ period: v.period,
1586
+ offset: v.offset,
1587
+ layout: v.layout
1588
+ })
1543
1589
  }
1544
- });
1590
+ }, ctx);
1545
1591
  }
1546
- function mapGiveaway(widget) {
1592
+ function mapGiveaway(widget, ctx) {
1547
1593
  const v = widget.variables ?? {};
1548
1594
  const primary = v.colors?.primary ?? "#393853";
1549
1595
  const accent = v.colors?.accent ?? "#FF4076";
@@ -1569,8 +1615,6 @@ function mapGiveaway(widget) {
1569
1615
  // preference via provenance — a future renderer can read it without us
1570
1616
  // having lied about the toggle. Same for `displayTicketsPurchased`.
1571
1617
  hideParticipantOnStart: false,
1572
- se_displayEntrants: v.displayEntrants !== false,
1573
- se_displayTicketsPurchased: v.displayTicketsPurchased !== false,
1574
1618
  removeWhenRedeemed: false,
1575
1619
  showWinningModal: true,
1576
1620
  playSoundFx: true,
@@ -1587,14 +1631,20 @@ function mapGiveaway(widget) {
1587
1631
  raffleEndedText: "The raffle has ended, and the selection of the winner will take place shortly.",
1588
1632
  raffleTimerText: "Time since raffle started",
1589
1633
  raffleCountdownText: "Raffle will end after",
1590
- // SE-specific knobs with no Lumia equivalent — preserved as provenance.
1591
- se_timeout: v.timeout,
1592
- se_enable_timeout: v.enableTimeOut,
1593
- se_display_entrants: v.displayEntrants,
1594
- se_display_tickets_purchased: v.displayTicketsPurchased,
1595
- se_image_style_width: v.imageStyle?.width
1634
+ // Source-side knobs with no Lumia equivalent — preserved under
1635
+ // the generic importMeta envelope. `displayEntrants` / `displayTicketsPurchased`
1636
+ // are SE's persistent "never show" toggles; Lumia has no equivalent
1637
+ // today (its `hideParticipantOnStart` is transient), so we preserve
1638
+ // the preference for a future renderer.
1639
+ importMeta: buildImportMeta(widget, {
1640
+ timeout: v.timeout,
1641
+ enableTimeout: v.enableTimeOut,
1642
+ displayEntrants: v.displayEntrants,
1643
+ displayTicketsPurchased: v.displayTicketsPurchased,
1644
+ imageStyleWidth: v.imageStyle?.width
1645
+ })
1596
1646
  }
1597
- });
1647
+ }, ctx);
1598
1648
  }
1599
1649
  var HYPETRAIN_LISTENER_KEYS = /* @__PURE__ */ new Set([
1600
1650
  "hypetrain-latest",
@@ -1625,7 +1675,7 @@ function isHypetrainCustomWidget(widget) {
1625
1675
  if (enabled.length === 0) return false;
1626
1676
  return enabled.every((listener) => HYPETRAIN_LISTENER_KEYS.has(listener));
1627
1677
  }
1628
- function mapHypetrain(widget) {
1678
+ function mapHypetrain(widget, ctx) {
1629
1679
  const v = widget.variables ?? {};
1630
1680
  return buildUnit(widget, "hypetrain", {
1631
1681
  content: {
@@ -1662,15 +1712,16 @@ function mapHypetrain(widget) {
1662
1712
  trainLocoImageUrl: "https://storage.lumiastream.com/overlays/global/hypetrain/train_loco_default.png",
1663
1713
  rocketImageUrl: "https://storage.lumiastream.com/overlays/global/hypetrain/rocket_default.png",
1664
1714
  infernoImageUrl: "https://storage.lumiastream.com/overlays/global/hypetrain/inferno_default.png",
1665
- // SE config preserved verbatim as provenance no Lumia equivalent for
1666
- // most of these knobs (per-widget animation tunables, sound URLs the
1667
- // streamer might want to migrate manually, etc.). Users can inspect
1668
- // these via the overlay JSON if they want to recreate a specific look.
1669
- se_variables: v
1715
+ // Source config preserved verbatim under the generic importMeta
1716
+ // envelope no Lumia equivalent for most of these knobs
1717
+ // (per-widget animation tunables, sound URLs the streamer might
1718
+ // want to migrate manually, etc.). Users can inspect via the
1719
+ // overlay JSON if they want to recreate a specific look.
1720
+ importMeta: buildImportMeta(widget, { variables: v })
1670
1721
  }
1671
- });
1722
+ }, ctx);
1672
1723
  }
1673
- function mapUnsupportedAsText(widget) {
1724
+ function mapUnsupportedAsText(widget, ctx) {
1674
1725
  return buildUnit(widget, "text", {
1675
1726
  content: {
1676
1727
  value: `[Unsupported SE widget: ${widget.type}]`,
@@ -1684,7 +1735,7 @@ function mapUnsupportedAsText(widget) {
1684
1735
  fontWeight: "bold",
1685
1736
  background: "rgba(0,0,0,0.4)"
1686
1737
  }
1687
- });
1738
+ }, ctx);
1688
1739
  }
1689
1740
 
1690
1741
  // src/se-import/mappers/hypecup.ts
@@ -1754,7 +1805,7 @@ function categoriesFromListeners(listeners) {
1754
1805
  if (events.subs) events.resubs = true;
1755
1806
  return events;
1756
1807
  }
1757
- function mapHypeCup(widget) {
1808
+ function mapHypeCup(widget, ctx) {
1758
1809
  const v = widget.variables ?? {};
1759
1810
  const widgetWidth = typeof v.width === "number" && v.width > 0 ? v.width : parsePx(widget.css?.width);
1760
1811
  const widgetHeight = typeof v.height === "number" && v.height > 0 ? v.height : parsePx(widget.css?.height);
@@ -1801,18 +1852,21 @@ function mapHypeCup(widget) {
1801
1852
  // can enable in Settings; SE has no equivalent so nothing to lift.
1802
1853
  goal: { active: false, currency: "tips", target: 100, current: 0, readoutTemplate: "{{current}} / {{target}}", textColor: "#ffffff", hitAt: null },
1803
1854
  break: { enabled: false, command: "!breakjar", rebuildDelay: 5e3 },
1804
- // SE-only knobs that don't have a Lumia equivalent — preserved as
1805
- // provenance for future phases (per-cup polygons + enter/movement
1806
- // physics tweaks). `se_types` removed — now consumed into sprites above.
1807
- se_enter_from: v.enterFrom ?? null,
1808
- se_prevent_movement: !!v.preventMovement,
1809
- se_fullscreen: !!v.fullscreen,
1810
- se_cups: v.cups ?? null
1855
+ // Source-side knobs without a Lumia equivalent — preserved under
1856
+ // the generic `importMeta` envelope for future phases (per-cup
1857
+ // polygons + enter/movement physics tweaks). The per-event
1858
+ // sprite sets were already lifted into native `sprites` above.
1859
+ importMeta: buildImportMeta(widget, {
1860
+ enterFrom: v.enterFrom ?? null,
1861
+ preventMovement: !!v.preventMovement,
1862
+ fullscreen: !!v.fullscreen,
1863
+ cups: v.cups ?? null
1864
+ })
1811
1865
  },
1812
1866
  css: {
1813
1867
  background: "transparent"
1814
1868
  }
1815
- });
1869
+ }, ctx);
1816
1870
  }
1817
1871
  function clamp01(n) {
1818
1872
  if (!Number.isFinite(n)) return 0.5;
@@ -1859,7 +1913,7 @@ function listenerIsOn(listeners, key) {
1859
1913
  if (Array.isArray(listeners)) return listeners.includes(key);
1860
1914
  return listeners[key] === true;
1861
1915
  }
1862
- function mapStreamBoss(widget) {
1916
+ function mapStreamBoss(widget, ctx) {
1863
1917
  const v = widget.variables ?? {};
1864
1918
  const baseHp = typeof v.baseHP === "number" && v.baseHP > 0 ? Math.round(v.baseHP) : 1e3;
1865
1919
  const seDamage = v.damage ?? {};
@@ -1926,22 +1980,24 @@ function mapStreamBoss(widget) {
1926
1980
  bossNameColor: "",
1927
1981
  botChat: "off",
1928
1982
  chatAsSelf: false,
1929
- // SE-only knobs preserved as provenance for future passes. None
1930
- // have direct Lumia analogs today:
1931
- // - `right` anchors the boss to the right edge of its bounds;
1932
- // Lumia uses layer position instead, so this is informational.
1933
- // - `bossName` controls vertical name position in SE; our themes
1934
- // don't expose that level of fine-grained tweaking yet.
1935
- // - `mode` SE's gameplay mode hint ('overkill', etc.).
1936
- se_right: !!v.right,
1937
- se_bossNamePosition: typeof v.bossName === "string" ? v.bossName : null,
1938
- se_mode: typeof v.mode === "string" ? v.mode : null,
1939
- se_showName: v.showName !== false
1983
+ // Source-side knobs preserved as provenance for future passes.
1984
+ // None have direct Lumia analogs today:
1985
+ // - `right` anchors the boss to the right edge of its
1986
+ // bounds; Lumia uses layer position instead.
1987
+ // - `bossNamePosition` SE-controlled vertical name position; our
1988
+ // themes don't expose that level of tweaking.
1989
+ // - `mode` source-side gameplay mode hint ('overkill').
1990
+ importMeta: buildImportMeta(widget, {
1991
+ right: !!v.right,
1992
+ bossNamePosition: typeof v.bossName === "string" ? v.bossName : null,
1993
+ mode: typeof v.mode === "string" ? v.mode : null,
1994
+ showName: v.showName !== false
1995
+ })
1940
1996
  },
1941
1997
  css: {
1942
1998
  background: "transparent"
1943
1999
  }
1944
- });
2000
+ }, ctx);
1945
2001
  if (theme === "card") {
1946
2002
  const MIN_CARD_WIDTH = 480;
1947
2003
  const MIN_CARD_HEIGHT = 110;
@@ -1952,25 +2008,6 @@ function mapStreamBoss(widget) {
1952
2008
  }
1953
2009
 
1954
2010
  // 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
2011
  function isWidgetFlaggedOff(_seType) {
1975
2012
  return false;
1976
2013
  }
@@ -2025,7 +2062,7 @@ var GOAL_TYPES = /* @__PURE__ */ new Set([
2025
2062
  "se-widget-merch-goal"
2026
2063
  ]);
2027
2064
  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) {
2065
+ function dispatch(widget, ctx) {
2029
2066
  const t = widget.type;
2030
2067
  console.log("[SE-IMPORT] dispatch widget", {
2031
2068
  seType: t,
@@ -2036,46 +2073,46 @@ function dispatch(widget) {
2036
2073
  hasCss: !!widget.css
2037
2074
  });
2038
2075
  if (isWidgetFlaggedOff(t)) {
2039
- return { unit: mapUnsupportedAsText(widget), status: "placeholder", lumiaType: "text", flaggedOff: true };
2076
+ return { unit: mapUnsupportedAsText(widget, ctx), status: "placeholder", lumiaType: "text", flaggedOff: true };
2040
2077
  }
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" };
2078
+ if (t === "text") return { unit: mapText(widget, ctx), status: "direct", lumiaType: "text" };
2079
+ if (t === "image") return { unit: mapImage(widget, ctx), status: "direct", lumiaType: "image" };
2080
+ if (t === "video") return { unit: mapVideo(widget, ctx), status: "direct", lumiaType: "video" };
2044
2081
  if (t in READOUT_TYPES) {
2045
- return { unit: mapReadout(widget, READOUT_TYPES[t]), status: "direct", lumiaType: "text" };
2082
+ return { unit: mapReadout(widget, READOUT_TYPES[t], ctx), status: "direct", lumiaType: "text" };
2046
2083
  }
2047
- if (GOAL_TYPES.has(t)) return { unit: mapGoal(widget), status: "direct", lumiaType: "goal" };
2084
+ if (GOAL_TYPES.has(t)) return { unit: mapGoal(widget, ctx), status: "direct", lumiaType: "goal" };
2048
2085
  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 };
2086
+ return { unit: mapUnsupportedAsText(widget, ctx), status: "placeholder", lumiaType: "text", flaggedOff: true };
2050
2087
  }
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" };
2088
+ if (t === "se-widget-alert-box") return { unit: mapAlertBox(widget, {}, ctx), status: "direct", lumiaType: "alert" };
2089
+ if (t === "se-widget-alert-box-merch") return { unit: mapAlertBox(widget, { onlyEvents: ["merch"] }, ctx), status: "direct", lumiaType: "alert" };
2090
+ if (t === "se-widget-donor-drive-alert") return { unit: mapSingleAlert(widget, "donordrive-donation", ctx), status: "direct", lumiaType: "alert" };
2091
+ if (t === "se-widget-extralife-alert") return { unit: mapSingleAlert(widget, "extralife-donation", ctx), status: "direct", lumiaType: "alert" };
2092
+ if (t === "se-widget-tiltify-alert") return { unit: mapSingleAlert(widget, "tiltify-campaignDonation", ctx), status: "direct", lumiaType: "alert" };
2093
+ if (EVENTLIST_TYPES.has(t)) return { unit: mapEventList(widget, ctx), status: "direct", lumiaType: "eventlist" };
2094
+ if (t === "se-widget-top-tippers-list") return { unit: mapLeaderboardFromVariables(widget, "tips", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
2095
+ if (t === "se-widget-top-cheerers-list") return { unit: mapLeaderboardFromVariables(widget, "cheers", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
2096
+ if (t === "se-widget-top-gifters-list") return { unit: mapLeaderboardFromVariables(widget, "gifts", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
2097
+ if (t === "se-widget-credit-roll") return { unit: mapCredits(widget, ctx), status: "partial", lumiaType: "credits" };
2098
+ if (t === "se-widget-twitch-chat") return { unit: mapChatbox(widget, ctx), status: "partial", lumiaType: "chatbox" };
2099
+ 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" };
2100
+ if (t === "se-widget-countdown") return { unit: mapTimer(widget, ctx), status: "partial", lumiaType: "timer" };
2101
+ if (t === "se-widget-current-song") return { unit: mapNowPlaying(widget, ctx), status: "partial", lumiaType: "nowplaying" };
2102
+ if (t === "se-widget-kappagen") return { unit: mapKappagen(widget, ctx), status: "partial", lumiaType: "emotealert" };
2103
+ if (t === "se-widget-giveaway") return { unit: mapGiveaway(widget, ctx), status: "direct", lumiaType: "raffle" };
2104
+ if (t === "se-widget-media-share") return { unit: mapMediaShare(widget, ctx), status: "partial", lumiaType: "songrequest" };
2105
+ if (t === "se-widget-hype-cup") return { unit: mapHypeCup(widget, ctx), status: "partial", lumiaType: "tipjar" };
2106
+ if (t === "se-widget-bit-boss") return { unit: mapStreamBoss(widget, ctx), status: "partial", lumiaType: "streamboss" };
2107
+ if (t === "se-widget-hype-train") return { unit: mapHypetrain(widget, ctx), status: "partial", lumiaType: "hypetrain" };
2071
2108
  if (t === "se-widget-custom-event-list" && isHypetrainCustomWidget(widget)) {
2072
- return { unit: mapHypetrain(widget), status: "partial", lumiaType: "hypetrain" };
2109
+ return { unit: mapHypetrain(widget, ctx), status: "partial", lumiaType: "hypetrain" };
2073
2110
  }
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" };
2111
+ if (t === "se-widget-custom-event-list") return { unit: mapCustom(widget, ctx), status: "partial", lumiaType: "custom" };
2112
+ if (t === "se-widget-snow") return { unit: mapSnow(widget, ctx), status: "direct", lumiaType: "video" };
2113
+ if (t === "se-widget-halloween-2019") return { unit: mapSlideshow(widget, ctx), status: "direct", lumiaType: "slideshow" };
2114
+ if (SEASONAL_IMAGE_TYPES.has(t)) return { unit: mapImage(widget, ctx), status: "direct", lumiaType: "image" };
2115
+ return { unit: mapUnsupportedAsText(widget, ctx), status: "placeholder", lumiaType: "text" };
2079
2116
  }
2080
2117
  function recordCoverage(coverage, result, seType) {
2081
2118
  const existing = coverage.mappings.find((m) => m.seType === seType);
@@ -2133,11 +2170,19 @@ function rewriteAssetURLs(overlay, mapping) {
2133
2170
  settings: JSON.parse(rewritten)
2134
2171
  };
2135
2172
  }
2173
+ var MAX_MIRROR_ASSET_BYTES = 100 * 1024 * 1024;
2136
2174
  async function mirrorOneAsset(url, upload, signal) {
2137
2175
  const res = await fetch(url, { mode: "cors", signal });
2138
2176
  if (!res.ok) throw new Error(`Source returned HTTP ${res.status}`);
2177
+ const declared = Number(res.headers.get("content-length"));
2178
+ if (Number.isFinite(declared) && declared > MAX_MIRROR_ASSET_BYTES) {
2179
+ throw new Error(`Asset is ${(declared / (1024 * 1024)).toFixed(1)} MB \u2014 over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
2180
+ }
2139
2181
  const blob = await res.blob();
2140
2182
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
2183
+ if (blob.size > MAX_MIRROR_ASSET_BYTES) {
2184
+ throw new Error(`Asset is ${(blob.size / (1024 * 1024)).toFixed(1)} MB \u2014 over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
2185
+ }
2141
2186
  const filename = filenameFromURL(url);
2142
2187
  const type = blob.type || guessMime(filename);
2143
2188
  const file = new File([blob], filename, { type });
@@ -4835,13 +4880,19 @@ function CustomOverlayPreview({
4835
4880
  }) {
4836
4881
  const srcdoc = useMemo5(() => {
4837
4882
  const safeData = JSON.stringify(data ?? {});
4838
- const safeJs = JSON.stringify(`(async () => { ${js || ""} })()`);
4883
+ const safeJs = (js || "").replace(/<\/script>/gi, "<\\/script>");
4839
4884
  const safeHtml = (html || "").replace(/<\/script>/gi, "<\\/script>");
4840
4885
  const safeCss = (css || "").replace(/<\/style>/gi, "<\\/style>");
4841
4886
  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
4887
  window.DATA = ${safeData};
4843
4888
  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>'); }
4889
+ function __seImportShowError(e){document.body.insertAdjacentHTML('beforeend','<pre style="color:#fca5a5;font:11px monospace;padding:8px;">' + (e && e.message ? e.message : e) + '</pre>');}
4890
+ window.addEventListener('error', function(ev){__seImportShowError(ev.error || ev.message);});
4891
+ window.addEventListener('unhandledrejection', function(ev){__seImportShowError(ev.reason);});
4892
+ </script><script>
4893
+ (async function __seImportUserScript(){
4894
+ ${safeJs}
4895
+ })().catch(__seImportShowError);
4845
4896
  </script></body></html>`;
4846
4897
  }, [html, css, js, data]);
4847
4898
  return /* @__PURE__ */ jsx14(
@@ -5399,7 +5450,8 @@ function StepURL({
5399
5450
  url,
5400
5451
  setUrl,
5401
5452
  error,
5402
- busy
5453
+ busy,
5454
+ onSwitchToJwt
5403
5455
  }) {
5404
5456
  return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--narrow"), children: [
5405
5457
  /* @__PURE__ */ jsx14(
@@ -5445,7 +5497,16 @@ function StepURL({
5445
5497
  ] })
5446
5498
  ] })
5447
5499
  ] }),
5448
- error && /* @__PURE__ */ jsx14(ErrorPanel, { message: error.message, hint: error.hint })
5500
+ error && /* @__PURE__ */ jsx14(ErrorPanel, { message: error.message, hint: error.hint }),
5501
+ error?.editorUrl && onSwitchToJwt && /* @__PURE__ */ jsx14("div", { style: { marginTop: 12 }, children: /* @__PURE__ */ jsx14(
5502
+ LSButton,
5503
+ {
5504
+ type: "button",
5505
+ color: "primary",
5506
+ onClick: onSwitchToJwt,
5507
+ label: "Connect with JWT instead"
5508
+ }
5509
+ ) })
5449
5510
  ] });
5450
5511
  }
5451
5512
  function StepDiscovery({
@@ -5952,10 +6013,7 @@ function SEImportWizard({
5952
6013
  const mirrorStartedRef = useRef4(false);
5953
6014
  const mirrorAbortRef = useRef4(null);
5954
6015
  const filenameCacheRef = useRef4(/* @__PURE__ */ new Map());
5955
- const assetUrls = useMemo5(
5956
- () => result ? findSEAssetURLs(result.overlay) : [],
5957
- [result]
5958
- );
6016
+ const [assetUrls, setAssetUrls] = useState6([]);
5959
6017
  const currentItem = originalReviewItems[reviewIndex];
5960
6018
  const currentRow = currentItem ? rowStates[currentItem.moduleId] ?? { state: "pending" } : void 0;
5961
6019
  const handleConnect = useCallback3(async () => {
@@ -6053,6 +6111,7 @@ function SEImportWizard({
6053
6111
  }
6054
6112
  const imported = importSEBootstrap(bootstrap);
6055
6113
  setResult(imported);
6114
+ setAssetUrls(findSEAssetURLs(imported.overlay));
6056
6115
  setOptions((o) => ({ ...o, name: imported.overlay.name }));
6057
6116
  setOriginalReviewItems(imported.reviewItems);
6058
6117
  setRowStates(
@@ -6068,9 +6127,10 @@ function SEImportWizard({
6068
6127
  mirrorStartedRef.current = false;
6069
6128
  setStep("discovery");
6070
6129
  } catch (e) {
6130
+ const detail = e?.message ?? String(e);
6071
6131
  setLoadError({
6072
- message: "Could not load the overlay.",
6073
- hint: e?.message ?? String(e)
6132
+ message: detail || "Could not load the overlay.",
6133
+ hint: detail ? "If this looks transient, retry in a moment. Otherwise check the URL / token and try again." : void 0
6074
6134
  });
6075
6135
  } finally {
6076
6136
  setLoading(false);
@@ -6101,7 +6161,8 @@ function SEImportWizard({
6101
6161
  if (!parts.apikey) {
6102
6162
  setLoadError({
6103
6163
  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}`
6164
+ 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}`,
6165
+ editorUrl: true
6105
6166
  });
6106
6167
  return;
6107
6168
  }
@@ -6110,6 +6171,7 @@ function SEImportWizard({
6110
6171
  const bootstrap = await fetchSEBootstrap(parts);
6111
6172
  const imported = importSEBootstrap(bootstrap);
6112
6173
  setResult(imported);
6174
+ setAssetUrls(findSEAssetURLs(imported.overlay));
6113
6175
  setOptions((previous) => ({ ...previous, name: imported.overlay.name }));
6114
6176
  setOriginalReviewItems(imported.reviewItems);
6115
6177
  setRowStates(
@@ -6123,27 +6185,21 @@ function SEImportWizard({
6123
6185
  setReviewIndex(0);
6124
6186
  setStep("discovery");
6125
6187
  } catch (error) {
6188
+ const detail = error?.message ?? String(error);
6126
6189
  setLoadError({
6127
- message: "Could not load the overlay.",
6128
- hint: error?.message
6190
+ message: detail || "Could not load the overlay.",
6191
+ hint: detail ? "If this looks transient, retry in a moment. Otherwise check the URL / token and try again." : void 0
6129
6192
  });
6130
6193
  } finally {
6131
6194
  setLoading(false);
6132
6195
  }
6133
6196
  }, [url]);
6197
+ const MIRROR_CONCURRENCY = 5;
6134
6198
  const processAssetRow = useCallback3(
6135
6199
  async (assetUrl, idx, existingByFilename, signal) => {
6136
6200
  const filename = filenameFromURL(assetUrl);
6137
6201
  const reuseUrl = existingByFilename.get(filename);
6138
6202
  if (reuseUrl) {
6139
- setResult(
6140
- (current) => current ? {
6141
- ...current,
6142
- overlay: rewriteAssetURLs(current.overlay, {
6143
- [assetUrl]: reuseUrl
6144
- })
6145
- } : current
6146
- );
6147
6203
  return { state: "reused", newUrl: reuseUrl };
6148
6204
  }
6149
6205
  if (filename.toLowerCase().endsWith(".svg"))
@@ -6160,14 +6216,6 @@ function SEImportWizard({
6160
6216
  signal
6161
6217
  );
6162
6218
  existingByFilename.set(filename, newUrl);
6163
- setResult(
6164
- (current) => current ? {
6165
- ...current,
6166
- overlay: rewriteAssetURLs(current.overlay, {
6167
- [assetUrl]: newUrl
6168
- })
6169
- } : current
6170
- );
6171
6219
  return { state: "done", newUrl };
6172
6220
  } catch (error) {
6173
6221
  if (error?.name === "AbortError" || signal?.aborted)
@@ -6177,8 +6225,15 @@ function SEImportWizard({
6177
6225
  },
6178
6226
  [uploadAsset]
6179
6227
  );
6228
+ const applyRewrite = useCallback3((mapping) => {
6229
+ if (Object.keys(mapping).length === 0) return;
6230
+ setResult(
6231
+ (current) => current ? { ...current, overlay: rewriteAssetURLs(current.overlay, mapping) } : current
6232
+ );
6233
+ }, []);
6180
6234
  const runMirror = useCallback3(async () => {
6181
6235
  if (!result) return;
6236
+ mirrorAbortRef.current?.abort();
6182
6237
  mirrorAbortRef.current = new AbortController();
6183
6238
  const signal = mirrorAbortRef.current.signal;
6184
6239
  setMirrorRunning(true);
@@ -6193,21 +6248,33 @@ function SEImportWizard({
6193
6248
  if (name && asset.url) existingByFilename.set(name, asset.url);
6194
6249
  }
6195
6250
  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
- }
6251
+ const rewriteMap = {};
6252
+ let cursor = 0;
6253
+ const worker = async () => {
6254
+ while (true) {
6255
+ if (signal.aborted) return;
6256
+ const i = cursor++;
6257
+ if (i >= initialRows.length) return;
6258
+ const outcome = await processAssetRow(
6259
+ initialRows[i].url,
6260
+ i,
6261
+ existingByFilename,
6262
+ signal
6263
+ );
6264
+ if (signal.aborted) return;
6265
+ if (outcome.newUrl && (outcome.state === "done" || outcome.state === "reused")) {
6266
+ rewriteMap[initialRows[i].url] = outcome.newUrl;
6267
+ }
6268
+ setMirrorRows(
6269
+ (previous) => previous.map((row, idx) => idx === i ? { ...row, ...outcome } : row)
6270
+ );
6271
+ }
6272
+ };
6273
+ const poolSize = Math.min(MIRROR_CONCURRENCY, initialRows.length);
6274
+ await Promise.all(Array.from({ length: poolSize }, worker));
6275
+ if (!signal.aborted) applyRewrite(rewriteMap);
6209
6276
  setMirrorRunning(false);
6210
- }, [assetUrls, existingAssets, processAssetRow, result]);
6277
+ }, [applyRewrite, assetUrls, existingAssets, processAssetRow, result]);
6211
6278
  const retryMirrorRow = useCallback3(
6212
6279
  async (idx) => {
6213
6280
  const rowUrl = mirrorRows[idx]?.url;
@@ -6221,8 +6288,11 @@ function SEImportWizard({
6221
6288
  setMirrorRows(
6222
6289
  (previous) => previous.map((row, i) => i === idx ? { ...row, ...outcome } : row)
6223
6290
  );
6291
+ if (outcome.newUrl && (outcome.state === "done" || outcome.state === "reused")) {
6292
+ applyRewrite({ [rowUrl]: outcome.newUrl });
6293
+ }
6224
6294
  },
6225
- [mirrorRows, processAssetRow]
6295
+ [applyRewrite, mirrorRows, processAssetRow]
6226
6296
  );
6227
6297
  useEffect6(() => {
6228
6298
  if (step === "mirror" && !mirrorStartedRef.current) {
@@ -6230,6 +6300,13 @@ function SEImportWizard({
6230
6300
  void runMirror();
6231
6301
  }
6232
6302
  }, [step, runMirror]);
6303
+ useEffect6(() => {
6304
+ return () => {
6305
+ setJwt("");
6306
+ setSeClient(null);
6307
+ mirrorAbortRef.current?.abort();
6308
+ };
6309
+ }, []);
6233
6310
  useEffect6(() => {
6234
6311
  if (step === "pick" && seClient && overlayList === null && !overlaysLoading) {
6235
6312
  void loadOverlayList(seClient);
@@ -6624,7 +6701,12 @@ function SEImportWizard({
6624
6701
  url,
6625
6702
  setUrl,
6626
6703
  error: loadError,
6627
- busy: loading
6704
+ busy: loading,
6705
+ onSwitchToJwt: () => {
6706
+ setLoadError(null);
6707
+ setEntryMode("jwt");
6708
+ setStep("connect");
6709
+ }
6628
6710
  }
6629
6711
  ),
6630
6712
  step === "discovery" && result && /* @__PURE__ */ jsx14(StepDiscovery, { result, assetCount: assetUrls.length }),
@@ -6737,6 +6819,7 @@ function extractErrorMessage(err) {
6737
6819
  }
6738
6820
 
6739
6821
  // src/se-import/index.ts
6822
+ var IMPORT_META_KEY = "importMeta";
6740
6823
  var REVIEW_REASONS = {
6741
6824
  "se-widget-bit-boss": "No native Lumia equivalent \u2014 bit boss fight bar with HP/damage states.",
6742
6825
  // `se-widget-hype-cup` intentionally omitted — it now routes to the native
@@ -6828,145 +6911,148 @@ function importSEBootstrap(bootstrap) {
6828
6911
  name: seOverlay?.name,
6829
6912
  canvas: { width: seOverlay?.settings?.width, height: seOverlay?.settings?.height },
6830
6913
  widgetCount: widgets.length,
6831
- widgetTypes: widgets.map((w) => w.type),
6832
- fullBootstrap: bootstrap
6914
+ widgetTypes: widgets.map((w) => w.type)
6833
6915
  });
6834
6916
  const importCanvas = {
6835
6917
  width: seOverlay?.settings?.width ?? 1920,
6836
6918
  height: seOverlay?.settings?.height ?? 1080
6837
6919
  };
6838
- setImportCanvas(importCanvas);
6920
+ const ctx = { canvas: importCanvas };
6839
6921
  const layers = [];
6840
6922
  const modules = {};
6841
6923
  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;
6924
+ const seUidToLumiaGroupId = {};
6925
+ const groupWidgetByUid = {};
6926
+ for (const w of widgets) {
6927
+ if (w.type !== "se-widget-group") continue;
6928
+ const uid = w.variables?.uid;
6929
+ if (typeof uid !== "string") continue;
6930
+ const lumiaGroupId = nanoid3();
6931
+ seUidToLumiaGroupId[uid] = lumiaGroupId;
6932
+ groupWidgetByUid[uid] = w;
6933
+ }
6934
+ for (const uid of Object.keys(groupWidgetByUid)) {
6935
+ const w = groupWidgetByUid[uid];
6936
+ const lumiaGroupId = seUidToLumiaGroupId[uid];
6937
+ const parentUid = typeof w.group === "string" ? w.group : null;
6938
+ const parentLumiaGroupId = parentUid ? seUidToLumiaGroupId[parentUid] ?? null : null;
6939
+ const bounds = seCssToBounds(w.css, { width: 200, height: 200 }, importCanvas);
6940
+ const layer = {
6941
+ id: lumiaGroupId,
6942
+ type: "group",
6943
+ group: parentLumiaGroupId,
6944
+ bounds: {
6945
+ x: bounds.x,
6946
+ y: bounds.y,
6947
+ width: bounds.width,
6948
+ height: bounds.height,
6949
+ opacity: bounds.opacity,
6950
+ zIndex: bounds.zIndex,
6951
+ rotate: 0,
6952
+ scale: [1, 1],
6953
+ matrix: ""
6954
+ },
6955
+ state: {
6956
+ locked: w.locked ?? false,
6957
+ visible: w.visible ?? true,
6958
+ // SE's `variables.expanded` is the editor's collapsed/open state.
6959
+ expanded: w.variables?.expanded ?? true
6960
+ }
6961
+ };
6962
+ const module = {
6963
+ id: lumiaGroupId,
6964
+ loaded: true,
6965
+ settings: { title: defaultLabelForSEWidget(w), type: "group" },
6966
+ lights: [],
6967
+ css: {},
6968
+ content: {
6969
+ [IMPORT_META_KEY]: {
6970
+ source: "streamelements",
6971
+ widgetType: w.type,
6972
+ importedAt: (/* @__PURE__ */ new Date()).toISOString(),
6973
+ widget: w,
6974
+ reason: "Imported as native group",
6975
+ status: "group"
6904
6976
  }
6977
+ },
6978
+ events: {},
6979
+ variables: {}
6980
+ };
6981
+ layers.push(layer);
6982
+ modules[lumiaGroupId] = module;
6983
+ recordCoverage(coverage, { unit: { layer, module }, status: "direct", lumiaType: "group" }, w.type);
6984
+ }
6985
+ for (const w of widgets) {
6986
+ if (w.type === "se-widget-group") continue;
6987
+ const result = dispatch(w, ctx);
6988
+ if (typeof w.group === "string") {
6989
+ const parentLumiaGroupId = seUidToLumiaGroupId[w.group];
6990
+ if (parentLumiaGroupId) {
6991
+ result.unit.layer.group = parentLumiaGroupId;
6905
6992
  }
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,
6993
+ }
6994
+ layers.push(result.unit.layer);
6995
+ const module = result.unit.module;
6996
+ if (result.status === "placeholder" || result.status === "template") {
6997
+ module.content = {
6998
+ ...module.content ?? {},
6999
+ [IMPORT_META_KEY]: {
7000
+ source: "streamelements",
7001
+ widgetType: w.type,
7002
+ importedAt: (/* @__PURE__ */ new Date()).toISOString(),
7003
+ widget: w,
6922
7004
  reason: reasonFor(w.type, result.status, result.flaggedOff),
7005
+ status: result.status,
6923
7006
  flaggedOff: result.flaggedOff
6924
- });
6925
- }
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
- }
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.");
7007
+ }
7008
+ };
7009
+ reviewItems.push({
7010
+ moduleId: module.id,
7011
+ seWidget: w,
7012
+ status: result.status,
7013
+ reason: reasonFor(w.type, result.status, result.flaggedOff),
7014
+ flaggedOff: result.flaggedOff
7015
+ });
6937
7016
  }
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
- },
7017
+ modules[module.id] = module;
7018
+ recordCoverage(coverage, result, w.type);
7019
+ }
7020
+ if (coverage.mappings.some((m) => m.status === "placeholder")) {
7021
+ coverage.notes.push("Some widgets imported as placeholder text layers \u2014 see SE/05-widget-to-module-mapping.md for status.");
7022
+ }
7023
+ if (coverage.mappings.some((m) => m.seType === "se-widget-merch-goal")) {
7024
+ coverage.notes.push("Merch-goal widgets imported as generic goals \u2014 see SE/03-missing-variables.md \xA78 for the missing data source.");
7025
+ }
7026
+ if (coverage.mappings.some((m) => m.seType === "se-widget-custom-event-list")) {
7027
+ 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.");
7028
+ }
7029
+ const overlay = {
7030
+ uuid: "",
7031
+ listen_id: "",
7032
+ name: seOverlay?.name ? `[SE] ${seOverlay.name}` : `Imported from StreamElements (${seOverlay?._id ?? ""})`,
7033
+ description: seOverlay?._id ? `Imported from StreamElements overlay ${seOverlay._id}` : "Imported from StreamElements",
7034
+ settings: {
6951
7035
  metadata: {
6952
- width: seOverlay?.settings?.width ?? null,
6953
- height: seOverlay?.settings?.height ?? null
7036
+ width: seOverlay?.settings?.width ?? 1920,
7037
+ height: seOverlay?.settings?.height ?? 1080
6954
7038
  },
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
- }
7039
+ layers,
7040
+ modules
7041
+ },
7042
+ metadata: {
7043
+ width: seOverlay?.settings?.width ?? null,
7044
+ height: seOverlay?.settings?.height ?? null
7045
+ },
7046
+ layers_count: layers.length,
7047
+ hasFullSettings: true
7048
+ };
7049
+ console.log("[SE-IMPORT] import complete", {
7050
+ overlayName: overlay.name,
7051
+ layerCount: overlay.layers_count,
7052
+ coverage,
7053
+ reviewItemCount: reviewItems.length
7054
+ });
7055
+ return { overlay, coverage, reviewItems };
6970
7056
  }
6971
7057
  function applyReviewAction(result, moduleId, action, payload) {
6972
7058
  if (action === "keep") return result;
@@ -6994,11 +7080,17 @@ function applyReviewAction(result, moduleId, action, payload) {
6994
7080
  bounds: existingLayer.bounds,
6995
7081
  state: existingLayer.state
6996
7082
  };
6997
- const provenance2 = existing2.content?.se_provenance;
7083
+ const importMeta2 = existing2.content?.[IMPORT_META_KEY];
7084
+ const mergedLights = (existing2.lights?.length ?? 0) > 0 ? existing2.lights : transplant.module.lights;
7085
+ const mergedEvents = existing2.events && Object.keys(existing2.events).length > 0 ? { ...transplant.module.events, ...existing2.events } : transplant.module.events;
7086
+ const mergedVariables = existing2.variables && Object.keys(existing2.variables).length > 0 ? { ...transplant.module.variables, ...existing2.variables } : transplant.module.variables;
6998
7087
  const newModule = {
6999
7088
  ...transplant.module,
7000
7089
  id: moduleId,
7001
- content: { ...transplant.module.content ?? {}, se_provenance: provenance2 }
7090
+ lights: mergedLights,
7091
+ events: mergedEvents,
7092
+ variables: mergedVariables,
7093
+ content: { ...transplant.module.content ?? {}, [IMPORT_META_KEY]: importMeta2 }
7002
7094
  };
7003
7095
  const rootOldNewId = transplant.layer.id;
7004
7096
  const remappedExtras = (transplant.extras ?? []).map((e) => ({
@@ -7025,7 +7117,7 @@ function applyReviewAction(result, moduleId, action, payload) {
7025
7117
  const existing = modules[moduleId];
7026
7118
  const generated = payload;
7027
7119
  if (!existing || !generated) return result;
7028
- const provenance = existing.content?.se_provenance;
7120
+ const importMeta = existing.content?.[IMPORT_META_KEY];
7029
7121
  const replaced = {
7030
7122
  ...existing,
7031
7123
  settings: { ...existing.settings ?? {}, type: "custom" },
@@ -7039,7 +7131,7 @@ function applyReviewAction(result, moduleId, action, payload) {
7039
7131
  data: parseLooseJson(generated.data) ?? {},
7040
7132
  configs: parseLooseJson(generated.configs) ?? [],
7041
7133
  flavor: "ai-generated",
7042
- se_provenance: provenance
7134
+ [IMPORT_META_KEY]: importMeta
7043
7135
  }
7044
7136
  };
7045
7137
  modules[moduleId] = replaced;
@@ -7068,6 +7160,11 @@ function buildAIPromptForSEWidget(widget, canvas) {
7068
7160
  const listener = typeof widget.listener === "string" ? widget.listener : null;
7069
7161
  const listeners = widget.listeners && typeof widget.listeners === "object" && !Array.isArray(widget.listeners) ? Object.keys(widget.listeners) : null;
7070
7162
  const landing = getAILandingBounds(widget, canvas);
7163
+ const VARIABLES_BUDGET = 6e3;
7164
+ const fullVariables = JSON.stringify(variables, null, 2);
7165
+ const truncated = fullVariables.length > VARIABLES_BUDGET;
7166
+ const variablesJson = truncated ? `${fullVariables.slice(0, VARIABLES_BUDGET)}
7167
+ \u2026[truncated \u2014 ${fullVariables.length - VARIABLES_BUDGET} more bytes elided]` : fullVariables;
7071
7168
  return [
7072
7169
  "Recreate a StreamElements overlay widget as a Lumia Stream custom overlay (HTML + CSS + JS).",
7073
7170
  "",
@@ -7075,10 +7172,11 @@ function buildAIPromptForSEWidget(widget, canvas) {
7075
7172
  listener ? `Primary event listener: ${listener}` : "",
7076
7173
  listeners ? `Subscribes to events: ${listeners.join(", ")}` : "",
7077
7174
  `Canvas: ${landing.width} \xD7 ${landing.height}`,
7175
+ truncated ? "Note: the widget config below is truncated \u2014 focus on visible/structural fields and ignore tail content you cannot see." : "",
7078
7176
  "",
7079
7177
  "Widget configuration (JSON):",
7080
7178
  "```json",
7081
- JSON.stringify(variables, null, 2).slice(0, 6e3),
7179
+ variablesJson,
7082
7180
  "```",
7083
7181
  "",
7084
7182
  "Constraints \u2014 your output must run inside a Lumia Custom Overlay sandboxed iframe:",
@@ -7098,7 +7196,6 @@ export {
7098
7196
  SEAuthError,
7099
7197
  SEClient,
7100
7198
  SEImportWizard,
7101
- SE_IMPORT_FLAGS,
7102
7199
  SE_WIDGET_TO_MARKETPLACE_CANDIDATES,
7103
7200
  applyReviewAction,
7104
7201
  buildAIPromptForSEWidget,