@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/index.js CHANGED
@@ -3205,10 +3205,11 @@ function defaultLabelForSEWidget(widget) {
3205
3205
  const tail = words.slice(1).join(" ");
3206
3206
  return tail ? `${head} ${tail}` : head;
3207
3207
  }
3208
- function translateSeText(input, listener) {
3208
+ function translateSeText(input, listener, passthrough) {
3209
3209
  if (!input) return "";
3210
3210
  const ctx = listener ? listenerPlaceholderContext(listener) : {};
3211
3211
  return input.replace(/\{([a-zA-Z][a-zA-Z0-9_.]*)\}/g, (_match, raw) => {
3212
+ if (passthrough?.has(raw)) return `{{${raw}}}`;
3212
3213
  const mapped = ctx[raw] ?? SE_TO_LUMIA_PLACEHOLDER[raw] ?? raw;
3213
3214
  return `{{${mapped}}}`;
3214
3215
  });
@@ -3402,14 +3403,10 @@ function seLayoutUsesPushText(seLayout) {
3402
3403
 
3403
3404
  // src/se-import/mappers/build.ts
3404
3405
  import { nanoid } from "nanoid";
3405
- var currentImportCanvas = null;
3406
- function setImportCanvas(canvas) {
3407
- currentImportCanvas = canvas;
3408
- }
3409
- function buildUnit(widget, lumiaType, moduleExtras) {
3406
+ function buildUnit(widget, lumiaType, moduleExtras, ctx) {
3410
3407
  const id = nanoid();
3411
3408
  const defaults = LUMIA_DEFAULT_SIZES[lumiaType] ?? { width: 400, height: 400 };
3412
- const bounds = seCssToBounds(widget.css, defaults, currentImportCanvas ?? void 0);
3409
+ const bounds = seCssToBounds(widget.css, defaults, ctx?.canvas);
3413
3410
  const layer = {
3414
3411
  id,
3415
3412
  state: { visible: widget.visible ?? true, locked: widget.locked ?? false },
@@ -3461,7 +3458,7 @@ function seExtraTextCss(seCss, scrolling) {
3461
3458
  }
3462
3459
  return extra;
3463
3460
  }
3464
- function mapText(widget) {
3461
+ function mapText(widget, ctx) {
3465
3462
  const value = translateSeText(widget.text?.value ?? "");
3466
3463
  const seCss = widget.text?.css ?? {};
3467
3464
  return buildUnit(widget, "text", {
@@ -3480,17 +3477,17 @@ function mapText(widget) {
3480
3477
  background: "transparent",
3481
3478
  ...seExtraTextCss(seCss, widget.text?.scrolling)
3482
3479
  }
3483
- });
3480
+ }, ctx);
3484
3481
  }
3485
- function mapImage(widget) {
3482
+ function mapImage(widget, ctx) {
3486
3483
  return buildUnit(widget, "image", {
3487
3484
  content: {
3488
3485
  src: widget.image?.src ?? "",
3489
3486
  loop: true
3490
3487
  }
3491
- });
3488
+ }, ctx);
3492
3489
  }
3493
- function mapVideo(widget) {
3490
+ function mapVideo(widget, ctx) {
3494
3491
  return buildUnit(widget, "video", {
3495
3492
  content: {
3496
3493
  src: widget.video?.src ?? "",
@@ -3498,10 +3495,10 @@ function mapVideo(widget) {
3498
3495
  loop: true,
3499
3496
  muted: false
3500
3497
  }
3501
- });
3498
+ }, ctx);
3502
3499
  }
3503
3500
  var LUMIA_SNOW_WEBM = "https://storage.lumiastream.com/overlays/global/seasonal/snow.webm";
3504
- function mapSnow(widget) {
3501
+ function mapSnow(widget, ctx) {
3505
3502
  return buildUnit(widget, "video", {
3506
3503
  content: {
3507
3504
  src: LUMIA_SNOW_WEBM,
@@ -3509,9 +3506,9 @@ function mapSnow(widget) {
3509
3506
  loop: true,
3510
3507
  muted: true
3511
3508
  }
3512
- });
3509
+ }, ctx);
3513
3510
  }
3514
- function mapReadout(widget, fallbackVar) {
3511
+ function mapReadout(widget, fallbackVar, ctx) {
3515
3512
  const explicitListener = widget.variables?.listener;
3516
3513
  const explicit = typeof explicitListener === "string" ? explicitListener : null;
3517
3514
  const widgetListener = typeof widget.listener === "string" ? widget.listener : null;
@@ -3534,7 +3531,7 @@ function mapReadout(widget, fallbackVar) {
3534
3531
  background: "transparent",
3535
3532
  ...seExtraTextCss(seCss, widget.text?.scrolling)
3536
3533
  }
3537
- });
3534
+ }, ctx);
3538
3535
  }
3539
3536
 
3540
3537
  // src/se-import/mappers/alert.ts
@@ -3566,13 +3563,14 @@ var SE_EVENT_TO_LUMIA_ALERT = {
3566
3563
  // mapping prevents misleading "channel-point redemption" alerts.
3567
3564
  };
3568
3565
  function pickGraphics(g) {
3569
- if (!g) return { type: "image", content: { src: "", loop: false, volume: 1 } };
3566
+ const DEFAULT_GRAPHIC_VOLUME = 0.5;
3567
+ if (!g) return { type: "image", content: { src: "", loop: false, volume: DEFAULT_GRAPHIC_VOLUME } };
3570
3568
  return {
3571
3569
  type: g.type === "video" ? "video" : "image",
3572
3570
  content: {
3573
3571
  src: g.src ?? "",
3574
3572
  loop: false,
3575
- volume: g.volume ?? 0.5
3573
+ volume: g.volume ?? DEFAULT_GRAPHIC_VOLUME
3576
3574
  }
3577
3575
  };
3578
3576
  }
@@ -3743,7 +3741,7 @@ function pickBaseAlertFields(event) {
3743
3741
  const { on: _on, variations: _variations, matchEmptyCondition: _mec, ...base } = event;
3744
3742
  return base;
3745
3743
  }
3746
- function mapAlertBox(widget, opts = {}) {
3744
+ function mapAlertBox(widget, opts = {}, ctx) {
3747
3745
  const v = widget.variables ?? {};
3748
3746
  const events = {};
3749
3747
  const eventKeys = opts.onlyEvents ?? ["follower", "subscriber", "tip", "cheer", "raid", "merch", "purchase", "charityCampaignDonation"];
@@ -3758,9 +3756,9 @@ function mapAlertBox(widget, opts = {}) {
3758
3756
  if (firstEvent == null) firstEvent = built;
3759
3757
  }
3760
3758
  const baseAlert = firstEvent ? pickBaseAlertFields(firstEvent) : {};
3761
- return buildUnit(widget, "alert", { alert: { ...baseAlert, events } });
3759
+ return buildUnit(widget, "alert", { alert: { ...baseAlert, events } }, ctx);
3762
3760
  }
3763
- function mapSingleAlert(widget, lumiaAlertKey) {
3761
+ function mapSingleAlert(widget, lumiaAlertKey, ctx) {
3764
3762
  const v = widget.variables ?? {};
3765
3763
  const topText = widget.text;
3766
3764
  const merged = { ...v };
@@ -3784,7 +3782,7 @@ function mapSingleAlert(widget, lumiaAlertKey) {
3784
3782
  [lumiaAlertKey]: built
3785
3783
  };
3786
3784
  const baseAlert = pickBaseAlertFields(built);
3787
- return buildUnit(widget, "alert", { alert: { ...baseAlert, events } });
3785
+ return buildUnit(widget, "alert", { alert: { ...baseAlert, events } }, ctx);
3788
3786
  }
3789
3787
 
3790
3788
  // src/se-import/mappers/goal.ts
@@ -3822,7 +3820,7 @@ function sourceFromListener(listener) {
3822
3820
  if (listener.startsWith("merch-")) return "merch_orders";
3823
3821
  return "custom";
3824
3822
  }
3825
- function mapGoal(widget) {
3823
+ function mapGoal(widget, ctx) {
3826
3824
  const v = widget.variables ?? {};
3827
3825
  const listener = v.goalListener ?? (typeof widget.listener === "string" ? widget.listener : null);
3828
3826
  const charity = charityGoalVariables(widget.type);
@@ -3883,7 +3881,7 @@ function mapGoal(widget) {
3883
3881
  textShadow: seTextCss["text-shadow"] ?? "rgb(0, 0, 0) 1px 1px 1px",
3884
3882
  background: "transparent"
3885
3883
  }
3886
- });
3884
+ }, ctx);
3887
3885
  }
3888
3886
 
3889
3887
  // src/se-import/mappers/custom.ts
@@ -3895,19 +3893,46 @@ var SE_FIELD_TYPE_TO_LUMIA = {
3895
3893
  slider: "slider",
3896
3894
  colorpicker: "colorpicker",
3897
3895
  fontpicker: "fontpicker",
3898
- image: "input"
3896
+ // SE's font picker variant — same control surface as `fontpicker` for our
3897
+ // purposes; the SE renderer pulls Google Fonts at runtime which Lumia's
3898
+ // fontpicker also does.
3899
+ googleFont: "fontpicker",
3900
+ // SE asset uploads — the asset URL lands in the `value` as a CDN string,
3901
+ // which our `input` renders fine as-is. Once Lumia ships dedicated asset/
3902
+ // audio pickers wired through the asset library, flip these mappings.
3903
+ image: "input",
3904
+ "image-input": "input",
3905
+ "sound-input": "input",
3906
+ "video-input": "input",
3907
+ // Multi-line text — `input` accepts strings of any length; the editor
3908
+ // surfaces a textarea by inspecting `value.length` heuristically.
3909
+ textarea: "input",
3910
+ hidden: "input",
3911
+ // SE's button is a "fire a JS action" trigger with no persisted value.
3912
+ // Lumia has no equivalent; surface as a non-editable label.
3913
+ button: "input"
3899
3914
  };
3915
+ var SE_RESERVED_FIELD_KEYS = /* @__PURE__ */ new Set(["widgetName", "widgetAuthor", "widgetDuration"]);
3900
3916
  function translateConfigs(seFields) {
3901
3917
  if (!seFields) return [];
3902
- return Object.entries(seFields).map(([key, f]) => ({
3903
- key,
3904
- label: f.label ?? key,
3905
- type: SE_FIELD_TYPE_TO_LUMIA[f.type ?? "text"] ?? "input",
3906
- value: f.value,
3907
- options: f.options
3908
- }));
3918
+ return Object.entries(seFields).filter(([key]) => !SE_RESERVED_FIELD_KEYS.has(key)).map(([key, f]) => {
3919
+ const seType = f.type ?? "text";
3920
+ return {
3921
+ key,
3922
+ label: f.label ?? key,
3923
+ type: SE_FIELD_TYPE_TO_LUMIA[seType] ?? "input",
3924
+ value: f.value,
3925
+ options: f.options,
3926
+ // Preserve the original SE type + group so a future editor UX
3927
+ // upgrade can render the right control (e.g. an actual asset
3928
+ // picker for `image-input`) without re-importing. Mentioned in
3929
+ // docs/SE_IMPORT_GAPS.md under "Other widgets > custom-event-list".
3930
+ seType,
3931
+ ...f.group ? { seGroup: f.group } : {}
3932
+ };
3933
+ });
3909
3934
  }
3910
- function mapCustom(widget) {
3935
+ function mapCustom(widget, ctx) {
3911
3936
  const v = widget.variables ?? {};
3912
3937
  const parse = (x) => {
3913
3938
  if (x == null) return void 0;
@@ -3930,7 +3955,7 @@ function mapCustom(widget) {
3930
3955
  data: fieldData ?? {},
3931
3956
  flavor: "streamelements"
3932
3957
  }
3933
- });
3958
+ }, ctx);
3934
3959
  }
3935
3960
 
3936
3961
  // src/se-import/mappers/misc.ts
@@ -3958,7 +3983,7 @@ function paddingShorthand(message) {
3958
3983
  const px = (n) => `${n ?? 0}px`;
3959
3984
  return `${px(t)} ${px(r)} ${px(b)} ${px(l)}`;
3960
3985
  }
3961
- function mapChatbox(widget) {
3986
+ function mapChatbox(widget, ctx) {
3962
3987
  const v = widget.variables ?? {};
3963
3988
  const textCss = widget.text?.css ?? {};
3964
3989
  const messageCss = textCss.message ?? {};
@@ -4042,7 +4067,7 @@ function mapChatbox(widget) {
4042
4067
  se_highlight_users: v.highlight,
4043
4068
  se_text_css: textCss
4044
4069
  }
4045
- });
4070
+ }, ctx);
4046
4071
  }
4047
4072
  var SE_LISTENER_TO_LUMIA_CATEGORY = {
4048
4073
  "follower-latest": "follower",
@@ -4108,7 +4133,7 @@ function computeListenerFilters(seListeners, explicitListener) {
4108
4133
  if (allowed.size === 0) return [];
4109
4134
  return ALL_LUMIA_EVENTLIST_CATEGORIES.filter((c) => !allowed.has(c));
4110
4135
  }
4111
- function mapEventList(widget) {
4136
+ function mapEventList(widget, ctx) {
4112
4137
  const v = widget.variables ?? {};
4113
4138
  const filters = computeListenerFilters(widget.listeners, widget.listener);
4114
4139
  const maxItems = v.visibleEvents ?? v.visibleItems ?? 5;
@@ -4137,7 +4162,7 @@ function mapEventList(widget) {
4137
4162
  showDetailedText: false,
4138
4163
  hideAlertMessage: false
4139
4164
  }
4140
- });
4165
+ }, ctx);
4141
4166
  }
4142
4167
  var SE_CREDITS_EVENTS_TO_SHOW = {
4143
4168
  mods: false,
@@ -4277,7 +4302,7 @@ var CREDITS_DEFAULT_TEMPLATES = {
4277
4302
  topSuperchats: "{{username}} x{{amount}}",
4278
4303
  topRedemptions: "{{username}} x{{amount}}"
4279
4304
  };
4280
- function mapCredits(widget) {
4305
+ function mapCredits(widget, ctx) {
4281
4306
  const v = widget.variables ?? {};
4282
4307
  const itemGap = typeof v.padding === "number" ? v.padding : 8;
4283
4308
  const reverseFlow = v.direction === "bottom";
@@ -4331,7 +4356,7 @@ function mapCredits(widget) {
4331
4356
  textAlign: "center",
4332
4357
  background: "transparent"
4333
4358
  }
4334
- });
4359
+ }, ctx);
4335
4360
  }
4336
4361
  function inferSlideshowMediaType(mime, url) {
4337
4362
  if (typeof mime === "string") {
@@ -4350,7 +4375,7 @@ function nameFromUrl(url) {
4350
4375
  return url;
4351
4376
  }
4352
4377
  }
4353
- function mapSlideshow(widget) {
4378
+ function mapSlideshow(widget, ctx) {
4354
4379
  const v = widget.variables ?? {};
4355
4380
  const items = (v.images ?? []).map((raw) => {
4356
4381
  const url = typeof raw === "string" ? raw : raw.url ?? raw.src ?? "";
@@ -4390,21 +4415,13 @@ function mapSlideshow(widget) {
4390
4415
  se_interval: v.interval,
4391
4416
  se_raw_duration: v.duration
4392
4417
  }
4393
- });
4418
+ }, ctx);
4394
4419
  }
4395
- function mapTimer(widget) {
4420
+ function mapTimer(widget, ctx) {
4396
4421
  const v = widget.variables ?? {};
4397
4422
  const textValue = typeof widget.text?.value === "string" ? widget.text.value : void 0;
4398
- const mapTemplate = (template) => {
4399
- const protectedTemplate = template.replace(
4400
- /\{(days|hours|minutes|seconds)\}/g,
4401
- "__SE_COUNTDOWN_$1__"
4402
- );
4403
- return translateSeText(protectedTemplate).replace(
4404
- /__SE_COUNTDOWN_(days|hours|minutes|seconds)__/g,
4405
- "{{$1}}"
4406
- );
4407
- };
4423
+ const TIMER_PASSTHROUGH = /* @__PURE__ */ new Set(["days", "hours", "minutes", "seconds"]);
4424
+ const mapTemplate = (template) => translateSeText(template, null, TIMER_PASSTHROUGH);
4408
4425
  const absoluteDate = v.date ?? v.endsAt ?? v.endDate;
4409
4426
  const target = absoluteDate ? Date.parse(absoluteDate) : NaN;
4410
4427
  const hasValidAbsoluteDate = Number.isFinite(target);
@@ -4480,9 +4497,9 @@ function mapTimer(widget) {
4480
4497
  se_date_picker: v.datePicker,
4481
4498
  se_exact_time: v.exactTime
4482
4499
  }
4483
- });
4500
+ }, ctx);
4484
4501
  }
4485
- function mapNowPlaying(widget) {
4502
+ function mapNowPlaying(widget, ctx) {
4486
4503
  const v = widget.variables ?? {};
4487
4504
  const source = v.provider === "youtube" ? "youtubemusic" : "spotify";
4488
4505
  return buildUnit(widget, "nowplaying", {
@@ -4512,9 +4529,9 @@ function mapNowPlaying(widget) {
4512
4529
  textShadow: "rgb(0, 0, 0) 1px 1px 1px",
4513
4530
  background: "transparent"
4514
4531
  }
4515
- });
4532
+ }, ctx);
4516
4533
  }
4517
- function mapMediaShare(widget) {
4534
+ function mapMediaShare(widget, ctx) {
4518
4535
  const v = widget.variables ?? {};
4519
4536
  return buildUnit(widget, "songrequest", {
4520
4537
  content: {
@@ -4555,7 +4572,7 @@ function mapMediaShare(widget) {
4555
4572
  background: "transparent",
4556
4573
  fontSize: 18
4557
4574
  }
4558
- });
4575
+ }, ctx);
4559
4576
  }
4560
4577
  var SE_KAPPAGEN_EVENT_TO_LUMIA_ALERT = {
4561
4578
  follower: "twitch-follower",
@@ -4578,7 +4595,7 @@ var SE_KAPPAGEN_EVENT_TO_LUMIA_ALERT = {
4578
4595
  // All three drop silently; the rest of the widget (size, spawn zone,
4579
4596
  // per-event flags for the remaining events) still imports.
4580
4597
  };
4581
- function mapKappagen(widget) {
4598
+ function mapKappagen(widget, ctx) {
4582
4599
  const v = widget.variables ?? {};
4583
4600
  const eventsFromNested = v.eventsConfig ? {
4584
4601
  subscriber: v.eventsConfig.subscriber?.enabled ?? true,
@@ -4630,10 +4647,11 @@ function mapKappagen(widget) {
4630
4647
  se_direction: v.direction
4631
4648
  },
4632
4649
  css: { background: "transparent" }
4633
- }
4650
+ },
4651
+ ctx
4634
4652
  );
4635
4653
  }
4636
- function mapLeaderboardFromVariables(widget, currency) {
4654
+ function mapLeaderboardFromVariables(widget, currency, ctx) {
4637
4655
  const v = widget.variables ?? {};
4638
4656
  const count = v.limit ?? v.visibleItems ?? 10;
4639
4657
  const window = v.period === "weekly" ? "week" : v.period === "monthly" ? "month" : "alltime";
@@ -4656,9 +4674,9 @@ function mapLeaderboardFromVariables(widget, currency) {
4656
4674
  se_offset: v.offset,
4657
4675
  se_layout: v.layout
4658
4676
  }
4659
- });
4677
+ }, ctx);
4660
4678
  }
4661
- function mapGiveaway(widget) {
4679
+ function mapGiveaway(widget, ctx) {
4662
4680
  const v = widget.variables ?? {};
4663
4681
  const primary = v.colors?.primary ?? "#393853";
4664
4682
  const accent = v.colors?.accent ?? "#FF4076";
@@ -4709,7 +4727,7 @@ function mapGiveaway(widget) {
4709
4727
  se_display_tickets_purchased: v.displayTicketsPurchased,
4710
4728
  se_image_style_width: v.imageStyle?.width
4711
4729
  }
4712
- });
4730
+ }, ctx);
4713
4731
  }
4714
4732
  var HYPETRAIN_LISTENER_KEYS = /* @__PURE__ */ new Set([
4715
4733
  "hypetrain-latest",
@@ -4740,7 +4758,7 @@ function isHypetrainCustomWidget(widget) {
4740
4758
  if (enabled.length === 0) return false;
4741
4759
  return enabled.every((listener) => HYPETRAIN_LISTENER_KEYS.has(listener));
4742
4760
  }
4743
- function mapHypetrain(widget) {
4761
+ function mapHypetrain(widget, ctx) {
4744
4762
  const v = widget.variables ?? {};
4745
4763
  return buildUnit(widget, "hypetrain", {
4746
4764
  content: {
@@ -4783,9 +4801,9 @@ function mapHypetrain(widget) {
4783
4801
  // these via the overlay JSON if they want to recreate a specific look.
4784
4802
  se_variables: v
4785
4803
  }
4786
- });
4804
+ }, ctx);
4787
4805
  }
4788
- function mapUnsupportedAsText(widget) {
4806
+ function mapUnsupportedAsText(widget, ctx) {
4789
4807
  return buildUnit(widget, "text", {
4790
4808
  content: {
4791
4809
  value: `[Unsupported SE widget: ${widget.type}]`,
@@ -4799,7 +4817,7 @@ function mapUnsupportedAsText(widget) {
4799
4817
  fontWeight: "bold",
4800
4818
  background: "rgba(0,0,0,0.4)"
4801
4819
  }
4802
- });
4820
+ }, ctx);
4803
4821
  }
4804
4822
 
4805
4823
  // src/se-import/mappers/hypecup.ts
@@ -4869,7 +4887,7 @@ function categoriesFromListeners(listeners) {
4869
4887
  if (events.subs) events.resubs = true;
4870
4888
  return events;
4871
4889
  }
4872
- function mapHypeCup(widget) {
4890
+ function mapHypeCup(widget, ctx) {
4873
4891
  const v = widget.variables ?? {};
4874
4892
  const widgetWidth = typeof v.width === "number" && v.width > 0 ? v.width : parsePx(widget.css?.width);
4875
4893
  const widgetHeight = typeof v.height === "number" && v.height > 0 ? v.height : parsePx(widget.css?.height);
@@ -4927,7 +4945,7 @@ function mapHypeCup(widget) {
4927
4945
  css: {
4928
4946
  background: "transparent"
4929
4947
  }
4930
- });
4948
+ }, ctx);
4931
4949
  }
4932
4950
  function clamp01(n) {
4933
4951
  if (!Number.isFinite(n)) return 0.5;
@@ -4974,7 +4992,7 @@ function listenerIsOn(listeners, key) {
4974
4992
  if (Array.isArray(listeners)) return listeners.includes(key);
4975
4993
  return listeners[key] === true;
4976
4994
  }
4977
- function mapStreamBoss(widget) {
4995
+ function mapStreamBoss(widget, ctx) {
4978
4996
  const v = widget.variables ?? {};
4979
4997
  const baseHp = typeof v.baseHP === "number" && v.baseHP > 0 ? Math.round(v.baseHP) : 1e3;
4980
4998
  const seDamage = v.damage ?? {};
@@ -5056,7 +5074,7 @@ function mapStreamBoss(widget) {
5056
5074
  css: {
5057
5075
  background: "transparent"
5058
5076
  }
5059
- });
5077
+ }, ctx);
5060
5078
  if (theme === "card") {
5061
5079
  const MIN_CARD_WIDTH = 480;
5062
5080
  const MIN_CARD_HEIGHT = 110;
@@ -5067,25 +5085,6 @@ function mapStreamBoss(widget) {
5067
5085
  }
5068
5086
 
5069
5087
  // src/se-import/feature-flags.ts
5070
- var SE_IMPORT_FLAGS = {
5071
- // (Removed) IMPORT_CUSTOM_EMBED: custom-event-list now routes to native eventlist
5072
- // by default with a one-click "switch to custom HTML" option in the review step.
5073
- // (Removed) IMPORT_MERCH_GOAL: Fourthwall integration provides session/total order
5074
- // counters now (see fourthwall.manager.ts ORDER_PLACED handler); merch goal maps
5075
- // to {{fourthwall_total_*}} via the goal mapper (per-listener: orders → order_count,
5076
- // items → items_count, total → order_amount).
5077
- // (Removed) IMPORT_SEASONAL: seasonal widgets now route per-widget into existing
5078
- // modules (image / slideshow / video) rather than a single approximate canvas-
5079
- // particle template. See dispatcher.ts SEASONAL_IMAGE_TYPES comment for the
5080
- // rationale and /SE/05-widget-to-module-mapping.md §D for the current mapping.
5081
- // (Removed) IMPORT_MERCH_PRODUCTS: the merch-products-rotator now imports as an
5082
- // empty slideshow with a friendly layer title prompting the user to add product
5083
- // images manually. This is a degraded but honest UX — better than a placeholder
5084
- // text layer for the (rare) streamer who uses SE's merch rotator. A Fourthwall
5085
- // catalog fetcher would unlock fully-automatic import, but that's a multi-day
5086
- // project gated on a new Fourthwall API auth scope, an endpoint, and a refresh
5087
- // cadence.
5088
- };
5089
5088
  function isWidgetFlaggedOff(_seType) {
5090
5089
  return false;
5091
5090
  }
@@ -5140,7 +5139,7 @@ var GOAL_TYPES = /* @__PURE__ */ new Set([
5140
5139
  "se-widget-merch-goal"
5141
5140
  ]);
5142
5141
  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"]);
5143
- function dispatch(widget) {
5142
+ function dispatch(widget, ctx) {
5144
5143
  const t = widget.type;
5145
5144
  console.log("[SE-IMPORT] dispatch widget", {
5146
5145
  seType: t,
@@ -5151,46 +5150,46 @@ function dispatch(widget) {
5151
5150
  hasCss: !!widget.css
5152
5151
  });
5153
5152
  if (isWidgetFlaggedOff(t)) {
5154
- return { unit: mapUnsupportedAsText(widget), status: "placeholder", lumiaType: "text", flaggedOff: true };
5153
+ return { unit: mapUnsupportedAsText(widget, ctx), status: "placeholder", lumiaType: "text", flaggedOff: true };
5155
5154
  }
5156
- if (t === "text") return { unit: mapText(widget), status: "direct", lumiaType: "text" };
5157
- if (t === "image") return { unit: mapImage(widget), status: "direct", lumiaType: "image" };
5158
- if (t === "video") return { unit: mapVideo(widget), status: "direct", lumiaType: "video" };
5155
+ if (t === "text") return { unit: mapText(widget, ctx), status: "direct", lumiaType: "text" };
5156
+ if (t === "image") return { unit: mapImage(widget, ctx), status: "direct", lumiaType: "image" };
5157
+ if (t === "video") return { unit: mapVideo(widget, ctx), status: "direct", lumiaType: "video" };
5159
5158
  if (t in READOUT_TYPES) {
5160
- return { unit: mapReadout(widget, READOUT_TYPES[t]), status: "direct", lumiaType: "text" };
5159
+ return { unit: mapReadout(widget, READOUT_TYPES[t], ctx), status: "direct", lumiaType: "text" };
5161
5160
  }
5162
- if (GOAL_TYPES.has(t)) return { unit: mapGoal(widget), status: "direct", lumiaType: "goal" };
5161
+ if (GOAL_TYPES.has(t)) return { unit: mapGoal(widget, ctx), status: "direct", lumiaType: "goal" };
5163
5162
  if ((t === "se-widget-alert-box" || t === "se-widget-alert-box-merch") && hasSECustomCode(widget)) {
5164
- return { unit: mapUnsupportedAsText(widget), status: "placeholder", lumiaType: "text", flaggedOff: true };
5165
- }
5166
- if (t === "se-widget-alert-box") return { unit: mapAlertBox(widget), status: "direct", lumiaType: "alert" };
5167
- if (t === "se-widget-alert-box-merch") return { unit: mapAlertBox(widget, { onlyEvents: ["merch"] }), status: "direct", lumiaType: "alert" };
5168
- if (t === "se-widget-donor-drive-alert") return { unit: mapSingleAlert(widget, "donordrive-donation"), status: "direct", lumiaType: "alert" };
5169
- if (t === "se-widget-extralife-alert") return { unit: mapSingleAlert(widget, "extralife-donation"), status: "direct", lumiaType: "alert" };
5170
- if (t === "se-widget-tiltify-alert") return { unit: mapSingleAlert(widget, "tiltify-campaignDonation"), status: "direct", lumiaType: "alert" };
5171
- if (EVENTLIST_TYPES.has(t)) return { unit: mapEventList(widget), status: "direct", lumiaType: "eventlist" };
5172
- if (t === "se-widget-top-tippers-list") return { unit: mapLeaderboardFromVariables(widget, "tips"), status: "direct", lumiaType: "loyaltyleaderboard" };
5173
- if (t === "se-widget-top-cheerers-list") return { unit: mapLeaderboardFromVariables(widget, "cheers"), status: "direct", lumiaType: "loyaltyleaderboard" };
5174
- if (t === "se-widget-top-gifters-list") return { unit: mapLeaderboardFromVariables(widget, "gifts"), status: "direct", lumiaType: "loyaltyleaderboard" };
5175
- if (t === "se-widget-credit-roll") return { unit: mapCredits(widget), status: "partial", lumiaType: "credits" };
5176
- if (t === "se-widget-twitch-chat") return { unit: mapChatbox(widget), status: "partial", lumiaType: "chatbox" };
5177
- 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" };
5178
- if (t === "se-widget-countdown") return { unit: mapTimer(widget), status: "partial", lumiaType: "timer" };
5179
- if (t === "se-widget-current-song") return { unit: mapNowPlaying(widget), status: "partial", lumiaType: "nowplaying" };
5180
- if (t === "se-widget-kappagen") return { unit: mapKappagen(widget), status: "partial", lumiaType: "emotealert" };
5181
- if (t === "se-widget-giveaway") return { unit: mapGiveaway(widget), status: "direct", lumiaType: "raffle" };
5182
- if (t === "se-widget-media-share") return { unit: mapMediaShare(widget), status: "partial", lumiaType: "songrequest" };
5183
- if (t === "se-widget-hype-cup") return { unit: mapHypeCup(widget), status: "partial", lumiaType: "tipjar" };
5184
- if (t === "se-widget-bit-boss") return { unit: mapStreamBoss(widget), status: "partial", lumiaType: "streamboss" };
5185
- if (t === "se-widget-hype-train") return { unit: mapHypetrain(widget), status: "partial", lumiaType: "hypetrain" };
5163
+ return { unit: mapUnsupportedAsText(widget, ctx), status: "placeholder", lumiaType: "text", flaggedOff: true };
5164
+ }
5165
+ if (t === "se-widget-alert-box") return { unit: mapAlertBox(widget, {}, ctx), status: "direct", lumiaType: "alert" };
5166
+ if (t === "se-widget-alert-box-merch") return { unit: mapAlertBox(widget, { onlyEvents: ["merch"] }, ctx), status: "direct", lumiaType: "alert" };
5167
+ if (t === "se-widget-donor-drive-alert") return { unit: mapSingleAlert(widget, "donordrive-donation", ctx), status: "direct", lumiaType: "alert" };
5168
+ if (t === "se-widget-extralife-alert") return { unit: mapSingleAlert(widget, "extralife-donation", ctx), status: "direct", lumiaType: "alert" };
5169
+ if (t === "se-widget-tiltify-alert") return { unit: mapSingleAlert(widget, "tiltify-campaignDonation", ctx), status: "direct", lumiaType: "alert" };
5170
+ if (EVENTLIST_TYPES.has(t)) return { unit: mapEventList(widget, ctx), status: "direct", lumiaType: "eventlist" };
5171
+ if (t === "se-widget-top-tippers-list") return { unit: mapLeaderboardFromVariables(widget, "tips", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
5172
+ if (t === "se-widget-top-cheerers-list") return { unit: mapLeaderboardFromVariables(widget, "cheers", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
5173
+ if (t === "se-widget-top-gifters-list") return { unit: mapLeaderboardFromVariables(widget, "gifts", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
5174
+ if (t === "se-widget-credit-roll") return { unit: mapCredits(widget, ctx), status: "partial", lumiaType: "credits" };
5175
+ if (t === "se-widget-twitch-chat") return { unit: mapChatbox(widget, ctx), status: "partial", lumiaType: "chatbox" };
5176
+ 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" };
5177
+ if (t === "se-widget-countdown") return { unit: mapTimer(widget, ctx), status: "partial", lumiaType: "timer" };
5178
+ if (t === "se-widget-current-song") return { unit: mapNowPlaying(widget, ctx), status: "partial", lumiaType: "nowplaying" };
5179
+ if (t === "se-widget-kappagen") return { unit: mapKappagen(widget, ctx), status: "partial", lumiaType: "emotealert" };
5180
+ if (t === "se-widget-giveaway") return { unit: mapGiveaway(widget, ctx), status: "direct", lumiaType: "raffle" };
5181
+ if (t === "se-widget-media-share") return { unit: mapMediaShare(widget, ctx), status: "partial", lumiaType: "songrequest" };
5182
+ if (t === "se-widget-hype-cup") return { unit: mapHypeCup(widget, ctx), status: "partial", lumiaType: "tipjar" };
5183
+ if (t === "se-widget-bit-boss") return { unit: mapStreamBoss(widget, ctx), status: "partial", lumiaType: "streamboss" };
5184
+ if (t === "se-widget-hype-train") return { unit: mapHypetrain(widget, ctx), status: "partial", lumiaType: "hypetrain" };
5186
5185
  if (t === "se-widget-custom-event-list" && isHypetrainCustomWidget(widget)) {
5187
- return { unit: mapHypetrain(widget), status: "partial", lumiaType: "hypetrain" };
5186
+ return { unit: mapHypetrain(widget, ctx), status: "partial", lumiaType: "hypetrain" };
5188
5187
  }
5189
- if (t === "se-widget-custom-event-list") return { unit: mapCustom(widget), status: "partial", lumiaType: "custom" };
5190
- if (t === "se-widget-snow") return { unit: mapSnow(widget), status: "direct", lumiaType: "video" };
5191
- if (t === "se-widget-halloween-2019") return { unit: mapSlideshow(widget), status: "direct", lumiaType: "slideshow" };
5192
- if (SEASONAL_IMAGE_TYPES.has(t)) return { unit: mapImage(widget), status: "direct", lumiaType: "image" };
5193
- return { unit: mapUnsupportedAsText(widget), status: "placeholder", lumiaType: "text" };
5188
+ if (t === "se-widget-custom-event-list") return { unit: mapCustom(widget, ctx), status: "partial", lumiaType: "custom" };
5189
+ if (t === "se-widget-snow") return { unit: mapSnow(widget, ctx), status: "direct", lumiaType: "video" };
5190
+ if (t === "se-widget-halloween-2019") return { unit: mapSlideshow(widget, ctx), status: "direct", lumiaType: "slideshow" };
5191
+ if (SEASONAL_IMAGE_TYPES.has(t)) return { unit: mapImage(widget, ctx), status: "direct", lumiaType: "image" };
5192
+ return { unit: mapUnsupportedAsText(widget, ctx), status: "placeholder", lumiaType: "text" };
5194
5193
  }
5195
5194
  function recordCoverage(coverage, result, seType) {
5196
5195
  const existing = coverage.mappings.find((m) => m.seType === seType);
@@ -5248,11 +5247,19 @@ function rewriteAssetURLs(overlay, mapping) {
5248
5247
  settings: JSON.parse(rewritten)
5249
5248
  };
5250
5249
  }
5250
+ var MAX_MIRROR_ASSET_BYTES = 100 * 1024 * 1024;
5251
5251
  async function mirrorOneAsset(url, upload, signal) {
5252
5252
  const res = await fetch(url, { mode: "cors", signal });
5253
5253
  if (!res.ok) throw new Error(`Source returned HTTP ${res.status}`);
5254
+ const declared = Number(res.headers.get("content-length"));
5255
+ if (Number.isFinite(declared) && declared > MAX_MIRROR_ASSET_BYTES) {
5256
+ throw new Error(`Asset is ${(declared / (1024 * 1024)).toFixed(1)} MB \u2014 over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
5257
+ }
5254
5258
  const blob = await res.blob();
5255
5259
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
5260
+ if (blob.size > MAX_MIRROR_ASSET_BYTES) {
5261
+ throw new Error(`Asset is ${(blob.size / (1024 * 1024)).toFixed(1)} MB \u2014 over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
5262
+ }
5256
5263
  const filename = filenameFromURL(url);
5257
5264
  const type = blob.type || guessMime(filename);
5258
5265
  const file = new File([blob], filename, { type });
@@ -5965,13 +5972,19 @@ function CustomOverlayPreview({
5965
5972
  }) {
5966
5973
  const srcdoc = useMemo5(() => {
5967
5974
  const safeData = JSON.stringify(data ?? {});
5968
- const safeJs = JSON.stringify(`(async () => { ${js || ""} })()`);
5975
+ const safeJs = (js || "").replace(/<\/script>/gi, "<\\/script>");
5969
5976
  const safeHtml = (html || "").replace(/<\/script>/gi, "<\\/script>");
5970
5977
  const safeCss = (css || "").replace(/<\/style>/gi, "<\\/style>");
5971
5978
  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>
5972
5979
  window.DATA = ${safeData};
5973
5980
  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() };
5974
- 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>'); }
5981
+ function __seImportShowError(e){document.body.insertAdjacentHTML('beforeend','<pre style="color:#fca5a5;font:11px monospace;padding:8px;">' + (e && e.message ? e.message : e) + '</pre>');}
5982
+ window.addEventListener('error', function(ev){__seImportShowError(ev.error || ev.message);});
5983
+ window.addEventListener('unhandledrejection', function(ev){__seImportShowError(ev.reason);});
5984
+ </script><script>
5985
+ (async function __seImportUserScript(){
5986
+ ${safeJs}
5987
+ })().catch(__seImportShowError);
5975
5988
  </script></body></html>`;
5976
5989
  }, [html, css, js, data]);
5977
5990
  return /* @__PURE__ */ jsx14(
@@ -6529,7 +6542,8 @@ function StepURL({
6529
6542
  url,
6530
6543
  setUrl,
6531
6544
  error,
6532
- busy
6545
+ busy,
6546
+ onSwitchToJwt
6533
6547
  }) {
6534
6548
  return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--narrow"), children: [
6535
6549
  /* @__PURE__ */ jsx14(
@@ -6575,7 +6589,16 @@ function StepURL({
6575
6589
  ] })
6576
6590
  ] })
6577
6591
  ] }),
6578
- error && /* @__PURE__ */ jsx14(ErrorPanel, { message: error.message, hint: error.hint })
6592
+ error && /* @__PURE__ */ jsx14(ErrorPanel, { message: error.message, hint: error.hint }),
6593
+ error?.editorUrl && onSwitchToJwt && /* @__PURE__ */ jsx14("div", { style: { marginTop: 12 }, children: /* @__PURE__ */ jsx14(
6594
+ LSButton,
6595
+ {
6596
+ type: "button",
6597
+ color: "primary",
6598
+ onClick: onSwitchToJwt,
6599
+ label: "Connect with JWT instead"
6600
+ }
6601
+ ) })
6579
6602
  ] });
6580
6603
  }
6581
6604
  function StepDiscovery({
@@ -7082,10 +7105,7 @@ function SEImportWizard({
7082
7105
  const mirrorStartedRef = useRef4(false);
7083
7106
  const mirrorAbortRef = useRef4(null);
7084
7107
  const filenameCacheRef = useRef4(/* @__PURE__ */ new Map());
7085
- const assetUrls = useMemo5(
7086
- () => result ? findSEAssetURLs(result.overlay) : [],
7087
- [result]
7088
- );
7108
+ const [assetUrls, setAssetUrls] = useState6([]);
7089
7109
  const currentItem = originalReviewItems[reviewIndex];
7090
7110
  const currentRow = currentItem ? rowStates[currentItem.moduleId] ?? { state: "pending" } : void 0;
7091
7111
  const handleConnect = useCallback3(async () => {
@@ -7183,6 +7203,7 @@ function SEImportWizard({
7183
7203
  }
7184
7204
  const imported = importSEBootstrap(bootstrap);
7185
7205
  setResult(imported);
7206
+ setAssetUrls(findSEAssetURLs(imported.overlay));
7186
7207
  setOptions((o) => ({ ...o, name: imported.overlay.name }));
7187
7208
  setOriginalReviewItems(imported.reviewItems);
7188
7209
  setRowStates(
@@ -7198,9 +7219,10 @@ function SEImportWizard({
7198
7219
  mirrorStartedRef.current = false;
7199
7220
  setStep("discovery");
7200
7221
  } catch (e) {
7222
+ const detail = e?.message ?? String(e);
7201
7223
  setLoadError({
7202
- message: "Could not load the overlay.",
7203
- hint: e?.message ?? String(e)
7224
+ message: detail || "Could not load the overlay.",
7225
+ hint: detail ? "If this looks transient, retry in a moment. Otherwise check the URL / token and try again." : void 0
7204
7226
  });
7205
7227
  } finally {
7206
7228
  setLoading(false);
@@ -7231,7 +7253,8 @@ function SEImportWizard({
7231
7253
  if (!parts.apikey) {
7232
7254
  setLoadError({
7233
7255
  message: "That looks like an editor URL, which doesn't include an access token.",
7234
- hint: `Use the public preview URL (Overlay menu \u2192 Get URL \u2192 Public link). Overlay id: ${parts.overlayId}`
7256
+ 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}`,
7257
+ editorUrl: true
7235
7258
  });
7236
7259
  return;
7237
7260
  }
@@ -7240,6 +7263,7 @@ function SEImportWizard({
7240
7263
  const bootstrap = await fetchSEBootstrap(parts);
7241
7264
  const imported = importSEBootstrap(bootstrap);
7242
7265
  setResult(imported);
7266
+ setAssetUrls(findSEAssetURLs(imported.overlay));
7243
7267
  setOptions((previous) => ({ ...previous, name: imported.overlay.name }));
7244
7268
  setOriginalReviewItems(imported.reviewItems);
7245
7269
  setRowStates(
@@ -7253,27 +7277,21 @@ function SEImportWizard({
7253
7277
  setReviewIndex(0);
7254
7278
  setStep("discovery");
7255
7279
  } catch (error) {
7280
+ const detail = error?.message ?? String(error);
7256
7281
  setLoadError({
7257
- message: "Could not load the overlay.",
7258
- hint: error?.message
7282
+ message: detail || "Could not load the overlay.",
7283
+ hint: detail ? "If this looks transient, retry in a moment. Otherwise check the URL / token and try again." : void 0
7259
7284
  });
7260
7285
  } finally {
7261
7286
  setLoading(false);
7262
7287
  }
7263
7288
  }, [url]);
7289
+ const MIRROR_CONCURRENCY = 5;
7264
7290
  const processAssetRow = useCallback3(
7265
7291
  async (assetUrl, idx, existingByFilename, signal) => {
7266
7292
  const filename = filenameFromURL(assetUrl);
7267
7293
  const reuseUrl = existingByFilename.get(filename);
7268
7294
  if (reuseUrl) {
7269
- setResult(
7270
- (current) => current ? {
7271
- ...current,
7272
- overlay: rewriteAssetURLs(current.overlay, {
7273
- [assetUrl]: reuseUrl
7274
- })
7275
- } : current
7276
- );
7277
7295
  return { state: "reused", newUrl: reuseUrl };
7278
7296
  }
7279
7297
  if (filename.toLowerCase().endsWith(".svg"))
@@ -7290,14 +7308,6 @@ function SEImportWizard({
7290
7308
  signal
7291
7309
  );
7292
7310
  existingByFilename.set(filename, newUrl);
7293
- setResult(
7294
- (current) => current ? {
7295
- ...current,
7296
- overlay: rewriteAssetURLs(current.overlay, {
7297
- [assetUrl]: newUrl
7298
- })
7299
- } : current
7300
- );
7301
7311
  return { state: "done", newUrl };
7302
7312
  } catch (error) {
7303
7313
  if (error?.name === "AbortError" || signal?.aborted)
@@ -7307,8 +7317,15 @@ function SEImportWizard({
7307
7317
  },
7308
7318
  [uploadAsset]
7309
7319
  );
7320
+ const applyRewrite = useCallback3((mapping) => {
7321
+ if (Object.keys(mapping).length === 0) return;
7322
+ setResult(
7323
+ (current) => current ? { ...current, overlay: rewriteAssetURLs(current.overlay, mapping) } : current
7324
+ );
7325
+ }, []);
7310
7326
  const runMirror = useCallback3(async () => {
7311
7327
  if (!result) return;
7328
+ mirrorAbortRef.current?.abort();
7312
7329
  mirrorAbortRef.current = new AbortController();
7313
7330
  const signal = mirrorAbortRef.current.signal;
7314
7331
  setMirrorRunning(true);
@@ -7323,21 +7340,33 @@ function SEImportWizard({
7323
7340
  if (name && asset.url) existingByFilename.set(name, asset.url);
7324
7341
  }
7325
7342
  filenameCacheRef.current = existingByFilename;
7326
- for (let i = 0; i < initialRows.length; i += 1) {
7327
- if (signal.aborted) break;
7328
- const outcome = await processAssetRow(
7329
- initialRows[i].url,
7330
- i,
7331
- existingByFilename,
7332
- signal
7333
- );
7334
- if (signal.aborted) break;
7335
- setMirrorRows(
7336
- (previous) => previous.map((row, idx) => idx === i ? { ...row, ...outcome } : row)
7337
- );
7338
- }
7343
+ const rewriteMap = {};
7344
+ let cursor = 0;
7345
+ const worker = async () => {
7346
+ while (true) {
7347
+ if (signal.aborted) return;
7348
+ const i = cursor++;
7349
+ if (i >= initialRows.length) return;
7350
+ const outcome = await processAssetRow(
7351
+ initialRows[i].url,
7352
+ i,
7353
+ existingByFilename,
7354
+ signal
7355
+ );
7356
+ if (signal.aborted) return;
7357
+ if (outcome.newUrl && (outcome.state === "done" || outcome.state === "reused")) {
7358
+ rewriteMap[initialRows[i].url] = outcome.newUrl;
7359
+ }
7360
+ setMirrorRows(
7361
+ (previous) => previous.map((row, idx) => idx === i ? { ...row, ...outcome } : row)
7362
+ );
7363
+ }
7364
+ };
7365
+ const poolSize = Math.min(MIRROR_CONCURRENCY, initialRows.length);
7366
+ await Promise.all(Array.from({ length: poolSize }, worker));
7367
+ if (!signal.aborted) applyRewrite(rewriteMap);
7339
7368
  setMirrorRunning(false);
7340
- }, [assetUrls, existingAssets, processAssetRow, result]);
7369
+ }, [applyRewrite, assetUrls, existingAssets, processAssetRow, result]);
7341
7370
  const retryMirrorRow = useCallback3(
7342
7371
  async (idx) => {
7343
7372
  const rowUrl = mirrorRows[idx]?.url;
@@ -7351,8 +7380,11 @@ function SEImportWizard({
7351
7380
  setMirrorRows(
7352
7381
  (previous) => previous.map((row, i) => i === idx ? { ...row, ...outcome } : row)
7353
7382
  );
7383
+ if (outcome.newUrl && (outcome.state === "done" || outcome.state === "reused")) {
7384
+ applyRewrite({ [rowUrl]: outcome.newUrl });
7385
+ }
7354
7386
  },
7355
- [mirrorRows, processAssetRow]
7387
+ [applyRewrite, mirrorRows, processAssetRow]
7356
7388
  );
7357
7389
  useEffect6(() => {
7358
7390
  if (step === "mirror" && !mirrorStartedRef.current) {
@@ -7360,6 +7392,13 @@ function SEImportWizard({
7360
7392
  void runMirror();
7361
7393
  }
7362
7394
  }, [step, runMirror]);
7395
+ useEffect6(() => {
7396
+ return () => {
7397
+ setJwt("");
7398
+ setSeClient(null);
7399
+ mirrorAbortRef.current?.abort();
7400
+ };
7401
+ }, []);
7363
7402
  useEffect6(() => {
7364
7403
  if (step === "pick" && seClient && overlayList === null && !overlaysLoading) {
7365
7404
  void loadOverlayList(seClient);
@@ -7754,7 +7793,12 @@ function SEImportWizard({
7754
7793
  url,
7755
7794
  setUrl,
7756
7795
  error: loadError,
7757
- busy: loading
7796
+ busy: loading,
7797
+ onSwitchToJwt: () => {
7798
+ setLoadError(null);
7799
+ setEntryMode("jwt");
7800
+ setStep("connect");
7801
+ }
7758
7802
  }
7759
7803
  ),
7760
7804
  step === "discovery" && result && /* @__PURE__ */ jsx14(StepDiscovery, { result, assetCount: assetUrls.length }),
@@ -7958,145 +8002,138 @@ function importSEBootstrap(bootstrap) {
7958
8002
  name: seOverlay?.name,
7959
8003
  canvas: { width: seOverlay?.settings?.width, height: seOverlay?.settings?.height },
7960
8004
  widgetCount: widgets.length,
7961
- widgetTypes: widgets.map((w) => w.type),
7962
- fullBootstrap: bootstrap
8005
+ widgetTypes: widgets.map((w) => w.type)
7963
8006
  });
7964
8007
  const importCanvas = {
7965
8008
  width: seOverlay?.settings?.width ?? 1920,
7966
8009
  height: seOverlay?.settings?.height ?? 1080
7967
8010
  };
7968
- setImportCanvas(importCanvas);
8011
+ const ctx = { canvas: importCanvas };
7969
8012
  const layers = [];
7970
8013
  const modules = {};
7971
8014
  const reviewItems = [];
7972
- try {
7973
- const seUidToLumiaGroupId = {};
7974
- const groupWidgetByUid = {};
7975
- for (const w of widgets) {
7976
- if (w.type !== "se-widget-group") continue;
7977
- const uid = w.variables?.uid;
7978
- if (typeof uid !== "string") continue;
7979
- const lumiaGroupId = nanoid3();
7980
- seUidToLumiaGroupId[uid] = lumiaGroupId;
7981
- groupWidgetByUid[uid] = w;
7982
- }
7983
- for (const uid of Object.keys(groupWidgetByUid)) {
7984
- const w = groupWidgetByUid[uid];
7985
- const lumiaGroupId = seUidToLumiaGroupId[uid];
7986
- const parentUid = typeof w.group === "string" ? w.group : null;
7987
- const parentLumiaGroupId = parentUid ? seUidToLumiaGroupId[parentUid] ?? null : null;
7988
- const bounds = seCssToBounds(w.css, { width: 200, height: 200 }, importCanvas);
7989
- const layer = {
7990
- id: lumiaGroupId,
7991
- type: "group",
7992
- group: parentLumiaGroupId,
7993
- bounds: {
7994
- x: bounds.x,
7995
- y: bounds.y,
7996
- width: bounds.width,
7997
- height: bounds.height,
7998
- opacity: bounds.opacity,
7999
- zIndex: bounds.zIndex,
8000
- rotate: 0,
8001
- scale: [1, 1],
8002
- matrix: ""
8003
- },
8004
- state: {
8005
- locked: w.locked ?? false,
8006
- visible: w.visible ?? true,
8007
- // SE's `variables.expanded` is the editor's collapsed/open state.
8008
- expanded: w.variables?.expanded ?? true
8009
- }
8010
- };
8011
- const module = {
8012
- id: lumiaGroupId,
8013
- loaded: true,
8014
- settings: { title: defaultLabelForSEWidget(w), type: "group" },
8015
- lights: [],
8016
- css: {},
8017
- content: {
8018
- se_provenance: { widget: w, reason: "Imported as native group", status: "group" }
8019
- },
8020
- events: {},
8021
- variables: {}
8022
- };
8023
- layers.push(layer);
8024
- modules[lumiaGroupId] = module;
8025
- recordCoverage(coverage, { unit: { layer, module }, status: "direct", lumiaType: "group" }, w.type);
8026
- }
8027
- for (const w of widgets) {
8028
- if (w.type === "se-widget-group") continue;
8029
- const result = dispatch(w);
8030
- if (typeof w.group === "string") {
8031
- const parentLumiaGroupId = seUidToLumiaGroupId[w.group];
8032
- if (parentLumiaGroupId) {
8033
- result.unit.layer.group = parentLumiaGroupId;
8034
- }
8015
+ const seUidToLumiaGroupId = {};
8016
+ const groupWidgetByUid = {};
8017
+ for (const w of widgets) {
8018
+ if (w.type !== "se-widget-group") continue;
8019
+ const uid = w.variables?.uid;
8020
+ if (typeof uid !== "string") continue;
8021
+ const lumiaGroupId = nanoid3();
8022
+ seUidToLumiaGroupId[uid] = lumiaGroupId;
8023
+ groupWidgetByUid[uid] = w;
8024
+ }
8025
+ for (const uid of Object.keys(groupWidgetByUid)) {
8026
+ const w = groupWidgetByUid[uid];
8027
+ const lumiaGroupId = seUidToLumiaGroupId[uid];
8028
+ const parentUid = typeof w.group === "string" ? w.group : null;
8029
+ const parentLumiaGroupId = parentUid ? seUidToLumiaGroupId[parentUid] ?? null : null;
8030
+ const bounds = seCssToBounds(w.css, { width: 200, height: 200 }, importCanvas);
8031
+ const layer = {
8032
+ id: lumiaGroupId,
8033
+ type: "group",
8034
+ group: parentLumiaGroupId,
8035
+ bounds: {
8036
+ x: bounds.x,
8037
+ y: bounds.y,
8038
+ width: bounds.width,
8039
+ height: bounds.height,
8040
+ opacity: bounds.opacity,
8041
+ zIndex: bounds.zIndex,
8042
+ rotate: 0,
8043
+ scale: [1, 1],
8044
+ matrix: ""
8045
+ },
8046
+ state: {
8047
+ locked: w.locked ?? false,
8048
+ visible: w.visible ?? true,
8049
+ // SE's `variables.expanded` is the editor's collapsed/open state.
8050
+ expanded: w.variables?.expanded ?? true
8035
8051
  }
8036
- layers.push(result.unit.layer);
8037
- const module = result.unit.module;
8038
- if (result.status === "placeholder" || result.status === "template") {
8039
- module.content = {
8040
- ...module.content ?? {},
8041
- se_provenance: {
8042
- widget: w,
8043
- reason: reasonFor(w.type, result.status, result.flaggedOff),
8044
- status: result.status,
8045
- flaggedOff: result.flaggedOff
8046
- }
8047
- };
8048
- reviewItems.push({
8049
- moduleId: module.id,
8050
- seWidget: w,
8051
- status: result.status,
8052
- reason: reasonFor(w.type, result.status, result.flaggedOff),
8053
- flaggedOff: result.flaggedOff
8054
- });
8052
+ };
8053
+ const module = {
8054
+ id: lumiaGroupId,
8055
+ loaded: true,
8056
+ settings: { title: defaultLabelForSEWidget(w), type: "group" },
8057
+ lights: [],
8058
+ css: {},
8059
+ content: {
8060
+ se_provenance: { widget: w, reason: "Imported as native group", status: "group" }
8061
+ },
8062
+ events: {},
8063
+ variables: {}
8064
+ };
8065
+ layers.push(layer);
8066
+ modules[lumiaGroupId] = module;
8067
+ recordCoverage(coverage, { unit: { layer, module }, status: "direct", lumiaType: "group" }, w.type);
8068
+ }
8069
+ for (const w of widgets) {
8070
+ if (w.type === "se-widget-group") continue;
8071
+ const result = dispatch(w, ctx);
8072
+ if (typeof w.group === "string") {
8073
+ const parentLumiaGroupId = seUidToLumiaGroupId[w.group];
8074
+ if (parentLumiaGroupId) {
8075
+ result.unit.layer.group = parentLumiaGroupId;
8055
8076
  }
8056
- modules[module.id] = module;
8057
- recordCoverage(coverage, result, w.type);
8058
- }
8059
- if (coverage.mappings.some((m) => m.status === "placeholder")) {
8060
- coverage.notes.push("Some widgets imported as placeholder text layers \u2014 see SE/05-widget-to-module-mapping.md for status.");
8061
8077
  }
8062
- if (coverage.mappings.some((m) => m.seType === "se-widget-merch-goal")) {
8063
- coverage.notes.push("Merch-goal widgets imported as generic goals \u2014 see SE/03-missing-variables.md \xA78 for the missing data source.");
8064
- }
8065
- if (coverage.mappings.some((m) => m.seType === "se-widget-custom-event-list")) {
8066
- 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.");
8078
+ layers.push(result.unit.layer);
8079
+ const module = result.unit.module;
8080
+ if (result.status === "placeholder" || result.status === "template") {
8081
+ module.content = {
8082
+ ...module.content ?? {},
8083
+ se_provenance: {
8084
+ widget: w,
8085
+ reason: reasonFor(w.type, result.status, result.flaggedOff),
8086
+ status: result.status,
8087
+ flaggedOff: result.flaggedOff
8088
+ }
8089
+ };
8090
+ reviewItems.push({
8091
+ moduleId: module.id,
8092
+ seWidget: w,
8093
+ status: result.status,
8094
+ reason: reasonFor(w.type, result.status, result.flaggedOff),
8095
+ flaggedOff: result.flaggedOff
8096
+ });
8067
8097
  }
8068
- const overlay = {
8069
- uuid: "",
8070
- listen_id: "",
8071
- name: seOverlay?.name ? `[SE] ${seOverlay.name}` : `Imported from StreamElements (${seOverlay?._id ?? ""})`,
8072
- description: seOverlay?._id ? `Imported from StreamElements overlay ${seOverlay._id}` : "Imported from StreamElements",
8073
- settings: {
8074
- metadata: {
8075
- width: seOverlay?.settings?.width ?? 1920,
8076
- height: seOverlay?.settings?.height ?? 1080
8077
- },
8078
- layers,
8079
- modules
8080
- },
8098
+ modules[module.id] = module;
8099
+ recordCoverage(coverage, result, w.type);
8100
+ }
8101
+ if (coverage.mappings.some((m) => m.status === "placeholder")) {
8102
+ coverage.notes.push("Some widgets imported as placeholder text layers \u2014 see SE/05-widget-to-module-mapping.md for status.");
8103
+ }
8104
+ if (coverage.mappings.some((m) => m.seType === "se-widget-merch-goal")) {
8105
+ coverage.notes.push("Merch-goal widgets imported as generic goals \u2014 see SE/03-missing-variables.md \xA78 for the missing data source.");
8106
+ }
8107
+ if (coverage.mappings.some((m) => m.seType === "se-widget-custom-event-list")) {
8108
+ 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.");
8109
+ }
8110
+ const overlay = {
8111
+ uuid: "",
8112
+ listen_id: "",
8113
+ name: seOverlay?.name ? `[SE] ${seOverlay.name}` : `Imported from StreamElements (${seOverlay?._id ?? ""})`,
8114
+ description: seOverlay?._id ? `Imported from StreamElements overlay ${seOverlay._id}` : "Imported from StreamElements",
8115
+ settings: {
8081
8116
  metadata: {
8082
- width: seOverlay?.settings?.width ?? null,
8083
- height: seOverlay?.settings?.height ?? null
8117
+ width: seOverlay?.settings?.width ?? 1920,
8118
+ height: seOverlay?.settings?.height ?? 1080
8084
8119
  },
8085
- layers_count: layers.length,
8086
- hasFullSettings: true
8087
- };
8088
- console.log("[SE-IMPORT] import complete", {
8089
- overlayName: overlay.name,
8090
- layerCount: overlay.layers_count,
8091
- coverage,
8092
- reviewItemCount: reviewItems.length,
8093
- reviewItems,
8094
- modules: overlay.settings.modules
8095
- });
8096
- return { overlay, coverage, reviewItems };
8097
- } finally {
8098
- setImportCanvas(null);
8099
- }
8120
+ layers,
8121
+ modules
8122
+ },
8123
+ metadata: {
8124
+ width: seOverlay?.settings?.width ?? null,
8125
+ height: seOverlay?.settings?.height ?? null
8126
+ },
8127
+ layers_count: layers.length,
8128
+ hasFullSettings: true
8129
+ };
8130
+ console.log("[SE-IMPORT] import complete", {
8131
+ overlayName: overlay.name,
8132
+ layerCount: overlay.layers_count,
8133
+ coverage,
8134
+ reviewItemCount: reviewItems.length
8135
+ });
8136
+ return { overlay, coverage, reviewItems };
8100
8137
  }
8101
8138
  function applyReviewAction(result, moduleId, action, payload) {
8102
8139
  if (action === "keep") return result;
@@ -8125,9 +8162,15 @@ function applyReviewAction(result, moduleId, action, payload) {
8125
8162
  state: existingLayer.state
8126
8163
  };
8127
8164
  const provenance2 = existing2.content?.se_provenance;
8165
+ const mergedLights = (existing2.lights?.length ?? 0) > 0 ? existing2.lights : transplant.module.lights;
8166
+ const mergedEvents = existing2.events && Object.keys(existing2.events).length > 0 ? { ...transplant.module.events, ...existing2.events } : transplant.module.events;
8167
+ const mergedVariables = existing2.variables && Object.keys(existing2.variables).length > 0 ? { ...transplant.module.variables, ...existing2.variables } : transplant.module.variables;
8128
8168
  const newModule = {
8129
8169
  ...transplant.module,
8130
8170
  id: moduleId,
8171
+ lights: mergedLights,
8172
+ events: mergedEvents,
8173
+ variables: mergedVariables,
8131
8174
  content: { ...transplant.module.content ?? {}, se_provenance: provenance2 }
8132
8175
  };
8133
8176
  const rootOldNewId = transplant.layer.id;
@@ -8198,6 +8241,11 @@ function buildAIPromptForSEWidget(widget, canvas) {
8198
8241
  const listener = typeof widget.listener === "string" ? widget.listener : null;
8199
8242
  const listeners = widget.listeners && typeof widget.listeners === "object" && !Array.isArray(widget.listeners) ? Object.keys(widget.listeners) : null;
8200
8243
  const landing = getAILandingBounds(widget, canvas);
8244
+ const VARIABLES_BUDGET = 6e3;
8245
+ const fullVariables = JSON.stringify(variables, null, 2);
8246
+ const truncated = fullVariables.length > VARIABLES_BUDGET;
8247
+ const variablesJson = truncated ? `${fullVariables.slice(0, VARIABLES_BUDGET)}
8248
+ \u2026[truncated \u2014 ${fullVariables.length - VARIABLES_BUDGET} more bytes elided]` : fullVariables;
8201
8249
  return [
8202
8250
  "Recreate a StreamElements overlay widget as a Lumia Stream custom overlay (HTML + CSS + JS).",
8203
8251
  "",
@@ -8205,10 +8253,11 @@ function buildAIPromptForSEWidget(widget, canvas) {
8205
8253
  listener ? `Primary event listener: ${listener}` : "",
8206
8254
  listeners ? `Subscribes to events: ${listeners.join(", ")}` : "",
8207
8255
  `Canvas: ${landing.width} \xD7 ${landing.height}`,
8256
+ truncated ? "Note: the widget config below is truncated \u2014 focus on visible/structural fields and ignore tail content you cannot see." : "",
8208
8257
  "",
8209
8258
  "Widget configuration (JSON):",
8210
8259
  "```json",
8211
- JSON.stringify(variables, null, 2).slice(0, 6e3),
8260
+ variablesJson,
8212
8261
  "```",
8213
8262
  "",
8214
8263
  "Constraints \u2014 your output must run inside a Lumia Custom Overlay sandboxed iframe:",
@@ -8244,7 +8293,6 @@ export {
8244
8293
  SEAuthError,
8245
8294
  SEClient,
8246
8295
  SEImportWizard,
8247
- SE_IMPORT_FLAGS,
8248
8296
  SE_WIDGET_TO_MARKETPLACE_CANDIDATES,
8249
8297
  applyReviewAction,
8250
8298
  buildAIPromptForSEWidget,