@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/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,17 @@ 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);
3786
+ }
3787
+
3788
+ // src/se-import/mappers/importMeta.ts
3789
+ function buildImportMeta(widget, config) {
3790
+ return {
3791
+ source: "streamelements",
3792
+ widgetType: widget.type,
3793
+ importedAt: (/* @__PURE__ */ new Date()).toISOString(),
3794
+ ...config && Object.keys(config).length > 0 ? { config } : {}
3795
+ };
3788
3796
  }
3789
3797
 
3790
3798
  // src/se-import/mappers/goal.ts
@@ -3822,7 +3830,7 @@ function sourceFromListener(listener) {
3822
3830
  if (listener.startsWith("merch-")) return "merch_orders";
3823
3831
  return "custom";
3824
3832
  }
3825
- function mapGoal(widget) {
3833
+ function mapGoal(widget, ctx) {
3826
3834
  const v = widget.variables ?? {};
3827
3835
  const listener = v.goalListener ?? (typeof widget.listener === "string" ? widget.listener : null);
3828
3836
  const charity = charityGoalVariables(widget.type);
@@ -3862,9 +3870,12 @@ function mapGoal(widget) {
3862
3870
  border: "solid 1px transparent",
3863
3871
  borderRadius: "40px",
3864
3872
  highlightColor: "inherit",
3865
- // SE-only fields preserved as provenance no native Lumia equivalent.
3866
- se_source_listener: listener,
3867
- se_simple_design: !!v.simpleDesign
3873
+ // Source-side knobs without a native Lumia equivalent live under the
3874
+ // generic `importMeta` envelope (see mappers/importMeta.ts).
3875
+ importMeta: buildImportMeta(widget, {
3876
+ sourceListener: listener,
3877
+ simpleDesign: !!v.simpleDesign
3878
+ })
3868
3879
  },
3869
3880
  css: {
3870
3881
  // `borderRadius` / `borderStyle` / `borderWidth` / `borderColor`
@@ -3883,11 +3894,11 @@ function mapGoal(widget) {
3883
3894
  textShadow: seTextCss["text-shadow"] ?? "rgb(0, 0, 0) 1px 1px 1px",
3884
3895
  background: "transparent"
3885
3896
  }
3886
- });
3897
+ }, ctx);
3887
3898
  }
3888
3899
 
3889
3900
  // src/se-import/mappers/custom.ts
3890
- var SE_FIELD_TYPE_TO_LUMIA = {
3901
+ var SOURCE_TO_LUMIA_FIELD_TYPE = {
3891
3902
  text: "input",
3892
3903
  number: "number",
3893
3904
  checkbox: "checkbox",
@@ -3895,19 +3906,45 @@ var SE_FIELD_TYPE_TO_LUMIA = {
3895
3906
  slider: "slider",
3896
3907
  colorpicker: "colorpicker",
3897
3908
  fontpicker: "fontpicker",
3898
- image: "input"
3909
+ // SE's `googleFont` picker is functionally the same as Lumia's native
3910
+ // fontpicker (both load Google Fonts on demand).
3911
+ googleFont: "fontpicker",
3912
+ // Asset uploads land on the native upload types added to Lumia's
3913
+ // ConfigsFieldType. `image` (legacy SE alias) folds into `imageupload`.
3914
+ image: "imageupload",
3915
+ "image-input": "imageupload",
3916
+ "sound-input": "soundupload",
3917
+ "video-input": "videoupload",
3918
+ // Multi-line text → native textarea.
3919
+ textarea: "textarea",
3920
+ // Hidden fields keep their value in `Overlay.data` but don't render a
3921
+ // control; we map to `input` + the BaseConfigField `hidden: true` flag
3922
+ // (applied below in translateConfigs).
3923
+ hidden: "input",
3924
+ // SE-side trigger buttons → native action button.
3925
+ button: "actionbutton"
3899
3926
  };
3900
- function translateConfigs(seFields) {
3901
- 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
- }));
3927
+ var RESERVED_FIELD_KEYS = /* @__PURE__ */ new Set(["widgetName", "widgetAuthor", "widgetDuration"]);
3928
+ function translateConfigs(sourceFields) {
3929
+ if (!sourceFields) return [];
3930
+ return Object.entries(sourceFields).filter(([key]) => !RESERVED_FIELD_KEYS.has(key)).map(([key, f]) => {
3931
+ const sourceType = f.type ?? "text";
3932
+ const nativeType = SOURCE_TO_LUMIA_FIELD_TYPE[sourceType] ?? "input";
3933
+ const base = {
3934
+ key,
3935
+ label: f.label ?? key,
3936
+ // The emitted `type` is a native Lumia ConfigsFieldType — no
3937
+ // source-prefixed shadow field rides along. Anything that needs
3938
+ // richer control behavior should land as a new native type.
3939
+ type: nativeType,
3940
+ value: f.value,
3941
+ options: f.options
3942
+ };
3943
+ if (sourceType === "hidden") base.hidden = true;
3944
+ return base;
3945
+ });
3909
3946
  }
3910
- function mapCustom(widget) {
3947
+ function mapCustom(widget, ctx) {
3911
3948
  const v = widget.variables ?? {};
3912
3949
  const parse = (x) => {
3913
3950
  if (x == null) return void 0;
@@ -3930,7 +3967,7 @@ function mapCustom(widget) {
3930
3967
  data: fieldData ?? {},
3931
3968
  flavor: "streamelements"
3932
3969
  }
3933
- });
3970
+ }, ctx);
3934
3971
  }
3935
3972
 
3936
3973
  // src/se-import/mappers/misc.ts
@@ -3958,7 +3995,7 @@ function paddingShorthand(message) {
3958
3995
  const px = (n) => `${n ?? 0}px`;
3959
3996
  return `${px(t)} ${px(r)} ${px(b)} ${px(l)}`;
3960
3997
  }
3961
- function mapChatbox(widget) {
3998
+ function mapChatbox(widget, ctx) {
3962
3999
  const v = widget.variables ?? {};
3963
4000
  const textCss = widget.text?.css ?? {};
3964
4001
  const messageCss = textCss.message ?? {};
@@ -4033,16 +4070,19 @@ function mapChatbox(widget) {
4033
4070
  twitch: { themeConfig: { primaryColor: accentColor, showUsernameColors: true } }
4034
4071
  }
4035
4072
  } : {},
4036
- // SE-only fields preserved as provenance no native Lumia equivalent.
4037
- // `delay` / `messageDelay` (initial + per-message hold), and the
4038
- // raw highlight block in case a future Lumia feature wants the
4039
- // fine-grained styling.
4040
- se_message_delay: v.messageDelay,
4041
- se_initial_delay: v.delay,
4042
- se_highlight_users: v.highlight,
4043
- se_text_css: textCss
4073
+ // Source-side knobs preserved under the generic importMeta envelope
4074
+ // no native Lumia equivalent today. `messageDelay` is per-message
4075
+ // hold, `initialDelay` is the show-after-connect delay, and we
4076
+ // stash the raw highlight block + text css for any future feature
4077
+ // that wants the fine-grained styling.
4078
+ importMeta: buildImportMeta(widget, {
4079
+ messageDelay: v.messageDelay,
4080
+ initialDelay: v.delay,
4081
+ highlightUsers: v.highlight,
4082
+ textCss
4083
+ })
4044
4084
  }
4045
- });
4085
+ }, ctx);
4046
4086
  }
4047
4087
  var SE_LISTENER_TO_LUMIA_CATEGORY = {
4048
4088
  "follower-latest": "follower",
@@ -4108,7 +4148,7 @@ function computeListenerFilters(seListeners, explicitListener) {
4108
4148
  if (allowed.size === 0) return [];
4109
4149
  return ALL_LUMIA_EVENTLIST_CATEGORIES.filter((c) => !allowed.has(c));
4110
4150
  }
4111
- function mapEventList(widget) {
4151
+ function mapEventList(widget, ctx) {
4112
4152
  const v = widget.variables ?? {};
4113
4153
  const filters = computeListenerFilters(widget.listeners, widget.listener);
4114
4154
  const maxItems = v.visibleEvents ?? v.visibleItems ?? 5;
@@ -4137,7 +4177,7 @@ function mapEventList(widget) {
4137
4177
  showDetailedText: false,
4138
4178
  hideAlertMessage: false
4139
4179
  }
4140
- });
4180
+ }, ctx);
4141
4181
  }
4142
4182
  var SE_CREDITS_EVENTS_TO_SHOW = {
4143
4183
  mods: false,
@@ -4277,7 +4317,7 @@ var CREDITS_DEFAULT_TEMPLATES = {
4277
4317
  topSuperchats: "{{username}} x{{amount}}",
4278
4318
  topRedemptions: "{{username}} x{{amount}}"
4279
4319
  };
4280
- function mapCredits(widget) {
4320
+ function mapCredits(widget, ctx) {
4281
4321
  const v = widget.variables ?? {};
4282
4322
  const itemGap = typeof v.padding === "number" ? v.padding : 8;
4283
4323
  const reverseFlow = v.direction === "bottom";
@@ -4314,12 +4354,15 @@ function mapCredits(widget) {
4314
4354
  audio: {
4315
4355
  content: { src: [], name: [], loop: false, volume: 1 }
4316
4356
  },
4317
- // SE-specific knobs preserved as provenance; no direct Lumia equivalent for
4318
- // these. `chosenFilter` would narrow eventsToShow if we had a clean mapping;
4319
- // `separator` would join entries but Lumia credits uses sectioned cards instead.
4320
- se_chosen_filter: v.chosenFilter,
4321
- se_separator: v.separator,
4322
- se_amount: v.amount
4357
+ // Source-side knobs with no direct Lumia equivalent — preserved
4358
+ // under the generic importMeta envelope. `chosenFilter` would
4359
+ // narrow eventsToShow if we had a clean mapping; `separator` would
4360
+ // join entries but Lumia credits uses sectioned cards instead.
4361
+ importMeta: buildImportMeta(widget, {
4362
+ chosenFilter: v.chosenFilter,
4363
+ separator: v.separator,
4364
+ amount: v.amount
4365
+ })
4323
4366
  },
4324
4367
  css: {
4325
4368
  fontSize: 30,
@@ -4331,7 +4374,7 @@ function mapCredits(widget) {
4331
4374
  textAlign: "center",
4332
4375
  background: "transparent"
4333
4376
  }
4334
- });
4377
+ }, ctx);
4335
4378
  }
4336
4379
  function inferSlideshowMediaType(mime, url) {
4337
4380
  if (typeof mime === "string") {
@@ -4350,7 +4393,7 @@ function nameFromUrl(url) {
4350
4393
  return url;
4351
4394
  }
4352
4395
  }
4353
- function mapSlideshow(widget) {
4396
+ function mapSlideshow(widget, ctx) {
4354
4397
  const v = widget.variables ?? {};
4355
4398
  const items = (v.images ?? []).map((raw) => {
4356
4399
  const url = typeof raw === "string" ? raw : raw.url ?? raw.src ?? "";
@@ -4387,24 +4430,18 @@ function mapSlideshow(widget) {
4387
4430
  enterAnimationDelay: (seAnim.inDelay ?? 0) * 1e3,
4388
4431
  exitAnimationDelay: (seAnim.outDelay ?? 0) * 1e3
4389
4432
  },
4390
- se_interval: v.interval,
4391
- se_raw_duration: v.duration
4433
+ importMeta: buildImportMeta(widget, {
4434
+ interval: v.interval,
4435
+ rawDuration: v.duration
4436
+ })
4392
4437
  }
4393
- });
4438
+ }, ctx);
4394
4439
  }
4395
- function mapTimer(widget) {
4440
+ function mapTimer(widget, ctx) {
4396
4441
  const v = widget.variables ?? {};
4397
4442
  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
- };
4443
+ const TIMER_PASSTHROUGH = /* @__PURE__ */ new Set(["days", "hours", "minutes", "seconds"]);
4444
+ const mapTemplate = (template) => translateSeText(template, null, TIMER_PASSTHROUGH);
4408
4445
  const absoluteDate = v.date ?? v.endsAt ?? v.endDate;
4409
4446
  const target = absoluteDate ? Date.parse(absoluteDate) : NaN;
4410
4447
  const hasValidAbsoluteDate = Number.isFinite(target);
@@ -4476,13 +4513,15 @@ function mapTimer(widget) {
4476
4513
  changeMessageAfter: !!v.changeMessageAfterCountdown,
4477
4514
  countDate: hasValidAbsoluteDate && (!!v.exactTime || !!v.datePicker || !!absoluteDate),
4478
4515
  date: hasValidAbsoluteDate ? new Date(target).toISOString() : "",
4479
- se_date: absoluteDate,
4480
- se_date_picker: v.datePicker,
4481
- se_exact_time: v.exactTime
4516
+ importMeta: buildImportMeta(widget, {
4517
+ rawDate: absoluteDate,
4518
+ datePicker: v.datePicker,
4519
+ exactTime: v.exactTime
4520
+ })
4482
4521
  }
4483
- });
4522
+ }, ctx);
4484
4523
  }
4485
- function mapNowPlaying(widget) {
4524
+ function mapNowPlaying(widget, ctx) {
4486
4525
  const v = widget.variables ?? {};
4487
4526
  const source = v.provider === "youtube" ? "youtubemusic" : "spotify";
4488
4527
  return buildUnit(widget, "nowplaying", {
@@ -4498,7 +4537,7 @@ function mapNowPlaying(widget) {
4498
4537
  showImg: true,
4499
4538
  oneLineTitle: false,
4500
4539
  highlightColor: "inherit",
4501
- se_simple_design: !!v.simpleDesign
4540
+ importMeta: buildImportMeta(widget, { simpleDesign: !!v.simpleDesign })
4502
4541
  },
4503
4542
  css: {
4504
4543
  borderRadius: "0px",
@@ -4512,9 +4551,9 @@ function mapNowPlaying(widget) {
4512
4551
  textShadow: "rgb(0, 0, 0) 1px 1px 1px",
4513
4552
  background: "transparent"
4514
4553
  }
4515
- });
4554
+ }, ctx);
4516
4555
  }
4517
- function mapMediaShare(widget) {
4556
+ function mapMediaShare(widget, ctx) {
4518
4557
  const v = widget.variables ?? {};
4519
4558
  return buildUnit(widget, "songrequest", {
4520
4559
  content: {
@@ -4543,9 +4582,10 @@ function mapMediaShare(widget) {
4543
4582
  playerVisible: false,
4544
4583
  defaultVolume: 70,
4545
4584
  autoAdvanceOnEnd: true,
4546
- // SE-only knob preserved for round-trip provenance; Lumia ignores
4547
- // it (LumiaStream auto-advance fires on `durationSeconds + grace`).
4548
- se_timeLeft: v.timeLeft === true
4585
+ // Source-side knob preserved for round-trip provenance; Lumia
4586
+ // ignores it at runtime (auto-advance fires on
4587
+ // `durationSeconds + grace`).
4588
+ importMeta: buildImportMeta(widget, { timeLeft: v.timeLeft === true })
4549
4589
  },
4550
4590
  css: {
4551
4591
  fontFamily: "Roboto",
@@ -4555,7 +4595,7 @@ function mapMediaShare(widget) {
4555
4595
  background: "transparent",
4556
4596
  fontSize: 18
4557
4597
  }
4558
- });
4598
+ }, ctx);
4559
4599
  }
4560
4600
  var SE_KAPPAGEN_EVENT_TO_LUMIA_ALERT = {
4561
4601
  follower: "twitch-follower",
@@ -4578,7 +4618,7 @@ var SE_KAPPAGEN_EVENT_TO_LUMIA_ALERT = {
4578
4618
  // All three drop silently; the rest of the widget (size, spawn zone,
4579
4619
  // per-event flags for the remaining events) still imports.
4580
4620
  };
4581
- function mapKappagen(widget) {
4621
+ function mapKappagen(widget, ctx) {
4582
4622
  const v = widget.variables ?? {};
4583
4623
  const eventsFromNested = v.eventsConfig ? {
4584
4624
  subscriber: v.eventsConfig.subscriber?.enabled ?? true,
@@ -4619,21 +4659,25 @@ function mapKappagen(widget) {
4619
4659
  ffz: v.ffz !== false,
4620
4660
  sevenTv: v.sevenTv !== false
4621
4661
  },
4622
- // SE-only knobs preserved as provenance; no Lumia rendering for these.
4623
- se_mode: v.mode,
4624
- se_cheersplosions: !!v.cheersplosions,
4625
- se_emotesplosion: v.emotesplosion,
4626
- se_emotesplosion_dir: v.emotesplosiondir,
4627
- se_emotesplosion_duration: v.emotesplosionduration,
4628
- se_spawn_zone: v.spawnzone,
4629
- se_fullscreen: !!v.fullscreen,
4630
- se_direction: v.direction
4662
+ // Source-side knobs preserved under the generic importMeta
4663
+ // envelope — no Lumia rendering for these today.
4664
+ importMeta: buildImportMeta(widget, {
4665
+ mode: v.mode,
4666
+ cheersplosions: !!v.cheersplosions,
4667
+ emotesplosion: v.emotesplosion,
4668
+ emotesplosionDir: v.emotesplosiondir,
4669
+ emotesplosionDuration: v.emotesplosionduration,
4670
+ spawnZone: v.spawnzone,
4671
+ fullscreen: !!v.fullscreen,
4672
+ direction: v.direction
4673
+ })
4631
4674
  },
4632
4675
  css: { background: "transparent" }
4633
- }
4676
+ },
4677
+ ctx
4634
4678
  );
4635
4679
  }
4636
- function mapLeaderboardFromVariables(widget, currency) {
4680
+ function mapLeaderboardFromVariables(widget, currency, ctx) {
4637
4681
  const v = widget.variables ?? {};
4638
4682
  const count = v.limit ?? v.visibleItems ?? 10;
4639
4683
  const window = v.period === "weekly" ? "week" : v.period === "monthly" ? "month" : "alltime";
@@ -4650,15 +4694,17 @@ function mapLeaderboardFromVariables(widget, currency) {
4650
4694
  showCurrentPoints: false,
4651
4695
  showAllTimePoints: true,
4652
4696
  itemGap: 8,
4653
- // Preserve SE knobs that don't map cleanly so they survive the round-trip
4654
- // and the user can re-introspect later if behaviour differs.
4655
- se_period: v.period,
4656
- se_offset: v.offset,
4657
- se_layout: v.layout
4697
+ // Source-side knobs that don't map cleanly survive under the
4698
+ // generic importMeta envelope so the user can re-introspect.
4699
+ importMeta: buildImportMeta(widget, {
4700
+ period: v.period,
4701
+ offset: v.offset,
4702
+ layout: v.layout
4703
+ })
4658
4704
  }
4659
- });
4705
+ }, ctx);
4660
4706
  }
4661
- function mapGiveaway(widget) {
4707
+ function mapGiveaway(widget, ctx) {
4662
4708
  const v = widget.variables ?? {};
4663
4709
  const primary = v.colors?.primary ?? "#393853";
4664
4710
  const accent = v.colors?.accent ?? "#FF4076";
@@ -4684,8 +4730,6 @@ function mapGiveaway(widget) {
4684
4730
  // preference via provenance — a future renderer can read it without us
4685
4731
  // having lied about the toggle. Same for `displayTicketsPurchased`.
4686
4732
  hideParticipantOnStart: false,
4687
- se_displayEntrants: v.displayEntrants !== false,
4688
- se_displayTicketsPurchased: v.displayTicketsPurchased !== false,
4689
4733
  removeWhenRedeemed: false,
4690
4734
  showWinningModal: true,
4691
4735
  playSoundFx: true,
@@ -4702,14 +4746,20 @@ function mapGiveaway(widget) {
4702
4746
  raffleEndedText: "The raffle has ended, and the selection of the winner will take place shortly.",
4703
4747
  raffleTimerText: "Time since raffle started",
4704
4748
  raffleCountdownText: "Raffle will end after",
4705
- // SE-specific knobs with no Lumia equivalent — preserved as provenance.
4706
- se_timeout: v.timeout,
4707
- se_enable_timeout: v.enableTimeOut,
4708
- se_display_entrants: v.displayEntrants,
4709
- se_display_tickets_purchased: v.displayTicketsPurchased,
4710
- se_image_style_width: v.imageStyle?.width
4749
+ // Source-side knobs with no Lumia equivalent — preserved under
4750
+ // the generic importMeta envelope. `displayEntrants` / `displayTicketsPurchased`
4751
+ // are SE's persistent "never show" toggles; Lumia has no equivalent
4752
+ // today (its `hideParticipantOnStart` is transient), so we preserve
4753
+ // the preference for a future renderer.
4754
+ importMeta: buildImportMeta(widget, {
4755
+ timeout: v.timeout,
4756
+ enableTimeout: v.enableTimeOut,
4757
+ displayEntrants: v.displayEntrants,
4758
+ displayTicketsPurchased: v.displayTicketsPurchased,
4759
+ imageStyleWidth: v.imageStyle?.width
4760
+ })
4711
4761
  }
4712
- });
4762
+ }, ctx);
4713
4763
  }
4714
4764
  var HYPETRAIN_LISTENER_KEYS = /* @__PURE__ */ new Set([
4715
4765
  "hypetrain-latest",
@@ -4740,7 +4790,7 @@ function isHypetrainCustomWidget(widget) {
4740
4790
  if (enabled.length === 0) return false;
4741
4791
  return enabled.every((listener) => HYPETRAIN_LISTENER_KEYS.has(listener));
4742
4792
  }
4743
- function mapHypetrain(widget) {
4793
+ function mapHypetrain(widget, ctx) {
4744
4794
  const v = widget.variables ?? {};
4745
4795
  return buildUnit(widget, "hypetrain", {
4746
4796
  content: {
@@ -4777,15 +4827,16 @@ function mapHypetrain(widget) {
4777
4827
  trainLocoImageUrl: "https://storage.lumiastream.com/overlays/global/hypetrain/train_loco_default.png",
4778
4828
  rocketImageUrl: "https://storage.lumiastream.com/overlays/global/hypetrain/rocket_default.png",
4779
4829
  infernoImageUrl: "https://storage.lumiastream.com/overlays/global/hypetrain/inferno_default.png",
4780
- // SE config preserved verbatim as provenance no Lumia equivalent for
4781
- // most of these knobs (per-widget animation tunables, sound URLs the
4782
- // streamer might want to migrate manually, etc.). Users can inspect
4783
- // these via the overlay JSON if they want to recreate a specific look.
4784
- se_variables: v
4830
+ // Source config preserved verbatim under the generic importMeta
4831
+ // envelope no Lumia equivalent for most of these knobs
4832
+ // (per-widget animation tunables, sound URLs the streamer might
4833
+ // want to migrate manually, etc.). Users can inspect via the
4834
+ // overlay JSON if they want to recreate a specific look.
4835
+ importMeta: buildImportMeta(widget, { variables: v })
4785
4836
  }
4786
- });
4837
+ }, ctx);
4787
4838
  }
4788
- function mapUnsupportedAsText(widget) {
4839
+ function mapUnsupportedAsText(widget, ctx) {
4789
4840
  return buildUnit(widget, "text", {
4790
4841
  content: {
4791
4842
  value: `[Unsupported SE widget: ${widget.type}]`,
@@ -4799,7 +4850,7 @@ function mapUnsupportedAsText(widget) {
4799
4850
  fontWeight: "bold",
4800
4851
  background: "rgba(0,0,0,0.4)"
4801
4852
  }
4802
- });
4853
+ }, ctx);
4803
4854
  }
4804
4855
 
4805
4856
  // src/se-import/mappers/hypecup.ts
@@ -4869,7 +4920,7 @@ function categoriesFromListeners(listeners) {
4869
4920
  if (events.subs) events.resubs = true;
4870
4921
  return events;
4871
4922
  }
4872
- function mapHypeCup(widget) {
4923
+ function mapHypeCup(widget, ctx) {
4873
4924
  const v = widget.variables ?? {};
4874
4925
  const widgetWidth = typeof v.width === "number" && v.width > 0 ? v.width : parsePx(widget.css?.width);
4875
4926
  const widgetHeight = typeof v.height === "number" && v.height > 0 ? v.height : parsePx(widget.css?.height);
@@ -4916,18 +4967,21 @@ function mapHypeCup(widget) {
4916
4967
  // can enable in Settings; SE has no equivalent so nothing to lift.
4917
4968
  goal: { active: false, currency: "tips", target: 100, current: 0, readoutTemplate: "{{current}} / {{target}}", textColor: "#ffffff", hitAt: null },
4918
4969
  break: { enabled: false, command: "!breakjar", rebuildDelay: 5e3 },
4919
- // SE-only knobs that don't have a Lumia equivalent — preserved as
4920
- // provenance for future phases (per-cup polygons + enter/movement
4921
- // physics tweaks). `se_types` removed — now consumed into sprites above.
4922
- se_enter_from: v.enterFrom ?? null,
4923
- se_prevent_movement: !!v.preventMovement,
4924
- se_fullscreen: !!v.fullscreen,
4925
- se_cups: v.cups ?? null
4970
+ // Source-side knobs without a Lumia equivalent — preserved under
4971
+ // the generic `importMeta` envelope for future phases (per-cup
4972
+ // polygons + enter/movement physics tweaks). The per-event
4973
+ // sprite sets were already lifted into native `sprites` above.
4974
+ importMeta: buildImportMeta(widget, {
4975
+ enterFrom: v.enterFrom ?? null,
4976
+ preventMovement: !!v.preventMovement,
4977
+ fullscreen: !!v.fullscreen,
4978
+ cups: v.cups ?? null
4979
+ })
4926
4980
  },
4927
4981
  css: {
4928
4982
  background: "transparent"
4929
4983
  }
4930
- });
4984
+ }, ctx);
4931
4985
  }
4932
4986
  function clamp01(n) {
4933
4987
  if (!Number.isFinite(n)) return 0.5;
@@ -4974,7 +5028,7 @@ function listenerIsOn(listeners, key) {
4974
5028
  if (Array.isArray(listeners)) return listeners.includes(key);
4975
5029
  return listeners[key] === true;
4976
5030
  }
4977
- function mapStreamBoss(widget) {
5031
+ function mapStreamBoss(widget, ctx) {
4978
5032
  const v = widget.variables ?? {};
4979
5033
  const baseHp = typeof v.baseHP === "number" && v.baseHP > 0 ? Math.round(v.baseHP) : 1e3;
4980
5034
  const seDamage = v.damage ?? {};
@@ -5041,22 +5095,24 @@ function mapStreamBoss(widget) {
5041
5095
  bossNameColor: "",
5042
5096
  botChat: "off",
5043
5097
  chatAsSelf: false,
5044
- // SE-only knobs preserved as provenance for future passes. None
5045
- // have direct Lumia analogs today:
5046
- // - `right` anchors the boss to the right edge of its bounds;
5047
- // Lumia uses layer position instead, so this is informational.
5048
- // - `bossName` controls vertical name position in SE; our themes
5049
- // don't expose that level of fine-grained tweaking yet.
5050
- // - `mode` SE's gameplay mode hint ('overkill', etc.).
5051
- se_right: !!v.right,
5052
- se_bossNamePosition: typeof v.bossName === "string" ? v.bossName : null,
5053
- se_mode: typeof v.mode === "string" ? v.mode : null,
5054
- se_showName: v.showName !== false
5098
+ // Source-side knobs preserved as provenance for future passes.
5099
+ // None have direct Lumia analogs today:
5100
+ // - `right` anchors the boss to the right edge of its
5101
+ // bounds; Lumia uses layer position instead.
5102
+ // - `bossNamePosition` SE-controlled vertical name position; our
5103
+ // themes don't expose that level of tweaking.
5104
+ // - `mode` source-side gameplay mode hint ('overkill').
5105
+ importMeta: buildImportMeta(widget, {
5106
+ right: !!v.right,
5107
+ bossNamePosition: typeof v.bossName === "string" ? v.bossName : null,
5108
+ mode: typeof v.mode === "string" ? v.mode : null,
5109
+ showName: v.showName !== false
5110
+ })
5055
5111
  },
5056
5112
  css: {
5057
5113
  background: "transparent"
5058
5114
  }
5059
- });
5115
+ }, ctx);
5060
5116
  if (theme === "card") {
5061
5117
  const MIN_CARD_WIDTH = 480;
5062
5118
  const MIN_CARD_HEIGHT = 110;
@@ -5067,25 +5123,6 @@ function mapStreamBoss(widget) {
5067
5123
  }
5068
5124
 
5069
5125
  // 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
5126
  function isWidgetFlaggedOff(_seType) {
5090
5127
  return false;
5091
5128
  }
@@ -5140,7 +5177,7 @@ var GOAL_TYPES = /* @__PURE__ */ new Set([
5140
5177
  "se-widget-merch-goal"
5141
5178
  ]);
5142
5179
  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) {
5180
+ function dispatch(widget, ctx) {
5144
5181
  const t = widget.type;
5145
5182
  console.log("[SE-IMPORT] dispatch widget", {
5146
5183
  seType: t,
@@ -5151,46 +5188,46 @@ function dispatch(widget) {
5151
5188
  hasCss: !!widget.css
5152
5189
  });
5153
5190
  if (isWidgetFlaggedOff(t)) {
5154
- return { unit: mapUnsupportedAsText(widget), status: "placeholder", lumiaType: "text", flaggedOff: true };
5191
+ return { unit: mapUnsupportedAsText(widget, ctx), status: "placeholder", lumiaType: "text", flaggedOff: true };
5155
5192
  }
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" };
5193
+ if (t === "text") return { unit: mapText(widget, ctx), status: "direct", lumiaType: "text" };
5194
+ if (t === "image") return { unit: mapImage(widget, ctx), status: "direct", lumiaType: "image" };
5195
+ if (t === "video") return { unit: mapVideo(widget, ctx), status: "direct", lumiaType: "video" };
5159
5196
  if (t in READOUT_TYPES) {
5160
- return { unit: mapReadout(widget, READOUT_TYPES[t]), status: "direct", lumiaType: "text" };
5197
+ return { unit: mapReadout(widget, READOUT_TYPES[t], ctx), status: "direct", lumiaType: "text" };
5161
5198
  }
5162
- if (GOAL_TYPES.has(t)) return { unit: mapGoal(widget), status: "direct", lumiaType: "goal" };
5199
+ if (GOAL_TYPES.has(t)) return { unit: mapGoal(widget, ctx), status: "direct", lumiaType: "goal" };
5163
5200
  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" };
5201
+ return { unit: mapUnsupportedAsText(widget, ctx), status: "placeholder", lumiaType: "text", flaggedOff: true };
5202
+ }
5203
+ if (t === "se-widget-alert-box") return { unit: mapAlertBox(widget, {}, ctx), status: "direct", lumiaType: "alert" };
5204
+ if (t === "se-widget-alert-box-merch") return { unit: mapAlertBox(widget, { onlyEvents: ["merch"] }, ctx), status: "direct", lumiaType: "alert" };
5205
+ if (t === "se-widget-donor-drive-alert") return { unit: mapSingleAlert(widget, "donordrive-donation", ctx), status: "direct", lumiaType: "alert" };
5206
+ if (t === "se-widget-extralife-alert") return { unit: mapSingleAlert(widget, "extralife-donation", ctx), status: "direct", lumiaType: "alert" };
5207
+ if (t === "se-widget-tiltify-alert") return { unit: mapSingleAlert(widget, "tiltify-campaignDonation", ctx), status: "direct", lumiaType: "alert" };
5208
+ if (EVENTLIST_TYPES.has(t)) return { unit: mapEventList(widget, ctx), status: "direct", lumiaType: "eventlist" };
5209
+ if (t === "se-widget-top-tippers-list") return { unit: mapLeaderboardFromVariables(widget, "tips", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
5210
+ if (t === "se-widget-top-cheerers-list") return { unit: mapLeaderboardFromVariables(widget, "cheers", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
5211
+ if (t === "se-widget-top-gifters-list") return { unit: mapLeaderboardFromVariables(widget, "gifts", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
5212
+ if (t === "se-widget-credit-roll") return { unit: mapCredits(widget, ctx), status: "partial", lumiaType: "credits" };
5213
+ if (t === "se-widget-twitch-chat") return { unit: mapChatbox(widget, ctx), status: "partial", lumiaType: "chatbox" };
5214
+ 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" };
5215
+ if (t === "se-widget-countdown") return { unit: mapTimer(widget, ctx), status: "partial", lumiaType: "timer" };
5216
+ if (t === "se-widget-current-song") return { unit: mapNowPlaying(widget, ctx), status: "partial", lumiaType: "nowplaying" };
5217
+ if (t === "se-widget-kappagen") return { unit: mapKappagen(widget, ctx), status: "partial", lumiaType: "emotealert" };
5218
+ if (t === "se-widget-giveaway") return { unit: mapGiveaway(widget, ctx), status: "direct", lumiaType: "raffle" };
5219
+ if (t === "se-widget-media-share") return { unit: mapMediaShare(widget, ctx), status: "partial", lumiaType: "songrequest" };
5220
+ if (t === "se-widget-hype-cup") return { unit: mapHypeCup(widget, ctx), status: "partial", lumiaType: "tipjar" };
5221
+ if (t === "se-widget-bit-boss") return { unit: mapStreamBoss(widget, ctx), status: "partial", lumiaType: "streamboss" };
5222
+ if (t === "se-widget-hype-train") return { unit: mapHypetrain(widget, ctx), status: "partial", lumiaType: "hypetrain" };
5186
5223
  if (t === "se-widget-custom-event-list" && isHypetrainCustomWidget(widget)) {
5187
- return { unit: mapHypetrain(widget), status: "partial", lumiaType: "hypetrain" };
5224
+ return { unit: mapHypetrain(widget, ctx), status: "partial", lumiaType: "hypetrain" };
5188
5225
  }
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" };
5226
+ if (t === "se-widget-custom-event-list") return { unit: mapCustom(widget, ctx), status: "partial", lumiaType: "custom" };
5227
+ if (t === "se-widget-snow") return { unit: mapSnow(widget, ctx), status: "direct", lumiaType: "video" };
5228
+ if (t === "se-widget-halloween-2019") return { unit: mapSlideshow(widget, ctx), status: "direct", lumiaType: "slideshow" };
5229
+ if (SEASONAL_IMAGE_TYPES.has(t)) return { unit: mapImage(widget, ctx), status: "direct", lumiaType: "image" };
5230
+ return { unit: mapUnsupportedAsText(widget, ctx), status: "placeholder", lumiaType: "text" };
5194
5231
  }
5195
5232
  function recordCoverage(coverage, result, seType) {
5196
5233
  const existing = coverage.mappings.find((m) => m.seType === seType);
@@ -5248,11 +5285,19 @@ function rewriteAssetURLs(overlay, mapping) {
5248
5285
  settings: JSON.parse(rewritten)
5249
5286
  };
5250
5287
  }
5288
+ var MAX_MIRROR_ASSET_BYTES = 100 * 1024 * 1024;
5251
5289
  async function mirrorOneAsset(url, upload, signal) {
5252
5290
  const res = await fetch(url, { mode: "cors", signal });
5253
5291
  if (!res.ok) throw new Error(`Source returned HTTP ${res.status}`);
5292
+ const declared = Number(res.headers.get("content-length"));
5293
+ if (Number.isFinite(declared) && declared > MAX_MIRROR_ASSET_BYTES) {
5294
+ throw new Error(`Asset is ${(declared / (1024 * 1024)).toFixed(1)} MB \u2014 over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
5295
+ }
5254
5296
  const blob = await res.blob();
5255
5297
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
5298
+ if (blob.size > MAX_MIRROR_ASSET_BYTES) {
5299
+ throw new Error(`Asset is ${(blob.size / (1024 * 1024)).toFixed(1)} MB \u2014 over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
5300
+ }
5256
5301
  const filename = filenameFromURL(url);
5257
5302
  const type = blob.type || guessMime(filename);
5258
5303
  const file = new File([blob], filename, { type });
@@ -5965,13 +6010,19 @@ function CustomOverlayPreview({
5965
6010
  }) {
5966
6011
  const srcdoc = useMemo5(() => {
5967
6012
  const safeData = JSON.stringify(data ?? {});
5968
- const safeJs = JSON.stringify(`(async () => { ${js || ""} })()`);
6013
+ const safeJs = (js || "").replace(/<\/script>/gi, "<\\/script>");
5969
6014
  const safeHtml = (html || "").replace(/<\/script>/gi, "<\\/script>");
5970
6015
  const safeCss = (css || "").replace(/<\/style>/gi, "<\\/style>");
5971
6016
  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
6017
  window.DATA = ${safeData};
5973
6018
  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>'); }
6019
+ function __seImportShowError(e){document.body.insertAdjacentHTML('beforeend','<pre style="color:#fca5a5;font:11px monospace;padding:8px;">' + (e && e.message ? e.message : e) + '</pre>');}
6020
+ window.addEventListener('error', function(ev){__seImportShowError(ev.error || ev.message);});
6021
+ window.addEventListener('unhandledrejection', function(ev){__seImportShowError(ev.reason);});
6022
+ </script><script>
6023
+ (async function __seImportUserScript(){
6024
+ ${safeJs}
6025
+ })().catch(__seImportShowError);
5975
6026
  </script></body></html>`;
5976
6027
  }, [html, css, js, data]);
5977
6028
  return /* @__PURE__ */ jsx14(
@@ -6529,7 +6580,8 @@ function StepURL({
6529
6580
  url,
6530
6581
  setUrl,
6531
6582
  error,
6532
- busy
6583
+ busy,
6584
+ onSwitchToJwt
6533
6585
  }) {
6534
6586
  return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--narrow"), children: [
6535
6587
  /* @__PURE__ */ jsx14(
@@ -6575,7 +6627,16 @@ function StepURL({
6575
6627
  ] })
6576
6628
  ] })
6577
6629
  ] }),
6578
- error && /* @__PURE__ */ jsx14(ErrorPanel, { message: error.message, hint: error.hint })
6630
+ error && /* @__PURE__ */ jsx14(ErrorPanel, { message: error.message, hint: error.hint }),
6631
+ error?.editorUrl && onSwitchToJwt && /* @__PURE__ */ jsx14("div", { style: { marginTop: 12 }, children: /* @__PURE__ */ jsx14(
6632
+ LSButton,
6633
+ {
6634
+ type: "button",
6635
+ color: "primary",
6636
+ onClick: onSwitchToJwt,
6637
+ label: "Connect with JWT instead"
6638
+ }
6639
+ ) })
6579
6640
  ] });
6580
6641
  }
6581
6642
  function StepDiscovery({
@@ -7082,10 +7143,7 @@ function SEImportWizard({
7082
7143
  const mirrorStartedRef = useRef4(false);
7083
7144
  const mirrorAbortRef = useRef4(null);
7084
7145
  const filenameCacheRef = useRef4(/* @__PURE__ */ new Map());
7085
- const assetUrls = useMemo5(
7086
- () => result ? findSEAssetURLs(result.overlay) : [],
7087
- [result]
7088
- );
7146
+ const [assetUrls, setAssetUrls] = useState6([]);
7089
7147
  const currentItem = originalReviewItems[reviewIndex];
7090
7148
  const currentRow = currentItem ? rowStates[currentItem.moduleId] ?? { state: "pending" } : void 0;
7091
7149
  const handleConnect = useCallback3(async () => {
@@ -7183,6 +7241,7 @@ function SEImportWizard({
7183
7241
  }
7184
7242
  const imported = importSEBootstrap(bootstrap);
7185
7243
  setResult(imported);
7244
+ setAssetUrls(findSEAssetURLs(imported.overlay));
7186
7245
  setOptions((o) => ({ ...o, name: imported.overlay.name }));
7187
7246
  setOriginalReviewItems(imported.reviewItems);
7188
7247
  setRowStates(
@@ -7198,9 +7257,10 @@ function SEImportWizard({
7198
7257
  mirrorStartedRef.current = false;
7199
7258
  setStep("discovery");
7200
7259
  } catch (e) {
7260
+ const detail = e?.message ?? String(e);
7201
7261
  setLoadError({
7202
- message: "Could not load the overlay.",
7203
- hint: e?.message ?? String(e)
7262
+ message: detail || "Could not load the overlay.",
7263
+ hint: detail ? "If this looks transient, retry in a moment. Otherwise check the URL / token and try again." : void 0
7204
7264
  });
7205
7265
  } finally {
7206
7266
  setLoading(false);
@@ -7231,7 +7291,8 @@ function SEImportWizard({
7231
7291
  if (!parts.apikey) {
7232
7292
  setLoadError({
7233
7293
  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}`
7294
+ 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}`,
7295
+ editorUrl: true
7235
7296
  });
7236
7297
  return;
7237
7298
  }
@@ -7240,6 +7301,7 @@ function SEImportWizard({
7240
7301
  const bootstrap = await fetchSEBootstrap(parts);
7241
7302
  const imported = importSEBootstrap(bootstrap);
7242
7303
  setResult(imported);
7304
+ setAssetUrls(findSEAssetURLs(imported.overlay));
7243
7305
  setOptions((previous) => ({ ...previous, name: imported.overlay.name }));
7244
7306
  setOriginalReviewItems(imported.reviewItems);
7245
7307
  setRowStates(
@@ -7253,27 +7315,21 @@ function SEImportWizard({
7253
7315
  setReviewIndex(0);
7254
7316
  setStep("discovery");
7255
7317
  } catch (error) {
7318
+ const detail = error?.message ?? String(error);
7256
7319
  setLoadError({
7257
- message: "Could not load the overlay.",
7258
- hint: error?.message
7320
+ message: detail || "Could not load the overlay.",
7321
+ hint: detail ? "If this looks transient, retry in a moment. Otherwise check the URL / token and try again." : void 0
7259
7322
  });
7260
7323
  } finally {
7261
7324
  setLoading(false);
7262
7325
  }
7263
7326
  }, [url]);
7327
+ const MIRROR_CONCURRENCY = 5;
7264
7328
  const processAssetRow = useCallback3(
7265
7329
  async (assetUrl, idx, existingByFilename, signal) => {
7266
7330
  const filename = filenameFromURL(assetUrl);
7267
7331
  const reuseUrl = existingByFilename.get(filename);
7268
7332
  if (reuseUrl) {
7269
- setResult(
7270
- (current) => current ? {
7271
- ...current,
7272
- overlay: rewriteAssetURLs(current.overlay, {
7273
- [assetUrl]: reuseUrl
7274
- })
7275
- } : current
7276
- );
7277
7333
  return { state: "reused", newUrl: reuseUrl };
7278
7334
  }
7279
7335
  if (filename.toLowerCase().endsWith(".svg"))
@@ -7290,14 +7346,6 @@ function SEImportWizard({
7290
7346
  signal
7291
7347
  );
7292
7348
  existingByFilename.set(filename, newUrl);
7293
- setResult(
7294
- (current) => current ? {
7295
- ...current,
7296
- overlay: rewriteAssetURLs(current.overlay, {
7297
- [assetUrl]: newUrl
7298
- })
7299
- } : current
7300
- );
7301
7349
  return { state: "done", newUrl };
7302
7350
  } catch (error) {
7303
7351
  if (error?.name === "AbortError" || signal?.aborted)
@@ -7307,8 +7355,15 @@ function SEImportWizard({
7307
7355
  },
7308
7356
  [uploadAsset]
7309
7357
  );
7358
+ const applyRewrite = useCallback3((mapping) => {
7359
+ if (Object.keys(mapping).length === 0) return;
7360
+ setResult(
7361
+ (current) => current ? { ...current, overlay: rewriteAssetURLs(current.overlay, mapping) } : current
7362
+ );
7363
+ }, []);
7310
7364
  const runMirror = useCallback3(async () => {
7311
7365
  if (!result) return;
7366
+ mirrorAbortRef.current?.abort();
7312
7367
  mirrorAbortRef.current = new AbortController();
7313
7368
  const signal = mirrorAbortRef.current.signal;
7314
7369
  setMirrorRunning(true);
@@ -7323,21 +7378,33 @@ function SEImportWizard({
7323
7378
  if (name && asset.url) existingByFilename.set(name, asset.url);
7324
7379
  }
7325
7380
  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
- }
7381
+ const rewriteMap = {};
7382
+ let cursor = 0;
7383
+ const worker = async () => {
7384
+ while (true) {
7385
+ if (signal.aborted) return;
7386
+ const i = cursor++;
7387
+ if (i >= initialRows.length) return;
7388
+ const outcome = await processAssetRow(
7389
+ initialRows[i].url,
7390
+ i,
7391
+ existingByFilename,
7392
+ signal
7393
+ );
7394
+ if (signal.aborted) return;
7395
+ if (outcome.newUrl && (outcome.state === "done" || outcome.state === "reused")) {
7396
+ rewriteMap[initialRows[i].url] = outcome.newUrl;
7397
+ }
7398
+ setMirrorRows(
7399
+ (previous) => previous.map((row, idx) => idx === i ? { ...row, ...outcome } : row)
7400
+ );
7401
+ }
7402
+ };
7403
+ const poolSize = Math.min(MIRROR_CONCURRENCY, initialRows.length);
7404
+ await Promise.all(Array.from({ length: poolSize }, worker));
7405
+ if (!signal.aborted) applyRewrite(rewriteMap);
7339
7406
  setMirrorRunning(false);
7340
- }, [assetUrls, existingAssets, processAssetRow, result]);
7407
+ }, [applyRewrite, assetUrls, existingAssets, processAssetRow, result]);
7341
7408
  const retryMirrorRow = useCallback3(
7342
7409
  async (idx) => {
7343
7410
  const rowUrl = mirrorRows[idx]?.url;
@@ -7351,8 +7418,11 @@ function SEImportWizard({
7351
7418
  setMirrorRows(
7352
7419
  (previous) => previous.map((row, i) => i === idx ? { ...row, ...outcome } : row)
7353
7420
  );
7421
+ if (outcome.newUrl && (outcome.state === "done" || outcome.state === "reused")) {
7422
+ applyRewrite({ [rowUrl]: outcome.newUrl });
7423
+ }
7354
7424
  },
7355
- [mirrorRows, processAssetRow]
7425
+ [applyRewrite, mirrorRows, processAssetRow]
7356
7426
  );
7357
7427
  useEffect6(() => {
7358
7428
  if (step === "mirror" && !mirrorStartedRef.current) {
@@ -7360,6 +7430,13 @@ function SEImportWizard({
7360
7430
  void runMirror();
7361
7431
  }
7362
7432
  }, [step, runMirror]);
7433
+ useEffect6(() => {
7434
+ return () => {
7435
+ setJwt("");
7436
+ setSeClient(null);
7437
+ mirrorAbortRef.current?.abort();
7438
+ };
7439
+ }, []);
7363
7440
  useEffect6(() => {
7364
7441
  if (step === "pick" && seClient && overlayList === null && !overlaysLoading) {
7365
7442
  void loadOverlayList(seClient);
@@ -7754,7 +7831,12 @@ function SEImportWizard({
7754
7831
  url,
7755
7832
  setUrl,
7756
7833
  error: loadError,
7757
- busy: loading
7834
+ busy: loading,
7835
+ onSwitchToJwt: () => {
7836
+ setLoadError(null);
7837
+ setEntryMode("jwt");
7838
+ setStep("connect");
7839
+ }
7758
7840
  }
7759
7841
  ),
7760
7842
  step === "discovery" && result && /* @__PURE__ */ jsx14(StepDiscovery, { result, assetCount: assetUrls.length }),
@@ -7867,6 +7949,7 @@ function extractErrorMessage(err) {
7867
7949
  }
7868
7950
 
7869
7951
  // src/se-import/index.ts
7952
+ var IMPORT_META_KEY = "importMeta";
7870
7953
  var REVIEW_REASONS = {
7871
7954
  "se-widget-bit-boss": "No native Lumia equivalent \u2014 bit boss fight bar with HP/damage states.",
7872
7955
  // `se-widget-hype-cup` intentionally omitted — it now routes to the native
@@ -7958,145 +8041,148 @@ function importSEBootstrap(bootstrap) {
7958
8041
  name: seOverlay?.name,
7959
8042
  canvas: { width: seOverlay?.settings?.width, height: seOverlay?.settings?.height },
7960
8043
  widgetCount: widgets.length,
7961
- widgetTypes: widgets.map((w) => w.type),
7962
- fullBootstrap: bootstrap
8044
+ widgetTypes: widgets.map((w) => w.type)
7963
8045
  });
7964
8046
  const importCanvas = {
7965
8047
  width: seOverlay?.settings?.width ?? 1920,
7966
8048
  height: seOverlay?.settings?.height ?? 1080
7967
8049
  };
7968
- setImportCanvas(importCanvas);
8050
+ const ctx = { canvas: importCanvas };
7969
8051
  const layers = [];
7970
8052
  const modules = {};
7971
8053
  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;
8054
+ const seUidToLumiaGroupId = {};
8055
+ const groupWidgetByUid = {};
8056
+ for (const w of widgets) {
8057
+ if (w.type !== "se-widget-group") continue;
8058
+ const uid = w.variables?.uid;
8059
+ if (typeof uid !== "string") continue;
8060
+ const lumiaGroupId = nanoid3();
8061
+ seUidToLumiaGroupId[uid] = lumiaGroupId;
8062
+ groupWidgetByUid[uid] = w;
8063
+ }
8064
+ for (const uid of Object.keys(groupWidgetByUid)) {
8065
+ const w = groupWidgetByUid[uid];
8066
+ const lumiaGroupId = seUidToLumiaGroupId[uid];
8067
+ const parentUid = typeof w.group === "string" ? w.group : null;
8068
+ const parentLumiaGroupId = parentUid ? seUidToLumiaGroupId[parentUid] ?? null : null;
8069
+ const bounds = seCssToBounds(w.css, { width: 200, height: 200 }, importCanvas);
8070
+ const layer = {
8071
+ id: lumiaGroupId,
8072
+ type: "group",
8073
+ group: parentLumiaGroupId,
8074
+ bounds: {
8075
+ x: bounds.x,
8076
+ y: bounds.y,
8077
+ width: bounds.width,
8078
+ height: bounds.height,
8079
+ opacity: bounds.opacity,
8080
+ zIndex: bounds.zIndex,
8081
+ rotate: 0,
8082
+ scale: [1, 1],
8083
+ matrix: ""
8084
+ },
8085
+ state: {
8086
+ locked: w.locked ?? false,
8087
+ visible: w.visible ?? true,
8088
+ // SE's `variables.expanded` is the editor's collapsed/open state.
8089
+ expanded: w.variables?.expanded ?? true
8090
+ }
8091
+ };
8092
+ const module = {
8093
+ id: lumiaGroupId,
8094
+ loaded: true,
8095
+ settings: { title: defaultLabelForSEWidget(w), type: "group" },
8096
+ lights: [],
8097
+ css: {},
8098
+ content: {
8099
+ [IMPORT_META_KEY]: {
8100
+ source: "streamelements",
8101
+ widgetType: w.type,
8102
+ importedAt: (/* @__PURE__ */ new Date()).toISOString(),
8103
+ widget: w,
8104
+ reason: "Imported as native group",
8105
+ status: "group"
8034
8106
  }
8107
+ },
8108
+ events: {},
8109
+ variables: {}
8110
+ };
8111
+ layers.push(layer);
8112
+ modules[lumiaGroupId] = module;
8113
+ recordCoverage(coverage, { unit: { layer, module }, status: "direct", lumiaType: "group" }, w.type);
8114
+ }
8115
+ for (const w of widgets) {
8116
+ if (w.type === "se-widget-group") continue;
8117
+ const result = dispatch(w, ctx);
8118
+ if (typeof w.group === "string") {
8119
+ const parentLumiaGroupId = seUidToLumiaGroupId[w.group];
8120
+ if (parentLumiaGroupId) {
8121
+ result.unit.layer.group = parentLumiaGroupId;
8035
8122
  }
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,
8123
+ }
8124
+ layers.push(result.unit.layer);
8125
+ const module = result.unit.module;
8126
+ if (result.status === "placeholder" || result.status === "template") {
8127
+ module.content = {
8128
+ ...module.content ?? {},
8129
+ [IMPORT_META_KEY]: {
8130
+ source: "streamelements",
8131
+ widgetType: w.type,
8132
+ importedAt: (/* @__PURE__ */ new Date()).toISOString(),
8133
+ widget: w,
8052
8134
  reason: reasonFor(w.type, result.status, result.flaggedOff),
8135
+ status: result.status,
8053
8136
  flaggedOff: result.flaggedOff
8054
- });
8055
- }
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
- }
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.");
8137
+ }
8138
+ };
8139
+ reviewItems.push({
8140
+ moduleId: module.id,
8141
+ seWidget: w,
8142
+ status: result.status,
8143
+ reason: reasonFor(w.type, result.status, result.flaggedOff),
8144
+ flaggedOff: result.flaggedOff
8145
+ });
8067
8146
  }
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
- },
8147
+ modules[module.id] = module;
8148
+ recordCoverage(coverage, result, w.type);
8149
+ }
8150
+ if (coverage.mappings.some((m) => m.status === "placeholder")) {
8151
+ coverage.notes.push("Some widgets imported as placeholder text layers \u2014 see SE/05-widget-to-module-mapping.md for status.");
8152
+ }
8153
+ if (coverage.mappings.some((m) => m.seType === "se-widget-merch-goal")) {
8154
+ coverage.notes.push("Merch-goal widgets imported as generic goals \u2014 see SE/03-missing-variables.md \xA78 for the missing data source.");
8155
+ }
8156
+ if (coverage.mappings.some((m) => m.seType === "se-widget-custom-event-list")) {
8157
+ 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.");
8158
+ }
8159
+ const overlay = {
8160
+ uuid: "",
8161
+ listen_id: "",
8162
+ name: seOverlay?.name ? `[SE] ${seOverlay.name}` : `Imported from StreamElements (${seOverlay?._id ?? ""})`,
8163
+ description: seOverlay?._id ? `Imported from StreamElements overlay ${seOverlay._id}` : "Imported from StreamElements",
8164
+ settings: {
8081
8165
  metadata: {
8082
- width: seOverlay?.settings?.width ?? null,
8083
- height: seOverlay?.settings?.height ?? null
8166
+ width: seOverlay?.settings?.width ?? 1920,
8167
+ height: seOverlay?.settings?.height ?? 1080
8084
8168
  },
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
- }
8169
+ layers,
8170
+ modules
8171
+ },
8172
+ metadata: {
8173
+ width: seOverlay?.settings?.width ?? null,
8174
+ height: seOverlay?.settings?.height ?? null
8175
+ },
8176
+ layers_count: layers.length,
8177
+ hasFullSettings: true
8178
+ };
8179
+ console.log("[SE-IMPORT] import complete", {
8180
+ overlayName: overlay.name,
8181
+ layerCount: overlay.layers_count,
8182
+ coverage,
8183
+ reviewItemCount: reviewItems.length
8184
+ });
8185
+ return { overlay, coverage, reviewItems };
8100
8186
  }
8101
8187
  function applyReviewAction(result, moduleId, action, payload) {
8102
8188
  if (action === "keep") return result;
@@ -8124,11 +8210,17 @@ function applyReviewAction(result, moduleId, action, payload) {
8124
8210
  bounds: existingLayer.bounds,
8125
8211
  state: existingLayer.state
8126
8212
  };
8127
- const provenance2 = existing2.content?.se_provenance;
8213
+ const importMeta2 = existing2.content?.[IMPORT_META_KEY];
8214
+ const mergedLights = (existing2.lights?.length ?? 0) > 0 ? existing2.lights : transplant.module.lights;
8215
+ const mergedEvents = existing2.events && Object.keys(existing2.events).length > 0 ? { ...transplant.module.events, ...existing2.events } : transplant.module.events;
8216
+ const mergedVariables = existing2.variables && Object.keys(existing2.variables).length > 0 ? { ...transplant.module.variables, ...existing2.variables } : transplant.module.variables;
8128
8217
  const newModule = {
8129
8218
  ...transplant.module,
8130
8219
  id: moduleId,
8131
- content: { ...transplant.module.content ?? {}, se_provenance: provenance2 }
8220
+ lights: mergedLights,
8221
+ events: mergedEvents,
8222
+ variables: mergedVariables,
8223
+ content: { ...transplant.module.content ?? {}, [IMPORT_META_KEY]: importMeta2 }
8132
8224
  };
8133
8225
  const rootOldNewId = transplant.layer.id;
8134
8226
  const remappedExtras = (transplant.extras ?? []).map((e) => ({
@@ -8155,7 +8247,7 @@ function applyReviewAction(result, moduleId, action, payload) {
8155
8247
  const existing = modules[moduleId];
8156
8248
  const generated = payload;
8157
8249
  if (!existing || !generated) return result;
8158
- const provenance = existing.content?.se_provenance;
8250
+ const importMeta = existing.content?.[IMPORT_META_KEY];
8159
8251
  const replaced = {
8160
8252
  ...existing,
8161
8253
  settings: { ...existing.settings ?? {}, type: "custom" },
@@ -8169,7 +8261,7 @@ function applyReviewAction(result, moduleId, action, payload) {
8169
8261
  data: parseLooseJson(generated.data) ?? {},
8170
8262
  configs: parseLooseJson(generated.configs) ?? [],
8171
8263
  flavor: "ai-generated",
8172
- se_provenance: provenance
8264
+ [IMPORT_META_KEY]: importMeta
8173
8265
  }
8174
8266
  };
8175
8267
  modules[moduleId] = replaced;
@@ -8198,6 +8290,11 @@ function buildAIPromptForSEWidget(widget, canvas) {
8198
8290
  const listener = typeof widget.listener === "string" ? widget.listener : null;
8199
8291
  const listeners = widget.listeners && typeof widget.listeners === "object" && !Array.isArray(widget.listeners) ? Object.keys(widget.listeners) : null;
8200
8292
  const landing = getAILandingBounds(widget, canvas);
8293
+ const VARIABLES_BUDGET = 6e3;
8294
+ const fullVariables = JSON.stringify(variables, null, 2);
8295
+ const truncated = fullVariables.length > VARIABLES_BUDGET;
8296
+ const variablesJson = truncated ? `${fullVariables.slice(0, VARIABLES_BUDGET)}
8297
+ \u2026[truncated \u2014 ${fullVariables.length - VARIABLES_BUDGET} more bytes elided]` : fullVariables;
8201
8298
  return [
8202
8299
  "Recreate a StreamElements overlay widget as a Lumia Stream custom overlay (HTML + CSS + JS).",
8203
8300
  "",
@@ -8205,10 +8302,11 @@ function buildAIPromptForSEWidget(widget, canvas) {
8205
8302
  listener ? `Primary event listener: ${listener}` : "",
8206
8303
  listeners ? `Subscribes to events: ${listeners.join(", ")}` : "",
8207
8304
  `Canvas: ${landing.width} \xD7 ${landing.height}`,
8305
+ truncated ? "Note: the widget config below is truncated \u2014 focus on visible/structural fields and ignore tail content you cannot see." : "",
8208
8306
  "",
8209
8307
  "Widget configuration (JSON):",
8210
8308
  "```json",
8211
- JSON.stringify(variables, null, 2).slice(0, 6e3),
8309
+ variablesJson,
8212
8310
  "```",
8213
8311
  "",
8214
8312
  "Constraints \u2014 your output must run inside a Lumia Custom Overlay sandboxed iframe:",
@@ -8244,7 +8342,6 @@ export {
8244
8342
  SEAuthError,
8245
8343
  SEClient,
8246
8344
  SEImportWizard,
8247
- SE_IMPORT_FLAGS,
8248
8345
  SE_WIDGET_TO_MARKETPLACE_CANDIDATES,
8249
8346
  applyReviewAction,
8250
8347
  buildAIPromptForSEWidget,