@lumiastream/ui 0.2.9 → 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
@@ -3129,7 +3129,13 @@ var SE_TO_LUMIA_PLACEHOLDER = {
3129
3129
  item: "firstItem.name",
3130
3130
  count: "amount",
3131
3131
  total: "total",
3132
- gifts: "giftAmount"
3132
+ gifts: "giftAmount",
3133
+ // Platform-agnostic streamer identity — SE exposes `{streamer}` in static
3134
+ // widgets and alert bodies. Lumia has no event-scoped equivalent (it's not
3135
+ // tied to an alert payload), so we route it at the *global* template
3136
+ // variable `{{streamer}}` populated by VariablesManager.refreshStreamerVariable
3137
+ // (Twitch → Kick → YouTube → Facebook → TikTok fallback chain).
3138
+ streamer: "streamer"
3133
3139
  };
3134
3140
  var TWITCH_CONTEXTS = {
3135
3141
  "follower-latest": { name: "twitch_last_follower", username: "twitch_last_follower" },
@@ -3739,35 +3745,38 @@ function mapReadout(widget, fallbackVar, ctx) {
3739
3745
  );
3740
3746
  }
3741
3747
 
3748
+ // src/se-import/mappers/alert.ts
3749
+ import { LumiaAlertConfigs } from "@lumiastream/lumia-types";
3750
+
3742
3751
  // src/se-import/mappers/provider-alerts.ts
3743
3752
  var ALERT_BOX_KEYS = {
3744
3753
  twitch: {
3745
- follower: "twitch-follower",
3746
- subscriber: "twitch-subscriber",
3747
- tip: "streamelements-donation",
3748
- cheer: "twitch-bits",
3749
- raid: "twitch-raid",
3750
- merch: "fourthwall-shopOrder",
3751
- purchase: "fourthwall-shopOrder",
3752
- 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"]
3753
3762
  },
3754
3763
  kick: {
3755
- follower: "kick-follower",
3756
- subscriber: "kick-subscriber",
3757
- tip: "streamelements-donation",
3758
- cheer: "kick-kicks",
3759
- raid: "kick-host",
3760
- host: "kick-host",
3761
- merch: "fourthwall-shopOrder",
3762
- 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"]
3763
3772
  },
3764
3773
  youtube: {
3765
- follower: "youtube-subscriber",
3766
- subscriber: "youtube-member",
3767
- tip: "streamelements-donation",
3768
- cheer: "youtube-superchat",
3769
- merch: "fourthwall-shopOrder",
3770
- 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"]
3771
3780
  }
3772
3781
  };
3773
3782
  var KAPPAGEN_KEYS = {
@@ -3776,52 +3785,93 @@ var KAPPAGEN_KEYS = {
3776
3785
  subscriber: "twitch-subscriber",
3777
3786
  cheer: "twitch-bits",
3778
3787
  raid: "twitch-raid",
3779
- tip: "streamelements-donation"
3788
+ tip: "lumiastream-donation"
3780
3789
  },
3781
3790
  kick: {
3782
3791
  follower: "kick-follower",
3783
3792
  subscriber: "kick-subscriber",
3784
3793
  cheer: "kick-kicks",
3785
3794
  raid: "kick-host",
3786
- tip: "streamelements-donation"
3795
+ tip: "lumiastream-donation"
3787
3796
  },
3788
3797
  youtube: {
3789
3798
  follower: "youtube-subscriber",
3790
3799
  subscriber: "youtube-member",
3791
3800
  cheer: "youtube-superchat",
3792
- tip: "streamelements-donation"
3801
+ tip: "lumiastream-donation"
3793
3802
  }
3794
3803
  };
3795
3804
  var STREAMBOSS_LISTENER_KEYS = {
3796
3805
  twitch: {
3797
- "follower-latest": "twitch-follower",
3798
- "subscriber-latest": "twitch-subscriber",
3799
- "tip-latest": "lumiastream-donation",
3800
- "cheer-latest": "twitch-bits",
3801
- "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"]
3802
3811
  },
3803
3812
  kick: {
3804
- "follower-latest": "kick-follower",
3805
- "subscriber-latest": "kick-subscriber",
3806
- "tip-latest": "lumiastream-donation",
3807
- "cheer-latest": "kick-kicks",
3808
- "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"]
3809
3818
  },
3810
3819
  youtube: {
3811
- "follower-latest": "youtube-subscriber",
3812
- "subscriber-latest": "youtube-member",
3813
- "tip-latest": "lumiastream-donation",
3814
- "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"]
3815
3824
  }
3816
3825
  };
3817
- function alertBoxKeyFor(seEvent, provider = "twitch") {
3818
- 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] ?? [];
3819
3869
  }
3820
3870
  function kappagenKeyFor(seEvent, provider = "twitch") {
3821
3871
  return KAPPAGEN_KEYS[provider]?.[seEvent];
3822
3872
  }
3823
- function streamBossListenerKeyFor(seListener, provider = "twitch") {
3824
- return STREAMBOSS_LISTENER_KEYS[provider]?.[seListener];
3873
+ function streamBossListenerKeysFor(seListener, provider = "twitch") {
3874
+ return STREAMBOSS_LISTENER_KEYS[provider]?.[seListener] ?? [];
3825
3875
  }
3826
3876
  function readBootstrapProvider(bootstrap) {
3827
3877
  const ch = bootstrap.channel;
@@ -4011,6 +4061,16 @@ function pickBaseAlertFields(event) {
4011
4061
  const { on: _on, variations: _variations, matchEmptyCondition: _mec, ...base } = event;
4012
4062
  return base;
4013
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
+ }
4014
4074
  function mapAlertBox(widget, opts = {}, ctx) {
4015
4075
  const v = widget.variables ?? {};
4016
4076
  const events = {};
@@ -4019,12 +4079,20 @@ function mapAlertBox(widget, opts = {}, ctx) {
4019
4079
  for (const seKey of eventKeys) {
4020
4080
  const ev = v[seKey];
4021
4081
  if (!ev || typeof ev !== "object") continue;
4022
- const lumiaAlert = alertBoxKeyFor(seKey, ctx?.provider);
4023
- if (!lumiaAlert) continue;
4082
+ const lumiaKeys = alertBoxKeysFor(seKey, ctx?.provider);
4083
+ if (lumiaKeys.length === 0) continue;
4024
4084
  const built = buildAlertEvent(ev, seKey);
4025
- events[lumiaAlert] = built;
4085
+ for (const lumiaKey of lumiaKeys) {
4086
+ events[lumiaKey] = built;
4087
+ }
4026
4088
  if (firstEvent == null) firstEvent = built;
4027
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
+ }
4028
4096
  const baseAlert = firstEvent ? pickBaseAlertFields(firstEvent) : {};
4029
4097
  return buildUnit(widget, "alert", { alert: { ...baseAlert, events } }, ctx);
4030
4098
  }
@@ -4252,6 +4320,63 @@ import {
4252
4320
  LumiaEventListTypes,
4253
4321
  LumiaMapAlertTypeToEventListType
4254
4322
  } from "@lumiastream/lumia-types";
4323
+
4324
+ // src/se-import/mappers/module-defaults.ts
4325
+ var CHATBOX_DEFAULT_CONTENT_FIELDS = {
4326
+ type: "lumia",
4327
+ src: "",
4328
+ userLevels: [1, 2, 3, 4, 5, 6, 7, 8, 9],
4329
+ permanent: true,
4330
+ maxItemsToShow: 20,
4331
+ removeAfter: 3e4,
4332
+ animations: {
4333
+ enterAnimation: "fadeIn",
4334
+ exitAnimation: "fadeOut",
4335
+ enterAnimationDuration: 1e3,
4336
+ exitAnimationDuration: 1e3,
4337
+ enterAnimationDelay: 0,
4338
+ exitAnimationDelay: 0
4339
+ },
4340
+ ignoredList: [],
4341
+ background: "transparent",
4342
+ theme: "simple",
4343
+ chatboxStreamingSite: {
4344
+ twitch: { message: "", icon: "", themeConfig: { primaryColor: "#915dff", showUsernameColors: true } },
4345
+ youtube: { message: "", icon: "", themeConfig: { primaryColor: "#ff0000", usePlatformColor: false } },
4346
+ facebook: { message: "", icon: "", themeConfig: { primaryColor: "#4267B2", usePlatformColor: false } },
4347
+ tiktok: { message: "", icon: "", themeConfig: { primaryColor: "#ff0050", usePlatformColor: false } },
4348
+ kick: { message: "", icon: "", themeConfig: { primaryColor: "#3AD305", usePlatformColor: false } },
4349
+ discord: { message: "", icon: "", themeConfig: { primaryColor: "#7289da", usePlatformColor: false } }
4350
+ },
4351
+ borderRadius: "10px",
4352
+ bgOpacity: 0.4,
4353
+ ignoredStreamingSites: [],
4354
+ horizontal: false,
4355
+ reverseFlow: false,
4356
+ showBadges: true,
4357
+ showSiteIcon: false,
4358
+ showEmotes: true,
4359
+ showAvatar: false,
4360
+ showTimestamp: false,
4361
+ hideAlerts: false,
4362
+ hyperClickableLinks: true,
4363
+ clickableChatterProfiles: false,
4364
+ previewMediaInChat: true,
4365
+ previewMediaUserLevels: ["streamer", "moderators", "vips", "tier3", "tier2", "subscribers", "regular", "follower", "anyone"],
4366
+ breakLine: true,
4367
+ fadeOutAfterDelay: false,
4368
+ fadeOutDelayTime: 5,
4369
+ useAnimatedEmotes: true,
4370
+ sites: ["twitch", "kick", "youtube", "facebook", "discord", "ffz", "seventv", "bttv"],
4371
+ border: "solid 1px transparent",
4372
+ itemGap: 8
4373
+ };
4374
+ var CHATBOX_DEFAULT_MODULE = {
4375
+ version: 7,
4376
+ content: CHATBOX_DEFAULT_CONTENT_FIELDS
4377
+ };
4378
+
4379
+ // src/se-import/mappers/misc.ts
4255
4380
  function mapChatboxTheme(seTheme) {
4256
4381
  const t = (seTheme ?? "").toString().toLowerCase();
4257
4382
  if (!t || t === "default") return "simple";
@@ -4311,6 +4436,8 @@ function paddingShorthand(message) {
4311
4436
  return `${px(t)} ${px(r)} ${px(b)} ${px(l)}`;
4312
4437
  }
4313
4438
  function mapChatbox(widget, ctx) {
4439
+ const chatboxDefaults = CHATBOX_DEFAULT_MODULE.content;
4440
+ const chatboxVersion = CHATBOX_DEFAULT_MODULE.version;
4314
4441
  const v = widget.variables ?? {};
4315
4442
  const textCss = widget.text?.css ?? {};
4316
4443
  const messageCss = textCss.message ?? {};
@@ -4345,59 +4472,58 @@ function mapChatbox(widget, ctx) {
4345
4472
  widget,
4346
4473
  "chatbox",
4347
4474
  {
4475
+ // Stamp the module version from the authoritative defaults so
4476
+ // Overlay-UI's per-module Settings.tsx migration check
4477
+ // (`module?.version !== currentVersion`) treats this import as
4478
+ // already-current. Without this, every freshly-imported chatbox
4479
+ // would trip the migration toast on first open even though the
4480
+ // content is already complete.
4481
+ version: chatboxVersion,
4348
4482
  // Visual styling (font / color / padding etc) lives on module.css, not
4349
4483
  // content. Lumia's TextStyle settings panel + the chatbox renderer both
4350
4484
  // read from here.
4351
4485
  css: moduleCss,
4352
4486
  content: {
4487
+ // `chatboxDefaults` resolves host-injected first (preferred), then
4488
+ // the local snapshot — see the top of this function. Spreading it
4489
+ // here guarantees every field the chatbox renderer expects is
4490
+ // present (`type`, `sites`, `useAnimatedEmotes`, etc.); the
4491
+ // SE-specific override block below wins for the opinions that
4492
+ // differ from a fresh-add.
4493
+ ...chatboxDefaults,
4494
+ // ─── SE-specific overrides ────────────────────────────────────────
4495
+ // The fresh-Lumia defaults assume the streamer is creating a new
4496
+ // chatbox; SE-imported ones have opinions that differ. Override
4497
+ // only those.
4353
4498
  theme: mapChatboxTheme(v.theme),
4354
4499
  fadeOutAfterDelay: fadeOutSeconds > 0,
4355
4500
  fadeOutDelayTime: fadeOutSeconds,
4356
- // Defaults Lumia's chatbox renderer + UI expect on every module.
4357
- // Without these the chatbox silently dropped messages (the
4358
- // `ignoredStreamingSites` truthy gate), rendered in the wrong
4359
- // theme (no theme → blank list), or showed missing controls in
4360
- // settings. `ignoredList` carries SE's per-user ignore list across.
4361
4501
  ignoredList: Array.isArray(v.ignored) ? v.ignored : [],
4362
- ignoredStreamingSites: [],
4363
- horizontal: false,
4364
- reverseFlow: false,
4365
- hideAlerts: false,
4366
- hideAlertMessage: false,
4502
+ // SE chat widgets default to showing avatars + site icons + tight
4503
+ // line wrapping; Lumia's fresh default is the opposite.
4367
4504
  showAvatar: true,
4368
- showBadges: true,
4369
- showEmotes: true,
4370
4505
  showSiteIcon: true,
4371
- showTimestamp: false,
4372
4506
  breakLine: false,
4373
- hyperClickableLinks: true,
4374
4507
  clickableChatterProfiles: true,
4375
- previewMediaInChat: true,
4376
- maxItemsToShow: 20,
4377
- removeAfter: 3e4,
4378
- itemGap: 8,
4379
- borderRadius: "10px",
4380
- bgOpacity: 0.4,
4381
- animations: {
4382
- enterAnimation: "fadeIn",
4383
- exitAnimation: "fadeOut",
4384
- enterAnimationDuration: 1e3,
4385
- exitAnimationDuration: 1e3,
4386
- enterAnimationDelay: 0,
4387
- exitAnimationDelay: 0
4388
- },
4508
+ hideAlertMessage: false,
4389
4509
  // Carry the SE message-level font override and the streamer's
4390
4510
  // accent color across so the imported chatbox looks right at
4391
4511
  // first paint. Background lives on `module.css.background`
4392
4512
  // above — `content.background` was a dead write the renderer
4393
4513
  // never read.
4394
4514
  ...messageFontFamily ? { messageFontFamily } : {},
4515
+ // Twitch primary color override — deep-merge into the full
4516
+ // chatboxStreamingSite map from the default (otherwise we'd
4517
+ // clobber the youtube/kick/facebook/tiktok/discord entries the
4518
+ // chatbox renderer reads for per-platform theming).
4395
4519
  ...accentColor ? {
4396
4520
  chatboxStreamingSite: {
4521
+ ...chatboxDefaults.chatboxStreamingSite,
4397
4522
  twitch: {
4523
+ ...chatboxDefaults.chatboxStreamingSite.twitch,
4398
4524
  themeConfig: {
4399
- primaryColor: accentColor,
4400
- showUsernameColors: true
4525
+ ...chatboxDefaults.chatboxStreamingSite.twitch.themeConfig,
4526
+ primaryColor: accentColor
4401
4527
  }
4402
4528
  }
4403
4529
  }
@@ -4420,20 +4546,28 @@ function mapChatbox(widget, ctx) {
4420
4546
  }
4421
4547
  var SE_LISTENER_TO_LUMIA_CATEGORY = {
4422
4548
  "follower-latest": "follower",
4549
+ "follower-recent": "follower",
4423
4550
  "subscriber-latest": "subscribers",
4424
4551
  "subscriber-new-latest": "subscribers",
4425
4552
  "subscriber-resub-latest": "subscribers",
4553
+ "subscriber-recent": "subscribers",
4426
4554
  "subscriber-gifted-latest": "gifts",
4427
4555
  "community-gift-latest": "gifts",
4428
4556
  "cheer-latest": "bits",
4557
+ "cheer-recent": "bits",
4429
4558
  "cheerPurchase-latest": "bits",
4430
4559
  "tip-latest": "donation",
4560
+ "tip-recent": "donation",
4431
4561
  "raid-latest": "raids",
4562
+ "raid-recent": "raids",
4432
4563
  "host-latest": "hosts",
4564
+ "host-recent": "hosts",
4433
4565
  "redemption-latest": "redemption",
4434
4566
  "merch-latest": "purchases",
4567
+ "merch-recent": "purchases",
4435
4568
  "purchase-latest": "purchases",
4436
4569
  "superchat-latest": "superchats",
4570
+ "superchat-recent": "superchats",
4437
4571
  "hypetrain-latest": "hypetrain"
4438
4572
  };
4439
4573
  var ALL_LUMIA_EVENTLIST_CATEGORIES = [
@@ -4458,8 +4592,7 @@ var ALL_LUMIA_EVENTLIST_CATEGORIES = [
4458
4592
  "stars",
4459
4593
  "fans",
4460
4594
  "shares",
4461
- "raffles",
4462
- "others"
4595
+ "raffles"
4463
4596
  ];
4464
4597
  function computeListenerFilters(seListeners, explicitListener) {
4465
4598
  const enabled = [];
@@ -4484,10 +4617,27 @@ function computeListenerFilters(seListeners, explicitListener) {
4484
4617
  if (allowed.size === 0) return [];
4485
4618
  return ALL_LUMIA_EVENTLIST_CATEGORIES.filter((c) => !allowed.has(c));
4486
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
+ }
4487
4630
  function mapEventList(widget, ctx) {
4488
4631
  const v = widget.variables ?? {};
4489
4632
  const filters = computeListenerFilters(widget.listeners, widget.listener);
4490
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";
4491
4641
  return buildUnit(
4492
4642
  widget,
4493
4643
  "eventlist",
@@ -4496,7 +4646,7 @@ function mapEventList(widget, ctx) {
4496
4646
  // Lumia eventlist reads font/size/color from module.css.
4497
4647
  css: mapSeTextCssToModuleCss(widget, { fontSize: 18 }),
4498
4648
  content: {
4499
- theme: v.theme ?? "simple",
4649
+ theme,
4500
4650
  direction: v.direction ?? "top",
4501
4651
  maxItemsToShow: maxItems,
4502
4652
  visibleEvents: maxItems,
@@ -4508,14 +4658,26 @@ function mapEventList(widget, ctx) {
4508
4658
  // `sites` is a platform blacklist (same shape). Default to "no platforms hidden".
4509
4659
  sites: [],
4510
4660
  // Eventlist Manager assumes these objects exist; provide an empty stub so
4511
- // imported overlays render without throwing.
4512
- 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 } : {},
4513
4666
  eventListType: {},
4514
4667
  condensedText: false,
4515
- horizontal: false,
4516
- showAlertIcon: true,
4517
- showSiteIcon: true,
4518
- 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,
4519
4681
  showDetailedText: false,
4520
4682
  hideAlertMessage: false
4521
4683
  }
@@ -5565,13 +5727,15 @@ function mapStreamBoss(widget, ctx) {
5565
5727
  const selectedEvents = [];
5566
5728
  for (const seListener of SE_LISTENERS) {
5567
5729
  if (!listenerIsOn(widget.listeners, seListener)) continue;
5568
- const alertKey = streamBossListenerKeyFor(seListener, ctx?.provider);
5569
- if (!alertKey) continue;
5730
+ const alertKeys = streamBossListenerKeysFor(seListener, ctx?.provider);
5731
+ if (alertKeys.length === 0) continue;
5570
5732
  const raw = seDamage[seListener];
5571
5733
  const value = typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : null;
5572
5734
  if (value === null) continue;
5573
- damageMap[alertKey] = { value, mode: SE_LISTENER_MODE[seListener] };
5574
- selectedEvents.push(alertKey);
5735
+ for (const alertKey of alertKeys) {
5736
+ damageMap[alertKey] = { value, mode: SE_LISTENER_MODE[seListener] };
5737
+ selectedEvents.push(alertKey);
5738
+ }
5575
5739
  }
5576
5740
  const barColor = typeof v.colors?.frame === "string" && v.colors.frame ? v.colors.frame : "#2bff00";
5577
5741
  const theme = v.showImage === false ? "bar-only" : "card";
@@ -5708,23 +5872,6 @@ var GOAL_TYPES = /* @__PURE__ */ new Set([
5708
5872
  "se-widget-merch-goal"
5709
5873
  ]);
5710
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"]);
5711
- var SCROLLING_RECENT_TYPES = /* @__PURE__ */ new Set([
5712
- "se-widget-follower-recent",
5713
- "se-widget-subscriber-recent",
5714
- "se-widget-cheer-recent",
5715
- "se-widget-tip-recent"
5716
- ]);
5717
- var RECENT_TO_LATEST_FALLBACK_VAR = {
5718
- "se-widget-follower-recent": "twitch_last_follower",
5719
- "se-widget-subscriber-recent": "twitch_last_subscriber",
5720
- "se-widget-cheer-recent": "twitch_last_bit",
5721
- "se-widget-tip-recent": "latest_donator"
5722
- };
5723
- function isScrollingRecentWidget(widget) {
5724
- if (!SCROLLING_RECENT_TYPES.has(widget.type)) return false;
5725
- const scrolling = widget.text?.scrolling;
5726
- return !!(scrolling && typeof scrolling === "object" && scrolling.enabled === true);
5727
- }
5728
5875
  function dispatch(widget, ctx) {
5729
5876
  const t = widget.type;
5730
5877
  console.log("[SE-IMPORT] dispatch widget", {
@@ -5753,10 +5900,6 @@ function dispatch(widget, ctx) {
5753
5900
  if (t === "se-widget-donor-drive-alert") return { unit: mapSingleAlert(widget, "donordrive-donation", ctx), status: "direct", lumiaType: "alert" };
5754
5901
  if (t === "se-widget-extralife-alert") return { unit: mapSingleAlert(widget, "extralife-donation", ctx), status: "direct", lumiaType: "alert" };
5755
5902
  if (t === "se-widget-tiltify-alert") return { unit: mapSingleAlert(widget, "tiltify-campaignDonation", ctx), status: "direct", lumiaType: "alert" };
5756
- if (isScrollingRecentWidget(widget)) {
5757
- const fallbackVar = RECENT_TO_LATEST_FALLBACK_VAR[t] ?? "username";
5758
- return { unit: mapReadout(widget, fallbackVar, ctx), status: "direct", lumiaType: "text" };
5759
- }
5760
5903
  if (EVENTLIST_TYPES.has(t)) return { unit: mapEventList(widget, ctx), status: "direct", lumiaType: "eventlist" };
5761
5904
  if (t === "se-widget-top-tippers-list") return { unit: mapViewerProfilesLeaderboard(widget, "tips", ctx), status: "direct", lumiaType: "viewerprofiles" };
5762
5905
  if (t === "se-widget-top-cheerers-list") return { unit: mapViewerProfilesLeaderboard(widget, "cheers", ctx), status: "direct", lumiaType: "viewerprofiles" };
@@ -8472,7 +8615,11 @@ function SEImportWizard({
8472
8615
  );
8473
8616
  mirrorAbortRef.current?.abort();
8474
8617
  onClose();
8475
- if (lastUuid) onComplete({ overlayId: lastUuid });
8618
+ if (importsToSave.length === 1 && lastUuid) {
8619
+ onComplete({ overlayId: lastUuid });
8620
+ } else if (importsToSave.length > 1) {
8621
+ onComplete({});
8622
+ }
8476
8623
  } catch (error) {
8477
8624
  notify.error(`Import failed: ${extractErrorMessage(error)}`);
8478
8625
  } finally {
@@ -342,7 +342,7 @@ interface SEImportBindings {
342
342
  uuid: string;
343
343
  }>;
344
344
  onComplete: (result: {
345
- overlayId: string;
345
+ overlayId?: string;
346
346
  }) => void;
347
347
  onOverlayImported?: (result: {
348
348
  overlayId: string;
package/dist/se-import.js CHANGED
@@ -14,7 +14,13 @@ var SE_TO_LUMIA_PLACEHOLDER = {
14
14
  item: "firstItem.name",
15
15
  count: "amount",
16
16
  total: "total",
17
- gifts: "giftAmount"
17
+ gifts: "giftAmount",
18
+ // Platform-agnostic streamer identity — SE exposes `{streamer}` in static
19
+ // widgets and alert bodies. Lumia has no event-scoped equivalent (it's not
20
+ // tied to an alert payload), so we route it at the *global* template
21
+ // variable `{{streamer}}` populated by VariablesManager.refreshStreamerVariable
22
+ // (Twitch → Kick → YouTube → Facebook → TikTok fallback chain).
23
+ streamer: "streamer"
18
24
  };
19
25
  var TWITCH_CONTEXTS = {
20
26
  "follower-latest": { name: "twitch_last_follower", username: "twitch_last_follower" },
@@ -624,35 +630,38 @@ function mapReadout(widget, fallbackVar, ctx) {
624
630
  );
625
631
  }
626
632
 
633
+ // src/se-import/mappers/alert.ts
634
+ import { LumiaAlertConfigs } from "@lumiastream/lumia-types";
635
+
627
636
  // src/se-import/mappers/provider-alerts.ts
628
637
  var ALERT_BOX_KEYS = {
629
638
  twitch: {
630
- follower: "twitch-follower",
631
- subscriber: "twitch-subscriber",
632
- tip: "streamelements-donation",
633
- cheer: "twitch-bits",
634
- raid: "twitch-raid",
635
- merch: "fourthwall-shopOrder",
636
- purchase: "fourthwall-shopOrder",
637
- 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"]
638
647
  },
639
648
  kick: {
640
- follower: "kick-follower",
641
- subscriber: "kick-subscriber",
642
- tip: "streamelements-donation",
643
- cheer: "kick-kicks",
644
- raid: "kick-host",
645
- host: "kick-host",
646
- merch: "fourthwall-shopOrder",
647
- 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"]
648
657
  },
649
658
  youtube: {
650
- follower: "youtube-subscriber",
651
- subscriber: "youtube-member",
652
- tip: "streamelements-donation",
653
- cheer: "youtube-superchat",
654
- merch: "fourthwall-shopOrder",
655
- 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"]
656
665
  }
657
666
  };
658
667
  var KAPPAGEN_KEYS = {
@@ -661,52 +670,93 @@ var KAPPAGEN_KEYS = {
661
670
  subscriber: "twitch-subscriber",
662
671
  cheer: "twitch-bits",
663
672
  raid: "twitch-raid",
664
- tip: "streamelements-donation"
673
+ tip: "lumiastream-donation"
665
674
  },
666
675
  kick: {
667
676
  follower: "kick-follower",
668
677
  subscriber: "kick-subscriber",
669
678
  cheer: "kick-kicks",
670
679
  raid: "kick-host",
671
- tip: "streamelements-donation"
680
+ tip: "lumiastream-donation"
672
681
  },
673
682
  youtube: {
674
683
  follower: "youtube-subscriber",
675
684
  subscriber: "youtube-member",
676
685
  cheer: "youtube-superchat",
677
- tip: "streamelements-donation"
686
+ tip: "lumiastream-donation"
678
687
  }
679
688
  };
680
689
  var STREAMBOSS_LISTENER_KEYS = {
681
690
  twitch: {
682
- "follower-latest": "twitch-follower",
683
- "subscriber-latest": "twitch-subscriber",
684
- "tip-latest": "lumiastream-donation",
685
- "cheer-latest": "twitch-bits",
686
- "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"]
687
696
  },
688
697
  kick: {
689
- "follower-latest": "kick-follower",
690
- "subscriber-latest": "kick-subscriber",
691
- "tip-latest": "lumiastream-donation",
692
- "cheer-latest": "kick-kicks",
693
- "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"]
694
703
  },
695
704
  youtube: {
696
- "follower-latest": "youtube-subscriber",
697
- "subscriber-latest": "youtube-member",
698
- "tip-latest": "lumiastream-donation",
699
- "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"]
700
709
  }
701
710
  };
702
- function alertBoxKeyFor(seEvent, provider = "twitch") {
703
- 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] ?? [];
704
754
  }
705
755
  function kappagenKeyFor(seEvent, provider = "twitch") {
706
756
  return KAPPAGEN_KEYS[provider]?.[seEvent];
707
757
  }
708
- function streamBossListenerKeyFor(seListener, provider = "twitch") {
709
- return STREAMBOSS_LISTENER_KEYS[provider]?.[seListener];
758
+ function streamBossListenerKeysFor(seListener, provider = "twitch") {
759
+ return STREAMBOSS_LISTENER_KEYS[provider]?.[seListener] ?? [];
710
760
  }
711
761
  function readBootstrapProvider(bootstrap) {
712
762
  const ch = bootstrap.channel;
@@ -896,6 +946,16 @@ function pickBaseAlertFields(event) {
896
946
  const { on: _on, variations: _variations, matchEmptyCondition: _mec, ...base } = event;
897
947
  return base;
898
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
+ }
899
959
  function mapAlertBox(widget, opts = {}, ctx) {
900
960
  const v = widget.variables ?? {};
901
961
  const events = {};
@@ -904,12 +964,20 @@ function mapAlertBox(widget, opts = {}, ctx) {
904
964
  for (const seKey of eventKeys) {
905
965
  const ev = v[seKey];
906
966
  if (!ev || typeof ev !== "object") continue;
907
- const lumiaAlert = alertBoxKeyFor(seKey, ctx?.provider);
908
- if (!lumiaAlert) continue;
967
+ const lumiaKeys = alertBoxKeysFor(seKey, ctx?.provider);
968
+ if (lumiaKeys.length === 0) continue;
909
969
  const built = buildAlertEvent(ev, seKey);
910
- events[lumiaAlert] = built;
970
+ for (const lumiaKey of lumiaKeys) {
971
+ events[lumiaKey] = built;
972
+ }
911
973
  if (firstEvent == null) firstEvent = built;
912
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
+ }
913
981
  const baseAlert = firstEvent ? pickBaseAlertFields(firstEvent) : {};
914
982
  return buildUnit(widget, "alert", { alert: { ...baseAlert, events } }, ctx);
915
983
  }
@@ -1137,6 +1205,63 @@ import {
1137
1205
  LumiaEventListTypes,
1138
1206
  LumiaMapAlertTypeToEventListType
1139
1207
  } from "@lumiastream/lumia-types";
1208
+
1209
+ // src/se-import/mappers/module-defaults.ts
1210
+ var CHATBOX_DEFAULT_CONTENT_FIELDS = {
1211
+ type: "lumia",
1212
+ src: "",
1213
+ userLevels: [1, 2, 3, 4, 5, 6, 7, 8, 9],
1214
+ permanent: true,
1215
+ maxItemsToShow: 20,
1216
+ removeAfter: 3e4,
1217
+ animations: {
1218
+ enterAnimation: "fadeIn",
1219
+ exitAnimation: "fadeOut",
1220
+ enterAnimationDuration: 1e3,
1221
+ exitAnimationDuration: 1e3,
1222
+ enterAnimationDelay: 0,
1223
+ exitAnimationDelay: 0
1224
+ },
1225
+ ignoredList: [],
1226
+ background: "transparent",
1227
+ theme: "simple",
1228
+ chatboxStreamingSite: {
1229
+ twitch: { message: "", icon: "", themeConfig: { primaryColor: "#915dff", showUsernameColors: true } },
1230
+ youtube: { message: "", icon: "", themeConfig: { primaryColor: "#ff0000", usePlatformColor: false } },
1231
+ facebook: { message: "", icon: "", themeConfig: { primaryColor: "#4267B2", usePlatformColor: false } },
1232
+ tiktok: { message: "", icon: "", themeConfig: { primaryColor: "#ff0050", usePlatformColor: false } },
1233
+ kick: { message: "", icon: "", themeConfig: { primaryColor: "#3AD305", usePlatformColor: false } },
1234
+ discord: { message: "", icon: "", themeConfig: { primaryColor: "#7289da", usePlatformColor: false } }
1235
+ },
1236
+ borderRadius: "10px",
1237
+ bgOpacity: 0.4,
1238
+ ignoredStreamingSites: [],
1239
+ horizontal: false,
1240
+ reverseFlow: false,
1241
+ showBadges: true,
1242
+ showSiteIcon: false,
1243
+ showEmotes: true,
1244
+ showAvatar: false,
1245
+ showTimestamp: false,
1246
+ hideAlerts: false,
1247
+ hyperClickableLinks: true,
1248
+ clickableChatterProfiles: false,
1249
+ previewMediaInChat: true,
1250
+ previewMediaUserLevels: ["streamer", "moderators", "vips", "tier3", "tier2", "subscribers", "regular", "follower", "anyone"],
1251
+ breakLine: true,
1252
+ fadeOutAfterDelay: false,
1253
+ fadeOutDelayTime: 5,
1254
+ useAnimatedEmotes: true,
1255
+ sites: ["twitch", "kick", "youtube", "facebook", "discord", "ffz", "seventv", "bttv"],
1256
+ border: "solid 1px transparent",
1257
+ itemGap: 8
1258
+ };
1259
+ var CHATBOX_DEFAULT_MODULE = {
1260
+ version: 7,
1261
+ content: CHATBOX_DEFAULT_CONTENT_FIELDS
1262
+ };
1263
+
1264
+ // src/se-import/mappers/misc.ts
1140
1265
  function mapChatboxTheme(seTheme) {
1141
1266
  const t = (seTheme ?? "").toString().toLowerCase();
1142
1267
  if (!t || t === "default") return "simple";
@@ -1196,6 +1321,8 @@ function paddingShorthand(message) {
1196
1321
  return `${px(t)} ${px(r)} ${px(b)} ${px(l)}`;
1197
1322
  }
1198
1323
  function mapChatbox(widget, ctx) {
1324
+ const chatboxDefaults = CHATBOX_DEFAULT_MODULE.content;
1325
+ const chatboxVersion = CHATBOX_DEFAULT_MODULE.version;
1199
1326
  const v = widget.variables ?? {};
1200
1327
  const textCss = widget.text?.css ?? {};
1201
1328
  const messageCss = textCss.message ?? {};
@@ -1230,59 +1357,58 @@ function mapChatbox(widget, ctx) {
1230
1357
  widget,
1231
1358
  "chatbox",
1232
1359
  {
1360
+ // Stamp the module version from the authoritative defaults so
1361
+ // Overlay-UI's per-module Settings.tsx migration check
1362
+ // (`module?.version !== currentVersion`) treats this import as
1363
+ // already-current. Without this, every freshly-imported chatbox
1364
+ // would trip the migration toast on first open even though the
1365
+ // content is already complete.
1366
+ version: chatboxVersion,
1233
1367
  // Visual styling (font / color / padding etc) lives on module.css, not
1234
1368
  // content. Lumia's TextStyle settings panel + the chatbox renderer both
1235
1369
  // read from here.
1236
1370
  css: moduleCss,
1237
1371
  content: {
1372
+ // `chatboxDefaults` resolves host-injected first (preferred), then
1373
+ // the local snapshot — see the top of this function. Spreading it
1374
+ // here guarantees every field the chatbox renderer expects is
1375
+ // present (`type`, `sites`, `useAnimatedEmotes`, etc.); the
1376
+ // SE-specific override block below wins for the opinions that
1377
+ // differ from a fresh-add.
1378
+ ...chatboxDefaults,
1379
+ // ─── SE-specific overrides ────────────────────────────────────────
1380
+ // The fresh-Lumia defaults assume the streamer is creating a new
1381
+ // chatbox; SE-imported ones have opinions that differ. Override
1382
+ // only those.
1238
1383
  theme: mapChatboxTheme(v.theme),
1239
1384
  fadeOutAfterDelay: fadeOutSeconds > 0,
1240
1385
  fadeOutDelayTime: fadeOutSeconds,
1241
- // Defaults Lumia's chatbox renderer + UI expect on every module.
1242
- // Without these the chatbox silently dropped messages (the
1243
- // `ignoredStreamingSites` truthy gate), rendered in the wrong
1244
- // theme (no theme → blank list), or showed missing controls in
1245
- // settings. `ignoredList` carries SE's per-user ignore list across.
1246
1386
  ignoredList: Array.isArray(v.ignored) ? v.ignored : [],
1247
- ignoredStreamingSites: [],
1248
- horizontal: false,
1249
- reverseFlow: false,
1250
- hideAlerts: false,
1251
- hideAlertMessage: false,
1387
+ // SE chat widgets default to showing avatars + site icons + tight
1388
+ // line wrapping; Lumia's fresh default is the opposite.
1252
1389
  showAvatar: true,
1253
- showBadges: true,
1254
- showEmotes: true,
1255
1390
  showSiteIcon: true,
1256
- showTimestamp: false,
1257
1391
  breakLine: false,
1258
- hyperClickableLinks: true,
1259
1392
  clickableChatterProfiles: true,
1260
- previewMediaInChat: true,
1261
- maxItemsToShow: 20,
1262
- removeAfter: 3e4,
1263
- itemGap: 8,
1264
- borderRadius: "10px",
1265
- bgOpacity: 0.4,
1266
- animations: {
1267
- enterAnimation: "fadeIn",
1268
- exitAnimation: "fadeOut",
1269
- enterAnimationDuration: 1e3,
1270
- exitAnimationDuration: 1e3,
1271
- enterAnimationDelay: 0,
1272
- exitAnimationDelay: 0
1273
- },
1393
+ hideAlertMessage: false,
1274
1394
  // Carry the SE message-level font override and the streamer's
1275
1395
  // accent color across so the imported chatbox looks right at
1276
1396
  // first paint. Background lives on `module.css.background`
1277
1397
  // above — `content.background` was a dead write the renderer
1278
1398
  // never read.
1279
1399
  ...messageFontFamily ? { messageFontFamily } : {},
1400
+ // Twitch primary color override — deep-merge into the full
1401
+ // chatboxStreamingSite map from the default (otherwise we'd
1402
+ // clobber the youtube/kick/facebook/tiktok/discord entries the
1403
+ // chatbox renderer reads for per-platform theming).
1280
1404
  ...accentColor ? {
1281
1405
  chatboxStreamingSite: {
1406
+ ...chatboxDefaults.chatboxStreamingSite,
1282
1407
  twitch: {
1408
+ ...chatboxDefaults.chatboxStreamingSite.twitch,
1283
1409
  themeConfig: {
1284
- primaryColor: accentColor,
1285
- showUsernameColors: true
1410
+ ...chatboxDefaults.chatboxStreamingSite.twitch.themeConfig,
1411
+ primaryColor: accentColor
1286
1412
  }
1287
1413
  }
1288
1414
  }
@@ -1305,20 +1431,28 @@ function mapChatbox(widget, ctx) {
1305
1431
  }
1306
1432
  var SE_LISTENER_TO_LUMIA_CATEGORY = {
1307
1433
  "follower-latest": "follower",
1434
+ "follower-recent": "follower",
1308
1435
  "subscriber-latest": "subscribers",
1309
1436
  "subscriber-new-latest": "subscribers",
1310
1437
  "subscriber-resub-latest": "subscribers",
1438
+ "subscriber-recent": "subscribers",
1311
1439
  "subscriber-gifted-latest": "gifts",
1312
1440
  "community-gift-latest": "gifts",
1313
1441
  "cheer-latest": "bits",
1442
+ "cheer-recent": "bits",
1314
1443
  "cheerPurchase-latest": "bits",
1315
1444
  "tip-latest": "donation",
1445
+ "tip-recent": "donation",
1316
1446
  "raid-latest": "raids",
1447
+ "raid-recent": "raids",
1317
1448
  "host-latest": "hosts",
1449
+ "host-recent": "hosts",
1318
1450
  "redemption-latest": "redemption",
1319
1451
  "merch-latest": "purchases",
1452
+ "merch-recent": "purchases",
1320
1453
  "purchase-latest": "purchases",
1321
1454
  "superchat-latest": "superchats",
1455
+ "superchat-recent": "superchats",
1322
1456
  "hypetrain-latest": "hypetrain"
1323
1457
  };
1324
1458
  var ALL_LUMIA_EVENTLIST_CATEGORIES = [
@@ -1343,8 +1477,7 @@ var ALL_LUMIA_EVENTLIST_CATEGORIES = [
1343
1477
  "stars",
1344
1478
  "fans",
1345
1479
  "shares",
1346
- "raffles",
1347
- "others"
1480
+ "raffles"
1348
1481
  ];
1349
1482
  function computeListenerFilters(seListeners, explicitListener) {
1350
1483
  const enabled = [];
@@ -1369,10 +1502,27 @@ function computeListenerFilters(seListeners, explicitListener) {
1369
1502
  if (allowed.size === 0) return [];
1370
1503
  return ALL_LUMIA_EVENTLIST_CATEGORIES.filter((c) => !allowed.has(c));
1371
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
+ }
1372
1515
  function mapEventList(widget, ctx) {
1373
1516
  const v = widget.variables ?? {};
1374
1517
  const filters = computeListenerFilters(widget.listeners, widget.listener);
1375
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";
1376
1526
  return buildUnit(
1377
1527
  widget,
1378
1528
  "eventlist",
@@ -1381,7 +1531,7 @@ function mapEventList(widget, ctx) {
1381
1531
  // Lumia eventlist reads font/size/color from module.css.
1382
1532
  css: mapSeTextCssToModuleCss(widget, { fontSize: 18 }),
1383
1533
  content: {
1384
- theme: v.theme ?? "simple",
1534
+ theme,
1385
1535
  direction: v.direction ?? "top",
1386
1536
  maxItemsToShow: maxItems,
1387
1537
  visibleEvents: maxItems,
@@ -1393,14 +1543,26 @@ function mapEventList(widget, ctx) {
1393
1543
  // `sites` is a platform blacklist (same shape). Default to "no platforms hidden".
1394
1544
  sites: [],
1395
1545
  // Eventlist Manager assumes these objects exist; provide an empty stub so
1396
- // imported overlays render without throwing.
1397
- 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 } : {},
1398
1551
  eventListType: {},
1399
1552
  condensedText: false,
1400
- horizontal: false,
1401
- showAlertIcon: true,
1402
- showSiteIcon: true,
1403
- 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,
1404
1566
  showDetailedText: false,
1405
1567
  hideAlertMessage: false
1406
1568
  }
@@ -2450,13 +2612,15 @@ function mapStreamBoss(widget, ctx) {
2450
2612
  const selectedEvents = [];
2451
2613
  for (const seListener of SE_LISTENERS) {
2452
2614
  if (!listenerIsOn(widget.listeners, seListener)) continue;
2453
- const alertKey = streamBossListenerKeyFor(seListener, ctx?.provider);
2454
- if (!alertKey) continue;
2615
+ const alertKeys = streamBossListenerKeysFor(seListener, ctx?.provider);
2616
+ if (alertKeys.length === 0) continue;
2455
2617
  const raw = seDamage[seListener];
2456
2618
  const value = typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : null;
2457
2619
  if (value === null) continue;
2458
- damageMap[alertKey] = { value, mode: SE_LISTENER_MODE[seListener] };
2459
- selectedEvents.push(alertKey);
2620
+ for (const alertKey of alertKeys) {
2621
+ damageMap[alertKey] = { value, mode: SE_LISTENER_MODE[seListener] };
2622
+ selectedEvents.push(alertKey);
2623
+ }
2460
2624
  }
2461
2625
  const barColor = typeof v.colors?.frame === "string" && v.colors.frame ? v.colors.frame : "#2bff00";
2462
2626
  const theme = v.showImage === false ? "bar-only" : "card";
@@ -2593,23 +2757,6 @@ var GOAL_TYPES = /* @__PURE__ */ new Set([
2593
2757
  "se-widget-merch-goal"
2594
2758
  ]);
2595
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"]);
2596
- var SCROLLING_RECENT_TYPES = /* @__PURE__ */ new Set([
2597
- "se-widget-follower-recent",
2598
- "se-widget-subscriber-recent",
2599
- "se-widget-cheer-recent",
2600
- "se-widget-tip-recent"
2601
- ]);
2602
- var RECENT_TO_LATEST_FALLBACK_VAR = {
2603
- "se-widget-follower-recent": "twitch_last_follower",
2604
- "se-widget-subscriber-recent": "twitch_last_subscriber",
2605
- "se-widget-cheer-recent": "twitch_last_bit",
2606
- "se-widget-tip-recent": "latest_donator"
2607
- };
2608
- function isScrollingRecentWidget(widget) {
2609
- if (!SCROLLING_RECENT_TYPES.has(widget.type)) return false;
2610
- const scrolling = widget.text?.scrolling;
2611
- return !!(scrolling && typeof scrolling === "object" && scrolling.enabled === true);
2612
- }
2613
2760
  function dispatch(widget, ctx) {
2614
2761
  const t = widget.type;
2615
2762
  console.log("[SE-IMPORT] dispatch widget", {
@@ -2638,10 +2785,6 @@ function dispatch(widget, ctx) {
2638
2785
  if (t === "se-widget-donor-drive-alert") return { unit: mapSingleAlert(widget, "donordrive-donation", ctx), status: "direct", lumiaType: "alert" };
2639
2786
  if (t === "se-widget-extralife-alert") return { unit: mapSingleAlert(widget, "extralife-donation", ctx), status: "direct", lumiaType: "alert" };
2640
2787
  if (t === "se-widget-tiltify-alert") return { unit: mapSingleAlert(widget, "tiltify-campaignDonation", ctx), status: "direct", lumiaType: "alert" };
2641
- if (isScrollingRecentWidget(widget)) {
2642
- const fallbackVar = RECENT_TO_LATEST_FALLBACK_VAR[t] ?? "username";
2643
- return { unit: mapReadout(widget, fallbackVar, ctx), status: "direct", lumiaType: "text" };
2644
- }
2645
2788
  if (EVENTLIST_TYPES.has(t)) return { unit: mapEventList(widget, ctx), status: "direct", lumiaType: "eventlist" };
2646
2789
  if (t === "se-widget-top-tippers-list") return { unit: mapViewerProfilesLeaderboard(widget, "tips", ctx), status: "direct", lumiaType: "viewerprofiles" };
2647
2790
  if (t === "se-widget-top-cheerers-list") return { unit: mapViewerProfilesLeaderboard(widget, "cheers", ctx), status: "direct", lumiaType: "viewerprofiles" };
@@ -7342,7 +7485,11 @@ function SEImportWizard({
7342
7485
  );
7343
7486
  mirrorAbortRef.current?.abort();
7344
7487
  onClose();
7345
- if (lastUuid) onComplete({ overlayId: lastUuid });
7488
+ if (importsToSave.length === 1 && lastUuid) {
7489
+ onComplete({ overlayId: lastUuid });
7490
+ } else if (importsToSave.length > 1) {
7491
+ onComplete({});
7492
+ }
7346
7493
  } catch (error) {
7347
7494
  notify.error(`Import failed: ${extractErrorMessage(error)}`);
7348
7495
  } finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumiastream/ui",
3
- "version": "0.2.9",
3
+ "version": "0.3.1",
4
4
  "author": "Lumia Stream",
5
5
  "license": "ISC",
6
6
  "description": "Lumia UI Kit",
@@ -91,7 +91,8 @@
91
91
  "test:watch": "vitest",
92
92
  "release": "changeset && changeset tag && npm run build && npm publish",
93
93
  "prepublishOnly": "npm run build",
94
- "postpublish": "npm cache clean --force && node ./scripts/postpublish-install.mjs"
94
+ "postpublish": "npm cache clean --force && node ./scripts/postpublish-install.mjs",
95
+ "check-module-defaults": "tsx scripts/check-module-defaults.ts"
95
96
  },
96
97
  "peerDependencies": {
97
98
  "@codemirror/autocomplete": "^6.0.0",
@@ -121,12 +122,13 @@
121
122
  "sass-embedded": "^1.99.0",
122
123
  "storybook": "^10.2.17",
123
124
  "tsup": "^8.5.1",
125
+ "tsx": "^4.22.3",
124
126
  "typescript": "^5.9.3",
125
127
  "vite": "^8.0.8",
126
128
  "vitest": "^4.1.6"
127
129
  },
128
130
  "dependencies": {
129
- "@lumiastream/lumia-translations": "1.15.8",
131
+ "@lumiastream/lumia-translations": "1.15.9",
130
132
  "@lumiastream/lumia-types": "^3.3.7-alpha.2",
131
133
  "classnames": "^2.5.1",
132
134
  "globals": "^17.4.0",