@lumiastream/ui 0.2.8-alpha.29 → 0.2.8-alpha.30

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
@@ -4248,15 +4248,53 @@ function mapCustom(widget, ctx) {
4248
4248
  }
4249
4249
 
4250
4250
  // src/se-import/mappers/misc.ts
4251
+ import {
4252
+ LumiaEventListTypes,
4253
+ LumiaMapAlertTypeToEventListType
4254
+ } from "@lumiastream/lumia-types";
4251
4255
  function mapChatboxTheme(seTheme) {
4252
4256
  const t = (seTheme ?? "").toString().toLowerCase();
4253
4257
  if (!t || t === "default") return "simple";
4254
- if (t.includes("flipin") || t.includes("bubble") || t.includes("bubbly")) return "bubbly";
4258
+ if (t.includes("flipin") || t.includes("bubble") || t.includes("bubbly"))
4259
+ return "bubbly";
4255
4260
  if (t.includes("glass") || t.includes("transparent")) return "glass";
4256
4261
  if (t.includes("boxy") || t.includes("checker")) return "boxy";
4257
4262
  if (t.includes("basic")) return "basic";
4258
4263
  return "simple";
4259
4264
  }
4265
+ var KAPPAGEN_EVENT_CATEGORIES = {
4266
+ tip: [LumiaEventListTypes.DONATION],
4267
+ cheer: [
4268
+ LumiaEventListTypes.BITS,
4269
+ LumiaEventListTypes.KICKS,
4270
+ LumiaEventListTypes.SUPERCHATS,
4271
+ LumiaEventListTypes.STARS
4272
+ ],
4273
+ embers: [LumiaEventListTypes.BITS],
4274
+ follower: [LumiaEventListTypes.FOLLOWER],
4275
+ host: [LumiaEventListTypes.HOSTS],
4276
+ raid: [LumiaEventListTypes.RAIDS],
4277
+ subscriber: [LumiaEventListTypes.SUBSCRIBERS],
4278
+ purchase: [LumiaEventListTypes.PURCHASES]
4279
+ };
4280
+ var alertKeysByEventListType = Object.entries(
4281
+ LumiaMapAlertTypeToEventListType
4282
+ ).reduce(
4283
+ (acc, [alertKey, eventListType]) => {
4284
+ acc[eventListType] = acc[eventListType] ?? [];
4285
+ acc[eventListType].push(alertKey);
4286
+ return acc;
4287
+ },
4288
+ {}
4289
+ );
4290
+ function kappagenAlertTriggersFor(seEvent, provider) {
4291
+ const categoryKeys = (KAPPAGEN_EVENT_CATEGORIES[seEvent] ?? []).flatMap(
4292
+ (eventListType) => alertKeysByEventListType[eventListType] ?? []
4293
+ );
4294
+ if (categoryKeys.length > 0) return categoryKeys;
4295
+ const fallback = kappagenKeyFor(seEvent, provider);
4296
+ return fallback ? [fallback] : [];
4297
+ }
4260
4298
  function paddingShorthand(message) {
4261
4299
  const pick = (key) => {
4262
4300
  const raw = message?.[key];
@@ -4280,13 +4318,18 @@ function mapChatbox(widget, ctx) {
4280
4318
  const messagePadding = paddingShorthand(messageCss);
4281
4319
  const DEFAULT_CHATBOX_FONT_SIZE = 14;
4282
4320
  const moduleCss = {};
4283
- if (textCss["font-family"] != null) moduleCss.fontFamily = textCss["font-family"];
4321
+ if (textCss["font-family"] != null)
4322
+ moduleCss.fontFamily = textCss["font-family"];
4284
4323
  moduleCss.fontSize = textCss["font-size"] != null ? textCss["font-size"] : DEFAULT_CHATBOX_FONT_SIZE;
4285
4324
  if (textCss["color"] != null) moduleCss.color = textCss["color"];
4286
- if (textCss["font-weight"] != null) moduleCss.fontWeight = textCss["font-weight"];
4287
- if (textCss["text-shadow"] != null) moduleCss.textShadow = textCss["text-shadow"];
4288
- if (textCss["text-align"] != null) moduleCss.textAlign = textCss["text-align"];
4289
- if (textCss["line-height"] != null) moduleCss.lineHeight = textCss["line-height"];
4325
+ if (textCss["font-weight"] != null)
4326
+ moduleCss.fontWeight = textCss["font-weight"];
4327
+ if (textCss["text-shadow"] != null)
4328
+ moduleCss.textShadow = textCss["text-shadow"];
4329
+ if (textCss["text-align"] != null)
4330
+ moduleCss.textAlign = textCss["text-align"];
4331
+ if (textCss["line-height"] != null)
4332
+ moduleCss.lineHeight = textCss["line-height"];
4290
4333
  if (messagePadding) moduleCss.padding = messagePadding;
4291
4334
  const DEFAULT_CHATBOX_BG = "#1e1e1e";
4292
4335
  const messageBackground = typeof messageCss["background"] === "string" && messageCss["background"].length > 0 ? messageCss["background"] : DEFAULT_CHATBOX_BG;
@@ -4294,72 +4337,82 @@ function mapChatbox(widget, ctx) {
4294
4337
  const messageFontFamily = typeof messageCss["font-family"] === "string" && messageCss["font-family"].length > 0 ? messageCss["font-family"] : void 0;
4295
4338
  const highlightBackground = v.highlight && typeof v.highlight === "object" && typeof v.highlight["background"] === "string" ? v.highlight["background"] : void 0;
4296
4339
  const accentColor = highlightBackground && highlightBackground.length > 0 ? highlightBackground : void 0;
4297
- return buildUnit(widget, "chatbox", {
4298
- // Visual styling (font / color / padding etc) lives on module.css, not
4299
- // content. Lumia's TextStyle settings panel + the chatbox renderer both
4300
- // read from here.
4301
- css: moduleCss,
4302
- content: {
4303
- theme: mapChatboxTheme(v.theme),
4304
- fadeOutAfterDelay: fadeOutSeconds > 0,
4305
- fadeOutDelayTime: fadeOutSeconds,
4306
- // Defaults Lumia's chatbox renderer + UI expect on every module.
4307
- // Without these the chatbox silently dropped messages (the
4308
- // `ignoredStreamingSites` truthy gate), rendered in the wrong
4309
- // theme (no theme blank list), or showed missing controls in
4310
- // settings. `ignoredList` carries SE's per-user ignore list across.
4311
- ignoredList: Array.isArray(v.ignored) ? v.ignored : [],
4312
- ignoredStreamingSites: [],
4313
- horizontal: false,
4314
- reverseFlow: false,
4315
- hideAlerts: false,
4316
- hideAlertMessage: false,
4317
- showAvatar: true,
4318
- showBadges: true,
4319
- showEmotes: true,
4320
- showSiteIcon: true,
4321
- showTimestamp: false,
4322
- breakLine: false,
4323
- hyperClickableLinks: true,
4324
- clickableChatterProfiles: true,
4325
- previewMediaInChat: true,
4326
- maxItemsToShow: 20,
4327
- removeAfter: 3e4,
4328
- itemGap: 8,
4329
- borderRadius: "10px",
4330
- bgOpacity: 0.4,
4331
- animations: {
4332
- enterAnimation: "fadeIn",
4333
- exitAnimation: "fadeOut",
4334
- enterAnimationDuration: 1e3,
4335
- exitAnimationDuration: 1e3,
4336
- enterAnimationDelay: 0,
4337
- exitAnimationDelay: 0
4338
- },
4339
- // Carry the SE message-level font override and the streamer's
4340
- // accent color across so the imported chatbox looks right at
4341
- // first paint. Background lives on `module.css.background`
4342
- // above `content.background` was a dead write the renderer
4343
- // never read.
4344
- ...messageFontFamily ? { messageFontFamily } : {},
4345
- ...accentColor ? {
4346
- chatboxStreamingSite: {
4347
- twitch: { themeConfig: { primaryColor: accentColor, showUsernameColors: true } }
4348
- }
4349
- } : {},
4350
- // Source-side knobs preserved under the generic importMeta envelope —
4351
- // no native Lumia equivalent today. `messageDelay` is per-message
4352
- // hold, `initialDelay` is the show-after-connect delay, and we
4353
- // stash the raw highlight block + text css for any future feature
4354
- // that wants the fine-grained styling.
4355
- importMeta: buildImportMeta(widget, {
4356
- messageDelay: v.messageDelay,
4357
- initialDelay: v.delay,
4358
- highlightUsers: v.highlight,
4359
- textCss
4360
- })
4361
- }
4362
- }, ctx);
4340
+ return buildUnit(
4341
+ widget,
4342
+ "chatbox",
4343
+ {
4344
+ // Visual styling (font / color / padding etc) lives on module.css, not
4345
+ // content. Lumia's TextStyle settings panel + the chatbox renderer both
4346
+ // read from here.
4347
+ css: moduleCss,
4348
+ content: {
4349
+ theme: mapChatboxTheme(v.theme),
4350
+ fadeOutAfterDelay: fadeOutSeconds > 0,
4351
+ fadeOutDelayTime: fadeOutSeconds,
4352
+ // Defaults Lumia's chatbox renderer + UI expect on every module.
4353
+ // Without these the chatbox silently dropped messages (the
4354
+ // `ignoredStreamingSites` truthy gate), rendered in the wrong
4355
+ // theme (no theme → blank list), or showed missing controls in
4356
+ // settings. `ignoredList` carries SE's per-user ignore list across.
4357
+ ignoredList: Array.isArray(v.ignored) ? v.ignored : [],
4358
+ ignoredStreamingSites: [],
4359
+ horizontal: false,
4360
+ reverseFlow: false,
4361
+ hideAlerts: false,
4362
+ hideAlertMessage: false,
4363
+ showAvatar: true,
4364
+ showBadges: true,
4365
+ showEmotes: true,
4366
+ showSiteIcon: true,
4367
+ showTimestamp: false,
4368
+ breakLine: false,
4369
+ hyperClickableLinks: true,
4370
+ clickableChatterProfiles: true,
4371
+ previewMediaInChat: true,
4372
+ maxItemsToShow: 20,
4373
+ removeAfter: 3e4,
4374
+ itemGap: 8,
4375
+ borderRadius: "10px",
4376
+ bgOpacity: 0.4,
4377
+ animations: {
4378
+ enterAnimation: "fadeIn",
4379
+ exitAnimation: "fadeOut",
4380
+ enterAnimationDuration: 1e3,
4381
+ exitAnimationDuration: 1e3,
4382
+ enterAnimationDelay: 0,
4383
+ exitAnimationDelay: 0
4384
+ },
4385
+ // Carry the SE message-level font override and the streamer's
4386
+ // accent color across so the imported chatbox looks right at
4387
+ // first paint. Background lives on `module.css.background`
4388
+ // above — `content.background` was a dead write the renderer
4389
+ // never read.
4390
+ ...messageFontFamily ? { messageFontFamily } : {},
4391
+ ...accentColor ? {
4392
+ chatboxStreamingSite: {
4393
+ twitch: {
4394
+ themeConfig: {
4395
+ primaryColor: accentColor,
4396
+ showUsernameColors: true
4397
+ }
4398
+ }
4399
+ }
4400
+ } : {},
4401
+ // Source-side knobs preserved under the generic importMeta envelope —
4402
+ // no native Lumia equivalent today. `messageDelay` is per-message
4403
+ // hold, `initialDelay` is the show-after-connect delay, and we
4404
+ // stash the raw highlight block + text css for any future feature
4405
+ // that wants the fine-grained styling.
4406
+ importMeta: buildImportMeta(widget, {
4407
+ messageDelay: v.messageDelay,
4408
+ initialDelay: v.delay,
4409
+ highlightUsers: v.highlight,
4410
+ textCss
4411
+ })
4412
+ }
4413
+ },
4414
+ ctx
4415
+ );
4363
4416
  }
4364
4417
  var SE_LISTENER_TO_LUMIA_CATEGORY = {
4365
4418
  "follower-latest": "follower",
@@ -4407,7 +4460,9 @@ var ALL_LUMIA_EVENTLIST_CATEGORIES = [
4407
4460
  function computeListenerFilters(seListeners, explicitListener) {
4408
4461
  const enabled = [];
4409
4462
  if (seListeners && typeof seListeners === "object" && !Array.isArray(seListeners)) {
4410
- for (const [k, v] of Object.entries(seListeners)) {
4463
+ for (const [k, v] of Object.entries(
4464
+ seListeners
4465
+ )) {
4411
4466
  if (v) enabled.push(k);
4412
4467
  }
4413
4468
  } else if (Array.isArray(seListeners)) {
@@ -4429,35 +4484,40 @@ function mapEventList(widget, ctx) {
4429
4484
  const v = widget.variables ?? {};
4430
4485
  const filters = computeListenerFilters(widget.listeners, widget.listener);
4431
4486
  const maxItems = v.visibleEvents ?? v.visibleItems ?? 5;
4432
- return buildUnit(widget, "eventlist", {
4433
- // SE event-list widgets style per-event lines via widget.text.css; the
4434
- // Lumia eventlist reads font/size/color from module.css.
4435
- css: mapSeTextCssToModuleCss(widget, { fontSize: 18 }),
4436
- content: {
4437
- theme: v.theme ?? "simple",
4438
- direction: v.direction ?? "top",
4439
- maxItemsToShow: maxItems,
4440
- visibleEvents: maxItems,
4441
- // legacy field name kept for back-compat
4442
- themeBgOpacity: v.themeBgOpacity ?? 0,
4443
- highlight: v.highlight,
4444
- // `filters` is a blacklist: every category here is HIDDEN. Empty = show all.
4445
- filters,
4446
- // `sites` is a platform blacklist (same shape). Default to "no platforms hidden".
4447
- sites: [],
4448
- // Eventlist Manager assumes these objects exist; provide an empty stub so
4449
- // imported overlays render without throwing.
4450
- themeConfig: {},
4451
- eventListType: {},
4452
- condensedText: false,
4453
- horizontal: false,
4454
- showAlertIcon: true,
4455
- showSiteIcon: true,
4456
- showUserAvatar: true,
4457
- showDetailedText: false,
4458
- hideAlertMessage: false
4459
- }
4460
- }, ctx);
4487
+ return buildUnit(
4488
+ widget,
4489
+ "eventlist",
4490
+ {
4491
+ // SE event-list widgets style per-event lines via widget.text.css; the
4492
+ // Lumia eventlist reads font/size/color from module.css.
4493
+ css: mapSeTextCssToModuleCss(widget, { fontSize: 18 }),
4494
+ content: {
4495
+ theme: v.theme ?? "simple",
4496
+ direction: v.direction ?? "top",
4497
+ maxItemsToShow: maxItems,
4498
+ visibleEvents: maxItems,
4499
+ // legacy field name kept for back-compat
4500
+ themeBgOpacity: v.themeBgOpacity ?? 0,
4501
+ highlight: v.highlight,
4502
+ // `filters` is a blacklist: every category here is HIDDEN. Empty = show all.
4503
+ filters,
4504
+ // `sites` is a platform blacklist (same shape). Default to "no platforms hidden".
4505
+ sites: [],
4506
+ // Eventlist Manager assumes these objects exist; provide an empty stub so
4507
+ // imported overlays render without throwing.
4508
+ themeConfig: {},
4509
+ eventListType: {},
4510
+ condensedText: false,
4511
+ horizontal: false,
4512
+ showAlertIcon: true,
4513
+ showSiteIcon: true,
4514
+ showUserAvatar: true,
4515
+ showDetailedText: false,
4516
+ hideAlertMessage: false
4517
+ }
4518
+ },
4519
+ ctx
4520
+ );
4461
4521
  }
4462
4522
  var SE_CREDITS_EVENTS_TO_SHOW = {
4463
4523
  mods: false,
@@ -4601,55 +4661,70 @@ function mapCredits(widget, ctx) {
4601
4661
  const v = widget.variables ?? {};
4602
4662
  const itemGap = typeof v.padding === "number" ? v.padding : 8;
4603
4663
  const reverseFlow = v.direction === "bottom";
4604
- return buildUnit(widget, "credits", {
4605
- content: {
4606
- version: 1,
4607
- type: "basic",
4608
- title: "Stream Credits",
4609
- titleColor: "#ffffff",
4610
- subtitle: "A massive thanks to everyone below",
4611
- subtitleColor: "#ffffff",
4612
- headerColor: "#ffffff",
4613
- endMessage: "See you next time!",
4614
- endMessageColor: "#ffffff",
4615
- eventColor: "#ffffff",
4616
- loop: false,
4617
- speed: 1,
4618
- sectionOrder: [...CREDITS_DEFAULT_SECTION_ORDER],
4619
- // SE only ships subscribers + gift-subs, so import with just those
4620
- // enabled. The Manager can re-enable additional sections at any time;
4621
- // the underlying activity feed is fetched session-wide regardless.
4622
- eventsToShow: { ...SE_CREDITS_EVENTS_TO_SHOW },
4623
- topLimits: { ...CREDITS_DEFAULT_TOP_LIMITS },
4624
- eventNames: { ...CREDITS_DEFAULT_EVENT_NAMES },
4625
- prefixes: { ...CREDITS_DEFAULT_PREFIXES },
4626
- templates: { ...CREDITS_DEFAULT_TEMPLATES },
4627
- daysToShow: "session",
4628
- itemGap,
4629
- reverseFlow,
4630
- media: {
4631
- type: "image",
4632
- content: { src: [], name: [], loop: true, playAudio: true, volume: 1 }
4633
- },
4634
- audio: {
4635
- content: { src: [], name: [], loop: false, volume: 1 }
4664
+ return buildUnit(
4665
+ widget,
4666
+ "credits",
4667
+ {
4668
+ content: {
4669
+ version: 1,
4670
+ type: "basic",
4671
+ title: "Stream Credits",
4672
+ titleColor: "#ffffff",
4673
+ subtitle: "A massive thanks to everyone below",
4674
+ subtitleColor: "#ffffff",
4675
+ headerColor: "#ffffff",
4676
+ endMessage: "See you next time!",
4677
+ endMessageColor: "#ffffff",
4678
+ eventColor: "#ffffff",
4679
+ loop: false,
4680
+ speed: 1,
4681
+ sectionOrder: [...CREDITS_DEFAULT_SECTION_ORDER],
4682
+ // SE only ships subscribers + gift-subs, so import with just those
4683
+ // enabled. The Manager can re-enable additional sections at any time;
4684
+ // the underlying activity feed is fetched session-wide regardless.
4685
+ eventsToShow: { ...SE_CREDITS_EVENTS_TO_SHOW },
4686
+ topLimits: { ...CREDITS_DEFAULT_TOP_LIMITS },
4687
+ eventNames: { ...CREDITS_DEFAULT_EVENT_NAMES },
4688
+ prefixes: { ...CREDITS_DEFAULT_PREFIXES },
4689
+ templates: { ...CREDITS_DEFAULT_TEMPLATES },
4690
+ daysToShow: "session",
4691
+ itemGap,
4692
+ reverseFlow,
4693
+ media: {
4694
+ type: "image",
4695
+ content: {
4696
+ src: [],
4697
+ name: [],
4698
+ loop: true,
4699
+ playAudio: true,
4700
+ volume: 1
4701
+ }
4702
+ },
4703
+ audio: {
4704
+ content: { src: [], name: [], loop: false, volume: 1 }
4705
+ },
4706
+ // Source-side knobs with no direct Lumia equivalent — preserved
4707
+ // under the generic importMeta envelope. `chosenFilter` would
4708
+ // narrow eventsToShow if we had a clean mapping; `separator` would
4709
+ // join entries but Lumia credits uses sectioned cards instead.
4710
+ importMeta: buildImportMeta(widget, {
4711
+ chosenFilter: v.chosenFilter,
4712
+ separator: v.separator,
4713
+ amount: v.amount
4714
+ })
4636
4715
  },
4637
- // Source-side knobs with no direct Lumia equivalent preserved
4638
- // under the generic importMeta envelope. `chosenFilter` would
4639
- // narrow eventsToShow if we had a clean mapping; `separator` would
4640
- // join entries but Lumia credits uses sectioned cards instead.
4641
- importMeta: buildImportMeta(widget, {
4642
- chosenFilter: v.chosenFilter,
4643
- separator: v.separator,
4644
- amount: v.amount
4716
+ // Credits renders scrolling section headers + per-event lines; the
4717
+ // renderer reads font/size/weight/color from module.css. Pull from SE's
4718
+ // text.css so an imported credits widget keeps the streamer's chosen
4719
+ // font and weight defaults match the prior hardcoded fallbacks.
4720
+ css: mapSeTextCssToModuleCss(widget, {
4721
+ fontSize: 30,
4722
+ fontWeight: "normal",
4723
+ textShadow: ""
4645
4724
  })
4646
4725
  },
4647
- // Credits renders scrolling section headers + per-event lines; the
4648
- // renderer reads font/size/weight/color from module.css. Pull from SE's
4649
- // text.css so an imported credits widget keeps the streamer's chosen
4650
- // font and weight — defaults match the prior hardcoded fallbacks.
4651
- css: mapSeTextCssToModuleCss(widget, { fontSize: 30, fontWeight: "normal", textShadow: "" })
4652
- }, ctx);
4726
+ ctx
4727
+ );
4653
4728
  }
4654
4729
  function inferSlideshowMediaType(mime, url) {
4655
4730
  if (typeof mime === "string") {
@@ -4674,49 +4749,61 @@ function mapSlideshow(widget, ctx) {
4674
4749
  const url = typeof raw === "string" ? raw : raw.url ?? raw.src ?? "";
4675
4750
  if (!url) return null;
4676
4751
  return {
4677
- type: inferSlideshowMediaType(typeof raw === "object" ? raw?.type : void 0, url),
4752
+ type: inferSlideshowMediaType(
4753
+ typeof raw === "object" ? raw?.type : void 0,
4754
+ url
4755
+ ),
4678
4756
  src: url,
4679
4757
  name: nameFromUrl(url)
4680
4758
  };
4681
- }).filter((item) => item !== null);
4759
+ }).filter(
4760
+ (item) => item !== null
4761
+ );
4682
4762
  const seAnim = typeof v.animation === "object" && v.animation !== null ? v.animation : {};
4683
- return buildUnit(widget, "slideshow", {
4684
- content: {
4685
- src: items,
4686
- // `interval` of 0 = "rapid cycle" in SE (no delay between images); Lumia's
4687
- // `imageDuration` is the total ms per image. Distinguish three cases:
4688
- // - undefined → use 5000ms default
4689
- // - 0 minimum sane cycle of 100ms (browsers can't render faster than ~60fps anyway)
4690
- // - >0 SE seconds * 1000
4691
- imageDuration: typeof v.interval === "number" ? v.interval === 0 ? 100 : v.interval * 1e3 : 5e3,
4692
- loop: v.loop !== false,
4693
- loopDelay: 0,
4694
- delay: 0,
4695
- playAudio: !!v.playAudio,
4696
- // Lumia's renderer reads `content.shuffle` (Slideshow/index.tsx:21);
4697
- // mirroring SE's `random` to that name keeps "play in random order"
4698
- // working out of the box.
4699
- shuffle: !!v.random,
4700
- animation: {
4701
- enterAnimation: typeof v.animation === "string" ? v.animation : seAnim.in ?? "fadeIn",
4702
- exitAnimation: typeof v.animation === "string" ? v.animation : seAnim.out ?? "fadeOut",
4703
- enterAnimationDuration: (seAnim.inDuration ?? 1) * 1e3,
4704
- exitAnimationDuration: (seAnim.outDuration ?? 1) * 1e3,
4705
- enterAnimationDelay: (seAnim.inDelay ?? 0) * 1e3,
4706
- exitAnimationDelay: (seAnim.outDelay ?? 0) * 1e3
4707
- },
4708
- importMeta: buildImportMeta(widget, {
4709
- interval: v.interval,
4710
- rawDuration: v.duration
4711
- })
4712
- }
4713
- }, ctx);
4763
+ return buildUnit(
4764
+ widget,
4765
+ "slideshow",
4766
+ {
4767
+ content: {
4768
+ src: items,
4769
+ // `interval` of 0 = "rapid cycle" in SE (no delay between images); Lumia's
4770
+ // `imageDuration` is the total ms per image. Distinguish three cases:
4771
+ // - undefined use 5000ms default
4772
+ // - 0 minimum sane cycle of 100ms (browsers can't render faster than ~60fps anyway)
4773
+ // - >0 → SE seconds * 1000
4774
+ imageDuration: typeof v.interval === "number" ? v.interval === 0 ? 100 : v.interval * 1e3 : 5e3,
4775
+ loop: v.loop !== false,
4776
+ loopDelay: 0,
4777
+ delay: 0,
4778
+ playAudio: !!v.playAudio,
4779
+ // Lumia's renderer reads `content.shuffle` (Slideshow/index.tsx:21);
4780
+ // mirroring SE's `random` to that name keeps "play in random order"
4781
+ // working out of the box.
4782
+ shuffle: !!v.random,
4783
+ animation: {
4784
+ enterAnimation: typeof v.animation === "string" ? v.animation : seAnim.in ?? "fadeIn",
4785
+ exitAnimation: typeof v.animation === "string" ? v.animation : seAnim.out ?? "fadeOut",
4786
+ enterAnimationDuration: (seAnim.inDuration ?? 1) * 1e3,
4787
+ exitAnimationDuration: (seAnim.outDuration ?? 1) * 1e3,
4788
+ enterAnimationDelay: (seAnim.inDelay ?? 0) * 1e3,
4789
+ exitAnimationDelay: (seAnim.outDelay ?? 0) * 1e3
4790
+ },
4791
+ importMeta: buildImportMeta(widget, {
4792
+ interval: v.interval,
4793
+ rawDuration: v.duration
4794
+ })
4795
+ }
4796
+ },
4797
+ ctx
4798
+ );
4714
4799
  }
4715
4800
  function mapTimer(widget, ctx) {
4716
4801
  const v = widget.variables ?? {};
4717
4802
  const textValue = typeof widget.text?.value === "string" ? widget.text.value : void 0;
4718
4803
  const TIMER_PASSTHROUGH = /* @__PURE__ */ new Set(["days", "hours", "minutes", "seconds"]);
4719
- const mapTemplate = (template) => translateSeText(template, null, TIMER_PASSTHROUGH, void 0, { context: "static" });
4804
+ const mapTemplate = (template) => translateSeText(template, null, TIMER_PASSTHROUGH, void 0, {
4805
+ context: "static"
4806
+ });
4720
4807
  const absoluteDate = v.date ?? v.endsAt ?? v.endDate;
4721
4808
  const target = absoluteDate ? Date.parse(absoluteDate) : NaN;
4722
4809
  const hasValidAbsoluteDate = Number.isFinite(target);
@@ -4736,7 +4823,9 @@ function mapTimer(widget, ctx) {
4736
4823
  resolved = true;
4737
4824
  }
4738
4825
  }
4739
- if (!resolved && [v.days, v.hours, v.minutes, v.seconds].some((part) => typeof part === "number")) {
4826
+ if (!resolved && [v.days, v.hours, v.minutes, v.seconds].some(
4827
+ (part) => typeof part === "number"
4828
+ )) {
4740
4829
  days = typeof v.days === "number" ? v.days : 0;
4741
4830
  hours = typeof v.hours === "number" ? v.hours : 0;
4742
4831
  minutes = typeof v.minutes === "number" ? v.minutes : 0;
@@ -4775,102 +4864,123 @@ function mapTimer(widget, ctx) {
4775
4864
  if (v.seconds !== false) clockBits.push("{{seconds}}");
4776
4865
  if (clockBits.length) parts.push(clockBits.join(":"));
4777
4866
  }
4778
- return buildUnit(widget, "timer", {
4779
- // Carry the streamer's SE text styling (font/size/color/shadow) so an
4780
- // imported countdown doesn't render in Lumia's default Roboto/24px.
4781
- css: mapSeTextCssToModuleCss(widget),
4782
- content: {
4783
- version: 1,
4784
- type: "countdown",
4785
- value: { days, hours, minutes, seconds },
4786
- display: textValue ? mapTemplate(textValue) : parts.join(" ") || "{{minutes}}:{{seconds}}",
4787
- showMilliseconds: false,
4788
- endAnimation: "",
4789
- hideAfter: 0,
4790
- highlightColor: "inherit",
4791
- messageAfterCountdown: translateSeText(v.messageAfterCountdown ?? "", null, void 0, void 0, { context: "static" }),
4792
- changeMessageAfter: !!v.changeMessageAfterCountdown,
4793
- countDate: hasValidAbsoluteDate && (!!v.exactTime || !!v.datePicker || !!absoluteDate),
4794
- date: hasValidAbsoluteDate ? new Date(target).toISOString() : "",
4795
- importMeta: buildImportMeta(widget, {
4796
- rawDate: absoluteDate,
4797
- datePicker: v.datePicker,
4798
- exactTime: v.exactTime
4799
- })
4800
- }
4801
- }, ctx);
4867
+ return buildUnit(
4868
+ widget,
4869
+ "timer",
4870
+ {
4871
+ // Carry the streamer's SE text styling (font/size/color/shadow) so an
4872
+ // imported countdown doesn't render in Lumia's default Roboto/24px.
4873
+ css: mapSeTextCssToModuleCss(widget),
4874
+ content: {
4875
+ version: 1,
4876
+ type: "countdown",
4877
+ value: { days, hours, minutes, seconds },
4878
+ display: textValue ? mapTemplate(textValue) : parts.join(" ") || "{{minutes}}:{{seconds}}",
4879
+ showMilliseconds: false,
4880
+ endAnimation: "",
4881
+ hideAfter: 0,
4882
+ highlightColor: "inherit",
4883
+ messageAfterCountdown: translateSeText(
4884
+ v.messageAfterCountdown ?? "",
4885
+ null,
4886
+ void 0,
4887
+ void 0,
4888
+ { context: "static" }
4889
+ ),
4890
+ changeMessageAfter: !!v.changeMessageAfterCountdown,
4891
+ countDate: hasValidAbsoluteDate && (!!v.exactTime || !!v.datePicker || !!absoluteDate),
4892
+ date: hasValidAbsoluteDate ? new Date(target).toISOString() : "",
4893
+ importMeta: buildImportMeta(widget, {
4894
+ rawDate: absoluteDate,
4895
+ datePicker: v.datePicker,
4896
+ exactTime: v.exactTime
4897
+ })
4898
+ }
4899
+ },
4900
+ ctx
4901
+ );
4802
4902
  }
4803
4903
  function mapNowPlaying(widget, ctx) {
4804
4904
  const v = widget.variables ?? {};
4805
4905
  const source = v.provider === "youtube" ? "youtubemusic" : "spotify";
4806
- return buildUnit(widget, "nowplaying", {
4807
- content: {
4808
- version: 1,
4809
- type: "nowplaying",
4810
- source,
4811
- theme: "basic",
4812
- borderRadius: "30px",
4813
- songTitle: `{{${source}_now_playing_song=Lumia Dream}}`,
4814
- songArtist: `{{${source}_now_playing_artist}}`,
4815
- songImg: `{{${source}_now_playing_image}}`,
4816
- showImg: true,
4817
- oneLineTitle: false,
4818
- highlightColor: "inherit",
4819
- importMeta: buildImportMeta(widget, { simpleDesign: !!v.simpleDesign })
4906
+ return buildUnit(
4907
+ widget,
4908
+ "nowplaying",
4909
+ {
4910
+ content: {
4911
+ version: 1,
4912
+ type: "nowplaying",
4913
+ source,
4914
+ theme: "basic",
4915
+ borderRadius: "30px",
4916
+ songTitle: `{{${source}_now_playing_song=Lumia Dream}}`,
4917
+ songArtist: `{{${source}_now_playing_artist}}`,
4918
+ songImg: `{{${source}_now_playing_image}}`,
4919
+ showImg: true,
4920
+ oneLineTitle: false,
4921
+ highlightColor: "inherit",
4922
+ importMeta: buildImportMeta(widget, { simpleDesign: !!v.simpleDesign })
4923
+ },
4924
+ css: {
4925
+ borderRadius: "0px",
4926
+ borderStyle: "solid",
4927
+ borderWidth: "0px",
4928
+ borderColor: "transparent",
4929
+ lineHeight: 1,
4930
+ textAlign: "left",
4931
+ fontFamily: "Be Vietnam Pro",
4932
+ color: "#ffffff",
4933
+ textShadow: "rgb(0, 0, 0) 1px 1px 1px",
4934
+ background: "transparent"
4935
+ }
4820
4936
  },
4821
- css: {
4822
- borderRadius: "0px",
4823
- borderStyle: "solid",
4824
- borderWidth: "0px",
4825
- borderColor: "transparent",
4826
- lineHeight: 1,
4827
- textAlign: "left",
4828
- fontFamily: "Be Vietnam Pro",
4829
- color: "#ffffff",
4830
- textShadow: "rgb(0, 0, 0) 1px 1px 1px",
4831
- background: "transparent"
4832
- }
4833
- }, ctx);
4937
+ ctx
4938
+ );
4834
4939
  }
4835
4940
  function mapMediaShare(widget, ctx) {
4836
4941
  const v = widget.variables ?? {};
4837
- return buildUnit(widget, "songrequest", {
4838
- content: {
4839
- version: 1,
4840
- // SE always shows the now-playing card; honour its cosmetic toggles
4841
- // for the rest. `showTitle` defaults true; the others default to
4842
- // SE's documented behavior (tipper visible, stats hidden).
4843
- showNowPlayingCard: true,
4844
- showQueueList: true,
4845
- showSkipButton: true,
4846
- showPauseButton: true,
4847
- showTitle: v.showTitle !== false,
4848
- showTipper: v.showTipper !== false,
4849
- showAmount: v.showAmount === true,
4850
- showStats: v.showStats === true,
4851
- // SE doesn't have a queue-size knob — pick a sensible default
4852
- // matching what fits in the SE widget's ~292px tall footprint.
4853
- maxQueueItemsVisible: 5,
4854
- // SE color knobs translate directly. Fall back to Lumia's defaults
4855
- // (purple accent, near-black card background) if SE values are
4856
- // missing — matches what the module ships with for native creates.
4857
- accentColor: v.accentColor ?? "#9146ff",
4858
- primaryColor: v.primaryColor ?? "#1a1a1a",
4859
- // SE's widget hides the YT player by default; mirror that for
4860
- // audio-only overlay use.
4861
- playerVisible: false,
4862
- defaultVolume: 70,
4863
- autoAdvanceOnEnd: true,
4864
- // Source-side knob preserved for round-trip provenance; Lumia
4865
- // ignores it at runtime (auto-advance fires on
4866
- // `durationSeconds + grace`).
4867
- importMeta: buildImportMeta(widget, { timeLeft: v.timeLeft === true })
4942
+ return buildUnit(
4943
+ widget,
4944
+ "songrequest",
4945
+ {
4946
+ content: {
4947
+ version: 1,
4948
+ // SE always shows the now-playing card; honour its cosmetic toggles
4949
+ // for the rest. `showTitle` defaults true; the others default to
4950
+ // SE's documented behavior (tipper visible, stats hidden).
4951
+ showNowPlayingCard: true,
4952
+ showQueueList: true,
4953
+ showSkipButton: true,
4954
+ showPauseButton: true,
4955
+ showTitle: v.showTitle !== false,
4956
+ showTipper: v.showTipper !== false,
4957
+ showAmount: v.showAmount === true,
4958
+ showStats: v.showStats === true,
4959
+ // SE doesn't have a queue-size knob pick a sensible default
4960
+ // matching what fits in the SE widget's ~292px tall footprint.
4961
+ maxQueueItemsVisible: 5,
4962
+ // SE color knobs translate directly. Fall back to Lumia's defaults
4963
+ // (purple accent, near-black card background) if SE values are
4964
+ // missing matches what the module ships with for native creates.
4965
+ accentColor: v.accentColor ?? "#9146ff",
4966
+ primaryColor: v.primaryColor ?? "#1a1a1a",
4967
+ // SE's widget hides the YT player by default; mirror that for
4968
+ // audio-only overlay use.
4969
+ playerVisible: false,
4970
+ defaultVolume: 70,
4971
+ autoAdvanceOnEnd: true,
4972
+ // Source-side knob preserved for round-trip provenance; Lumia
4973
+ // ignores it at runtime (auto-advance fires on
4974
+ // `durationSeconds + grace`).
4975
+ importMeta: buildImportMeta(widget, { timeLeft: v.timeLeft === true })
4976
+ },
4977
+ // Lumia songrequest renders the queue list as styled text; pull SE's
4978
+ // widget.text.css so font/size/color carry across (defaults match the
4979
+ // previous hardcoded fallbacks for this widget specifically).
4980
+ css: mapSeTextCssToModuleCss(widget, { fontSize: 18 })
4868
4981
  },
4869
- // Lumia songrequest renders the queue list as styled text; pull SE's
4870
- // widget.text.css so font/size/color carry across (defaults match the
4871
- // previous hardcoded fallbacks for this widget specifically).
4872
- css: mapSeTextCssToModuleCss(widget, { fontSize: 18 })
4873
- }, ctx);
4982
+ ctx
4983
+ );
4874
4984
  }
4875
4985
  function mapKappagen(widget, ctx) {
4876
4986
  const v = widget.variables ?? {};
@@ -4881,10 +4991,58 @@ function mapKappagen(widget, ctx) {
4881
4991
  raid: v.eventsConfig.raid?.enabled ?? true,
4882
4992
  tip: v.eventsConfig.tip?.enabled ?? true
4883
4993
  } : null;
4884
- const events = v.events ?? eventsFromNested ?? { subscriber: true, follower: true, cheer: true, raid: true, tip: true };
4885
- const alertTriggers = Object.entries(events).filter(([, enabled]) => !!enabled).map(([seKey]) => kappagenKeyFor(seKey, ctx?.provider)).filter((x) => !!x);
4886
- const alertEmotes = ["Kappa", "<3", "imGlitch"];
4887
- return buildUnit(
4994
+ const events = v.events ?? eventsFromNested ?? {
4995
+ subscriber: true,
4996
+ follower: true,
4997
+ cheer: true,
4998
+ raid: true,
4999
+ tip: true
5000
+ };
5001
+ const alertTriggers = Array.from(
5002
+ new Set(
5003
+ Object.entries(events).filter(([, enabled]) => !!enabled).flatMap(([seKey]) => kappagenAlertTriggersFor(seKey, ctx?.provider))
5004
+ )
5005
+ );
5006
+ const pickEmoteUrl = (urls) => {
5007
+ if (!urls) return void 0;
5008
+ return urls["4"] ?? urls["3"] ?? urls["2"] ?? urls["1"] ?? Object.values(urls).find(Boolean);
5009
+ };
5010
+ const importedChosenEmotes = Array.isArray(v.chosenEmotes) ? v.chosenEmotes.map((emote) => {
5011
+ const name = typeof emote?.name === "string" ? emote.name.trim() : "";
5012
+ const src = pickEmoteUrl(emote?.urls);
5013
+ if (!name || !src) return null;
5014
+ return { name, src };
5015
+ }).filter((emote) => !!emote) : [];
5016
+ const importedEmoteUrls = importedChosenEmotes.reduce(
5017
+ (acc, emote) => {
5018
+ acc[emote.name] = emote.src;
5019
+ return acc;
5020
+ },
5021
+ {}
5022
+ );
5023
+ const importedCustomEmotes = Array.isArray(v.customEmotes) ? v.customEmotes.map((emote) => {
5024
+ const name = typeof emote?.name === "string" ? emote.name.trim() : "";
5025
+ const src = typeof emote?.src === "string" ? emote.src.trim() : "";
5026
+ if (!name || !src) return null;
5027
+ return {
5028
+ name,
5029
+ src,
5030
+ source: "custom",
5031
+ isRegex: !!emote.isRegex,
5032
+ animated: !!emote.isGif
5033
+ };
5034
+ }).filter(
5035
+ (emote) => !!emote
5036
+ ) : [];
5037
+ const importedAlertEmoteNames = [
5038
+ ...importedChosenEmotes.map((emote) => emote.name),
5039
+ ...importedCustomEmotes.map((emote) => emote.name)
5040
+ ];
5041
+ const alertEmotes = importedAlertEmoteNames.length > 0 ? importedAlertEmoteNames : ["Kappa", "<3", "imGlitch"];
5042
+ const chatEmoteMode = typeof v.mode === "string" ? v.mode.toLowerCase() : "off";
5043
+ const animationTime = typeof v.emotesplosionduration === "number" && v.emotesplosionduration > 0 ? v.emotesplosionduration : typeof v.duration === "number" && v.duration > 0 ? v.duration : 5;
5044
+ const alertNumberEmotes = typeof v.emotesplosion === "number" && v.emotesplosion > 0 ? v.emotesplosion : typeof v.limit === "number" && v.limit > 0 ? v.limit : 30;
5045
+ const unit = buildUnit(
4888
5046
  widget,
4889
5047
  "emotealert",
4890
5048
  {
@@ -4901,12 +5059,17 @@ function mapKappagen(widget, ctx) {
4901
5059
  background: "transparent",
4902
5060
  theme: "dark",
4903
5061
  imageSize: typeof v.size === "number" ? v.size : 30,
4904
- animationTime: typeof v.duration === "number" ? v.duration : 5,
4905
- duration: typeof v.duration === "number" ? v.duration : 5,
5062
+ animationTime,
5063
+ duration: animationTime,
4906
5064
  useAnimatedEmotes: true,
4907
- alertNumberEmotes: typeof v.limit === "number" && v.limit > 0 ? v.limit : 30,
5065
+ alertNumberEmotes,
4908
5066
  alertEmotes,
5067
+ customEmotes: importedCustomEmotes,
5068
+ importedEmoteUrls,
4909
5069
  alertTriggers,
5070
+ chatEmotesOn: chatEmoteMode !== "off",
5071
+ comboMinimum: typeof v.combominimum === "number" ? v.combominimum : 1,
5072
+ comboTime: typeof v.combotime === "number" ? v.combotime : 20,
4910
5073
  // Lumia BTTV/FFZ/7TV per-source toggles — read by Emotealert/index.tsx:48.
4911
5074
  emoteSources: {
4912
5075
  bttv: v.bttv !== false,
@@ -4930,6 +5093,15 @@ function mapKappagen(widget, ctx) {
4930
5093
  },
4931
5094
  ctx
4932
5095
  );
5096
+ if (v.fullscreen && ctx?.canvas) {
5097
+ unit.layer.bounds.x = 0;
5098
+ unit.layer.bounds.y = 0;
5099
+ unit.layer.bounds.width = ctx.canvas.width;
5100
+ unit.layer.bounds.height = ctx.canvas.height;
5101
+ delete unit.layer.bounds.autoWidth;
5102
+ delete unit.layer.bounds.autoHeight;
5103
+ }
5104
+ return unit;
4933
5105
  }
4934
5106
  var TOP_LIST_SORT_BY = {
4935
5107
  tips: "higherDonations",
@@ -4944,100 +5116,110 @@ var TOP_LIST_TITLE = {
4944
5116
  function mapViewerProfilesLeaderboard(widget, currency, ctx) {
4945
5117
  const v = widget.variables ?? {};
4946
5118
  const count = v.limit ?? v.visibleItems ?? 10;
4947
- return buildUnit(widget, "viewerprofiles", {
4948
- // Carry SE text styling so the leaderboard keeps the streamer's
4949
- // chosen font / size / color.
4950
- css: mapSeTextCssToModuleCss(widget, { fontSize: 18 }),
4951
- content: {
4952
- sortBy: TOP_LIST_SORT_BY[currency],
4953
- count,
4954
- title: TOP_LIST_TITLE[currency],
4955
- showTitle: true,
4956
- showAvatar: true,
4957
- showPlatform: true,
4958
- // Hide noisy columns that aren't relevant for a single-metric
4959
- // top-X list — the streamer picked "top by gifts/tips/cheers"
4960
- // for a reason. They can re-enable in Settings if they want.
4961
- showCommands: false,
4962
- showAlerts: false,
4963
- showBits: currency === "cheers",
4964
- showLastSeen: false,
4965
- showTopActivity: false,
4966
- showAchievements: false,
4967
- showAchievementProgress: false,
4968
- refreshIntervalSeconds: 30,
4969
- itemGap: 8,
4970
- autoScroll: false,
4971
- // Source-side knobs that don't map cleanly survive under the
4972
- // generic importMeta envelope so the user can re-introspect later.
4973
- // `period` is interesting because viewerprofiles doesn't yet have
4974
- // a windowed mode if/when it does, we can promote this.
4975
- importMeta: buildImportMeta(widget, {
4976
- period: v.period,
4977
- offset: v.offset,
4978
- layout: v.layout
4979
- })
4980
- }
4981
- }, ctx);
5119
+ return buildUnit(
5120
+ widget,
5121
+ "viewerprofiles",
5122
+ {
5123
+ // Carry SE text styling so the leaderboard keeps the streamer's
5124
+ // chosen font / size / color.
5125
+ css: mapSeTextCssToModuleCss(widget, { fontSize: 18 }),
5126
+ content: {
5127
+ sortBy: TOP_LIST_SORT_BY[currency],
5128
+ count,
5129
+ title: TOP_LIST_TITLE[currency],
5130
+ showTitle: true,
5131
+ showAvatar: true,
5132
+ showPlatform: true,
5133
+ // Hide noisy columns that aren't relevant for a single-metric
5134
+ // top-X list — the streamer picked "top by gifts/tips/cheers"
5135
+ // for a reason. They can re-enable in Settings if they want.
5136
+ showCommands: false,
5137
+ showAlerts: false,
5138
+ showBits: currency === "cheers",
5139
+ showLastSeen: false,
5140
+ showTopActivity: false,
5141
+ showAchievements: false,
5142
+ showAchievementProgress: false,
5143
+ refreshIntervalSeconds: 30,
5144
+ itemGap: 8,
5145
+ autoScroll: false,
5146
+ // Source-side knobs that don't map cleanly survive under the
5147
+ // generic importMeta envelope so the user can re-introspect later.
5148
+ // `period` is interesting because viewerprofiles doesn't yet have
5149
+ // a windowed mode — if/when it does, we can promote this.
5150
+ importMeta: buildImportMeta(widget, {
5151
+ period: v.period,
5152
+ offset: v.offset,
5153
+ layout: v.layout
5154
+ })
5155
+ }
5156
+ },
5157
+ ctx
5158
+ );
4982
5159
  }
4983
5160
  function mapGiveaway(widget, ctx) {
4984
5161
  const v = widget.variables ?? {};
4985
5162
  const primary = v.colors?.primary ?? "#393853";
4986
5163
  const accent = v.colors?.accent ?? "#FF4076";
4987
- return buildUnit(widget, "raffle", {
4988
- // Raffle renders entrants/winner text via module.css.
4989
- css: mapSeTextCssToModuleCss(widget),
4990
- content: {
4991
- version: 1,
4992
- type: "basic",
4993
- title: "Play to win",
4994
- subtitle: "",
4995
- wonTitle: "{{winner}} just won",
4996
- showTitle: true,
4997
- showSubTitle: false,
4998
- // SE `displayRunningTime` toggles the running-timer label. Lumia's equivalent
4999
- // is `showTimeSince`. SE `enableTimeOut` enables auto-end which Lumia honours
5000
- // via raffle.auto_end on the raffle state itself, not the module — best we can
5001
- // do is leave Lumia's `showTimeSince` on so the countdown is visible.
5002
- showTimeSince: v.displayRunningTime ?? true,
5003
- // SE `displayEntrants: false` means "never show the entrant list" (persistent).
5004
- // Lumia's `hideParticipantOnStart` is "hide until spin starts, then show" (transient).
5005
- // Mapping one to the other reverses semantics: SE-hidden entrants would briefly
5006
- // flash on screen the moment the spin starts. Since Lumia has no permanent
5007
- // hide-entrants knob, keep Lumia at its default (show) and surface the SE
5008
- // preference via provenance a future renderer can read it without us
5009
- // having lied about the toggle. Same for `displayTicketsPurchased`.
5010
- hideParticipantOnStart: false,
5011
- removeWhenRedeemed: false,
5012
- showWinningModal: true,
5013
- playSoundFx: true,
5014
- spinSound: "https://storage.lumiastream.com/overlays/lumia/audio/Wheel_of_Fortune.mp3",
5015
- winSound: "https://storage.lumiastream.com/overlays/lumia/audio/crowdClap.mp3",
5016
- spinDuration: 4,
5017
- transparent: false,
5018
- backgroundColor: primary,
5019
- borderColor: accent,
5020
- border: "solid 1px transparent",
5021
- borderRadius: "10px",
5022
- flashOnNewEntry: true,
5023
- flashColor: accent,
5024
- raffleEndedText: "The raffle has ended, and the selection of the winner will take place shortly.",
5025
- raffleTimerText: "Time since raffle started",
5026
- raffleCountdownText: "Raffle will end after",
5027
- // Source-side knobs with no Lumia equivalent preserved under
5028
- // the generic importMeta envelope. `displayEntrants` / `displayTicketsPurchased`
5029
- // are SE's persistent "never show" toggles; Lumia has no equivalent
5030
- // today (its `hideParticipantOnStart` is transient), so we preserve
5031
- // the preference for a future renderer.
5032
- importMeta: buildImportMeta(widget, {
5033
- timeout: v.timeout,
5034
- enableTimeout: v.enableTimeOut,
5035
- displayEntrants: v.displayEntrants,
5036
- displayTicketsPurchased: v.displayTicketsPurchased,
5037
- imageStyleWidth: v.imageStyle?.width
5038
- })
5039
- }
5040
- }, ctx);
5164
+ return buildUnit(
5165
+ widget,
5166
+ "raffle",
5167
+ {
5168
+ // Raffle renders entrants/winner text via module.css.
5169
+ css: mapSeTextCssToModuleCss(widget),
5170
+ content: {
5171
+ version: 1,
5172
+ type: "basic",
5173
+ title: "Play to win",
5174
+ subtitle: "",
5175
+ wonTitle: "{{winner}} just won",
5176
+ showTitle: true,
5177
+ showSubTitle: false,
5178
+ // SE `displayRunningTime` toggles the running-timer label. Lumia's equivalent
5179
+ // is `showTimeSince`. SE `enableTimeOut` enables auto-end which Lumia honours
5180
+ // via raffle.auto_end on the raffle state itself, not the module best we can
5181
+ // do is leave Lumia's `showTimeSince` on so the countdown is visible.
5182
+ showTimeSince: v.displayRunningTime ?? true,
5183
+ // SE `displayEntrants: false` means "never show the entrant list" (persistent).
5184
+ // Lumia's `hideParticipantOnStart` is "hide until spin starts, then show" (transient).
5185
+ // Mapping one to the other reverses semantics: SE-hidden entrants would briefly
5186
+ // flash on screen the moment the spin starts. Since Lumia has no permanent
5187
+ // hide-entrants knob, keep Lumia at its default (show) and surface the SE
5188
+ // preference via provenance — a future renderer can read it without us
5189
+ // having lied about the toggle. Same for `displayTicketsPurchased`.
5190
+ hideParticipantOnStart: false,
5191
+ removeWhenRedeemed: false,
5192
+ showWinningModal: true,
5193
+ playSoundFx: true,
5194
+ spinSound: "https://storage.lumiastream.com/overlays/lumia/audio/Wheel_of_Fortune.mp3",
5195
+ winSound: "https://storage.lumiastream.com/overlays/lumia/audio/crowdClap.mp3",
5196
+ spinDuration: 4,
5197
+ transparent: false,
5198
+ backgroundColor: primary,
5199
+ borderColor: accent,
5200
+ border: "solid 1px transparent",
5201
+ borderRadius: "10px",
5202
+ flashOnNewEntry: true,
5203
+ flashColor: accent,
5204
+ raffleEndedText: "The raffle has ended, and the selection of the winner will take place shortly.",
5205
+ raffleTimerText: "Time since raffle started",
5206
+ raffleCountdownText: "Raffle will end after",
5207
+ // Source-side knobs with no Lumia equivalent preserved under
5208
+ // the generic importMeta envelope. `displayEntrants` / `displayTicketsPurchased`
5209
+ // are SE's persistent "never show" toggles; Lumia has no equivalent
5210
+ // today (its `hideParticipantOnStart` is transient), so we preserve
5211
+ // the preference for a future renderer.
5212
+ importMeta: buildImportMeta(widget, {
5213
+ timeout: v.timeout,
5214
+ enableTimeout: v.enableTimeOut,
5215
+ displayEntrants: v.displayEntrants,
5216
+ displayTicketsPurchased: v.displayTicketsPurchased,
5217
+ imageStyleWidth: v.imageStyle?.width
5218
+ })
5219
+ }
5220
+ },
5221
+ ctx
5222
+ );
5041
5223
  }
5042
5224
  var HYPETRAIN_LISTENER_KEYS = /* @__PURE__ */ new Set([
5043
5225
  "hypetrain-latest",
@@ -5070,52 +5252,57 @@ function isHypetrainCustomWidget(widget) {
5070
5252
  }
5071
5253
  function mapHypetrain(widget, ctx) {
5072
5254
  const v = widget.variables ?? {};
5073
- return buildUnit(widget, "hypetrain", {
5074
- // Hypetrain renders level / progress / contributor text — carry SE
5075
- // styling so an imported hype-train widget keeps its font + accent.
5076
- css: mapSeTextCssToModuleCss(widget),
5077
- content: {
5078
- // Twitch-native is the right default — SE hype-train widgets always
5079
- // reflected the Twitch native train. Users can flip to 'synthetic'
5080
- // or 'hybrid' from Settings if they want cross-platform pooling.
5081
- sourceMode: "twitch-native",
5082
- sources: null,
5083
- // default money-only whitelist; ignored in twitch-native anyway
5084
- customValues: {},
5085
- pretrainThreshold: 5,
5086
- pretrainDurationMs: 5 * 60 * 1e3,
5087
- levelDurationMs: 5 * 60 * 1e3,
5088
- levelStep: 5,
5089
- levelThresholds: [],
5090
- // Pick a moderately energetic default theme — SE hype-train widgets
5091
- // were never subtle. Users can switch in Settings.
5092
- theme: "train",
5093
- primaryColor: typeof v.primaryColor === "string" ? v.primaryColor : "#ff6b00",
5094
- accentColor: typeof v.accentColor === "string" ? v.accentColor : "#ffd84d",
5095
- textColor: typeof v.textColor === "string" ? v.textColor : "#ffffff",
5096
- showLevel: true,
5097
- showTotal: true,
5098
- showTimer: true,
5099
- showTopContributor: true,
5100
- showLeaderboard: true,
5101
- leaderboardLimit: 5,
5102
- leaderboardDurationMs: 30 * 1e3,
5103
- levelUpSoundUrl: "https://storage.lumiastream.com/overlays/lumia/audio/crowdClap.mp3",
5104
- trainEndSoundUrl: "https://storage.lumiastream.com/overlays/lumia/audio/crowdClap.mp3",
5105
- audioVolume: 0.5,
5106
- // Match native defaults so SE-imported trains get the same polished
5107
- // sprite pack the native module ships with.
5108
- trainLocoImageUrl: "https://storage.lumiastream.com/overlays/global/hypetrain/train_loco_default.png",
5109
- rocketImageUrl: "https://storage.lumiastream.com/overlays/global/hypetrain/rocket_default.png",
5110
- infernoImageUrl: "https://storage.lumiastream.com/overlays/global/hypetrain/inferno_default.png",
5111
- // Source config preserved verbatim under the generic importMeta
5112
- // envelope — no Lumia equivalent for most of these knobs
5113
- // (per-widget animation tunables, sound URLs the streamer might
5114
- // want to migrate manually, etc.). Users can inspect via the
5115
- // overlay JSON if they want to recreate a specific look.
5116
- importMeta: buildImportMeta(widget, { variables: v })
5117
- }
5118
- }, ctx);
5255
+ return buildUnit(
5256
+ widget,
5257
+ "hypetrain",
5258
+ {
5259
+ // Hypetrain renders level / progress / contributor text — carry SE
5260
+ // styling so an imported hype-train widget keeps its font + accent.
5261
+ css: mapSeTextCssToModuleCss(widget),
5262
+ content: {
5263
+ // Twitch-native is the right default — SE hype-train widgets always
5264
+ // reflected the Twitch native train. Users can flip to 'synthetic'
5265
+ // or 'hybrid' from Settings if they want cross-platform pooling.
5266
+ sourceMode: "twitch-native",
5267
+ sources: null,
5268
+ // default money-only whitelist; ignored in twitch-native anyway
5269
+ customValues: {},
5270
+ pretrainThreshold: 5,
5271
+ pretrainDurationMs: 5 * 60 * 1e3,
5272
+ levelDurationMs: 5 * 60 * 1e3,
5273
+ levelStep: 5,
5274
+ levelThresholds: [],
5275
+ // Pick a moderately energetic default theme SE hype-train widgets
5276
+ // were never subtle. Users can switch in Settings.
5277
+ theme: "train",
5278
+ primaryColor: typeof v.primaryColor === "string" ? v.primaryColor : "#ff6b00",
5279
+ accentColor: typeof v.accentColor === "string" ? v.accentColor : "#ffd84d",
5280
+ textColor: typeof v.textColor === "string" ? v.textColor : "#ffffff",
5281
+ showLevel: true,
5282
+ showTotal: true,
5283
+ showTimer: true,
5284
+ showTopContributor: true,
5285
+ showLeaderboard: true,
5286
+ leaderboardLimit: 5,
5287
+ leaderboardDurationMs: 30 * 1e3,
5288
+ levelUpSoundUrl: "https://storage.lumiastream.com/overlays/lumia/audio/crowdClap.mp3",
5289
+ trainEndSoundUrl: "https://storage.lumiastream.com/overlays/lumia/audio/crowdClap.mp3",
5290
+ audioVolume: 0.5,
5291
+ // Match native defaults so SE-imported trains get the same polished
5292
+ // sprite pack the native module ships with.
5293
+ trainLocoImageUrl: "https://storage.lumiastream.com/overlays/global/hypetrain/train_loco_default.png",
5294
+ rocketImageUrl: "https://storage.lumiastream.com/overlays/global/hypetrain/rocket_default.png",
5295
+ infernoImageUrl: "https://storage.lumiastream.com/overlays/global/hypetrain/inferno_default.png",
5296
+ // Source config preserved verbatim under the generic importMeta
5297
+ // envelope no Lumia equivalent for most of these knobs
5298
+ // (per-widget animation tunables, sound URLs the streamer might
5299
+ // want to migrate manually, etc.). Users can inspect via the
5300
+ // overlay JSON if they want to recreate a specific look.
5301
+ importMeta: buildImportMeta(widget, { variables: v })
5302
+ }
5303
+ },
5304
+ ctx
5305
+ );
5119
5306
  }
5120
5307
  function mapEmoteWall(widget, ctx) {
5121
5308
  const v = widget.variables ?? {};
@@ -5128,51 +5315,61 @@ function mapEmoteWall(widget, ctx) {
5128
5315
  if (v.enableFfz !== false && v.ffz !== false) sites.push("ffz");
5129
5316
  if (v.enable7tv !== false && v.sevenTv !== false) sites.push("seventv");
5130
5317
  const animationType = v.direction === "up" ? "rise" : v.direction === "down" ? "fall" : v.direction === "left" || v.direction === "right" ? "slide" : "random";
5131
- return buildUnit(widget, "emotebox", {
5132
- content: {
5133
- version: 1,
5134
- type: animationType,
5135
- imageSize,
5136
- animationTime: duration,
5137
- duration,
5138
- maxItemsToShow,
5139
- delay: typeof v.delay === "number" ? v.delay : 0,
5140
- sites,
5141
- background: "transparent",
5142
- theme: "dark",
5143
- userLevels: [1, 2, 3, 4, 5, 6, 7, 8, 9],
5144
- ignoredList: [],
5145
- excludedAnimations: [],
5146
- // Emote-wall is chat-driven, not alert-driven leave alert toggles
5147
- // off so the widget doesn't double-fire on subscribe/follower events.
5148
- alertOn: false,
5149
- alertTriggers: [],
5150
- alertEmotes: [],
5151
- alertNumberEmotes: 30,
5152
- useAnimatedEmotes: true,
5153
- importMeta: buildImportMeta(widget, {
5154
- fullscreen: !!v.fullscreen,
5155
- direction: v.direction
5156
- })
5318
+ return buildUnit(
5319
+ widget,
5320
+ "emotebox",
5321
+ {
5322
+ content: {
5323
+ version: 1,
5324
+ type: animationType,
5325
+ imageSize,
5326
+ animationTime: duration,
5327
+ duration,
5328
+ maxItemsToShow,
5329
+ delay: typeof v.delay === "number" ? v.delay : 0,
5330
+ sites,
5331
+ background: "transparent",
5332
+ theme: "dark",
5333
+ userLevels: [1, 2, 3, 4, 5, 6, 7, 8, 9],
5334
+ ignoredList: [],
5335
+ excludedAnimations: [],
5336
+ // Emote-wall is chat-driven, not alert-driven — leave alert toggles
5337
+ // off so the widget doesn't double-fire on subscribe/follower events.
5338
+ alertOn: false,
5339
+ alertTriggers: [],
5340
+ alertEmotes: [],
5341
+ alertNumberEmotes: 30,
5342
+ useAnimatedEmotes: true,
5343
+ importMeta: buildImportMeta(widget, {
5344
+ fullscreen: !!v.fullscreen,
5345
+ direction: v.direction
5346
+ })
5347
+ },
5348
+ css: { background: "transparent" }
5157
5349
  },
5158
- css: { background: "transparent" }
5159
- }, ctx);
5350
+ ctx
5351
+ );
5160
5352
  }
5161
5353
  function mapUnsupportedAsText(widget, ctx) {
5162
- return buildUnit(widget, "text", {
5163
- content: {
5164
- value: `[Unsupported SE widget: ${widget.type}]`,
5165
- highlightColor: "inherit"
5354
+ return buildUnit(
5355
+ widget,
5356
+ "text",
5357
+ {
5358
+ content: {
5359
+ value: `[Unsupported SE widget: ${widget.type}]`,
5360
+ highlightColor: "inherit"
5361
+ },
5362
+ css: {
5363
+ color: "#ff8c00",
5364
+ fontSize: 18,
5365
+ textAlign: "center",
5366
+ fontFamily: "Roboto",
5367
+ fontWeight: "bold",
5368
+ background: "rgba(0,0,0,0.4)"
5369
+ }
5166
5370
  },
5167
- css: {
5168
- color: "#ff8c00",
5169
- fontSize: 18,
5170
- textAlign: "center",
5171
- fontFamily: "Roboto",
5172
- fontWeight: "bold",
5173
- background: "rgba(0,0,0,0.4)"
5174
- }
5175
- }, ctx);
5371
+ ctx
5372
+ );
5176
5373
  }
5177
5374
 
5178
5375
  // src/se-import/mappers/hypecup.ts
@@ -6250,6 +6447,65 @@ function getJwtInstructionSteps({ pasteHint = "above" } = {}) {
6250
6447
 
6251
6448
  // src/se-import/ui/SEImportWizard.tsx
6252
6449
  import { Fragment as Fragment5, jsx as jsx14, jsxs as jsxs7 } from "react/jsx-runtime";
6450
+ function buildOverlaySaveBody(result, name, preview) {
6451
+ return {
6452
+ name: name.trim() || result.overlay.name,
6453
+ description: result.overlay.description,
6454
+ ...preview ? { image: preview, images: [preview] } : {},
6455
+ settings: result.overlay.settings
6456
+ };
6457
+ }
6458
+ function mergeCoverage(results) {
6459
+ const byKey = /* @__PURE__ */ new Map();
6460
+ const notes = /* @__PURE__ */ new Set();
6461
+ let totalWidgets = 0;
6462
+ for (const result of results) {
6463
+ totalWidgets += result.coverage.totalWidgets;
6464
+ for (const note of result.coverage.notes) notes.add(note);
6465
+ for (const mapping of result.coverage.mappings) {
6466
+ const key = `${mapping.seType}::${mapping.lumiaType}::${mapping.status}`;
6467
+ const existing = byKey.get(key);
6468
+ if (existing) {
6469
+ existing.count += mapping.count;
6470
+ } else {
6471
+ byKey.set(key, { ...mapping });
6472
+ }
6473
+ }
6474
+ }
6475
+ return {
6476
+ totalWidgets,
6477
+ mappings: Array.from(byKey.values()),
6478
+ notes: Array.from(notes)
6479
+ };
6480
+ }
6481
+ function combineImportResults(results) {
6482
+ if (results.length === 1) return results[0].result;
6483
+ const first = results[0].result.overlay;
6484
+ const importedResults = results.map((item) => item.result);
6485
+ const layers = importedResults.flatMap(
6486
+ (result) => result.overlay.settings.layers ?? []
6487
+ );
6488
+ const modules = importedResults.reduce(
6489
+ (acc, result) => ({ ...acc, ...result.overlay.settings.modules ?? {} }),
6490
+ {}
6491
+ );
6492
+ const names = results.map((item) => item.summary?.name ?? item.result.overlay.name).filter(Boolean);
6493
+ return {
6494
+ overlay: {
6495
+ ...first,
6496
+ name: `[SE] ${results.length} StreamElements overlays`,
6497
+ description: `Imported from ${results.length} StreamElements overlays${names.length ? `: ${names.join(", ")}` : ""}`,
6498
+ settings: {
6499
+ ...first.settings,
6500
+ layers,
6501
+ modules
6502
+ },
6503
+ layers_count: layers.length
6504
+ },
6505
+ coverage: mergeCoverage(importedResults),
6506
+ reviewItems: importedResults.flatMap((result) => result.reviewItems)
6507
+ };
6508
+ }
6253
6509
  var CORE_STEPS = [
6254
6510
  { key: "connect", label: "Connect" },
6255
6511
  { key: "pick", label: "Overlays" },
@@ -6381,7 +6637,9 @@ function OverlayBatchProgress({
6381
6637
  queuedCount
6382
6638
  }) {
6383
6639
  const selectedOverlays = useMemo5(() => {
6384
- const byId = new Map((overlays ?? []).map((overlay) => [overlay._id, overlay]));
6640
+ const byId = new Map(
6641
+ (overlays ?? []).map((overlay) => [overlay._id, overlay])
6642
+ );
6385
6643
  return Array.from(selectedIds).map((id) => byId.get(id)).filter(Boolean);
6386
6644
  }, [overlays, selectedIds]);
6387
6645
  if (!active || selectedOverlays.length <= 1) return null;
@@ -6390,7 +6648,10 @@ function OverlayBatchProgress({
6390
6648
  const queued = selectedOverlays.slice(currentIndex);
6391
6649
  const visibleCompleted = completed.slice(-3);
6392
6650
  const visibleQueued = queued.slice(0, 3);
6393
- const hiddenCompleted = Math.max(0, completed.length - visibleCompleted.length);
6651
+ const hiddenCompleted = Math.max(
6652
+ 0,
6653
+ completed.length - visibleCompleted.length
6654
+ );
6394
6655
  const hiddenQueued = Math.max(0, queued.length - visibleQueued.length);
6395
6656
  const renderThumb = (overlay, state) => /* @__PURE__ */ jsxs7(
6396
6657
  "div",
@@ -6404,29 +6665,36 @@ function OverlayBatchProgress({
6404
6665
  },
6405
6666
  overlay._id
6406
6667
  );
6407
- return /* @__PURE__ */ jsxs7("div", { className: "se-import-overlay-progress", "aria-label": "Overlay import progress", children: [
6408
- /* @__PURE__ */ jsxs7("div", { className: "se-import-overlay-progress__copy", children: [
6409
- "Importing overlay ",
6410
- currentIndex,
6411
- " of ",
6412
- total,
6413
- currentName ? ` \xB7 ${currentName}` : "",
6414
- queuedCount > 0 ? ` \xB7 ${queuedCount} queued` : ""
6415
- ] }),
6416
- /* @__PURE__ */ jsxs7("div", { className: "se-import-overlay-progress__strip", children: [
6417
- hiddenCompleted > 0 && /* @__PURE__ */ jsxs7("span", { className: "se-import-overlay-progress__more", children: [
6418
- "+",
6419
- hiddenCompleted
6420
- ] }),
6421
- /* @__PURE__ */ jsx14("div", { className: "se-import-overlay-progress__stack se-import-overlay-progress__stack--done", children: visibleCompleted.map((overlay) => renderThumb(overlay, "done")) }),
6422
- current && renderThumb(current, "current"),
6423
- /* @__PURE__ */ jsx14("div", { className: "se-import-overlay-progress__stack se-import-overlay-progress__stack--queued", children: visibleQueued.map((overlay) => renderThumb(overlay, "queued")) }),
6424
- hiddenQueued > 0 && /* @__PURE__ */ jsxs7("span", { className: "se-import-overlay-progress__more", children: [
6425
- "+",
6426
- hiddenQueued
6427
- ] })
6428
- ] })
6429
- ] });
6668
+ return /* @__PURE__ */ jsxs7(
6669
+ "div",
6670
+ {
6671
+ className: "se-import-overlay-progress",
6672
+ "aria-label": "Overlay import progress",
6673
+ children: [
6674
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-overlay-progress__copy", children: [
6675
+ "Importing overlay ",
6676
+ currentIndex,
6677
+ " of ",
6678
+ total,
6679
+ currentName ? ` \xB7 ${currentName}` : "",
6680
+ queuedCount > 0 ? ` \xB7 ${queuedCount} queued` : ""
6681
+ ] }),
6682
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-overlay-progress__strip", children: [
6683
+ hiddenCompleted > 0 && /* @__PURE__ */ jsxs7("span", { className: "se-import-overlay-progress__more", children: [
6684
+ "+",
6685
+ hiddenCompleted
6686
+ ] }),
6687
+ /* @__PURE__ */ jsx14("div", { className: "se-import-overlay-progress__stack se-import-overlay-progress__stack--done", children: visibleCompleted.map((overlay) => renderThumb(overlay, "done")) }),
6688
+ current && renderThumb(current, "current"),
6689
+ /* @__PURE__ */ jsx14("div", { className: "se-import-overlay-progress__stack se-import-overlay-progress__stack--queued", children: visibleQueued.map((overlay) => renderThumb(overlay, "queued")) }),
6690
+ hiddenQueued > 0 && /* @__PURE__ */ jsxs7("span", { className: "se-import-overlay-progress__more", children: [
6691
+ "+",
6692
+ hiddenQueued
6693
+ ] })
6694
+ ] })
6695
+ ]
6696
+ }
6697
+ );
6430
6698
  }
6431
6699
  function CustomOverlayPreview({
6432
6700
  html,
@@ -6781,7 +7049,7 @@ function StepPickOverlay({
6781
7049
  {
6782
7050
  number: 2,
6783
7051
  title: "Pick the overlays to import",
6784
- subtitle: `Select one or more StreamElements overlays from your account. We'll run each one through the discovery \u2192 review \u2192 import pipeline in sequence. ${total > 0 ? `${total} overlay${total === 1 ? "" : "s"} on this channel.` : ""}`
7052
+ subtitle: `Select one or more StreamElements overlays from your account. One overlay opens the full review flow; multiple overlays are combined and reviewed once. ${total > 0 ? `${total} overlay${total === 1 ? "" : "s"} on this channel.` : ""}`
6785
7053
  }
6786
7054
  ),
6787
7055
  loading && /* @__PURE__ */ jsxs7("div", { className: "se-import-help se-import-help--info", children: [
@@ -6852,7 +7120,7 @@ function StepPickOverlay({
6852
7120
  ]
6853
7121
  }
6854
7122
  ),
6855
- /* @__PURE__ */ jsx14("span", { style: { fontSize: 13, color: "var(--se-muted)" }, children: hasAnySelected ? `${selectedCount} overlay${selectedCount === 1 ? "" : "s"} queued. Each runs through the full import flow in sequence.` : "Click overlays below to select them." })
7123
+ /* @__PURE__ */ jsx14("span", { style: { fontSize: 13, color: "var(--se-muted)" }, children: hasAnySelected ? selectedCount > 1 ? `${selectedCount} overlays selected. They will be combined and reviewed once.` : "1 overlay selected. It will open the full review flow." : "Click overlays below to select them." })
6856
7124
  ]
6857
7125
  }
6858
7126
  ),
@@ -6888,7 +7156,12 @@ function StepPickOverlay({
6888
7156
  checked: selected,
6889
7157
  onChange: () => toggleId(o._id),
6890
7158
  size: "small",
6891
- style: { position: "absolute", top: 6, right: 6, zIndex: 1 }
7159
+ style: {
7160
+ position: "absolute",
7161
+ top: 6,
7162
+ right: 6,
7163
+ zIndex: 1
7164
+ }
6892
7165
  }
6893
7166
  ),
6894
7167
  o.preview ? /* @__PURE__ */ jsx14(
@@ -7032,21 +7305,37 @@ function StepURL({
7032
7305
  }
7033
7306
  function StepDiscovery({
7034
7307
  result,
7035
- assetCount
7308
+ assetCount,
7309
+ overlayCount = 1
7036
7310
  }) {
7037
7311
  const widgets = result.coverage.totalWidgets;
7038
7312
  const reviewCount = result.reviewItems.length;
7313
+ const layerCount = result.overlay.settings.layers?.length ?? 0;
7039
7314
  const customCount = result.coverage.mappings.find(
7040
7315
  (item) => item.seType === "se-widget-custom-event-list"
7041
7316
  )?.count ?? 0;
7042
7317
  const alertCount = result.coverage.mappings.filter((item) => item.lumiaType === "alert").reduce((sum, item) => sum + item.count, 0);
7043
7318
  const topCards = [
7319
+ {
7320
+ key: "overlays",
7321
+ label: "Overlays selected",
7322
+ value: overlayCount,
7323
+ icon: source_se_default,
7324
+ description: overlayCount > 1 ? "Selected StreamElements overlays combined into this import." : "Selected StreamElements overlay."
7325
+ },
7326
+ {
7327
+ key: "layers",
7328
+ label: "Layers imported",
7329
+ value: layerCount,
7330
+ icon: source_lumia_default,
7331
+ description: "Lumia layers prepared from the selected overlay content."
7332
+ },
7044
7333
  {
7045
7334
  key: "widgets",
7046
7335
  label: "Widgets found",
7047
7336
  value: widgets,
7048
7337
  icon: source_se_default,
7049
- description: "StreamElements layers detected in this overlay."
7338
+ description: overlayCount > 1 ? "StreamElements widgets detected across the selected overlays." : "StreamElements widgets detected in this overlay."
7050
7339
  },
7051
7340
  {
7052
7341
  key: "alerts",
@@ -7071,7 +7360,13 @@ function StepDiscovery({
7071
7360
  }
7072
7361
  ].filter((card) => card.value > 0);
7073
7362
  return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--wide"), children: [
7074
- /* @__PURE__ */ jsx14(StepHeader, { number: 2, title: "Here's what we found in your overlay" }),
7363
+ /* @__PURE__ */ jsx14(
7364
+ StepHeader,
7365
+ {
7366
+ number: 2,
7367
+ title: overlayCount > 1 ? "Here's what we found in your overlays" : "Here's what we found in your overlay"
7368
+ }
7369
+ ),
7075
7370
  topCards.length > 0 && /* @__PURE__ */ jsx14("div", { className: "se-import-grid se-import-grid--stats", children: topCards.map((card) => /* @__PURE__ */ jsx14(
7076
7371
  StatusCard,
7077
7372
  {
@@ -7098,7 +7393,8 @@ function StepDiscovery({
7098
7393
  function StepOptions({
7099
7394
  options,
7100
7395
  setOptions,
7101
- assetCount
7396
+ assetCount,
7397
+ hideName = false
7102
7398
  }) {
7103
7399
  return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--medium"), children: [
7104
7400
  /* @__PURE__ */ jsx14(
@@ -7136,7 +7432,7 @@ function StepOptions({
7136
7432
  ]
7137
7433
  }
7138
7434
  ),
7139
- /* @__PURE__ */ jsx14(
7435
+ !hideName && /* @__PURE__ */ jsx14(
7140
7436
  LSInput,
7141
7437
  {
7142
7438
  label: "Overlay name",
@@ -7450,9 +7746,11 @@ function StepConfirm({
7450
7746
  result,
7451
7747
  options,
7452
7748
  mirrorRows,
7453
- rowStates
7749
+ rowStates,
7750
+ overlayCount = 1
7454
7751
  }) {
7455
7752
  const widgets = result.coverage.totalWidgets;
7753
+ const layerCount = result.overlay.settings.layers?.length ?? 0;
7456
7754
  const alertCount = result.coverage.mappings.filter((item) => item.lumiaType === "alert").reduce((sum, item) => sum + item.count, 0);
7457
7755
  const mirrored = mirrorRows.filter(
7458
7756
  (row) => row.state === "done" || row.state === "reused" || row.state === "kept-on-cdn"
@@ -7475,7 +7773,15 @@ function StepConfirm({
7475
7773
  ),
7476
7774
  /* @__PURE__ */ jsxs7("div", { className: "se-import-overview", children: [
7477
7775
  /* @__PURE__ */ jsx14("h3", { children: "Overview" }),
7478
- /* @__PURE__ */ jsx14(Row, { label: "Overlay", value: options.name || result.overlay.name }),
7776
+ /* @__PURE__ */ jsx14(
7777
+ Row,
7778
+ {
7779
+ label: overlayCount > 1 ? "Combined overlay" : "Overlay",
7780
+ value: options.name || result.overlay.name
7781
+ }
7782
+ ),
7783
+ overlayCount > 1 ? /* @__PURE__ */ jsx14(Row, { label: "Source overlays", value: `${overlayCount}` }) : null,
7784
+ /* @__PURE__ */ jsx14(Row, { label: "Layers imported", value: `${layerCount}` }),
7479
7785
  /* @__PURE__ */ jsx14(Row, { label: "Widgets", value: `${widgets}` }),
7480
7786
  /* @__PURE__ */ jsx14(Row, { label: "Alerts", value: `${alertCount}` }),
7481
7787
  /* @__PURE__ */ jsx14(
@@ -7513,7 +7819,9 @@ function Row({
7513
7819
  function SEImportWizard({
7514
7820
  bindings,
7515
7821
  initialUrl = "",
7516
- initialJwt = ""
7822
+ initialJwt = "",
7823
+ initialOverlayIds,
7824
+ initialOverlayList
7517
7825
  }) {
7518
7826
  const {
7519
7827
  fetchMarketplaceOverlay,
@@ -7542,7 +7850,7 @@ function SEImportWizard({
7542
7850
  const [connectError, setConnectError] = useState6(null);
7543
7851
  const [connecting, setConnecting] = useState6(false);
7544
7852
  const [overlayList, setOverlayList] = useState6(
7545
- null
7853
+ initialOverlayList?.length ? initialOverlayList : null
7546
7854
  );
7547
7855
  const [overlaysLoading, setOverlaysLoading] = useState6(false);
7548
7856
  const [overlaysError, setOverlaysError] = useState6(null);
@@ -7552,6 +7860,8 @@ function SEImportWizard({
7552
7860
  const [overlayQueue, setOverlayQueue] = useState6([]);
7553
7861
  const [currentOverlayIndex, setCurrentOverlayIndex] = useState6(0);
7554
7862
  const [totalOverlaysToImport, setTotalOverlaysToImport] = useState6(0);
7863
+ const [activeOverlaySummaries, setActiveOverlaySummaries] = useState6([]);
7864
+ const [activeOverlayPreview, setActiveOverlayPreview] = useState6();
7555
7865
  const [url, setUrl] = useState6(initialUrl);
7556
7866
  const [loadError, setLoadError] = useState6(null);
7557
7867
  const [loading, setLoading] = useState6(false);
@@ -7571,6 +7881,7 @@ function SEImportWizard({
7571
7881
  const [marketplacePickerOpen, setMarketplacePickerOpen] = useState6(false);
7572
7882
  const mirrorStartedRef = useRef4(false);
7573
7883
  const mirrorAbortRef = useRef4(null);
7884
+ const initialOverlayImportStartedRef = useRef4(false);
7574
7885
  const filenameCacheRef = useRef4(/* @__PURE__ */ new Map());
7575
7886
  const [assetUrls, setAssetUrls] = useState6([]);
7576
7887
  const currentItem = originalReviewItems[reviewIndex];
@@ -7620,7 +7931,8 @@ function SEImportWizard({
7620
7931
  }
7621
7932
  }, [bindings, initialJwt, jwt]);
7622
7933
  useEffect6(() => {
7623
- if (initialJwt.trim() && step === "connect" && !seClient && !connecting && !connectError) void handleConnect();
7934
+ if (initialJwt.trim() && step === "connect" && !seClient && !connecting && !connectError)
7935
+ void handleConnect();
7624
7936
  }, [connectError, connecting, handleConnect, initialJwt, seClient, step]);
7625
7937
  const loadOverlayList = useCallback3(
7626
7938
  async (client) => {
@@ -7661,8 +7973,58 @@ function SEImportWizard({
7661
7973
  const clearAllOverlays = useCallback3(() => {
7662
7974
  setSelectedOverlayIds(/* @__PURE__ */ new Set());
7663
7975
  }, []);
7976
+ const loadSelectedOverlaysAndKickoff = useCallback3(
7977
+ async (client, ids) => {
7978
+ setLoadError(null);
7979
+ setLoading(true);
7980
+ try {
7981
+ const importedOverlays = [];
7982
+ for (let index = 0; index < ids.length; index += 1) {
7983
+ const overlayId = ids[index];
7984
+ const summary = (overlayList ?? []).find((o) => o._id === overlayId);
7985
+ const bootstrap = await fetchSEBootstrapForOverlay(client, overlayId);
7986
+ if (!isSEBootstrap(bootstrap)) {
7987
+ throw new Error(
7988
+ `StreamElements returned an invalid bootstrap for ${summary?.name ?? overlayId}.`
7989
+ );
7990
+ }
7991
+ const imported = importSEBootstrap(bootstrap);
7992
+ importedOverlays.push({ result: imported, summary });
7993
+ }
7994
+ const combined = combineImportResults(importedOverlays);
7995
+ setResult(combined);
7996
+ setAssetUrls(findSEAssetURLs(combined.overlay));
7997
+ setOptions((o) => ({ ...o, name: combined.overlay.name }));
7998
+ setOriginalReviewItems(combined.reviewItems);
7999
+ setRowStates(
8000
+ Object.fromEntries(
8001
+ combined.reviewItems.map((item) => [
8002
+ item.moduleId,
8003
+ { state: "pending" }
8004
+ ])
8005
+ )
8006
+ );
8007
+ setReviewIndex(0);
8008
+ setMirrorRows([]);
8009
+ mirrorStartedRef.current = false;
8010
+ const summaries = importedOverlays.map((item) => item.summary).filter(Boolean);
8011
+ setActiveOverlaySummaries(summaries);
8012
+ setActiveOverlayPreview(summaries[0]?.preview);
8013
+ setStep("discovery");
8014
+ } catch (e) {
8015
+ const detail = e?.message ?? String(e);
8016
+ setLoadError({
8017
+ message: detail || "Could not load the selected overlays.",
8018
+ hint: detail ? "If this looks transient, retry in a moment. Otherwise check the token and try again." : void 0
8019
+ });
8020
+ } finally {
8021
+ setLoading(false);
8022
+ }
8023
+ },
8024
+ [overlayList]
8025
+ );
7664
8026
  const loadBootstrapAndKickoff = useCallback3(
7665
- async (client, overlayId) => {
8027
+ async (client, overlayId, summary) => {
7666
8028
  setLoadError(null);
7667
8029
  setLoading(true);
7668
8030
  try {
@@ -7688,6 +8050,9 @@ function SEImportWizard({
7688
8050
  setReviewIndex(0);
7689
8051
  setMirrorRows([]);
7690
8052
  mirrorStartedRef.current = false;
8053
+ const summaries = summary ? [summary] : [];
8054
+ setActiveOverlaySummaries(summaries);
8055
+ setActiveOverlayPreview(summary?.preview ?? bootstrap.overlay.preview);
7691
8056
  setStep("discovery");
7692
8057
  } catch (e) {
7693
8058
  const detail = e?.message ?? String(e);
@@ -7705,12 +8070,55 @@ function SEImportWizard({
7705
8070
  if (!seClient) return;
7706
8071
  const ids = Array.from(selectedOverlayIds);
7707
8072
  if (ids.length === 0) return;
8073
+ if (ids.length > 1) {
8074
+ setOverlayQueue([]);
8075
+ setCurrentOverlayIndex(0);
8076
+ setTotalOverlaysToImport(0);
8077
+ await loadSelectedOverlaysAndKickoff(seClient, ids);
8078
+ return;
8079
+ }
7708
8080
  const [first, ...rest] = ids;
8081
+ const summary = (overlayList ?? []).find((o) => o._id === first);
8082
+ const summaries = summary ? [summary] : [];
8083
+ setActiveOverlaySummaries(summaries);
7709
8084
  setOverlayQueue(rest);
7710
8085
  setTotalOverlaysToImport(ids.length);
7711
8086
  setCurrentOverlayIndex(1);
7712
- await loadBootstrapAndKickoff(seClient, first);
7713
- }, [seClient, selectedOverlayIds, loadBootstrapAndKickoff]);
8087
+ await loadBootstrapAndKickoff(seClient, first, summary);
8088
+ }, [
8089
+ seClient,
8090
+ selectedOverlayIds,
8091
+ overlayList,
8092
+ loadSelectedOverlaysAndKickoff,
8093
+ loadBootstrapAndKickoff
8094
+ ]);
8095
+ useEffect6(() => {
8096
+ if (initialOverlayImportStartedRef.current || !seClient || !initialOverlayIds?.length || !overlayList || overlaysLoading || loading)
8097
+ return;
8098
+ const availableIds = new Set(overlayList.map((overlay) => overlay._id));
8099
+ const ids = initialOverlayIds.filter((id) => availableIds.has(id));
8100
+ if (ids.length === 0) return;
8101
+ initialOverlayImportStartedRef.current = true;
8102
+ setSelectedOverlayIds(new Set(ids));
8103
+ setOverlayQueue([]);
8104
+ setCurrentOverlayIndex(ids.length === 1 ? 1 : 0);
8105
+ setTotalOverlaysToImport(ids.length === 1 ? 1 : 0);
8106
+ if (ids.length > 1) {
8107
+ void loadSelectedOverlaysAndKickoff(seClient, ids);
8108
+ return;
8109
+ }
8110
+ const first = ids[0];
8111
+ const summary = overlayList.find((overlay) => overlay._id === first);
8112
+ void loadBootstrapAndKickoff(seClient, first, summary);
8113
+ }, [
8114
+ initialOverlayIds,
8115
+ loadBootstrapAndKickoff,
8116
+ loadSelectedOverlaysAndKickoff,
8117
+ loading,
8118
+ overlayList,
8119
+ overlaysLoading,
8120
+ seClient
8121
+ ]);
7714
8122
  const handleLoad = useCallback3(async () => {
7715
8123
  setLoadError(null);
7716
8124
  const parts = extractSEPreviewParts(url);
@@ -7746,6 +8154,8 @@ function SEImportWizard({
7746
8154
  )
7747
8155
  );
7748
8156
  setReviewIndex(0);
8157
+ setActiveOverlaySummaries([]);
8158
+ setActiveOverlayPreview(bootstrap.overlay.preview);
7749
8159
  setStep("discovery");
7750
8160
  } catch (error) {
7751
8161
  const detail = error?.message ?? String(error);
@@ -7788,6 +8198,25 @@ function SEImportWizard({
7788
8198
  },
7789
8199
  [uploadAsset]
7790
8200
  );
8201
+ const resolveOverlayThumbnailUrl = useCallback3(
8202
+ async (preview) => {
8203
+ if (!preview) return void 0;
8204
+ const filename = filenameFromURL(preview);
8205
+ const existing = existingAssets.find(
8206
+ (asset) => asset.file_name === filename || asset.name === filename
8207
+ );
8208
+ if (existing?.url) return existing.url;
8209
+ try {
8210
+ return await mirrorOneAsset(preview, (file) => uploadAsset(file));
8211
+ } catch (error) {
8212
+ notify.warning(
8213
+ `Could not import the StreamElements thumbnail: ${extractErrorMessage(error)}`
8214
+ );
8215
+ return preview;
8216
+ }
8217
+ },
8218
+ [existingAssets, notify, uploadAsset]
8219
+ );
7791
8220
  const applyRewrite = useCallback3((mapping) => {
7792
8221
  if (Object.keys(mapping).length === 0) return;
7793
8222
  setResult(
@@ -7829,7 +8258,9 @@ function SEImportWizard({
7829
8258
  rewriteMap[initialRows[i].url] = outcome.newUrl;
7830
8259
  }
7831
8260
  setMirrorRows(
7832
- (previous) => previous.map((row, idx) => idx === i ? { ...row, ...outcome } : row)
8261
+ (previous) => previous.map(
8262
+ (row, idx) => idx === i ? { ...row, ...outcome } : row
8263
+ )
7833
8264
  );
7834
8265
  }
7835
8266
  };
@@ -8012,11 +8443,8 @@ function SEImportWizard({
8012
8443
  if (!result) return;
8013
8444
  setImporting(true);
8014
8445
  try {
8015
- const body = {
8016
- name: options.name.trim() || result.overlay.name,
8017
- description: result.overlay.description,
8018
- settings: result.overlay.settings
8019
- };
8446
+ const thumbnailUrl = await resolveOverlayThumbnailUrl(activeOverlayPreview);
8447
+ const body = buildOverlaySaveBody(result, options.name, thumbnailUrl);
8020
8448
  const { uuid } = await saveOverlay(body);
8021
8449
  notify.success("Imported overlay from StreamElements.");
8022
8450
  mirrorAbortRef.current?.abort();
@@ -8039,38 +8467,35 @@ function SEImportWizard({
8039
8467
  }
8040
8468
  setImporting(true);
8041
8469
  try {
8042
- const body = {
8043
- name: options.name.trim() || result.overlay.name,
8044
- description: result.overlay.description,
8045
- settings: result.overlay.settings
8046
- };
8047
- const { uuid } = await saveOverlay(body);
8048
8470
  const orderedIds = Array.from(selectedOverlayIds);
8049
8471
  const sourceSEOverlayId = orderedIds[currentOverlayIndex - 1];
8050
- const sourceSummary = sourceSEOverlayId ? (overlayList ?? []).find((o) => o._id === sourceSEOverlayId) : void 0;
8472
+ const fallbackSourceSummary = sourceSEOverlayId ? (overlayList ?? []).find((o) => o._id === sourceSEOverlayId) : void 0;
8473
+ const sourceSummaries = activeOverlaySummaries.length > 0 ? activeOverlaySummaries : fallbackSourceSummary ? [fallbackSourceSummary] : [];
8474
+ const thumbnailUrl = await resolveOverlayThumbnailUrl(activeOverlayPreview);
8475
+ const body = buildOverlaySaveBody(result, options.name, thumbnailUrl);
8476
+ const { uuid } = await saveOverlay(body);
8051
8477
  if (uuid) {
8052
- onOverlayImported?.({
8053
- overlayId: uuid,
8054
- sourceSEOverlayId,
8055
- sourceSEOverlayName: sourceSummary?.name ?? sourceSummary?.settings?.name ?? result.overlay.name ?? void 0,
8056
- width: sourceSummary?.settings?.width ?? result.overlay.settings.metadata.width ?? void 0,
8057
- height: sourceSummary?.settings?.height ?? result.overlay.settings.metadata.height ?? void 0
8058
- });
8059
- }
8060
- const hasMore = overlayQueue.length > 0;
8061
- if (hasMore) {
8062
- notify.success(
8063
- `Imported ${currentOverlayIndex} of ${totalOverlaysToImport} overlays from StreamElements.`
8064
- );
8065
- const [nextId, ...rest] = overlayQueue;
8066
- setOverlayQueue(rest);
8067
- setCurrentOverlayIndex(currentOverlayIndex + 1);
8068
- setImporting(false);
8069
- await loadBootstrapAndKickoff(seClient, nextId);
8070
- return;
8478
+ if (sourceSummaries.length > 0) {
8479
+ for (const sourceSummary of sourceSummaries) {
8480
+ onOverlayImported?.({
8481
+ overlayId: uuid,
8482
+ sourceSEOverlayId: sourceSummary._id,
8483
+ sourceSEOverlayName: sourceSummary.name ?? sourceSummary.settings?.name ?? result.overlay.name ?? void 0,
8484
+ width: sourceSummary.settings?.width ?? result.overlay.settings.metadata.width ?? void 0,
8485
+ height: sourceSummary.settings?.height ?? result.overlay.settings.metadata.height ?? void 0
8486
+ });
8487
+ }
8488
+ } else {
8489
+ onOverlayImported?.({
8490
+ overlayId: uuid,
8491
+ sourceSEOverlayName: result.overlay.name ?? void 0,
8492
+ width: result.overlay.settings.metadata.width ?? void 0,
8493
+ height: result.overlay.settings.metadata.height ?? void 0
8494
+ });
8495
+ }
8071
8496
  }
8072
8497
  notify.success(
8073
- totalOverlaysToImport > 1 ? `Imported all ${totalOverlaysToImport} overlays from StreamElements.` : "Imported overlay from StreamElements."
8498
+ sourceSummaries.length > 1 ? `Imported ${sourceSummaries.length} StreamElements overlays as one Lumia overlay.` : "Imported overlay from StreamElements."
8074
8499
  );
8075
8500
  mirrorAbortRef.current?.abort();
8076
8501
  onClose();
@@ -8089,12 +8514,12 @@ function SEImportWizard({
8089
8514
  result,
8090
8515
  saveOverlay,
8091
8516
  seClient,
8092
- overlayQueue,
8093
8517
  currentOverlayIndex,
8094
- totalOverlaysToImport,
8095
- loadBootstrapAndKickoff,
8096
8518
  selectedOverlayIds,
8097
- overlayList
8519
+ overlayList,
8520
+ activeOverlaySummaries,
8521
+ activeOverlayPreview,
8522
+ resolveOverlayThumbnailUrl
8098
8523
  ]);
8099
8524
  const visibleSteps = useMemo5(() => {
8100
8525
  const showMirrorStep = options.mirrorAssets && (assetUrls.length > 0 || mirrorRows.length > 0 || step === "mirror");
@@ -8169,7 +8594,9 @@ function SEImportWizard({
8169
8594
  if (step === "connect")
8170
8595
  return connecting ? "Connecting..." : seClient ? "Continue" : "Connect";
8171
8596
  if (step === "pick") {
8172
- if (loading) return "Loading overlay...";
8597
+ if (importing) return "Importing...";
8598
+ if (loading)
8599
+ return selectedOverlayIds.size > 1 ? "Loading overlays..." : "Loading overlay...";
8173
8600
  const n = selectedOverlayIds.size;
8174
8601
  return n > 1 ? `Import ${n} overlays` : "Import overlay";
8175
8602
  }
@@ -8184,7 +8611,7 @@ function SEImportWizard({
8184
8611
  const nextDisabled = (() => {
8185
8612
  if (step === "connect") return connecting || !seClient && !jwt.trim();
8186
8613
  if (step === "pick")
8187
- return loading || overlaysLoading || selectedOverlayIds.size === 0;
8614
+ return importing || loading || overlaysLoading || selectedOverlayIds.size === 0;
8188
8615
  if (step === "url") return loading || !url.trim();
8189
8616
  if (step === "mirror") return mirrorRunning;
8190
8617
  if (step === "review")
@@ -8287,13 +8714,21 @@ function SEImportWizard({
8287
8714
  }
8288
8715
  }
8289
8716
  ),
8290
- step === "discovery" && result && /* @__PURE__ */ jsx14(StepDiscovery, { result, assetCount: assetUrls.length }),
8717
+ step === "discovery" && result && /* @__PURE__ */ jsx14(
8718
+ StepDiscovery,
8719
+ {
8720
+ result,
8721
+ assetCount: assetUrls.length,
8722
+ overlayCount: Math.max(1, activeOverlaySummaries.length)
8723
+ }
8724
+ ),
8291
8725
  step === "options" && /* @__PURE__ */ jsx14(
8292
8726
  StepOptions,
8293
8727
  {
8294
8728
  options,
8295
8729
  setOptions,
8296
- assetCount: assetUrls.length
8730
+ assetCount: assetUrls.length,
8731
+ hideName: activeOverlaySummaries.length > 1
8297
8732
  }
8298
8733
  ),
8299
8734
  step === "mirror" && /* @__PURE__ */ jsx14(
@@ -8349,7 +8784,8 @@ function SEImportWizard({
8349
8784
  result,
8350
8785
  options,
8351
8786
  mirrorRows,
8352
- rowStates
8787
+ rowStates,
8788
+ overlayCount: Math.max(1, activeOverlaySummaries.length)
8353
8789
  }
8354
8790
  )
8355
8791
  ] }),
@@ -8423,7 +8859,9 @@ function extractSEOverlayId(input) {
8423
8859
  }
8424
8860
  function extractSEPreviewParts(input) {
8425
8861
  const trimmed = input.trim();
8426
- const m = trimmed.match(/streamelements\.com\/overlay\/([0-9a-f]{24})(?:\/([A-Za-z0-9_-]+))?/i);
8862
+ const m = trimmed.match(
8863
+ /streamelements\.com\/overlay\/([0-9a-f]{24})(?:\/([A-Za-z0-9_-]+))?/i
8864
+ );
8427
8865
  if (!m) return null;
8428
8866
  const overlayId = m[1];
8429
8867
  const second = m[2];
@@ -8433,9 +8871,14 @@ function extractSEPreviewParts(input) {
8433
8871
  function buildBootstrapUrl(overlayId) {
8434
8872
  return `https://api.streamelements.com/kappa/v2/overlays/${overlayId}/bootstrap?isEditor=false&isMobile=false&isObs=false&isObsLive=false&isXsplit=false`;
8435
8873
  }
8436
- async function fetchSEBootstrap({ overlayId, apikey }) {
8874
+ async function fetchSEBootstrap({
8875
+ overlayId,
8876
+ apikey
8877
+ }) {
8437
8878
  if (!apikey) {
8438
- throw new Error("Missing access token. Use the public preview URL (Overlay menu \u2192 Get URL \u2192 Public link) \u2014 editor URLs do not include a token.");
8879
+ throw new Error(
8880
+ "Missing access token. Use the public preview URL (Overlay menu \u2192 Get URL \u2192 Public link) \u2014 editor URLs do not include a token."
8881
+ );
8439
8882
  }
8440
8883
  const res = await fetch(buildBootstrapUrl(overlayId), {
8441
8884
  method: "GET",
@@ -8453,7 +8896,9 @@ async function fetchSEBootstrap({ overlayId, apikey }) {
8453
8896
  }
8454
8897
  const json = await res.json();
8455
8898
  if (!isSEBootstrap(json)) {
8456
- throw new Error("Server returned a response that does not look like an SE bootstrap payload.");
8899
+ throw new Error(
8900
+ "Server returned a response that does not look like an SE bootstrap payload."
8901
+ );
8457
8902
  }
8458
8903
  return json;
8459
8904
  }
@@ -8476,7 +8921,10 @@ function importSEBootstrap(bootstrap) {
8476
8921
  console.log("[SE-IMPORT] bootstrap received", {
8477
8922
  overlayId: seOverlay?._id,
8478
8923
  name: seOverlay?.name,
8479
- canvas: { width: seOverlay?.settings?.width, height: seOverlay?.settings?.height },
8924
+ canvas: {
8925
+ width: seOverlay?.settings?.width,
8926
+ height: seOverlay?.settings?.height
8927
+ },
8480
8928
  widgetCount: widgets.length,
8481
8929
  widgetTypes: widgets.map((w) => w.type),
8482
8930
  provider
@@ -8504,7 +8952,11 @@ function importSEBootstrap(bootstrap) {
8504
8952
  const lumiaGroupId = seUidToLumiaGroupId[uid];
8505
8953
  const parentUid = typeof w.group === "string" ? w.group : null;
8506
8954
  const parentLumiaGroupId = parentUid ? seUidToLumiaGroupId[parentUid] ?? null : null;
8507
- const bounds = seCssToBounds(w.css, { width: 200, height: 200 }, importCanvas);
8955
+ const bounds = seCssToBounds(
8956
+ w.css,
8957
+ { width: 200, height: 200 },
8958
+ importCanvas
8959
+ );
8508
8960
  const layer = {
8509
8961
  id: lumiaGroupId,
8510
8962
  type: "group",
@@ -8548,7 +9000,15 @@ function importSEBootstrap(bootstrap) {
8548
9000
  };
8549
9001
  layers.push(layer);
8550
9002
  modules[lumiaGroupId] = module;
8551
- recordCoverage(coverage, { unit: { layer, module }, status: "direct", lumiaType: "group" }, w.type);
9003
+ recordCoverage(
9004
+ coverage,
9005
+ {
9006
+ unit: { layer, module },
9007
+ status: "direct",
9008
+ lumiaType: "group"
9009
+ },
9010
+ w.type
9011
+ );
8552
9012
  }
8553
9013
  for (const w of widgets) {
8554
9014
  if (w.type === "se-widget-group") continue;
@@ -8586,13 +9046,19 @@ function importSEBootstrap(bootstrap) {
8586
9046
  recordCoverage(coverage, result, w.type);
8587
9047
  }
8588
9048
  if (coverage.mappings.some((m) => m.status === "placeholder")) {
8589
- coverage.notes.push("Some widgets imported as placeholder text layers \u2014 see SE/05-widget-to-module-mapping.md for status.");
9049
+ coverage.notes.push(
9050
+ "Some widgets imported as placeholder text layers \u2014 see SE/05-widget-to-module-mapping.md for status."
9051
+ );
8590
9052
  }
8591
9053
  if (coverage.mappings.some((m) => m.seType === "se-widget-merch-goal")) {
8592
- coverage.notes.push("Merch-goal widgets imported as generic goals \u2014 see SE/03-missing-variables.md \xA78 for the missing data source.");
9054
+ coverage.notes.push(
9055
+ "Merch-goal widgets imported as generic goals \u2014 see SE/03-missing-variables.md \xA78 for the missing data source."
9056
+ );
8593
9057
  }
8594
9058
  if (coverage.mappings.some((m) => m.seType === "se-widget-custom-event-list")) {
8595
- 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.");
9059
+ coverage.notes.push(
9060
+ "\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."
9061
+ );
8596
9062
  }
8597
9063
  const overlay = {
8598
9064
  uuid: "",
@@ -8629,10 +9095,16 @@ function applyReviewAction(result, moduleId, action, payload) {
8629
9095
  if (action === "skip") {
8630
9096
  delete modules[moduleId];
8631
9097
  const nextLayers = layers.filter((l) => l.id !== moduleId);
8632
- const reviewItems2 = result.reviewItems.filter((r) => r.moduleId !== moduleId);
9098
+ const reviewItems2 = result.reviewItems.filter(
9099
+ (r) => r.moduleId !== moduleId
9100
+ );
8633
9101
  return {
8634
9102
  ...result,
8635
- overlay: { ...result.overlay, settings: { ...result.overlay.settings, layers: nextLayers, modules }, layers_count: nextLayers.length },
9103
+ overlay: {
9104
+ ...result.overlay,
9105
+ settings: { ...result.overlay.settings, layers: nextLayers, modules },
9106
+ layers_count: nextLayers.length
9107
+ },
8636
9108
  reviewItems: reviewItems2
8637
9109
  };
8638
9110
  }
@@ -8658,7 +9130,10 @@ function applyReviewAction(result, moduleId, action, payload) {
8658
9130
  lights: mergedLights,
8659
9131
  events: mergedEvents,
8660
9132
  variables: mergedVariables,
8661
- content: { ...transplant.module.content ?? {}, [IMPORT_META_KEY]: importMeta2 }
9133
+ content: {
9134
+ ...transplant.module.content ?? {},
9135
+ [IMPORT_META_KEY]: importMeta2
9136
+ }
8662
9137
  };
8663
9138
  const rootOldNewId = transplant.layer.id;
8664
9139
  const remappedExtras = (transplant.extras ?? []).map((e) => ({
@@ -8671,7 +9146,9 @@ function applyReviewAction(result, moduleId, action, payload) {
8671
9146
  modules[extra.module.id] = extra.module;
8672
9147
  }
8673
9148
  modules[moduleId] = newModule;
8674
- const reviewItems2 = result.reviewItems.filter((r) => r.moduleId !== moduleId);
9149
+ const reviewItems2 = result.reviewItems.filter(
9150
+ (r) => r.moduleId !== moduleId
9151
+ );
8675
9152
  return {
8676
9153
  ...result,
8677
9154
  overlay: {
@@ -8706,7 +9183,10 @@ function applyReviewAction(result, moduleId, action, payload) {
8706
9183
  const reviewItems = result.reviewItems.filter((r) => r.moduleId !== moduleId);
8707
9184
  return {
8708
9185
  ...result,
8709
- overlay: { ...result.overlay, settings: { ...result.overlay.settings, modules } },
9186
+ overlay: {
9187
+ ...result.overlay,
9188
+ settings: { ...result.overlay.settings, modules }
9189
+ },
8710
9190
  reviewItems
8711
9191
  };
8712
9192
  }
@@ -8792,6 +9272,7 @@ export {
8792
9272
  extractSEPreviewParts,
8793
9273
  fetchSEBootstrap,
8794
9274
  fetchSEFiltersRaw,
9275
+ fetchSEOverlays,
8795
9276
  fetchUsableChannels,
8796
9277
  filenameFromURL,
8797
9278
  findSEAssetURLs,