@lumiastream/ui 0.2.8-alpha.27 → 0.2.8-alpha.28

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
@@ -3133,31 +3133,86 @@ var SE_TO_LUMIA_PLACEHOLDER = {
3133
3133
  };
3134
3134
  var TWITCH_CONTEXTS = {
3135
3135
  "follower-latest": { name: "twitch_last_follower", username: "twitch_last_follower" },
3136
+ // Three SE listeners with genuinely distinct semantics — preserve the
3137
+ // distinction in the variable mapping so a widget configured for "Latest
3138
+ // new subscriber" doesn't render the latest *resub* (and vice versa).
3139
+ // `subscriber-latest` is the umbrella (anyone subscribing); the other two
3140
+ // are subtype-specific. The dedicated TWITCH_LAST_{NEW_SUBSCRIBER,
3141
+ // RESUBSCRIBER} variables are written by the Twitch event pipeline on
3142
+ // the matching sub.type event.
3136
3143
  "subscriber-latest": { name: "twitch_last_subscriber", username: "twitch_last_subscriber" },
3137
- "subscriber-new-latest": { name: "twitch_last_subscriber", username: "twitch_last_subscriber" },
3138
- "subscriber-resub-latest": { name: "twitch_last_subscriber", username: "twitch_last_subscriber" },
3139
- "subscriber-gifted-latest": { name: "twitch_last_gifter", username: "twitch_last_gifter", amount: "twitch_last_gifter_amount", count: "twitch_last_gifter_amount" },
3144
+ "subscriber-new-latest": { name: "twitch_last_new_subscriber", username: "twitch_last_new_subscriber" },
3145
+ "subscriber-resub-latest": { name: "twitch_last_resubscriber", username: "twitch_last_resubscriber" },
3146
+ // `subscriber-gifted-latest`: SE uses `{sender}` for the giver and `{name}` for
3147
+ // the recipient (e.g. "{sender} gifted to {name}"). The two roles must point
3148
+ // at distinct Lumia variables — collapsing both onto `twitch_last_gifter`
3149
+ // rendered "Mr.Beast gifted to Mr.Beast" while the actual recipient was
3150
+ // nowhere. `sender` keeps the gifter; `name` / `username` switch to the
3151
+ // dedicated recipient variable `twitch_last_gifted`. Amount stays on the
3152
+ // gifter (it's the count of subs in that drop, owned by the gifter).
3153
+ "subscriber-gifted-latest": { name: "twitch_last_gifted", username: "twitch_last_gifted", sender: "twitch_last_gifter", amount: "twitch_last_gifter_amount", count: "twitch_last_gifter_amount" },
3140
3154
  "subscriber-alltime-gifter": { name: "twitch_alltime_top_gifter", username: "twitch_alltime_top_gifter", amount: "twitch_alltime_top_gifter_amount", count: "twitch_alltime_top_gifter_amount" },
3141
3155
  "cheer-latest": { name: "twitch_last_bit", username: "twitch_last_bit", amount: "twitch_last_bit_amount", count: "twitch_last_bit_amount" },
3142
3156
  "tip-latest": { name: "latest_donator", username: "latest_donator", amount: "latest_donator_amount", count: "latest_donator_amount", currency: "latest_donator_currency_symbol" },
3143
3157
  latest_donator: { name: "latest_donator", username: "latest_donator", amount: "latest_donator_amount", count: "latest_donator_amount", currency: "latest_donator_currency_symbol" },
3144
3158
  "raid-latest": { name: "twitch_last_raider", username: "twitch_last_raider", amount: "twitch_last_raid_amount", count: "twitch_last_raid_amount" },
3145
- // Session counters: count/amount = the running session count.
3159
+ // Period counters: count/amount = the running counter for that window.
3160
+ // SE lets the streamer pick session / week / month / total on the same widget
3161
+ // type; the chosen period lands in `widget.listener` (e.g. `cheer-week`).
3162
+ // Each entry below mirrors a row in the Lumia variables table — the previous
3163
+ // table only carried session+total and broke any widget configured for
3164
+ // week/month because `{amount}` fell through to the unscoped placeholder
3165
+ // and rendered blank.
3146
3166
  "follower-session": { amount: "twitch_session_follower_count", count: "twitch_session_follower_count" },
3147
- "subscriber-session": { amount: "twitch_session_subscribers_count", count: "twitch_session_subscribers_count" },
3148
- "cheer-session": { amount: "twitch_session_bits_count", count: "twitch_session_bits_count" },
3149
- "tip-session": { amount: "session_donation_amount", count: "session_donation_count", currency: "session_donation_amount_currency_symbol" },
3150
- // Totals (all-time): count/amount = total counter.
3167
+ "follower-week": { amount: "twitch_week_follower_count", count: "twitch_week_follower_count" },
3168
+ "follower-month": { amount: "twitch_month_follower_count", count: "twitch_month_follower_count" },
3151
3169
  "follower-total": { amount: "twitch_total_follower_count", count: "twitch_total_follower_count" },
3170
+ "subscriber-session": { amount: "twitch_session_subscribers_count", count: "twitch_session_subscribers_count" },
3171
+ "subscriber-week": { amount: "twitch_week_subscriber_count", count: "twitch_week_subscriber_count" },
3172
+ "subscriber-month": { amount: "twitch_month_subscriber_count", count: "twitch_month_subscriber_count" },
3152
3173
  "subscriber-total": { amount: "twitch_total_subscriber_count", count: "twitch_total_subscriber_count" },
3174
+ "subscriber-new-session": { amount: "twitch_session_new_subscribers_count", count: "twitch_session_new_subscribers_count" },
3175
+ "subscriber-resub-session": { amount: "twitch_session_resub_subscribers_count", count: "twitch_session_resub_subscribers_count" },
3176
+ "subscriber-gifted-session": { amount: "twitch_session_gifted_subscribers_count", count: "twitch_session_gifted_subscribers_count" },
3177
+ "cheer-session": { amount: "twitch_session_bits_count", count: "twitch_session_bits_count" },
3178
+ "cheer-week": { amount: "twitch_week_bits_count", count: "twitch_week_bits_count" },
3179
+ "cheer-month": { amount: "twitch_month_bits_count", count: "twitch_month_bits_count" },
3153
3180
  "cheer-total": { amount: "twitch_total_bits_count", count: "twitch_total_bits_count" },
3181
+ "tip-session": { amount: "session_donation_amount", count: "session_donation_count", currency: "session_donation_amount_currency_symbol" },
3182
+ "tip-week": { amount: "week_donation_amount", count: "week_donation_count", currency: "week_donation_amount_currency_symbol" },
3183
+ "tip-month": { amount: "month_donation_amount", count: "month_donation_count", currency: "month_donation_amount_currency_symbol" },
3154
3184
  "tip-total": { amount: "total_donation_amount", count: "total_donation_amount", currency: "total_donation_amount_currency_symbol" },
3155
3185
  "tip-count": { amount: "donation_count", count: "donation_count" },
3156
- // Top cheer / cheerer / donator session readouts.
3157
- "cheer-session-top-donation": { amount: "twitch_session_top_cheer_amount", count: "twitch_session_top_cheer_amount" },
3186
+ // Top cheer / cheerer / donator readouts — per period. SE's `se-widget-top-*`
3187
+ // widgets encode the chosen period (session / weekly / monthly / alltime) in
3188
+ // the widget's `listener` field. Without these entries the user's `{amount}`
3189
+ // / `{name}` template tokens fall through to the unscoped placeholder map
3190
+ // (`{{amount}}` only resolves inside alert variations) and render blank.
3191
+ //
3192
+ // `*-top-donation` and `*-top-donator` are paired: SE nominally treats them
3193
+ // as "top single event" vs "top cumulative contributor", but the data
3194
+ // rendered for both lives in the same Lumia variable pair (`*_top_donator`
3195
+ // / `*_top_donator_amount`, `twitch_*_top_cheerer` / `..._amount`). SE
3196
+ // widgets routinely use `{name}` against the `-donation` listener to mean
3197
+ // "the user who made the top donation", so the two rows below carry the
3198
+ // same name/username mapping — anything else falls through to the unscoped
3199
+ // `{{username}}` and renders blank.
3200
+ "cheer-session-top-donation": { name: "twitch_session_top_cheerer", username: "twitch_session_top_cheerer", amount: "twitch_session_top_cheer_amount", count: "twitch_session_top_cheer_amount" },
3158
3201
  "cheer-session-top-donator": { name: "twitch_session_top_cheerer", username: "twitch_session_top_cheerer", amount: "twitch_session_top_cheerer_amount", count: "twitch_session_top_cheerer_amount" },
3159
- "tip-session-top-donation": { amount: "session_top_donator_amount", count: "session_top_donator_amount", currency: "session_top_donator_currency_symbol" },
3202
+ "cheer-weekly-top-donation": { name: "twitch_week_top_cheerer", username: "twitch_week_top_cheerer", amount: "twitch_week_top_cheerer_amount", count: "twitch_week_top_cheerer_amount" },
3203
+ "cheer-weekly-top-donator": { name: "twitch_week_top_cheerer", username: "twitch_week_top_cheerer", amount: "twitch_week_top_cheerer_amount", count: "twitch_week_top_cheerer_amount" },
3204
+ "cheer-monthly-top-donation": { name: "twitch_month_top_cheerer", username: "twitch_month_top_cheerer", amount: "twitch_month_top_cheerer_amount", count: "twitch_month_top_cheerer_amount" },
3205
+ "cheer-monthly-top-donator": { name: "twitch_month_top_cheerer", username: "twitch_month_top_cheerer", amount: "twitch_month_top_cheerer_amount", count: "twitch_month_top_cheerer_amount" },
3206
+ "cheer-alltime-top-donation": { name: "twitch_alltime_top_cheerer", username: "twitch_alltime_top_cheerer", amount: "twitch_alltime_top_cheerer_amount", count: "twitch_alltime_top_cheerer_amount" },
3207
+ "cheer-alltime-top-donator": { name: "twitch_alltime_top_cheerer", username: "twitch_alltime_top_cheerer", amount: "twitch_alltime_top_cheerer_amount", count: "twitch_alltime_top_cheerer_amount" },
3208
+ "tip-session-top-donation": { name: "session_top_donator", username: "session_top_donator", amount: "session_top_donator_amount", count: "session_top_donator_amount", currency: "session_top_donator_currency_symbol" },
3160
3209
  "tip-session-top-donator": { name: "session_top_donator", username: "session_top_donator", amount: "session_top_donator_amount", count: "session_top_donator_amount", currency: "session_top_donator_currency_symbol" },
3210
+ "tip-weekly-top-donation": { name: "week_top_donator", username: "week_top_donator", amount: "week_top_donator_amount", count: "week_top_donator_amount", currency: "week_top_donator_currency_symbol" },
3211
+ "tip-weekly-top-donator": { name: "week_top_donator", username: "week_top_donator", amount: "week_top_donator_amount", count: "week_top_donator_amount", currency: "week_top_donator_currency_symbol" },
3212
+ "tip-monthly-top-donation": { name: "month_top_donator", username: "month_top_donator", amount: "month_top_donator_amount", count: "month_top_donator_amount", currency: "month_top_donator_currency_symbol" },
3213
+ "tip-monthly-top-donator": { name: "month_top_donator", username: "month_top_donator", amount: "month_top_donator_amount", count: "month_top_donator_amount", currency: "month_top_donator_currency_symbol" },
3214
+ "tip-alltime-top-donation": { name: "top_donator", username: "top_donator", amount: "top_donator_amount", count: "top_donator_amount", currency: "top_donator_currency_symbol" },
3215
+ "tip-alltime-top-donator": { name: "top_donator", username: "top_donator", amount: "top_donator_amount", count: "top_donator_amount", currency: "top_donator_currency_symbol" },
3161
3216
  // Botcounter — SE uses `{wins}` for the running chat count; the dispatcher's
3162
3217
  // fallbackVar for `se-widget-botcounter` is `twitch_session_chat_count`, so
3163
3218
  // when the listener arg lands as that var name we treat it as the wins source.
@@ -3194,7 +3249,11 @@ var KICK_CONTEXTS = {
3194
3249
  "subscriber-latest": { name: "kick_last_subscriber", username: "kick_last_subscriber" },
3195
3250
  "subscriber-new-latest": { name: "kick_last_subscriber", username: "kick_last_subscriber" },
3196
3251
  "subscriber-resub-latest": { name: "kick_last_subscriber", username: "kick_last_subscriber" },
3197
- "subscriber-gifted-latest": { name: "kick_last_gifter", username: "kick_last_gifter", amount: "kick_last_gifter_amount", count: "kick_last_gifter_amount" },
3252
+ // Mirror the Twitch split: `{sender}` = giver, `{name}` = recipient.
3253
+ // `kick_last_gifted` was added alongside this mapping; see twitch context
3254
+ // above for the rationale (without the split, SE's "{sender} gifted to
3255
+ // {name}" renders the gifter on both sides).
3256
+ "subscriber-gifted-latest": { name: "kick_last_gifted", username: "kick_last_gifted", sender: "kick_last_gifter", amount: "kick_last_gifter_amount", count: "kick_last_gifter_amount" },
3198
3257
  "cheer-latest": { name: "kick_last_kicks", username: "kick_last_kicks", amount: "kick_last_kicks_amount", count: "kick_last_kicks_amount" },
3199
3258
  "raid-latest": { name: "kick_last_host", username: "kick_last_host", amount: "kick_last_host_amount", count: "kick_last_host_amount" },
3200
3259
  "follower-session": { amount: "kick_session_follower_count", count: "kick_session_follower_count" },
@@ -3272,12 +3331,13 @@ function defaultLabelForSEWidget(widget) {
3272
3331
  const tail = words.slice(1).join(" ");
3273
3332
  return tail ? `${head} ${tail}` : head;
3274
3333
  }
3275
- function translateSeText(input, listener, passthrough, provider) {
3334
+ function translateSeText(input, listener, passthrough, provider, options) {
3276
3335
  if (!input) return "";
3277
3336
  const ctx = listener ? listenerPlaceholderContext(listener, provider) : {};
3337
+ const useAlertFallback = (options?.context ?? "alert") === "alert";
3278
3338
  return input.replace(/\{([a-zA-Z][a-zA-Z0-9_.]*)\}/g, (_match, raw) => {
3279
3339
  if (passthrough?.has(raw)) return `{{${raw}}}`;
3280
- const mapped = ctx[raw] ?? SE_TO_LUMIA_PLACEHOLDER[raw] ?? raw;
3340
+ const mapped = ctx[raw] ?? (useAlertFallback ? SE_TO_LUMIA_PLACEHOLDER[raw] : void 0) ?? raw;
3281
3341
  return `{{${mapped}}}`;
3282
3342
  });
3283
3343
  }
@@ -3328,8 +3388,8 @@ function listenerToLumiaVariable(listener, provider = "twitch") {
3328
3388
  // Latest events
3329
3389
  "follower-latest": "twitch_last_follower",
3330
3390
  "subscriber-latest": "twitch_last_subscriber",
3331
- "subscriber-new-latest": "twitch_last_subscriber",
3332
- "subscriber-resub-latest": "twitch_last_subscriber",
3391
+ "subscriber-new-latest": "twitch_last_new_subscriber",
3392
+ "subscriber-resub-latest": "twitch_last_resubscriber",
3333
3393
  "subscriber-gifted-latest": "twitch_last_gifter",
3334
3394
  "subscriber-alltime-gifter": "twitch_alltime_top_gifter",
3335
3395
  "tip-latest": "latest_donator",
@@ -3484,9 +3544,16 @@ var LUMIA_DEFAULT_SIZES = {
3484
3544
  // just the card if they want SE's exact footprint.
3485
3545
  songrequest: { width: 480, height: 380 }
3486
3546
  };
3487
- function translateVariationCondition(seType, seCondition, requirement) {
3547
+ function translateVariationCondition(seType, seCondition, requirement, seEventType) {
3488
3548
  if (seType === "gift") return { conditionType: "IS_GIFT", condition: 1 };
3489
3549
  if (seType === "communityGift") return { conditionType: "GIFT_SUB_GREATER", condition: requirement ?? 0 };
3550
+ const isSubscriberEvent = seEventType === "subscriber";
3551
+ if (isSubscriberEvent && seCondition === "ATLEAST" && requirement != null) {
3552
+ return { conditionType: "SUBSCRIBED_MONTHS_GREATER", condition: requirement };
3553
+ }
3554
+ if (isSubscriberEvent && seCondition === "EXACT" && requirement != null) {
3555
+ return { conditionType: "SUBSCRIBED_MONTHS_EQUAL", condition: requirement };
3556
+ }
3490
3557
  if (seCondition === "EXACT" && requirement != null) return { conditionType: "EQUAL_NUMBER", condition: requirement };
3491
3558
  if (seCondition === "ATLEAST" && requirement != null) {
3492
3559
  return { conditionType: "GREATER_NUMBER", condition: requirement };
@@ -3586,7 +3653,7 @@ function seExtraTextCss(seCss, scrolling) {
3586
3653
  return extra;
3587
3654
  }
3588
3655
  function mapText(widget, ctx) {
3589
- const value = translateSeText(widget.text?.value ?? "");
3656
+ const value = translateSeText(widget.text?.value ?? "", null, void 0, ctx?.provider, { context: "static" });
3590
3657
  const seCss = widget.text?.css ?? {};
3591
3658
  return buildUnit(widget, "text", {
3592
3659
  content: {
@@ -3643,7 +3710,7 @@ function mapReadout(widget, fallbackVar, ctx) {
3643
3710
  const variable = listenerToLumiaVariable(listener, ctx?.provider);
3644
3711
  const userTemplate = widget.text?.value ?? "";
3645
3712
  const inheritsText = userTemplate.trim().length > 0 && /\{[a-zA-Z]/.test(userTemplate);
3646
- const value = inheritsText ? translateSeText(userTemplate, listener, void 0, ctx?.provider) : `{{${variable}}}`;
3713
+ const value = inheritsText ? translateSeText(userTemplate, listener, void 0, ctx?.provider, { context: "static" }) : `{{${variable}}}`;
3647
3714
  const seCss = widget.text?.css ?? {};
3648
3715
  return buildUnit(widget, "text", {
3649
3716
  content: { value, highlightColor: "inherit" },
@@ -3898,7 +3965,7 @@ function buildAlertEvent(seEvent, seEventType) {
3898
3965
  };
3899
3966
  }
3900
3967
  function buildAlertVariation(v, seEventType, parentSettings) {
3901
- const translated = translateVariationCondition(v.type ?? null, v.condition ?? null, v.requirement ?? null);
3968
+ const translated = translateVariationCondition(v.type ?? null, v.condition ?? null, v.requirement ?? null, seEventType);
3902
3969
  if (!translated) return null;
3903
3970
  const { variations: _parentVariations, ...parentInheritable } = parentSettings ?? {};
3904
3971
  const mergedSettings = deepMergeSeSettings(parentSettings ? parentInheritable : void 0, v.settings);
@@ -4034,7 +4101,8 @@ function mapGoal(widget, ctx) {
4034
4101
  // live `current` floating at the percentage position. See
4035
4102
  // components/Modules/Views/Goal/types.ts:GoalTypes.SLIM.
4036
4103
  type: "slim",
4037
- label: translateSeText(v.title ?? "Goal"),
4104
+ // Static label rendered above the goal bar — not an alert context.
4105
+ label: translateSeText(v.title ?? "Goal", null, void 0, void 0, { context: "static" }),
4038
4106
  subLabel: "",
4039
4107
  // The Goal module renders this template literal with {{current}} + {{goal}} substituted.
4040
4108
  // Charity goals track donation amounts — prefix `$` so the rendered bar reads
@@ -4637,7 +4705,7 @@ function mapTimer(widget, ctx) {
4637
4705
  const v = widget.variables ?? {};
4638
4706
  const textValue = typeof widget.text?.value === "string" ? widget.text.value : void 0;
4639
4707
  const TIMER_PASSTHROUGH = /* @__PURE__ */ new Set(["days", "hours", "minutes", "seconds"]);
4640
- const mapTemplate = (template) => translateSeText(template, null, TIMER_PASSTHROUGH);
4708
+ const mapTemplate = (template) => translateSeText(template, null, TIMER_PASSTHROUGH, void 0, { context: "static" });
4641
4709
  const absoluteDate = v.date ?? v.endsAt ?? v.endDate;
4642
4710
  const target = absoluteDate ? Date.parse(absoluteDate) : NaN;
4643
4711
  const hasValidAbsoluteDate = Number.isFinite(target);
@@ -4709,7 +4777,7 @@ function mapTimer(widget, ctx) {
4709
4777
  endAnimation: "",
4710
4778
  hideAfter: 0,
4711
4779
  highlightColor: "inherit",
4712
- messageAfterCountdown: translateSeText(v.messageAfterCountdown ?? ""),
4780
+ messageAfterCountdown: translateSeText(v.messageAfterCountdown ?? "", null, void 0, void 0, { context: "static" }),
4713
4781
  changeMessageAfter: !!v.changeMessageAfterCountdown,
4714
4782
  countDate: hasValidAbsoluteDate && (!!v.exactTime || !!v.datePicker || !!absoluteDate),
4715
4783
  date: hasValidAbsoluteDate ? new Date(target).toISOString() : "",
@@ -4852,28 +4920,47 @@ function mapKappagen(widget, ctx) {
4852
4920
  ctx
4853
4921
  );
4854
4922
  }
4855
- function mapLeaderboardFromVariables(widget, currency, ctx) {
4923
+ var TOP_LIST_SORT_BY = {
4924
+ tips: "higherDonations",
4925
+ cheers: "higherBits",
4926
+ gifts: "higherGifts"
4927
+ };
4928
+ var TOP_LIST_TITLE = {
4929
+ tips: "Top Tippers",
4930
+ cheers: "Top Cheerers",
4931
+ gifts: "Top Gifters"
4932
+ };
4933
+ function mapViewerProfilesLeaderboard(widget, currency, ctx) {
4856
4934
  const v = widget.variables ?? {};
4857
4935
  const count = v.limit ?? v.visibleItems ?? 10;
4858
- const window = v.period === "weekly" ? "week" : v.period === "monthly" ? "month" : "alltime";
4859
- const title = currency === "tips" ? "Top Tippers" : currency === "cheers" ? "Top Cheerers" : "Top Gifters";
4860
- return buildUnit(widget, "loyaltyleaderboard", {
4861
- // Carry SE text styling so the leaderboard list keeps the streamer's
4936
+ return buildUnit(widget, "viewerprofiles", {
4937
+ // Carry SE text styling so the leaderboard keeps the streamer's
4862
4938
  // chosen font / size / color.
4863
4939
  css: mapSeTextCssToModuleCss(widget, { fontSize: 18 }),
4864
4940
  content: {
4865
- source: "variables",
4866
- currency,
4867
- window,
4941
+ sortBy: TOP_LIST_SORT_BY[currency],
4868
4942
  count,
4869
- title,
4943
+ title: TOP_LIST_TITLE[currency],
4870
4944
  showTitle: true,
4871
- showHeader: false,
4872
- showCurrentPoints: false,
4873
- showAllTimePoints: true,
4945
+ showAvatar: true,
4946
+ showPlatform: true,
4947
+ // Hide noisy columns that aren't relevant for a single-metric
4948
+ // top-X list — the streamer picked "top by gifts/tips/cheers"
4949
+ // for a reason. They can re-enable in Settings if they want.
4950
+ showCommands: false,
4951
+ showAlerts: false,
4952
+ showBits: currency === "cheers",
4953
+ showLastSeen: false,
4954
+ showTopActivity: false,
4955
+ showAchievements: false,
4956
+ showAchievementProgress: false,
4957
+ refreshIntervalSeconds: 30,
4874
4958
  itemGap: 8,
4959
+ autoScroll: false,
4875
4960
  // Source-side knobs that don't map cleanly survive under the
4876
- // generic importMeta envelope so the user can re-introspect.
4961
+ // generic importMeta envelope so the user can re-introspect later.
4962
+ // `period` is interesting because viewerprofiles doesn't yet have
4963
+ // a windowed mode — if/when it does, we can promote this.
4877
4964
  importMeta: buildImportMeta(widget, {
4878
4965
  period: v.period,
4879
4966
  offset: v.offset,
@@ -5019,6 +5106,47 @@ function mapHypetrain(widget, ctx) {
5019
5106
  }
5020
5107
  }, ctx);
5021
5108
  }
5109
+ function mapEmoteWall(widget, ctx) {
5110
+ const v = widget.variables ?? {};
5111
+ const imageSize = typeof v.emoteSize === "number" ? v.emoteSize : typeof v.size === "number" ? v.size : 40;
5112
+ const maxItemsToShow = typeof v.maxEmotes === "number" && v.maxEmotes > 0 ? v.maxEmotes : typeof v.maxItems === "number" && v.maxItems > 0 ? v.maxItems : 40;
5113
+ const duration = typeof v.duration === "number" && v.duration > 0 ? v.duration : typeof v.decay === "number" && v.decay > 0 ? v.decay : typeof v.decayTime === "number" && v.decayTime > 0 ? v.decayTime : 5;
5114
+ const sites = [];
5115
+ if (v.enableTwitch !== false && v.twitch !== false) sites.push("twitch");
5116
+ if (v.enableBttv !== false && v.bttv !== false) sites.push("bttv");
5117
+ if (v.enableFfz !== false && v.ffz !== false) sites.push("ffz");
5118
+ if (v.enable7tv !== false && v.sevenTv !== false) sites.push("seventv");
5119
+ const animationType = v.direction === "up" ? "rise" : v.direction === "down" ? "fall" : v.direction === "left" || v.direction === "right" ? "slide" : "random";
5120
+ return buildUnit(widget, "emotebox", {
5121
+ content: {
5122
+ version: 1,
5123
+ type: animationType,
5124
+ imageSize,
5125
+ animationTime: duration,
5126
+ duration,
5127
+ maxItemsToShow,
5128
+ delay: typeof v.delay === "number" ? v.delay : 0,
5129
+ sites,
5130
+ background: "transparent",
5131
+ theme: "dark",
5132
+ userLevels: [1, 2, 3, 4, 5, 6, 7, 8, 9],
5133
+ ignoredList: [],
5134
+ excludedAnimations: [],
5135
+ // Emote-wall is chat-driven, not alert-driven — leave alert toggles
5136
+ // off so the widget doesn't double-fire on subscribe/follower events.
5137
+ alertOn: false,
5138
+ alertTriggers: [],
5139
+ alertEmotes: [],
5140
+ alertNumberEmotes: 30,
5141
+ useAnimatedEmotes: true,
5142
+ importMeta: buildImportMeta(widget, {
5143
+ fullscreen: !!v.fullscreen,
5144
+ direction: v.direction
5145
+ })
5146
+ },
5147
+ css: { background: "transparent" }
5148
+ }, ctx);
5149
+ }
5022
5150
  function mapUnsupportedAsText(widget, ctx) {
5023
5151
  return buildUnit(widget, "text", {
5024
5152
  content: {
@@ -5418,15 +5546,16 @@ function dispatch(widget, ctx) {
5418
5546
  return { unit: mapReadout(widget, fallbackVar, ctx), status: "direct", lumiaType: "text" };
5419
5547
  }
5420
5548
  if (EVENTLIST_TYPES.has(t)) return { unit: mapEventList(widget, ctx), status: "direct", lumiaType: "eventlist" };
5421
- if (t === "se-widget-top-tippers-list") return { unit: mapLeaderboardFromVariables(widget, "tips", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
5422
- if (t === "se-widget-top-cheerers-list") return { unit: mapLeaderboardFromVariables(widget, "cheers", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
5423
- if (t === "se-widget-top-gifters-list") return { unit: mapLeaderboardFromVariables(widget, "gifts", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
5549
+ if (t === "se-widget-top-tippers-list") return { unit: mapViewerProfilesLeaderboard(widget, "tips", ctx), status: "direct", lumiaType: "viewerprofiles" };
5550
+ if (t === "se-widget-top-cheerers-list") return { unit: mapViewerProfilesLeaderboard(widget, "cheers", ctx), status: "direct", lumiaType: "viewerprofiles" };
5551
+ if (t === "se-widget-top-gifters-list") return { unit: mapViewerProfilesLeaderboard(widget, "gifts", ctx), status: "direct", lumiaType: "viewerprofiles" };
5424
5552
  if (t === "se-widget-credit-roll") return { unit: mapCredits(widget, ctx), status: "partial", lumiaType: "credits" };
5425
5553
  if (t === "se-widget-twitch-chat") return { unit: mapChatbox(widget, ctx), status: "partial", lumiaType: "chatbox" };
5426
5554
  if (t === "se-widget-image-rotator" || t === "se-widget-merch-products-rotator") return { unit: mapSlideshow(widget, ctx), status: t === "se-widget-merch-products-rotator" ? "partial" : "direct", lumiaType: "slideshow" };
5427
5555
  if (t === "se-widget-countdown") return { unit: mapTimer(widget, ctx), status: "partial", lumiaType: "timer" };
5428
5556
  if (t === "se-widget-current-song") return { unit: mapNowPlaying(widget, ctx), status: "partial", lumiaType: "nowplaying" };
5429
5557
  if (t === "se-widget-kappagen") return { unit: mapKappagen(widget, ctx), status: "partial", lumiaType: "emotealert" };
5558
+ if (t === "se-widget-emote-wall") return { unit: mapEmoteWall(widget, ctx), status: "partial", lumiaType: "emotebox" };
5430
5559
  if (t === "se-widget-giveaway") return { unit: mapGiveaway(widget, ctx), status: "direct", lumiaType: "raffle" };
5431
5560
  if (t === "se-widget-media-share") return { unit: mapMediaShare(widget, ctx), status: "partial", lumiaType: "songrequest" };
5432
5561
  if (t === "se-widget-hype-cup") return { unit: mapHypeCup(widget, ctx), status: "partial", lumiaType: "tipjar" };
package/dist/se-import.js CHANGED
@@ -18,31 +18,86 @@ var SE_TO_LUMIA_PLACEHOLDER = {
18
18
  };
19
19
  var TWITCH_CONTEXTS = {
20
20
  "follower-latest": { name: "twitch_last_follower", username: "twitch_last_follower" },
21
+ // Three SE listeners with genuinely distinct semantics — preserve the
22
+ // distinction in the variable mapping so a widget configured for "Latest
23
+ // new subscriber" doesn't render the latest *resub* (and vice versa).
24
+ // `subscriber-latest` is the umbrella (anyone subscribing); the other two
25
+ // are subtype-specific. The dedicated TWITCH_LAST_{NEW_SUBSCRIBER,
26
+ // RESUBSCRIBER} variables are written by the Twitch event pipeline on
27
+ // the matching sub.type event.
21
28
  "subscriber-latest": { name: "twitch_last_subscriber", username: "twitch_last_subscriber" },
22
- "subscriber-new-latest": { name: "twitch_last_subscriber", username: "twitch_last_subscriber" },
23
- "subscriber-resub-latest": { name: "twitch_last_subscriber", username: "twitch_last_subscriber" },
24
- "subscriber-gifted-latest": { name: "twitch_last_gifter", username: "twitch_last_gifter", amount: "twitch_last_gifter_amount", count: "twitch_last_gifter_amount" },
29
+ "subscriber-new-latest": { name: "twitch_last_new_subscriber", username: "twitch_last_new_subscriber" },
30
+ "subscriber-resub-latest": { name: "twitch_last_resubscriber", username: "twitch_last_resubscriber" },
31
+ // `subscriber-gifted-latest`: SE uses `{sender}` for the giver and `{name}` for
32
+ // the recipient (e.g. "{sender} gifted to {name}"). The two roles must point
33
+ // at distinct Lumia variables — collapsing both onto `twitch_last_gifter`
34
+ // rendered "Mr.Beast gifted to Mr.Beast" while the actual recipient was
35
+ // nowhere. `sender` keeps the gifter; `name` / `username` switch to the
36
+ // dedicated recipient variable `twitch_last_gifted`. Amount stays on the
37
+ // gifter (it's the count of subs in that drop, owned by the gifter).
38
+ "subscriber-gifted-latest": { name: "twitch_last_gifted", username: "twitch_last_gifted", sender: "twitch_last_gifter", amount: "twitch_last_gifter_amount", count: "twitch_last_gifter_amount" },
25
39
  "subscriber-alltime-gifter": { name: "twitch_alltime_top_gifter", username: "twitch_alltime_top_gifter", amount: "twitch_alltime_top_gifter_amount", count: "twitch_alltime_top_gifter_amount" },
26
40
  "cheer-latest": { name: "twitch_last_bit", username: "twitch_last_bit", amount: "twitch_last_bit_amount", count: "twitch_last_bit_amount" },
27
41
  "tip-latest": { name: "latest_donator", username: "latest_donator", amount: "latest_donator_amount", count: "latest_donator_amount", currency: "latest_donator_currency_symbol" },
28
42
  latest_donator: { name: "latest_donator", username: "latest_donator", amount: "latest_donator_amount", count: "latest_donator_amount", currency: "latest_donator_currency_symbol" },
29
43
  "raid-latest": { name: "twitch_last_raider", username: "twitch_last_raider", amount: "twitch_last_raid_amount", count: "twitch_last_raid_amount" },
30
- // Session counters: count/amount = the running session count.
44
+ // Period counters: count/amount = the running counter for that window.
45
+ // SE lets the streamer pick session / week / month / total on the same widget
46
+ // type; the chosen period lands in `widget.listener` (e.g. `cheer-week`).
47
+ // Each entry below mirrors a row in the Lumia variables table — the previous
48
+ // table only carried session+total and broke any widget configured for
49
+ // week/month because `{amount}` fell through to the unscoped placeholder
50
+ // and rendered blank.
31
51
  "follower-session": { amount: "twitch_session_follower_count", count: "twitch_session_follower_count" },
32
- "subscriber-session": { amount: "twitch_session_subscribers_count", count: "twitch_session_subscribers_count" },
33
- "cheer-session": { amount: "twitch_session_bits_count", count: "twitch_session_bits_count" },
34
- "tip-session": { amount: "session_donation_amount", count: "session_donation_count", currency: "session_donation_amount_currency_symbol" },
35
- // Totals (all-time): count/amount = total counter.
52
+ "follower-week": { amount: "twitch_week_follower_count", count: "twitch_week_follower_count" },
53
+ "follower-month": { amount: "twitch_month_follower_count", count: "twitch_month_follower_count" },
36
54
  "follower-total": { amount: "twitch_total_follower_count", count: "twitch_total_follower_count" },
55
+ "subscriber-session": { amount: "twitch_session_subscribers_count", count: "twitch_session_subscribers_count" },
56
+ "subscriber-week": { amount: "twitch_week_subscriber_count", count: "twitch_week_subscriber_count" },
57
+ "subscriber-month": { amount: "twitch_month_subscriber_count", count: "twitch_month_subscriber_count" },
37
58
  "subscriber-total": { amount: "twitch_total_subscriber_count", count: "twitch_total_subscriber_count" },
59
+ "subscriber-new-session": { amount: "twitch_session_new_subscribers_count", count: "twitch_session_new_subscribers_count" },
60
+ "subscriber-resub-session": { amount: "twitch_session_resub_subscribers_count", count: "twitch_session_resub_subscribers_count" },
61
+ "subscriber-gifted-session": { amount: "twitch_session_gifted_subscribers_count", count: "twitch_session_gifted_subscribers_count" },
62
+ "cheer-session": { amount: "twitch_session_bits_count", count: "twitch_session_bits_count" },
63
+ "cheer-week": { amount: "twitch_week_bits_count", count: "twitch_week_bits_count" },
64
+ "cheer-month": { amount: "twitch_month_bits_count", count: "twitch_month_bits_count" },
38
65
  "cheer-total": { amount: "twitch_total_bits_count", count: "twitch_total_bits_count" },
66
+ "tip-session": { amount: "session_donation_amount", count: "session_donation_count", currency: "session_donation_amount_currency_symbol" },
67
+ "tip-week": { amount: "week_donation_amount", count: "week_donation_count", currency: "week_donation_amount_currency_symbol" },
68
+ "tip-month": { amount: "month_donation_amount", count: "month_donation_count", currency: "month_donation_amount_currency_symbol" },
39
69
  "tip-total": { amount: "total_donation_amount", count: "total_donation_amount", currency: "total_donation_amount_currency_symbol" },
40
70
  "tip-count": { amount: "donation_count", count: "donation_count" },
41
- // Top cheer / cheerer / donator session readouts.
42
- "cheer-session-top-donation": { amount: "twitch_session_top_cheer_amount", count: "twitch_session_top_cheer_amount" },
71
+ // Top cheer / cheerer / donator readouts — per period. SE's `se-widget-top-*`
72
+ // widgets encode the chosen period (session / weekly / monthly / alltime) in
73
+ // the widget's `listener` field. Without these entries the user's `{amount}`
74
+ // / `{name}` template tokens fall through to the unscoped placeholder map
75
+ // (`{{amount}}` only resolves inside alert variations) and render blank.
76
+ //
77
+ // `*-top-donation` and `*-top-donator` are paired: SE nominally treats them
78
+ // as "top single event" vs "top cumulative contributor", but the data
79
+ // rendered for both lives in the same Lumia variable pair (`*_top_donator`
80
+ // / `*_top_donator_amount`, `twitch_*_top_cheerer` / `..._amount`). SE
81
+ // widgets routinely use `{name}` against the `-donation` listener to mean
82
+ // "the user who made the top donation", so the two rows below carry the
83
+ // same name/username mapping — anything else falls through to the unscoped
84
+ // `{{username}}` and renders blank.
85
+ "cheer-session-top-donation": { name: "twitch_session_top_cheerer", username: "twitch_session_top_cheerer", amount: "twitch_session_top_cheer_amount", count: "twitch_session_top_cheer_amount" },
43
86
  "cheer-session-top-donator": { name: "twitch_session_top_cheerer", username: "twitch_session_top_cheerer", amount: "twitch_session_top_cheerer_amount", count: "twitch_session_top_cheerer_amount" },
44
- "tip-session-top-donation": { amount: "session_top_donator_amount", count: "session_top_donator_amount", currency: "session_top_donator_currency_symbol" },
87
+ "cheer-weekly-top-donation": { name: "twitch_week_top_cheerer", username: "twitch_week_top_cheerer", amount: "twitch_week_top_cheerer_amount", count: "twitch_week_top_cheerer_amount" },
88
+ "cheer-weekly-top-donator": { name: "twitch_week_top_cheerer", username: "twitch_week_top_cheerer", amount: "twitch_week_top_cheerer_amount", count: "twitch_week_top_cheerer_amount" },
89
+ "cheer-monthly-top-donation": { name: "twitch_month_top_cheerer", username: "twitch_month_top_cheerer", amount: "twitch_month_top_cheerer_amount", count: "twitch_month_top_cheerer_amount" },
90
+ "cheer-monthly-top-donator": { name: "twitch_month_top_cheerer", username: "twitch_month_top_cheerer", amount: "twitch_month_top_cheerer_amount", count: "twitch_month_top_cheerer_amount" },
91
+ "cheer-alltime-top-donation": { name: "twitch_alltime_top_cheerer", username: "twitch_alltime_top_cheerer", amount: "twitch_alltime_top_cheerer_amount", count: "twitch_alltime_top_cheerer_amount" },
92
+ "cheer-alltime-top-donator": { name: "twitch_alltime_top_cheerer", username: "twitch_alltime_top_cheerer", amount: "twitch_alltime_top_cheerer_amount", count: "twitch_alltime_top_cheerer_amount" },
93
+ "tip-session-top-donation": { name: "session_top_donator", username: "session_top_donator", amount: "session_top_donator_amount", count: "session_top_donator_amount", currency: "session_top_donator_currency_symbol" },
45
94
  "tip-session-top-donator": { name: "session_top_donator", username: "session_top_donator", amount: "session_top_donator_amount", count: "session_top_donator_amount", currency: "session_top_donator_currency_symbol" },
95
+ "tip-weekly-top-donation": { name: "week_top_donator", username: "week_top_donator", amount: "week_top_donator_amount", count: "week_top_donator_amount", currency: "week_top_donator_currency_symbol" },
96
+ "tip-weekly-top-donator": { name: "week_top_donator", username: "week_top_donator", amount: "week_top_donator_amount", count: "week_top_donator_amount", currency: "week_top_donator_currency_symbol" },
97
+ "tip-monthly-top-donation": { name: "month_top_donator", username: "month_top_donator", amount: "month_top_donator_amount", count: "month_top_donator_amount", currency: "month_top_donator_currency_symbol" },
98
+ "tip-monthly-top-donator": { name: "month_top_donator", username: "month_top_donator", amount: "month_top_donator_amount", count: "month_top_donator_amount", currency: "month_top_donator_currency_symbol" },
99
+ "tip-alltime-top-donation": { name: "top_donator", username: "top_donator", amount: "top_donator_amount", count: "top_donator_amount", currency: "top_donator_currency_symbol" },
100
+ "tip-alltime-top-donator": { name: "top_donator", username: "top_donator", amount: "top_donator_amount", count: "top_donator_amount", currency: "top_donator_currency_symbol" },
46
101
  // Botcounter — SE uses `{wins}` for the running chat count; the dispatcher's
47
102
  // fallbackVar for `se-widget-botcounter` is `twitch_session_chat_count`, so
48
103
  // when the listener arg lands as that var name we treat it as the wins source.
@@ -79,7 +134,11 @@ var KICK_CONTEXTS = {
79
134
  "subscriber-latest": { name: "kick_last_subscriber", username: "kick_last_subscriber" },
80
135
  "subscriber-new-latest": { name: "kick_last_subscriber", username: "kick_last_subscriber" },
81
136
  "subscriber-resub-latest": { name: "kick_last_subscriber", username: "kick_last_subscriber" },
82
- "subscriber-gifted-latest": { name: "kick_last_gifter", username: "kick_last_gifter", amount: "kick_last_gifter_amount", count: "kick_last_gifter_amount" },
137
+ // Mirror the Twitch split: `{sender}` = giver, `{name}` = recipient.
138
+ // `kick_last_gifted` was added alongside this mapping; see twitch context
139
+ // above for the rationale (without the split, SE's "{sender} gifted to
140
+ // {name}" renders the gifter on both sides).
141
+ "subscriber-gifted-latest": { name: "kick_last_gifted", username: "kick_last_gifted", sender: "kick_last_gifter", amount: "kick_last_gifter_amount", count: "kick_last_gifter_amount" },
83
142
  "cheer-latest": { name: "kick_last_kicks", username: "kick_last_kicks", amount: "kick_last_kicks_amount", count: "kick_last_kicks_amount" },
84
143
  "raid-latest": { name: "kick_last_host", username: "kick_last_host", amount: "kick_last_host_amount", count: "kick_last_host_amount" },
85
144
  "follower-session": { amount: "kick_session_follower_count", count: "kick_session_follower_count" },
@@ -157,12 +216,13 @@ function defaultLabelForSEWidget(widget) {
157
216
  const tail = words.slice(1).join(" ");
158
217
  return tail ? `${head} ${tail}` : head;
159
218
  }
160
- function translateSeText(input, listener, passthrough, provider) {
219
+ function translateSeText(input, listener, passthrough, provider, options) {
161
220
  if (!input) return "";
162
221
  const ctx = listener ? listenerPlaceholderContext(listener, provider) : {};
222
+ const useAlertFallback = (options?.context ?? "alert") === "alert";
163
223
  return input.replace(/\{([a-zA-Z][a-zA-Z0-9_.]*)\}/g, (_match, raw) => {
164
224
  if (passthrough?.has(raw)) return `{{${raw}}}`;
165
- const mapped = ctx[raw] ?? SE_TO_LUMIA_PLACEHOLDER[raw] ?? raw;
225
+ const mapped = ctx[raw] ?? (useAlertFallback ? SE_TO_LUMIA_PLACEHOLDER[raw] : void 0) ?? raw;
166
226
  return `{{${mapped}}}`;
167
227
  });
168
228
  }
@@ -213,8 +273,8 @@ function listenerToLumiaVariable(listener, provider = "twitch") {
213
273
  // Latest events
214
274
  "follower-latest": "twitch_last_follower",
215
275
  "subscriber-latest": "twitch_last_subscriber",
216
- "subscriber-new-latest": "twitch_last_subscriber",
217
- "subscriber-resub-latest": "twitch_last_subscriber",
276
+ "subscriber-new-latest": "twitch_last_new_subscriber",
277
+ "subscriber-resub-latest": "twitch_last_resubscriber",
218
278
  "subscriber-gifted-latest": "twitch_last_gifter",
219
279
  "subscriber-alltime-gifter": "twitch_alltime_top_gifter",
220
280
  "tip-latest": "latest_donator",
@@ -369,9 +429,16 @@ var LUMIA_DEFAULT_SIZES = {
369
429
  // just the card if they want SE's exact footprint.
370
430
  songrequest: { width: 480, height: 380 }
371
431
  };
372
- function translateVariationCondition(seType, seCondition, requirement) {
432
+ function translateVariationCondition(seType, seCondition, requirement, seEventType) {
373
433
  if (seType === "gift") return { conditionType: "IS_GIFT", condition: 1 };
374
434
  if (seType === "communityGift") return { conditionType: "GIFT_SUB_GREATER", condition: requirement ?? 0 };
435
+ const isSubscriberEvent = seEventType === "subscriber";
436
+ if (isSubscriberEvent && seCondition === "ATLEAST" && requirement != null) {
437
+ return { conditionType: "SUBSCRIBED_MONTHS_GREATER", condition: requirement };
438
+ }
439
+ if (isSubscriberEvent && seCondition === "EXACT" && requirement != null) {
440
+ return { conditionType: "SUBSCRIBED_MONTHS_EQUAL", condition: requirement };
441
+ }
375
442
  if (seCondition === "EXACT" && requirement != null) return { conditionType: "EQUAL_NUMBER", condition: requirement };
376
443
  if (seCondition === "ATLEAST" && requirement != null) {
377
444
  return { conditionType: "GREATER_NUMBER", condition: requirement };
@@ -471,7 +538,7 @@ function seExtraTextCss(seCss, scrolling) {
471
538
  return extra;
472
539
  }
473
540
  function mapText(widget, ctx) {
474
- const value = translateSeText(widget.text?.value ?? "");
541
+ const value = translateSeText(widget.text?.value ?? "", null, void 0, ctx?.provider, { context: "static" });
475
542
  const seCss = widget.text?.css ?? {};
476
543
  return buildUnit(widget, "text", {
477
544
  content: {
@@ -528,7 +595,7 @@ function mapReadout(widget, fallbackVar, ctx) {
528
595
  const variable = listenerToLumiaVariable(listener, ctx?.provider);
529
596
  const userTemplate = widget.text?.value ?? "";
530
597
  const inheritsText = userTemplate.trim().length > 0 && /\{[a-zA-Z]/.test(userTemplate);
531
- const value = inheritsText ? translateSeText(userTemplate, listener, void 0, ctx?.provider) : `{{${variable}}}`;
598
+ const value = inheritsText ? translateSeText(userTemplate, listener, void 0, ctx?.provider, { context: "static" }) : `{{${variable}}}`;
532
599
  const seCss = widget.text?.css ?? {};
533
600
  return buildUnit(widget, "text", {
534
601
  content: { value, highlightColor: "inherit" },
@@ -783,7 +850,7 @@ function buildAlertEvent(seEvent, seEventType) {
783
850
  };
784
851
  }
785
852
  function buildAlertVariation(v, seEventType, parentSettings) {
786
- const translated = translateVariationCondition(v.type ?? null, v.condition ?? null, v.requirement ?? null);
853
+ const translated = translateVariationCondition(v.type ?? null, v.condition ?? null, v.requirement ?? null, seEventType);
787
854
  if (!translated) return null;
788
855
  const { variations: _parentVariations, ...parentInheritable } = parentSettings ?? {};
789
856
  const mergedSettings = deepMergeSeSettings(parentSettings ? parentInheritable : void 0, v.settings);
@@ -919,7 +986,8 @@ function mapGoal(widget, ctx) {
919
986
  // live `current` floating at the percentage position. See
920
987
  // components/Modules/Views/Goal/types.ts:GoalTypes.SLIM.
921
988
  type: "slim",
922
- label: translateSeText(v.title ?? "Goal"),
989
+ // Static label rendered above the goal bar — not an alert context.
990
+ label: translateSeText(v.title ?? "Goal", null, void 0, void 0, { context: "static" }),
923
991
  subLabel: "",
924
992
  // The Goal module renders this template literal with {{current}} + {{goal}} substituted.
925
993
  // Charity goals track donation amounts — prefix `$` so the rendered bar reads
@@ -1522,7 +1590,7 @@ function mapTimer(widget, ctx) {
1522
1590
  const v = widget.variables ?? {};
1523
1591
  const textValue = typeof widget.text?.value === "string" ? widget.text.value : void 0;
1524
1592
  const TIMER_PASSTHROUGH = /* @__PURE__ */ new Set(["days", "hours", "minutes", "seconds"]);
1525
- const mapTemplate = (template) => translateSeText(template, null, TIMER_PASSTHROUGH);
1593
+ const mapTemplate = (template) => translateSeText(template, null, TIMER_PASSTHROUGH, void 0, { context: "static" });
1526
1594
  const absoluteDate = v.date ?? v.endsAt ?? v.endDate;
1527
1595
  const target = absoluteDate ? Date.parse(absoluteDate) : NaN;
1528
1596
  const hasValidAbsoluteDate = Number.isFinite(target);
@@ -1594,7 +1662,7 @@ function mapTimer(widget, ctx) {
1594
1662
  endAnimation: "",
1595
1663
  hideAfter: 0,
1596
1664
  highlightColor: "inherit",
1597
- messageAfterCountdown: translateSeText(v.messageAfterCountdown ?? ""),
1665
+ messageAfterCountdown: translateSeText(v.messageAfterCountdown ?? "", null, void 0, void 0, { context: "static" }),
1598
1666
  changeMessageAfter: !!v.changeMessageAfterCountdown,
1599
1667
  countDate: hasValidAbsoluteDate && (!!v.exactTime || !!v.datePicker || !!absoluteDate),
1600
1668
  date: hasValidAbsoluteDate ? new Date(target).toISOString() : "",
@@ -1737,28 +1805,47 @@ function mapKappagen(widget, ctx) {
1737
1805
  ctx
1738
1806
  );
1739
1807
  }
1740
- function mapLeaderboardFromVariables(widget, currency, ctx) {
1808
+ var TOP_LIST_SORT_BY = {
1809
+ tips: "higherDonations",
1810
+ cheers: "higherBits",
1811
+ gifts: "higherGifts"
1812
+ };
1813
+ var TOP_LIST_TITLE = {
1814
+ tips: "Top Tippers",
1815
+ cheers: "Top Cheerers",
1816
+ gifts: "Top Gifters"
1817
+ };
1818
+ function mapViewerProfilesLeaderboard(widget, currency, ctx) {
1741
1819
  const v = widget.variables ?? {};
1742
1820
  const count = v.limit ?? v.visibleItems ?? 10;
1743
- const window = v.period === "weekly" ? "week" : v.period === "monthly" ? "month" : "alltime";
1744
- const title = currency === "tips" ? "Top Tippers" : currency === "cheers" ? "Top Cheerers" : "Top Gifters";
1745
- return buildUnit(widget, "loyaltyleaderboard", {
1746
- // Carry SE text styling so the leaderboard list keeps the streamer's
1821
+ return buildUnit(widget, "viewerprofiles", {
1822
+ // Carry SE text styling so the leaderboard keeps the streamer's
1747
1823
  // chosen font / size / color.
1748
1824
  css: mapSeTextCssToModuleCss(widget, { fontSize: 18 }),
1749
1825
  content: {
1750
- source: "variables",
1751
- currency,
1752
- window,
1826
+ sortBy: TOP_LIST_SORT_BY[currency],
1753
1827
  count,
1754
- title,
1828
+ title: TOP_LIST_TITLE[currency],
1755
1829
  showTitle: true,
1756
- showHeader: false,
1757
- showCurrentPoints: false,
1758
- showAllTimePoints: true,
1830
+ showAvatar: true,
1831
+ showPlatform: true,
1832
+ // Hide noisy columns that aren't relevant for a single-metric
1833
+ // top-X list — the streamer picked "top by gifts/tips/cheers"
1834
+ // for a reason. They can re-enable in Settings if they want.
1835
+ showCommands: false,
1836
+ showAlerts: false,
1837
+ showBits: currency === "cheers",
1838
+ showLastSeen: false,
1839
+ showTopActivity: false,
1840
+ showAchievements: false,
1841
+ showAchievementProgress: false,
1842
+ refreshIntervalSeconds: 30,
1759
1843
  itemGap: 8,
1844
+ autoScroll: false,
1760
1845
  // Source-side knobs that don't map cleanly survive under the
1761
- // generic importMeta envelope so the user can re-introspect.
1846
+ // generic importMeta envelope so the user can re-introspect later.
1847
+ // `period` is interesting because viewerprofiles doesn't yet have
1848
+ // a windowed mode — if/when it does, we can promote this.
1762
1849
  importMeta: buildImportMeta(widget, {
1763
1850
  period: v.period,
1764
1851
  offset: v.offset,
@@ -1904,6 +1991,47 @@ function mapHypetrain(widget, ctx) {
1904
1991
  }
1905
1992
  }, ctx);
1906
1993
  }
1994
+ function mapEmoteWall(widget, ctx) {
1995
+ const v = widget.variables ?? {};
1996
+ const imageSize = typeof v.emoteSize === "number" ? v.emoteSize : typeof v.size === "number" ? v.size : 40;
1997
+ const maxItemsToShow = typeof v.maxEmotes === "number" && v.maxEmotes > 0 ? v.maxEmotes : typeof v.maxItems === "number" && v.maxItems > 0 ? v.maxItems : 40;
1998
+ const duration = typeof v.duration === "number" && v.duration > 0 ? v.duration : typeof v.decay === "number" && v.decay > 0 ? v.decay : typeof v.decayTime === "number" && v.decayTime > 0 ? v.decayTime : 5;
1999
+ const sites = [];
2000
+ if (v.enableTwitch !== false && v.twitch !== false) sites.push("twitch");
2001
+ if (v.enableBttv !== false && v.bttv !== false) sites.push("bttv");
2002
+ if (v.enableFfz !== false && v.ffz !== false) sites.push("ffz");
2003
+ if (v.enable7tv !== false && v.sevenTv !== false) sites.push("seventv");
2004
+ const animationType = v.direction === "up" ? "rise" : v.direction === "down" ? "fall" : v.direction === "left" || v.direction === "right" ? "slide" : "random";
2005
+ return buildUnit(widget, "emotebox", {
2006
+ content: {
2007
+ version: 1,
2008
+ type: animationType,
2009
+ imageSize,
2010
+ animationTime: duration,
2011
+ duration,
2012
+ maxItemsToShow,
2013
+ delay: typeof v.delay === "number" ? v.delay : 0,
2014
+ sites,
2015
+ background: "transparent",
2016
+ theme: "dark",
2017
+ userLevels: [1, 2, 3, 4, 5, 6, 7, 8, 9],
2018
+ ignoredList: [],
2019
+ excludedAnimations: [],
2020
+ // Emote-wall is chat-driven, not alert-driven — leave alert toggles
2021
+ // off so the widget doesn't double-fire on subscribe/follower events.
2022
+ alertOn: false,
2023
+ alertTriggers: [],
2024
+ alertEmotes: [],
2025
+ alertNumberEmotes: 30,
2026
+ useAnimatedEmotes: true,
2027
+ importMeta: buildImportMeta(widget, {
2028
+ fullscreen: !!v.fullscreen,
2029
+ direction: v.direction
2030
+ })
2031
+ },
2032
+ css: { background: "transparent" }
2033
+ }, ctx);
2034
+ }
1907
2035
  function mapUnsupportedAsText(widget, ctx) {
1908
2036
  return buildUnit(widget, "text", {
1909
2037
  content: {
@@ -2303,15 +2431,16 @@ function dispatch(widget, ctx) {
2303
2431
  return { unit: mapReadout(widget, fallbackVar, ctx), status: "direct", lumiaType: "text" };
2304
2432
  }
2305
2433
  if (EVENTLIST_TYPES.has(t)) return { unit: mapEventList(widget, ctx), status: "direct", lumiaType: "eventlist" };
2306
- if (t === "se-widget-top-tippers-list") return { unit: mapLeaderboardFromVariables(widget, "tips", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
2307
- if (t === "se-widget-top-cheerers-list") return { unit: mapLeaderboardFromVariables(widget, "cheers", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
2308
- if (t === "se-widget-top-gifters-list") return { unit: mapLeaderboardFromVariables(widget, "gifts", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
2434
+ if (t === "se-widget-top-tippers-list") return { unit: mapViewerProfilesLeaderboard(widget, "tips", ctx), status: "direct", lumiaType: "viewerprofiles" };
2435
+ if (t === "se-widget-top-cheerers-list") return { unit: mapViewerProfilesLeaderboard(widget, "cheers", ctx), status: "direct", lumiaType: "viewerprofiles" };
2436
+ if (t === "se-widget-top-gifters-list") return { unit: mapViewerProfilesLeaderboard(widget, "gifts", ctx), status: "direct", lumiaType: "viewerprofiles" };
2309
2437
  if (t === "se-widget-credit-roll") return { unit: mapCredits(widget, ctx), status: "partial", lumiaType: "credits" };
2310
2438
  if (t === "se-widget-twitch-chat") return { unit: mapChatbox(widget, ctx), status: "partial", lumiaType: "chatbox" };
2311
2439
  if (t === "se-widget-image-rotator" || t === "se-widget-merch-products-rotator") return { unit: mapSlideshow(widget, ctx), status: t === "se-widget-merch-products-rotator" ? "partial" : "direct", lumiaType: "slideshow" };
2312
2440
  if (t === "se-widget-countdown") return { unit: mapTimer(widget, ctx), status: "partial", lumiaType: "timer" };
2313
2441
  if (t === "se-widget-current-song") return { unit: mapNowPlaying(widget, ctx), status: "partial", lumiaType: "nowplaying" };
2314
2442
  if (t === "se-widget-kappagen") return { unit: mapKappagen(widget, ctx), status: "partial", lumiaType: "emotealert" };
2443
+ if (t === "se-widget-emote-wall") return { unit: mapEmoteWall(widget, ctx), status: "partial", lumiaType: "emotebox" };
2315
2444
  if (t === "se-widget-giveaway") return { unit: mapGiveaway(widget, ctx), status: "direct", lumiaType: "raffle" };
2316
2445
  if (t === "se-widget-media-share") return { unit: mapMediaShare(widget, ctx), status: "partial", lumiaType: "songrequest" };
2317
2446
  if (t === "se-widget-hype-cup") return { unit: mapHypeCup(widget, ctx), status: "partial", lumiaType: "tipjar" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumiastream/ui",
3
- "version": "0.2.8-alpha.27",
3
+ "version": "0.2.8-alpha.28",
4
4
  "author": "Lumia Stream",
5
5
  "license": "ISC",
6
6
  "description": "Lumia UI Kit",