@lumiastream/ui 0.3.0 → 0.3.1

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
@@ -3745,35 +3745,38 @@ function mapReadout(widget, fallbackVar, ctx) {
3745
3745
  );
3746
3746
  }
3747
3747
 
3748
+ // src/se-import/mappers/alert.ts
3749
+ import { LumiaAlertConfigs } from "@lumiastream/lumia-types";
3750
+
3748
3751
  // src/se-import/mappers/provider-alerts.ts
3749
3752
  var ALERT_BOX_KEYS = {
3750
3753
  twitch: {
3751
- follower: "twitch-follower",
3752
- subscriber: "twitch-subscriber",
3753
- tip: "streamelements-donation",
3754
- cheer: "twitch-bits",
3755
- raid: "twitch-raid",
3756
- merch: "fourthwall-shopOrder",
3757
- purchase: "fourthwall-shopOrder",
3758
- charityCampaignDonation: "twitch-charityDonation"
3754
+ follower: ["twitch-follower"],
3755
+ subscriber: ["twitch-subscriber"],
3756
+ tip: ["lumiastream-donation", "streamelements-donation"],
3757
+ cheer: ["twitch-bits"],
3758
+ raid: ["twitch-raid"],
3759
+ merch: ["fourthwall-shopOrder"],
3760
+ purchase: ["fourthwall-shopOrder"],
3761
+ charityCampaignDonation: ["twitch-charityDonation"]
3759
3762
  },
3760
3763
  kick: {
3761
- follower: "kick-follower",
3762
- subscriber: "kick-subscriber",
3763
- tip: "streamelements-donation",
3764
- cheer: "kick-kicks",
3765
- raid: "kick-host",
3766
- host: "kick-host",
3767
- merch: "fourthwall-shopOrder",
3768
- purchase: "fourthwall-shopOrder"
3764
+ follower: ["kick-follower"],
3765
+ subscriber: ["kick-subscriber"],
3766
+ tip: ["lumiastream-donation", "streamelements-donation"],
3767
+ cheer: ["kick-kicks"],
3768
+ raid: ["kick-host"],
3769
+ host: ["kick-host"],
3770
+ merch: ["fourthwall-shopOrder"],
3771
+ purchase: ["fourthwall-shopOrder"]
3769
3772
  },
3770
3773
  youtube: {
3771
- follower: "youtube-subscriber",
3772
- subscriber: "youtube-member",
3773
- tip: "streamelements-donation",
3774
- cheer: "youtube-superchat",
3775
- merch: "fourthwall-shopOrder",
3776
- purchase: "fourthwall-shopOrder"
3774
+ follower: ["youtube-subscriber"],
3775
+ subscriber: ["youtube-member"],
3776
+ tip: ["lumiastream-donation", "streamelements-donation"],
3777
+ cheer: ["youtube-superchat"],
3778
+ merch: ["fourthwall-shopOrder"],
3779
+ purchase: ["fourthwall-shopOrder"]
3777
3780
  }
3778
3781
  };
3779
3782
  var KAPPAGEN_KEYS = {
@@ -3782,52 +3785,93 @@ var KAPPAGEN_KEYS = {
3782
3785
  subscriber: "twitch-subscriber",
3783
3786
  cheer: "twitch-bits",
3784
3787
  raid: "twitch-raid",
3785
- tip: "streamelements-donation"
3788
+ tip: "lumiastream-donation"
3786
3789
  },
3787
3790
  kick: {
3788
3791
  follower: "kick-follower",
3789
3792
  subscriber: "kick-subscriber",
3790
3793
  cheer: "kick-kicks",
3791
3794
  raid: "kick-host",
3792
- tip: "streamelements-donation"
3795
+ tip: "lumiastream-donation"
3793
3796
  },
3794
3797
  youtube: {
3795
3798
  follower: "youtube-subscriber",
3796
3799
  subscriber: "youtube-member",
3797
3800
  cheer: "youtube-superchat",
3798
- tip: "streamelements-donation"
3801
+ tip: "lumiastream-donation"
3799
3802
  }
3800
3803
  };
3801
3804
  var STREAMBOSS_LISTENER_KEYS = {
3802
3805
  twitch: {
3803
- "follower-latest": "twitch-follower",
3804
- "subscriber-latest": "twitch-subscriber",
3805
- "tip-latest": "lumiastream-donation",
3806
- "cheer-latest": "twitch-bits",
3807
- "raid-latest": "twitch-raid"
3806
+ "follower-latest": ["twitch-follower"],
3807
+ "subscriber-latest": ["twitch-subscriber"],
3808
+ "tip-latest": ["lumiastream-donation", "streamelements-donation"],
3809
+ "cheer-latest": ["twitch-bits"],
3810
+ "raid-latest": ["twitch-raid"]
3808
3811
  },
3809
3812
  kick: {
3810
- "follower-latest": "kick-follower",
3811
- "subscriber-latest": "kick-subscriber",
3812
- "tip-latest": "lumiastream-donation",
3813
- "cheer-latest": "kick-kicks",
3814
- "raid-latest": "kick-host"
3813
+ "follower-latest": ["kick-follower"],
3814
+ "subscriber-latest": ["kick-subscriber"],
3815
+ "tip-latest": ["lumiastream-donation", "streamelements-donation"],
3816
+ "cheer-latest": ["kick-kicks"],
3817
+ "raid-latest": ["kick-host"]
3815
3818
  },
3816
3819
  youtube: {
3817
- "follower-latest": "youtube-subscriber",
3818
- "subscriber-latest": "youtube-member",
3819
- "tip-latest": "lumiastream-donation",
3820
- "cheer-latest": "youtube-superchat"
3820
+ "follower-latest": ["youtube-subscriber"],
3821
+ "subscriber-latest": ["youtube-member"],
3822
+ "tip-latest": ["lumiastream-donation", "streamelements-donation"],
3823
+ "cheer-latest": ["youtube-superchat"]
3821
3824
  }
3822
3825
  };
3823
- function alertBoxKeyFor(seEvent, provider = "twitch") {
3824
- return ALERT_BOX_KEYS[provider]?.[seEvent];
3826
+ var CURATED_ALERTS = {
3827
+ twitch: [
3828
+ "twitch-follower",
3829
+ "twitch-subscriber",
3830
+ "twitch-giftSubscription",
3831
+ "twitch-bits",
3832
+ "twitch-bitsCombo",
3833
+ "twitch-raid",
3834
+ "twitch-points",
3835
+ "twitch-redemption",
3836
+ "twitch-charityDonation",
3837
+ "twitch-hypetrainStarted",
3838
+ "twitch-clip",
3839
+ "twitch-shoutoutReceive",
3840
+ "twitch-watchStreak",
3841
+ "twitch-firstChatter",
3842
+ "twitch-extension"
3843
+ ],
3844
+ kick: [
3845
+ "kick-follower",
3846
+ "kick-subscriber",
3847
+ "kick-subscriptionGift",
3848
+ "kick-kicks",
3849
+ "kick-host",
3850
+ "kick-points",
3851
+ "kick-firstChatter"
3852
+ ],
3853
+ youtube: [
3854
+ "youtube-subscriber",
3855
+ "youtube-member",
3856
+ "youtube-giftMembers",
3857
+ "youtube-superchat",
3858
+ "youtube-supersticker",
3859
+ "youtube-gifts",
3860
+ "youtube-like",
3861
+ "youtube-firstChatter"
3862
+ ]
3863
+ };
3864
+ function curatedAlertsFor(provider = "twitch") {
3865
+ return CURATED_ALERTS[provider] ?? [];
3866
+ }
3867
+ function alertBoxKeysFor(seEvent, provider = "twitch") {
3868
+ return ALERT_BOX_KEYS[provider]?.[seEvent] ?? [];
3825
3869
  }
3826
3870
  function kappagenKeyFor(seEvent, provider = "twitch") {
3827
3871
  return KAPPAGEN_KEYS[provider]?.[seEvent];
3828
3872
  }
3829
- function streamBossListenerKeyFor(seListener, provider = "twitch") {
3830
- return STREAMBOSS_LISTENER_KEYS[provider]?.[seListener];
3873
+ function streamBossListenerKeysFor(seListener, provider = "twitch") {
3874
+ return STREAMBOSS_LISTENER_KEYS[provider]?.[seListener] ?? [];
3831
3875
  }
3832
3876
  function readBootstrapProvider(bootstrap) {
3833
3877
  const ch = bootstrap.channel;
@@ -4017,6 +4061,16 @@ function pickBaseAlertFields(event) {
4017
4061
  const { on: _on, variations: _variations, matchEmptyCondition: _mec, ...base } = event;
4018
4062
  return base;
4019
4063
  }
4064
+ function cloneEventForKey(template, lumiaKey) {
4065
+ const defaultMessage = LumiaAlertConfigs[lumiaKey]?.message ?? "{{username}}";
4066
+ const cloned = structuredClone(template);
4067
+ const c = cloned;
4068
+ if (c.themeSettings) c.themeSettings.firstMessageTemplate = defaultMessage;
4069
+ if (c.text?.content) c.text.content.value = defaultMessage;
4070
+ c.variations = [];
4071
+ c.on = true;
4072
+ return cloned;
4073
+ }
4020
4074
  function mapAlertBox(widget, opts = {}, ctx) {
4021
4075
  const v = widget.variables ?? {};
4022
4076
  const events = {};
@@ -4025,12 +4079,20 @@ function mapAlertBox(widget, opts = {}, ctx) {
4025
4079
  for (const seKey of eventKeys) {
4026
4080
  const ev = v[seKey];
4027
4081
  if (!ev || typeof ev !== "object") continue;
4028
- const lumiaAlert = alertBoxKeyFor(seKey, ctx?.provider);
4029
- if (!lumiaAlert) continue;
4082
+ const lumiaKeys = alertBoxKeysFor(seKey, ctx?.provider);
4083
+ if (lumiaKeys.length === 0) continue;
4030
4084
  const built = buildAlertEvent(ev, seKey);
4031
- events[lumiaAlert] = built;
4085
+ for (const lumiaKey of lumiaKeys) {
4086
+ events[lumiaKey] = built;
4087
+ }
4032
4088
  if (firstEvent == null) firstEvent = built;
4033
4089
  }
4090
+ if (!opts.onlyEvents && firstEvent && ctx?.provider) {
4091
+ for (const lumiaKey of curatedAlertsFor(ctx.provider)) {
4092
+ if (events[lumiaKey]) continue;
4093
+ events[lumiaKey] = cloneEventForKey(firstEvent, lumiaKey);
4094
+ }
4095
+ }
4034
4096
  const baseAlert = firstEvent ? pickBaseAlertFields(firstEvent) : {};
4035
4097
  return buildUnit(widget, "alert", { alert: { ...baseAlert, events } }, ctx);
4036
4098
  }
@@ -4484,20 +4546,28 @@ function mapChatbox(widget, ctx) {
4484
4546
  }
4485
4547
  var SE_LISTENER_TO_LUMIA_CATEGORY = {
4486
4548
  "follower-latest": "follower",
4549
+ "follower-recent": "follower",
4487
4550
  "subscriber-latest": "subscribers",
4488
4551
  "subscriber-new-latest": "subscribers",
4489
4552
  "subscriber-resub-latest": "subscribers",
4553
+ "subscriber-recent": "subscribers",
4490
4554
  "subscriber-gifted-latest": "gifts",
4491
4555
  "community-gift-latest": "gifts",
4492
4556
  "cheer-latest": "bits",
4557
+ "cheer-recent": "bits",
4493
4558
  "cheerPurchase-latest": "bits",
4494
4559
  "tip-latest": "donation",
4560
+ "tip-recent": "donation",
4495
4561
  "raid-latest": "raids",
4562
+ "raid-recent": "raids",
4496
4563
  "host-latest": "hosts",
4564
+ "host-recent": "hosts",
4497
4565
  "redemption-latest": "redemption",
4498
4566
  "merch-latest": "purchases",
4567
+ "merch-recent": "purchases",
4499
4568
  "purchase-latest": "purchases",
4500
4569
  "superchat-latest": "superchats",
4570
+ "superchat-recent": "superchats",
4501
4571
  "hypetrain-latest": "hypetrain"
4502
4572
  };
4503
4573
  var ALL_LUMIA_EVENTLIST_CATEGORIES = [
@@ -4522,8 +4592,7 @@ var ALL_LUMIA_EVENTLIST_CATEGORIES = [
4522
4592
  "stars",
4523
4593
  "fans",
4524
4594
  "shares",
4525
- "raffles",
4526
- "others"
4595
+ "raffles"
4527
4596
  ];
4528
4597
  function computeListenerFilters(seListeners, explicitListener) {
4529
4598
  const enabled = [];
@@ -4548,10 +4617,27 @@ function computeListenerFilters(seListeners, explicitListener) {
4548
4617
  if (allowed.size === 0) return [];
4549
4618
  return ALL_LUMIA_EVENTLIST_CATEGORIES.filter((c) => !allowed.has(c));
4550
4619
  }
4620
+ function seScrollSpeedToMarqueeDurationSec(seSpeed) {
4621
+ const n = typeof seSpeed === "number" && Number.isFinite(seSpeed) ? seSpeed : 6;
4622
+ const clamped = Math.min(20, Math.max(1, n));
4623
+ const duration = 60 / clamped;
4624
+ return Math.min(120, Math.max(5, Math.round(duration * 10) / 10));
4625
+ }
4626
+ function seTemplateToMarqueeTemplate(seTemplate) {
4627
+ if (typeof seTemplate !== "string" || !seTemplate.trim()) return void 0;
4628
+ return seTemplate.split("{name}").join("{username}").split("{username}").join("{username}");
4629
+ }
4551
4630
  function mapEventList(widget, ctx) {
4552
4631
  const v = widget.variables ?? {};
4553
4632
  const filters = computeListenerFilters(widget.listeners, widget.listener);
4554
4633
  const maxItems = v.visibleEvents ?? v.visibleItems ?? 5;
4634
+ const seText = widget.text;
4635
+ const scrolling = seText?.scrolling;
4636
+ const isMarquee = !!(scrolling && scrolling.enabled === true);
4637
+ const itemTemplate = isMarquee ? seTemplateToMarqueeTemplate(seText?.value) : void 0;
4638
+ const marqueeSpeed = isMarquee ? seScrollSpeedToMarqueeDurationSec(scrolling?.speed) : void 0;
4639
+ const marqueeDirection = isMarquee ? scrolling?.direction === "right" ? "right" : "left" : void 0;
4640
+ const theme = isMarquee ? "marquee" : v.theme ?? "simple";
4555
4641
  return buildUnit(
4556
4642
  widget,
4557
4643
  "eventlist",
@@ -4560,7 +4646,7 @@ function mapEventList(widget, ctx) {
4560
4646
  // Lumia eventlist reads font/size/color from module.css.
4561
4647
  css: mapSeTextCssToModuleCss(widget, { fontSize: 18 }),
4562
4648
  content: {
4563
- theme: v.theme ?? "simple",
4649
+ theme,
4564
4650
  direction: v.direction ?? "top",
4565
4651
  maxItemsToShow: maxItems,
4566
4652
  visibleEvents: maxItems,
@@ -4572,14 +4658,26 @@ function mapEventList(widget, ctx) {
4572
4658
  // `sites` is a platform blacklist (same shape). Default to "no platforms hidden".
4573
4659
  sites: [],
4574
4660
  // Eventlist Manager assumes these objects exist; provide an empty stub so
4575
- // imported overlays render without throwing.
4576
- themeConfig: {},
4661
+ // imported overlays render without throwing. The marquee theme reads
4662
+ // `itemTemplate` off either currentThemeConfig or themeConfig — stash
4663
+ // SE's per-item template here so the imported widget reproduces the
4664
+ // original (e.g. "{name} " → "{username} ").
4665
+ themeConfig: isMarquee && itemTemplate ? { itemTemplate } : {},
4577
4666
  eventListType: {},
4578
4667
  condensedText: false,
4579
- horizontal: false,
4580
- showAlertIcon: true,
4581
- showSiteIcon: true,
4582
- showUserAvatar: true,
4668
+ // `horizontal` keeps non-marquee widgets that had scrolling enabled in
4669
+ // SE laid out on a single row (eventlist already supports this); for
4670
+ // marquee, the CSS animation drives the scroll so this is informational.
4671
+ horizontal: isMarquee,
4672
+ ...isMarquee && marqueeSpeed !== void 0 ? { marqueeSpeed } : {},
4673
+ ...isMarquee && marqueeDirection ? { marqueeDirection } : {},
4674
+ // Inter-item spacing for the ticker. SE has no equivalent (their items
4675
+ // butt against each other) but a 24px gap matches the visual rhythm of
4676
+ // their default font sizes and keeps usernames from colliding.
4677
+ ...isMarquee ? { marqueeItemGap: 24 } : {},
4678
+ showAlertIcon: !isMarquee,
4679
+ showSiteIcon: !isMarquee,
4680
+ showUserAvatar: !isMarquee,
4583
4681
  showDetailedText: false,
4584
4682
  hideAlertMessage: false
4585
4683
  }
@@ -5629,13 +5727,15 @@ function mapStreamBoss(widget, ctx) {
5629
5727
  const selectedEvents = [];
5630
5728
  for (const seListener of SE_LISTENERS) {
5631
5729
  if (!listenerIsOn(widget.listeners, seListener)) continue;
5632
- const alertKey = streamBossListenerKeyFor(seListener, ctx?.provider);
5633
- if (!alertKey) continue;
5730
+ const alertKeys = streamBossListenerKeysFor(seListener, ctx?.provider);
5731
+ if (alertKeys.length === 0) continue;
5634
5732
  const raw = seDamage[seListener];
5635
5733
  const value = typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : null;
5636
5734
  if (value === null) continue;
5637
- damageMap[alertKey] = { value, mode: SE_LISTENER_MODE[seListener] };
5638
- selectedEvents.push(alertKey);
5735
+ for (const alertKey of alertKeys) {
5736
+ damageMap[alertKey] = { value, mode: SE_LISTENER_MODE[seListener] };
5737
+ selectedEvents.push(alertKey);
5738
+ }
5639
5739
  }
5640
5740
  const barColor = typeof v.colors?.frame === "string" && v.colors.frame ? v.colors.frame : "#2bff00";
5641
5741
  const theme = v.showImage === false ? "bar-only" : "card";
@@ -5772,23 +5872,6 @@ var GOAL_TYPES = /* @__PURE__ */ new Set([
5772
5872
  "se-widget-merch-goal"
5773
5873
  ]);
5774
5874
  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"]);
5775
- var SCROLLING_RECENT_TYPES = /* @__PURE__ */ new Set([
5776
- "se-widget-follower-recent",
5777
- "se-widget-subscriber-recent",
5778
- "se-widget-cheer-recent",
5779
- "se-widget-tip-recent"
5780
- ]);
5781
- var RECENT_TO_LATEST_FALLBACK_VAR = {
5782
- "se-widget-follower-recent": "twitch_last_follower",
5783
- "se-widget-subscriber-recent": "twitch_last_subscriber",
5784
- "se-widget-cheer-recent": "twitch_last_bit",
5785
- "se-widget-tip-recent": "latest_donator"
5786
- };
5787
- function isScrollingRecentWidget(widget) {
5788
- if (!SCROLLING_RECENT_TYPES.has(widget.type)) return false;
5789
- const scrolling = widget.text?.scrolling;
5790
- return !!(scrolling && typeof scrolling === "object" && scrolling.enabled === true);
5791
- }
5792
5875
  function dispatch(widget, ctx) {
5793
5876
  const t = widget.type;
5794
5877
  console.log("[SE-IMPORT] dispatch widget", {
@@ -5817,10 +5900,6 @@ function dispatch(widget, ctx) {
5817
5900
  if (t === "se-widget-donor-drive-alert") return { unit: mapSingleAlert(widget, "donordrive-donation", ctx), status: "direct", lumiaType: "alert" };
5818
5901
  if (t === "se-widget-extralife-alert") return { unit: mapSingleAlert(widget, "extralife-donation", ctx), status: "direct", lumiaType: "alert" };
5819
5902
  if (t === "se-widget-tiltify-alert") return { unit: mapSingleAlert(widget, "tiltify-campaignDonation", ctx), status: "direct", lumiaType: "alert" };
5820
- if (isScrollingRecentWidget(widget)) {
5821
- const fallbackVar = RECENT_TO_LATEST_FALLBACK_VAR[t] ?? "username";
5822
- return { unit: mapReadout(widget, fallbackVar, ctx), status: "direct", lumiaType: "text" };
5823
- }
5824
5903
  if (EVENTLIST_TYPES.has(t)) return { unit: mapEventList(widget, ctx), status: "direct", lumiaType: "eventlist" };
5825
5904
  if (t === "se-widget-top-tippers-list") return { unit: mapViewerProfilesLeaderboard(widget, "tips", ctx), status: "direct", lumiaType: "viewerprofiles" };
5826
5905
  if (t === "se-widget-top-cheerers-list") return { unit: mapViewerProfilesLeaderboard(widget, "cheers", ctx), status: "direct", lumiaType: "viewerprofiles" };
package/dist/se-import.js CHANGED
@@ -630,35 +630,38 @@ function mapReadout(widget, fallbackVar, ctx) {
630
630
  );
631
631
  }
632
632
 
633
+ // src/se-import/mappers/alert.ts
634
+ import { LumiaAlertConfigs } from "@lumiastream/lumia-types";
635
+
633
636
  // src/se-import/mappers/provider-alerts.ts
634
637
  var ALERT_BOX_KEYS = {
635
638
  twitch: {
636
- follower: "twitch-follower",
637
- subscriber: "twitch-subscriber",
638
- tip: "streamelements-donation",
639
- cheer: "twitch-bits",
640
- raid: "twitch-raid",
641
- merch: "fourthwall-shopOrder",
642
- purchase: "fourthwall-shopOrder",
643
- charityCampaignDonation: "twitch-charityDonation"
639
+ follower: ["twitch-follower"],
640
+ subscriber: ["twitch-subscriber"],
641
+ tip: ["lumiastream-donation", "streamelements-donation"],
642
+ cheer: ["twitch-bits"],
643
+ raid: ["twitch-raid"],
644
+ merch: ["fourthwall-shopOrder"],
645
+ purchase: ["fourthwall-shopOrder"],
646
+ charityCampaignDonation: ["twitch-charityDonation"]
644
647
  },
645
648
  kick: {
646
- follower: "kick-follower",
647
- subscriber: "kick-subscriber",
648
- tip: "streamelements-donation",
649
- cheer: "kick-kicks",
650
- raid: "kick-host",
651
- host: "kick-host",
652
- merch: "fourthwall-shopOrder",
653
- purchase: "fourthwall-shopOrder"
649
+ follower: ["kick-follower"],
650
+ subscriber: ["kick-subscriber"],
651
+ tip: ["lumiastream-donation", "streamelements-donation"],
652
+ cheer: ["kick-kicks"],
653
+ raid: ["kick-host"],
654
+ host: ["kick-host"],
655
+ merch: ["fourthwall-shopOrder"],
656
+ purchase: ["fourthwall-shopOrder"]
654
657
  },
655
658
  youtube: {
656
- follower: "youtube-subscriber",
657
- subscriber: "youtube-member",
658
- tip: "streamelements-donation",
659
- cheer: "youtube-superchat",
660
- merch: "fourthwall-shopOrder",
661
- purchase: "fourthwall-shopOrder"
659
+ follower: ["youtube-subscriber"],
660
+ subscriber: ["youtube-member"],
661
+ tip: ["lumiastream-donation", "streamelements-donation"],
662
+ cheer: ["youtube-superchat"],
663
+ merch: ["fourthwall-shopOrder"],
664
+ purchase: ["fourthwall-shopOrder"]
662
665
  }
663
666
  };
664
667
  var KAPPAGEN_KEYS = {
@@ -667,52 +670,93 @@ var KAPPAGEN_KEYS = {
667
670
  subscriber: "twitch-subscriber",
668
671
  cheer: "twitch-bits",
669
672
  raid: "twitch-raid",
670
- tip: "streamelements-donation"
673
+ tip: "lumiastream-donation"
671
674
  },
672
675
  kick: {
673
676
  follower: "kick-follower",
674
677
  subscriber: "kick-subscriber",
675
678
  cheer: "kick-kicks",
676
679
  raid: "kick-host",
677
- tip: "streamelements-donation"
680
+ tip: "lumiastream-donation"
678
681
  },
679
682
  youtube: {
680
683
  follower: "youtube-subscriber",
681
684
  subscriber: "youtube-member",
682
685
  cheer: "youtube-superchat",
683
- tip: "streamelements-donation"
686
+ tip: "lumiastream-donation"
684
687
  }
685
688
  };
686
689
  var STREAMBOSS_LISTENER_KEYS = {
687
690
  twitch: {
688
- "follower-latest": "twitch-follower",
689
- "subscriber-latest": "twitch-subscriber",
690
- "tip-latest": "lumiastream-donation",
691
- "cheer-latest": "twitch-bits",
692
- "raid-latest": "twitch-raid"
691
+ "follower-latest": ["twitch-follower"],
692
+ "subscriber-latest": ["twitch-subscriber"],
693
+ "tip-latest": ["lumiastream-donation", "streamelements-donation"],
694
+ "cheer-latest": ["twitch-bits"],
695
+ "raid-latest": ["twitch-raid"]
693
696
  },
694
697
  kick: {
695
- "follower-latest": "kick-follower",
696
- "subscriber-latest": "kick-subscriber",
697
- "tip-latest": "lumiastream-donation",
698
- "cheer-latest": "kick-kicks",
699
- "raid-latest": "kick-host"
698
+ "follower-latest": ["kick-follower"],
699
+ "subscriber-latest": ["kick-subscriber"],
700
+ "tip-latest": ["lumiastream-donation", "streamelements-donation"],
701
+ "cheer-latest": ["kick-kicks"],
702
+ "raid-latest": ["kick-host"]
700
703
  },
701
704
  youtube: {
702
- "follower-latest": "youtube-subscriber",
703
- "subscriber-latest": "youtube-member",
704
- "tip-latest": "lumiastream-donation",
705
- "cheer-latest": "youtube-superchat"
705
+ "follower-latest": ["youtube-subscriber"],
706
+ "subscriber-latest": ["youtube-member"],
707
+ "tip-latest": ["lumiastream-donation", "streamelements-donation"],
708
+ "cheer-latest": ["youtube-superchat"]
706
709
  }
707
710
  };
708
- function alertBoxKeyFor(seEvent, provider = "twitch") {
709
- return ALERT_BOX_KEYS[provider]?.[seEvent];
711
+ var CURATED_ALERTS = {
712
+ twitch: [
713
+ "twitch-follower",
714
+ "twitch-subscriber",
715
+ "twitch-giftSubscription",
716
+ "twitch-bits",
717
+ "twitch-bitsCombo",
718
+ "twitch-raid",
719
+ "twitch-points",
720
+ "twitch-redemption",
721
+ "twitch-charityDonation",
722
+ "twitch-hypetrainStarted",
723
+ "twitch-clip",
724
+ "twitch-shoutoutReceive",
725
+ "twitch-watchStreak",
726
+ "twitch-firstChatter",
727
+ "twitch-extension"
728
+ ],
729
+ kick: [
730
+ "kick-follower",
731
+ "kick-subscriber",
732
+ "kick-subscriptionGift",
733
+ "kick-kicks",
734
+ "kick-host",
735
+ "kick-points",
736
+ "kick-firstChatter"
737
+ ],
738
+ youtube: [
739
+ "youtube-subscriber",
740
+ "youtube-member",
741
+ "youtube-giftMembers",
742
+ "youtube-superchat",
743
+ "youtube-supersticker",
744
+ "youtube-gifts",
745
+ "youtube-like",
746
+ "youtube-firstChatter"
747
+ ]
748
+ };
749
+ function curatedAlertsFor(provider = "twitch") {
750
+ return CURATED_ALERTS[provider] ?? [];
751
+ }
752
+ function alertBoxKeysFor(seEvent, provider = "twitch") {
753
+ return ALERT_BOX_KEYS[provider]?.[seEvent] ?? [];
710
754
  }
711
755
  function kappagenKeyFor(seEvent, provider = "twitch") {
712
756
  return KAPPAGEN_KEYS[provider]?.[seEvent];
713
757
  }
714
- function streamBossListenerKeyFor(seListener, provider = "twitch") {
715
- return STREAMBOSS_LISTENER_KEYS[provider]?.[seListener];
758
+ function streamBossListenerKeysFor(seListener, provider = "twitch") {
759
+ return STREAMBOSS_LISTENER_KEYS[provider]?.[seListener] ?? [];
716
760
  }
717
761
  function readBootstrapProvider(bootstrap) {
718
762
  const ch = bootstrap.channel;
@@ -902,6 +946,16 @@ function pickBaseAlertFields(event) {
902
946
  const { on: _on, variations: _variations, matchEmptyCondition: _mec, ...base } = event;
903
947
  return base;
904
948
  }
949
+ function cloneEventForKey(template, lumiaKey) {
950
+ const defaultMessage = LumiaAlertConfigs[lumiaKey]?.message ?? "{{username}}";
951
+ const cloned = structuredClone(template);
952
+ const c = cloned;
953
+ if (c.themeSettings) c.themeSettings.firstMessageTemplate = defaultMessage;
954
+ if (c.text?.content) c.text.content.value = defaultMessage;
955
+ c.variations = [];
956
+ c.on = true;
957
+ return cloned;
958
+ }
905
959
  function mapAlertBox(widget, opts = {}, ctx) {
906
960
  const v = widget.variables ?? {};
907
961
  const events = {};
@@ -910,12 +964,20 @@ function mapAlertBox(widget, opts = {}, ctx) {
910
964
  for (const seKey of eventKeys) {
911
965
  const ev = v[seKey];
912
966
  if (!ev || typeof ev !== "object") continue;
913
- const lumiaAlert = alertBoxKeyFor(seKey, ctx?.provider);
914
- if (!lumiaAlert) continue;
967
+ const lumiaKeys = alertBoxKeysFor(seKey, ctx?.provider);
968
+ if (lumiaKeys.length === 0) continue;
915
969
  const built = buildAlertEvent(ev, seKey);
916
- events[lumiaAlert] = built;
970
+ for (const lumiaKey of lumiaKeys) {
971
+ events[lumiaKey] = built;
972
+ }
917
973
  if (firstEvent == null) firstEvent = built;
918
974
  }
975
+ if (!opts.onlyEvents && firstEvent && ctx?.provider) {
976
+ for (const lumiaKey of curatedAlertsFor(ctx.provider)) {
977
+ if (events[lumiaKey]) continue;
978
+ events[lumiaKey] = cloneEventForKey(firstEvent, lumiaKey);
979
+ }
980
+ }
919
981
  const baseAlert = firstEvent ? pickBaseAlertFields(firstEvent) : {};
920
982
  return buildUnit(widget, "alert", { alert: { ...baseAlert, events } }, ctx);
921
983
  }
@@ -1369,20 +1431,28 @@ function mapChatbox(widget, ctx) {
1369
1431
  }
1370
1432
  var SE_LISTENER_TO_LUMIA_CATEGORY = {
1371
1433
  "follower-latest": "follower",
1434
+ "follower-recent": "follower",
1372
1435
  "subscriber-latest": "subscribers",
1373
1436
  "subscriber-new-latest": "subscribers",
1374
1437
  "subscriber-resub-latest": "subscribers",
1438
+ "subscriber-recent": "subscribers",
1375
1439
  "subscriber-gifted-latest": "gifts",
1376
1440
  "community-gift-latest": "gifts",
1377
1441
  "cheer-latest": "bits",
1442
+ "cheer-recent": "bits",
1378
1443
  "cheerPurchase-latest": "bits",
1379
1444
  "tip-latest": "donation",
1445
+ "tip-recent": "donation",
1380
1446
  "raid-latest": "raids",
1447
+ "raid-recent": "raids",
1381
1448
  "host-latest": "hosts",
1449
+ "host-recent": "hosts",
1382
1450
  "redemption-latest": "redemption",
1383
1451
  "merch-latest": "purchases",
1452
+ "merch-recent": "purchases",
1384
1453
  "purchase-latest": "purchases",
1385
1454
  "superchat-latest": "superchats",
1455
+ "superchat-recent": "superchats",
1386
1456
  "hypetrain-latest": "hypetrain"
1387
1457
  };
1388
1458
  var ALL_LUMIA_EVENTLIST_CATEGORIES = [
@@ -1407,8 +1477,7 @@ var ALL_LUMIA_EVENTLIST_CATEGORIES = [
1407
1477
  "stars",
1408
1478
  "fans",
1409
1479
  "shares",
1410
- "raffles",
1411
- "others"
1480
+ "raffles"
1412
1481
  ];
1413
1482
  function computeListenerFilters(seListeners, explicitListener) {
1414
1483
  const enabled = [];
@@ -1433,10 +1502,27 @@ function computeListenerFilters(seListeners, explicitListener) {
1433
1502
  if (allowed.size === 0) return [];
1434
1503
  return ALL_LUMIA_EVENTLIST_CATEGORIES.filter((c) => !allowed.has(c));
1435
1504
  }
1505
+ function seScrollSpeedToMarqueeDurationSec(seSpeed) {
1506
+ const n = typeof seSpeed === "number" && Number.isFinite(seSpeed) ? seSpeed : 6;
1507
+ const clamped = Math.min(20, Math.max(1, n));
1508
+ const duration = 60 / clamped;
1509
+ return Math.min(120, Math.max(5, Math.round(duration * 10) / 10));
1510
+ }
1511
+ function seTemplateToMarqueeTemplate(seTemplate) {
1512
+ if (typeof seTemplate !== "string" || !seTemplate.trim()) return void 0;
1513
+ return seTemplate.split("{name}").join("{username}").split("{username}").join("{username}");
1514
+ }
1436
1515
  function mapEventList(widget, ctx) {
1437
1516
  const v = widget.variables ?? {};
1438
1517
  const filters = computeListenerFilters(widget.listeners, widget.listener);
1439
1518
  const maxItems = v.visibleEvents ?? v.visibleItems ?? 5;
1519
+ const seText = widget.text;
1520
+ const scrolling = seText?.scrolling;
1521
+ const isMarquee = !!(scrolling && scrolling.enabled === true);
1522
+ const itemTemplate = isMarquee ? seTemplateToMarqueeTemplate(seText?.value) : void 0;
1523
+ const marqueeSpeed = isMarquee ? seScrollSpeedToMarqueeDurationSec(scrolling?.speed) : void 0;
1524
+ const marqueeDirection = isMarquee ? scrolling?.direction === "right" ? "right" : "left" : void 0;
1525
+ const theme = isMarquee ? "marquee" : v.theme ?? "simple";
1440
1526
  return buildUnit(
1441
1527
  widget,
1442
1528
  "eventlist",
@@ -1445,7 +1531,7 @@ function mapEventList(widget, ctx) {
1445
1531
  // Lumia eventlist reads font/size/color from module.css.
1446
1532
  css: mapSeTextCssToModuleCss(widget, { fontSize: 18 }),
1447
1533
  content: {
1448
- theme: v.theme ?? "simple",
1534
+ theme,
1449
1535
  direction: v.direction ?? "top",
1450
1536
  maxItemsToShow: maxItems,
1451
1537
  visibleEvents: maxItems,
@@ -1457,14 +1543,26 @@ function mapEventList(widget, ctx) {
1457
1543
  // `sites` is a platform blacklist (same shape). Default to "no platforms hidden".
1458
1544
  sites: [],
1459
1545
  // Eventlist Manager assumes these objects exist; provide an empty stub so
1460
- // imported overlays render without throwing.
1461
- themeConfig: {},
1546
+ // imported overlays render without throwing. The marquee theme reads
1547
+ // `itemTemplate` off either currentThemeConfig or themeConfig — stash
1548
+ // SE's per-item template here so the imported widget reproduces the
1549
+ // original (e.g. "{name} " → "{username} ").
1550
+ themeConfig: isMarquee && itemTemplate ? { itemTemplate } : {},
1462
1551
  eventListType: {},
1463
1552
  condensedText: false,
1464
- horizontal: false,
1465
- showAlertIcon: true,
1466
- showSiteIcon: true,
1467
- showUserAvatar: true,
1553
+ // `horizontal` keeps non-marquee widgets that had scrolling enabled in
1554
+ // SE laid out on a single row (eventlist already supports this); for
1555
+ // marquee, the CSS animation drives the scroll so this is informational.
1556
+ horizontal: isMarquee,
1557
+ ...isMarquee && marqueeSpeed !== void 0 ? { marqueeSpeed } : {},
1558
+ ...isMarquee && marqueeDirection ? { marqueeDirection } : {},
1559
+ // Inter-item spacing for the ticker. SE has no equivalent (their items
1560
+ // butt against each other) but a 24px gap matches the visual rhythm of
1561
+ // their default font sizes and keeps usernames from colliding.
1562
+ ...isMarquee ? { marqueeItemGap: 24 } : {},
1563
+ showAlertIcon: !isMarquee,
1564
+ showSiteIcon: !isMarquee,
1565
+ showUserAvatar: !isMarquee,
1468
1566
  showDetailedText: false,
1469
1567
  hideAlertMessage: false
1470
1568
  }
@@ -2514,13 +2612,15 @@ function mapStreamBoss(widget, ctx) {
2514
2612
  const selectedEvents = [];
2515
2613
  for (const seListener of SE_LISTENERS) {
2516
2614
  if (!listenerIsOn(widget.listeners, seListener)) continue;
2517
- const alertKey = streamBossListenerKeyFor(seListener, ctx?.provider);
2518
- if (!alertKey) continue;
2615
+ const alertKeys = streamBossListenerKeysFor(seListener, ctx?.provider);
2616
+ if (alertKeys.length === 0) continue;
2519
2617
  const raw = seDamage[seListener];
2520
2618
  const value = typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : null;
2521
2619
  if (value === null) continue;
2522
- damageMap[alertKey] = { value, mode: SE_LISTENER_MODE[seListener] };
2523
- selectedEvents.push(alertKey);
2620
+ for (const alertKey of alertKeys) {
2621
+ damageMap[alertKey] = { value, mode: SE_LISTENER_MODE[seListener] };
2622
+ selectedEvents.push(alertKey);
2623
+ }
2524
2624
  }
2525
2625
  const barColor = typeof v.colors?.frame === "string" && v.colors.frame ? v.colors.frame : "#2bff00";
2526
2626
  const theme = v.showImage === false ? "bar-only" : "card";
@@ -2657,23 +2757,6 @@ var GOAL_TYPES = /* @__PURE__ */ new Set([
2657
2757
  "se-widget-merch-goal"
2658
2758
  ]);
2659
2759
  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"]);
2660
- var SCROLLING_RECENT_TYPES = /* @__PURE__ */ new Set([
2661
- "se-widget-follower-recent",
2662
- "se-widget-subscriber-recent",
2663
- "se-widget-cheer-recent",
2664
- "se-widget-tip-recent"
2665
- ]);
2666
- var RECENT_TO_LATEST_FALLBACK_VAR = {
2667
- "se-widget-follower-recent": "twitch_last_follower",
2668
- "se-widget-subscriber-recent": "twitch_last_subscriber",
2669
- "se-widget-cheer-recent": "twitch_last_bit",
2670
- "se-widget-tip-recent": "latest_donator"
2671
- };
2672
- function isScrollingRecentWidget(widget) {
2673
- if (!SCROLLING_RECENT_TYPES.has(widget.type)) return false;
2674
- const scrolling = widget.text?.scrolling;
2675
- return !!(scrolling && typeof scrolling === "object" && scrolling.enabled === true);
2676
- }
2677
2760
  function dispatch(widget, ctx) {
2678
2761
  const t = widget.type;
2679
2762
  console.log("[SE-IMPORT] dispatch widget", {
@@ -2702,10 +2785,6 @@ function dispatch(widget, ctx) {
2702
2785
  if (t === "se-widget-donor-drive-alert") return { unit: mapSingleAlert(widget, "donordrive-donation", ctx), status: "direct", lumiaType: "alert" };
2703
2786
  if (t === "se-widget-extralife-alert") return { unit: mapSingleAlert(widget, "extralife-donation", ctx), status: "direct", lumiaType: "alert" };
2704
2787
  if (t === "se-widget-tiltify-alert") return { unit: mapSingleAlert(widget, "tiltify-campaignDonation", ctx), status: "direct", lumiaType: "alert" };
2705
- if (isScrollingRecentWidget(widget)) {
2706
- const fallbackVar = RECENT_TO_LATEST_FALLBACK_VAR[t] ?? "username";
2707
- return { unit: mapReadout(widget, fallbackVar, ctx), status: "direct", lumiaType: "text" };
2708
- }
2709
2788
  if (EVENTLIST_TYPES.has(t)) return { unit: mapEventList(widget, ctx), status: "direct", lumiaType: "eventlist" };
2710
2789
  if (t === "se-widget-top-tippers-list") return { unit: mapViewerProfilesLeaderboard(widget, "tips", ctx), status: "direct", lumiaType: "viewerprofiles" };
2711
2790
  if (t === "se-widget-top-cheerers-list") return { unit: mapViewerProfilesLeaderboard(widget, "cheers", ctx), status: "direct", lumiaType: "viewerprofiles" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumiastream/ui",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "author": "Lumia Stream",
5
5
  "license": "ISC",
6
6
  "description": "Lumia UI Kit",