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

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 };
@@ -3530,9 +3597,10 @@ function mapSeTextCssToModuleCss(widget, defaults = {}) {
3530
3597
  background: defaults.background ?? "transparent"
3531
3598
  };
3532
3599
  }
3533
- function buildUnit(widget, lumiaType, moduleExtras, ctx) {
3600
+ var READOUT_AUTO_FALLBACK_SIZE = { width: 400, height: 50 };
3601
+ function buildUnit(widget, lumiaType, moduleExtras, ctx, sizeOverride) {
3534
3602
  const id = nanoid();
3535
- const defaults = LUMIA_DEFAULT_SIZES[lumiaType] ?? { width: 400, height: 400 };
3603
+ const defaults = sizeOverride ?? LUMIA_DEFAULT_SIZES[lumiaType] ?? { width: 400, height: 400 };
3536
3604
  const bounds = seCssToBounds(widget.css, defaults, ctx?.canvas);
3537
3605
  const layer = {
3538
3606
  id,
@@ -3586,7 +3654,7 @@ function seExtraTextCss(seCss, scrolling) {
3586
3654
  return extra;
3587
3655
  }
3588
3656
  function mapText(widget, ctx) {
3589
- const value = translateSeText(widget.text?.value ?? "");
3657
+ const value = translateSeText(widget.text?.value ?? "", null, void 0, ctx?.provider, { context: "static" });
3590
3658
  const seCss = widget.text?.css ?? {};
3591
3659
  return buildUnit(widget, "text", {
3592
3660
  content: {
@@ -3643,22 +3711,32 @@ function mapReadout(widget, fallbackVar, ctx) {
3643
3711
  const variable = listenerToLumiaVariable(listener, ctx?.provider);
3644
3712
  const userTemplate = widget.text?.value ?? "";
3645
3713
  const inheritsText = userTemplate.trim().length > 0 && /\{[a-zA-Z]/.test(userTemplate);
3646
- const value = inheritsText ? translateSeText(userTemplate, listener, void 0, ctx?.provider) : `{{${variable}}}`;
3714
+ const value = inheritsText ? translateSeText(userTemplate, listener, void 0, ctx?.provider, { context: "static" }) : `{{${variable}}}`;
3647
3715
  const seCss = widget.text?.css ?? {};
3648
- return buildUnit(widget, "text", {
3649
- content: { value, highlightColor: "inherit" },
3650
- css: {
3651
- fontSize: seCss["font-size"] ?? 28,
3652
- textAlign: seCss["text-align"] ?? "center",
3653
- fontFamily: seCss["font-family"] ?? "Roboto",
3654
- fontWeight: seCss["font-weight"] ?? "bold",
3655
- color: seCss["color"] ?? "#ffffff",
3656
- textShadow: seCss["text-shadow"] ?? "rgb(0, 0, 0) 1px 1px 1px",
3657
- lineHeight: seCss["line-height"] ?? 1,
3658
- background: "transparent",
3659
- ...seExtraTextCss(seCss, widget.text?.scrolling)
3660
- }
3661
- }, ctx);
3716
+ return buildUnit(
3717
+ widget,
3718
+ "text",
3719
+ {
3720
+ content: { value, highlightColor: "inherit" },
3721
+ css: {
3722
+ fontSize: seCss["font-size"] ?? 28,
3723
+ textAlign: seCss["text-align"] ?? "center",
3724
+ fontFamily: seCss["font-family"] ?? "Roboto",
3725
+ fontWeight: seCss["font-weight"] ?? "bold",
3726
+ color: seCss["color"] ?? "#ffffff",
3727
+ textShadow: seCss["text-shadow"] ?? "rgb(0, 0, 0) 1px 1px 1px",
3728
+ lineHeight: seCss["line-height"] ?? 1,
3729
+ background: "transparent",
3730
+ ...seExtraTextCss(seCss, widget.text?.scrolling)
3731
+ }
3732
+ },
3733
+ ctx,
3734
+ // SE readouts ship `width:auto; height:auto`. Override the generic
3735
+ // `text` 600×200 fallback with a single-line-friendly size so the
3736
+ // imported layer doesn't render as a half-canvas rectangle when the
3737
+ // rendered content is one line of 24px text.
3738
+ READOUT_AUTO_FALLBACK_SIZE
3739
+ );
3662
3740
  }
3663
3741
 
3664
3742
  // src/se-import/mappers/provider-alerts.ts
@@ -3898,7 +3976,7 @@ function buildAlertEvent(seEvent, seEventType) {
3898
3976
  };
3899
3977
  }
3900
3978
  function buildAlertVariation(v, seEventType, parentSettings) {
3901
- const translated = translateVariationCondition(v.type ?? null, v.condition ?? null, v.requirement ?? null);
3979
+ const translated = translateVariationCondition(v.type ?? null, v.condition ?? null, v.requirement ?? null, seEventType);
3902
3980
  if (!translated) return null;
3903
3981
  const { variations: _parentVariations, ...parentInheritable } = parentSettings ?? {};
3904
3982
  const mergedSettings = deepMergeSeSettings(parentSettings ? parentInheritable : void 0, v.settings);
@@ -4034,7 +4112,8 @@ function mapGoal(widget, ctx) {
4034
4112
  // live `current` floating at the percentage position. See
4035
4113
  // components/Modules/Views/Goal/types.ts:GoalTypes.SLIM.
4036
4114
  type: "slim",
4037
- label: translateSeText(v.title ?? "Goal"),
4115
+ // Static label rendered above the goal bar — not an alert context.
4116
+ label: translateSeText(v.title ?? "Goal", null, void 0, void 0, { context: "static" }),
4038
4117
  subLabel: "",
4039
4118
  // The Goal module renders this template literal with {{current}} + {{goal}} substituted.
4040
4119
  // Charity goals track donation amounts — prefix `$` so the rendered bar reads
@@ -4637,7 +4716,7 @@ function mapTimer(widget, ctx) {
4637
4716
  const v = widget.variables ?? {};
4638
4717
  const textValue = typeof widget.text?.value === "string" ? widget.text.value : void 0;
4639
4718
  const TIMER_PASSTHROUGH = /* @__PURE__ */ new Set(["days", "hours", "minutes", "seconds"]);
4640
- const mapTemplate = (template) => translateSeText(template, null, TIMER_PASSTHROUGH);
4719
+ const mapTemplate = (template) => translateSeText(template, null, TIMER_PASSTHROUGH, void 0, { context: "static" });
4641
4720
  const absoluteDate = v.date ?? v.endsAt ?? v.endDate;
4642
4721
  const target = absoluteDate ? Date.parse(absoluteDate) : NaN;
4643
4722
  const hasValidAbsoluteDate = Number.isFinite(target);
@@ -4709,7 +4788,7 @@ function mapTimer(widget, ctx) {
4709
4788
  endAnimation: "",
4710
4789
  hideAfter: 0,
4711
4790
  highlightColor: "inherit",
4712
- messageAfterCountdown: translateSeText(v.messageAfterCountdown ?? ""),
4791
+ messageAfterCountdown: translateSeText(v.messageAfterCountdown ?? "", null, void 0, void 0, { context: "static" }),
4713
4792
  changeMessageAfter: !!v.changeMessageAfterCountdown,
4714
4793
  countDate: hasValidAbsoluteDate && (!!v.exactTime || !!v.datePicker || !!absoluteDate),
4715
4794
  date: hasValidAbsoluteDate ? new Date(target).toISOString() : "",
@@ -4852,28 +4931,47 @@ function mapKappagen(widget, ctx) {
4852
4931
  ctx
4853
4932
  );
4854
4933
  }
4855
- function mapLeaderboardFromVariables(widget, currency, ctx) {
4934
+ var TOP_LIST_SORT_BY = {
4935
+ tips: "higherDonations",
4936
+ cheers: "higherBits",
4937
+ gifts: "higherGifts"
4938
+ };
4939
+ var TOP_LIST_TITLE = {
4940
+ tips: "Top Tippers",
4941
+ cheers: "Top Cheerers",
4942
+ gifts: "Top Gifters"
4943
+ };
4944
+ function mapViewerProfilesLeaderboard(widget, currency, ctx) {
4856
4945
  const v = widget.variables ?? {};
4857
4946
  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
4947
+ return buildUnit(widget, "viewerprofiles", {
4948
+ // Carry SE text styling so the leaderboard keeps the streamer's
4862
4949
  // chosen font / size / color.
4863
4950
  css: mapSeTextCssToModuleCss(widget, { fontSize: 18 }),
4864
4951
  content: {
4865
- source: "variables",
4866
- currency,
4867
- window,
4952
+ sortBy: TOP_LIST_SORT_BY[currency],
4868
4953
  count,
4869
- title,
4954
+ title: TOP_LIST_TITLE[currency],
4870
4955
  showTitle: true,
4871
- showHeader: false,
4872
- showCurrentPoints: false,
4873
- showAllTimePoints: true,
4956
+ showAvatar: true,
4957
+ showPlatform: true,
4958
+ // Hide noisy columns that aren't relevant for a single-metric
4959
+ // top-X list — the streamer picked "top by gifts/tips/cheers"
4960
+ // for a reason. They can re-enable in Settings if they want.
4961
+ showCommands: false,
4962
+ showAlerts: false,
4963
+ showBits: currency === "cheers",
4964
+ showLastSeen: false,
4965
+ showTopActivity: false,
4966
+ showAchievements: false,
4967
+ showAchievementProgress: false,
4968
+ refreshIntervalSeconds: 30,
4874
4969
  itemGap: 8,
4970
+ autoScroll: false,
4875
4971
  // Source-side knobs that don't map cleanly survive under the
4876
- // generic importMeta envelope so the user can re-introspect.
4972
+ // generic importMeta envelope so the user can re-introspect later.
4973
+ // `period` is interesting because viewerprofiles doesn't yet have
4974
+ // a windowed mode — if/when it does, we can promote this.
4877
4975
  importMeta: buildImportMeta(widget, {
4878
4976
  period: v.period,
4879
4977
  offset: v.offset,
@@ -5019,6 +5117,47 @@ function mapHypetrain(widget, ctx) {
5019
5117
  }
5020
5118
  }, ctx);
5021
5119
  }
5120
+ function mapEmoteWall(widget, ctx) {
5121
+ const v = widget.variables ?? {};
5122
+ const imageSize = typeof v.emoteSize === "number" ? v.emoteSize : typeof v.size === "number" ? v.size : 40;
5123
+ const maxItemsToShow = typeof v.maxEmotes === "number" && v.maxEmotes > 0 ? v.maxEmotes : typeof v.maxItems === "number" && v.maxItems > 0 ? v.maxItems : 40;
5124
+ 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;
5125
+ const sites = [];
5126
+ if (v.enableTwitch !== false && v.twitch !== false) sites.push("twitch");
5127
+ if (v.enableBttv !== false && v.bttv !== false) sites.push("bttv");
5128
+ if (v.enableFfz !== false && v.ffz !== false) sites.push("ffz");
5129
+ if (v.enable7tv !== false && v.sevenTv !== false) sites.push("seventv");
5130
+ const animationType = v.direction === "up" ? "rise" : v.direction === "down" ? "fall" : v.direction === "left" || v.direction === "right" ? "slide" : "random";
5131
+ return buildUnit(widget, "emotebox", {
5132
+ content: {
5133
+ version: 1,
5134
+ type: animationType,
5135
+ imageSize,
5136
+ animationTime: duration,
5137
+ duration,
5138
+ maxItemsToShow,
5139
+ delay: typeof v.delay === "number" ? v.delay : 0,
5140
+ sites,
5141
+ background: "transparent",
5142
+ theme: "dark",
5143
+ userLevels: [1, 2, 3, 4, 5, 6, 7, 8, 9],
5144
+ ignoredList: [],
5145
+ excludedAnimations: [],
5146
+ // Emote-wall is chat-driven, not alert-driven — leave alert toggles
5147
+ // off so the widget doesn't double-fire on subscribe/follower events.
5148
+ alertOn: false,
5149
+ alertTriggers: [],
5150
+ alertEmotes: [],
5151
+ alertNumberEmotes: 30,
5152
+ useAnimatedEmotes: true,
5153
+ importMeta: buildImportMeta(widget, {
5154
+ fullscreen: !!v.fullscreen,
5155
+ direction: v.direction
5156
+ })
5157
+ },
5158
+ css: { background: "transparent" }
5159
+ }, ctx);
5160
+ }
5022
5161
  function mapUnsupportedAsText(widget, ctx) {
5023
5162
  return buildUnit(widget, "text", {
5024
5163
  content: {
@@ -5418,15 +5557,16 @@ function dispatch(widget, ctx) {
5418
5557
  return { unit: mapReadout(widget, fallbackVar, ctx), status: "direct", lumiaType: "text" };
5419
5558
  }
5420
5559
  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" };
5560
+ if (t === "se-widget-top-tippers-list") return { unit: mapViewerProfilesLeaderboard(widget, "tips", ctx), status: "direct", lumiaType: "viewerprofiles" };
5561
+ if (t === "se-widget-top-cheerers-list") return { unit: mapViewerProfilesLeaderboard(widget, "cheers", ctx), status: "direct", lumiaType: "viewerprofiles" };
5562
+ if (t === "se-widget-top-gifters-list") return { unit: mapViewerProfilesLeaderboard(widget, "gifts", ctx), status: "direct", lumiaType: "viewerprofiles" };
5424
5563
  if (t === "se-widget-credit-roll") return { unit: mapCredits(widget, ctx), status: "partial", lumiaType: "credits" };
5425
5564
  if (t === "se-widget-twitch-chat") return { unit: mapChatbox(widget, ctx), status: "partial", lumiaType: "chatbox" };
5426
5565
  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
5566
  if (t === "se-widget-countdown") return { unit: mapTimer(widget, ctx), status: "partial", lumiaType: "timer" };
5428
5567
  if (t === "se-widget-current-song") return { unit: mapNowPlaying(widget, ctx), status: "partial", lumiaType: "nowplaying" };
5429
5568
  if (t === "se-widget-kappagen") return { unit: mapKappagen(widget, ctx), status: "partial", lumiaType: "emotealert" };
5569
+ if (t === "se-widget-emote-wall") return { unit: mapEmoteWall(widget, ctx), status: "partial", lumiaType: "emotebox" };
5430
5570
  if (t === "se-widget-giveaway") return { unit: mapGiveaway(widget, ctx), status: "direct", lumiaType: "raffle" };
5431
5571
  if (t === "se-widget-media-share") return { unit: mapMediaShare(widget, ctx), status: "partial", lumiaType: "songrequest" };
5432
5572
  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 };
@@ -415,9 +482,10 @@ function mapSeTextCssToModuleCss(widget, defaults = {}) {
415
482
  background: defaults.background ?? "transparent"
416
483
  };
417
484
  }
418
- function buildUnit(widget, lumiaType, moduleExtras, ctx) {
485
+ var READOUT_AUTO_FALLBACK_SIZE = { width: 400, height: 50 };
486
+ function buildUnit(widget, lumiaType, moduleExtras, ctx, sizeOverride) {
419
487
  const id = nanoid();
420
- const defaults = LUMIA_DEFAULT_SIZES[lumiaType] ?? { width: 400, height: 400 };
488
+ const defaults = sizeOverride ?? LUMIA_DEFAULT_SIZES[lumiaType] ?? { width: 400, height: 400 };
421
489
  const bounds = seCssToBounds(widget.css, defaults, ctx?.canvas);
422
490
  const layer = {
423
491
  id,
@@ -471,7 +539,7 @@ function seExtraTextCss(seCss, scrolling) {
471
539
  return extra;
472
540
  }
473
541
  function mapText(widget, ctx) {
474
- const value = translateSeText(widget.text?.value ?? "");
542
+ const value = translateSeText(widget.text?.value ?? "", null, void 0, ctx?.provider, { context: "static" });
475
543
  const seCss = widget.text?.css ?? {};
476
544
  return buildUnit(widget, "text", {
477
545
  content: {
@@ -528,22 +596,32 @@ function mapReadout(widget, fallbackVar, ctx) {
528
596
  const variable = listenerToLumiaVariable(listener, ctx?.provider);
529
597
  const userTemplate = widget.text?.value ?? "";
530
598
  const inheritsText = userTemplate.trim().length > 0 && /\{[a-zA-Z]/.test(userTemplate);
531
- const value = inheritsText ? translateSeText(userTemplate, listener, void 0, ctx?.provider) : `{{${variable}}}`;
599
+ const value = inheritsText ? translateSeText(userTemplate, listener, void 0, ctx?.provider, { context: "static" }) : `{{${variable}}}`;
532
600
  const seCss = widget.text?.css ?? {};
533
- return buildUnit(widget, "text", {
534
- content: { value, highlightColor: "inherit" },
535
- css: {
536
- fontSize: seCss["font-size"] ?? 28,
537
- textAlign: seCss["text-align"] ?? "center",
538
- fontFamily: seCss["font-family"] ?? "Roboto",
539
- fontWeight: seCss["font-weight"] ?? "bold",
540
- color: seCss["color"] ?? "#ffffff",
541
- textShadow: seCss["text-shadow"] ?? "rgb(0, 0, 0) 1px 1px 1px",
542
- lineHeight: seCss["line-height"] ?? 1,
543
- background: "transparent",
544
- ...seExtraTextCss(seCss, widget.text?.scrolling)
545
- }
546
- }, ctx);
601
+ return buildUnit(
602
+ widget,
603
+ "text",
604
+ {
605
+ content: { value, highlightColor: "inherit" },
606
+ css: {
607
+ fontSize: seCss["font-size"] ?? 28,
608
+ textAlign: seCss["text-align"] ?? "center",
609
+ fontFamily: seCss["font-family"] ?? "Roboto",
610
+ fontWeight: seCss["font-weight"] ?? "bold",
611
+ color: seCss["color"] ?? "#ffffff",
612
+ textShadow: seCss["text-shadow"] ?? "rgb(0, 0, 0) 1px 1px 1px",
613
+ lineHeight: seCss["line-height"] ?? 1,
614
+ background: "transparent",
615
+ ...seExtraTextCss(seCss, widget.text?.scrolling)
616
+ }
617
+ },
618
+ ctx,
619
+ // SE readouts ship `width:auto; height:auto`. Override the generic
620
+ // `text` 600×200 fallback with a single-line-friendly size so the
621
+ // imported layer doesn't render as a half-canvas rectangle when the
622
+ // rendered content is one line of 24px text.
623
+ READOUT_AUTO_FALLBACK_SIZE
624
+ );
547
625
  }
548
626
 
549
627
  // src/se-import/mappers/provider-alerts.ts
@@ -783,7 +861,7 @@ function buildAlertEvent(seEvent, seEventType) {
783
861
  };
784
862
  }
785
863
  function buildAlertVariation(v, seEventType, parentSettings) {
786
- const translated = translateVariationCondition(v.type ?? null, v.condition ?? null, v.requirement ?? null);
864
+ const translated = translateVariationCondition(v.type ?? null, v.condition ?? null, v.requirement ?? null, seEventType);
787
865
  if (!translated) return null;
788
866
  const { variations: _parentVariations, ...parentInheritable } = parentSettings ?? {};
789
867
  const mergedSettings = deepMergeSeSettings(parentSettings ? parentInheritable : void 0, v.settings);
@@ -919,7 +997,8 @@ function mapGoal(widget, ctx) {
919
997
  // live `current` floating at the percentage position. See
920
998
  // components/Modules/Views/Goal/types.ts:GoalTypes.SLIM.
921
999
  type: "slim",
922
- label: translateSeText(v.title ?? "Goal"),
1000
+ // Static label rendered above the goal bar — not an alert context.
1001
+ label: translateSeText(v.title ?? "Goal", null, void 0, void 0, { context: "static" }),
923
1002
  subLabel: "",
924
1003
  // The Goal module renders this template literal with {{current}} + {{goal}} substituted.
925
1004
  // Charity goals track donation amounts — prefix `$` so the rendered bar reads
@@ -1522,7 +1601,7 @@ function mapTimer(widget, ctx) {
1522
1601
  const v = widget.variables ?? {};
1523
1602
  const textValue = typeof widget.text?.value === "string" ? widget.text.value : void 0;
1524
1603
  const TIMER_PASSTHROUGH = /* @__PURE__ */ new Set(["days", "hours", "minutes", "seconds"]);
1525
- const mapTemplate = (template) => translateSeText(template, null, TIMER_PASSTHROUGH);
1604
+ const mapTemplate = (template) => translateSeText(template, null, TIMER_PASSTHROUGH, void 0, { context: "static" });
1526
1605
  const absoluteDate = v.date ?? v.endsAt ?? v.endDate;
1527
1606
  const target = absoluteDate ? Date.parse(absoluteDate) : NaN;
1528
1607
  const hasValidAbsoluteDate = Number.isFinite(target);
@@ -1594,7 +1673,7 @@ function mapTimer(widget, ctx) {
1594
1673
  endAnimation: "",
1595
1674
  hideAfter: 0,
1596
1675
  highlightColor: "inherit",
1597
- messageAfterCountdown: translateSeText(v.messageAfterCountdown ?? ""),
1676
+ messageAfterCountdown: translateSeText(v.messageAfterCountdown ?? "", null, void 0, void 0, { context: "static" }),
1598
1677
  changeMessageAfter: !!v.changeMessageAfterCountdown,
1599
1678
  countDate: hasValidAbsoluteDate && (!!v.exactTime || !!v.datePicker || !!absoluteDate),
1600
1679
  date: hasValidAbsoluteDate ? new Date(target).toISOString() : "",
@@ -1737,28 +1816,47 @@ function mapKappagen(widget, ctx) {
1737
1816
  ctx
1738
1817
  );
1739
1818
  }
1740
- function mapLeaderboardFromVariables(widget, currency, ctx) {
1819
+ var TOP_LIST_SORT_BY = {
1820
+ tips: "higherDonations",
1821
+ cheers: "higherBits",
1822
+ gifts: "higherGifts"
1823
+ };
1824
+ var TOP_LIST_TITLE = {
1825
+ tips: "Top Tippers",
1826
+ cheers: "Top Cheerers",
1827
+ gifts: "Top Gifters"
1828
+ };
1829
+ function mapViewerProfilesLeaderboard(widget, currency, ctx) {
1741
1830
  const v = widget.variables ?? {};
1742
1831
  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
1832
+ return buildUnit(widget, "viewerprofiles", {
1833
+ // Carry SE text styling so the leaderboard keeps the streamer's
1747
1834
  // chosen font / size / color.
1748
1835
  css: mapSeTextCssToModuleCss(widget, { fontSize: 18 }),
1749
1836
  content: {
1750
- source: "variables",
1751
- currency,
1752
- window,
1837
+ sortBy: TOP_LIST_SORT_BY[currency],
1753
1838
  count,
1754
- title,
1839
+ title: TOP_LIST_TITLE[currency],
1755
1840
  showTitle: true,
1756
- showHeader: false,
1757
- showCurrentPoints: false,
1758
- showAllTimePoints: true,
1841
+ showAvatar: true,
1842
+ showPlatform: true,
1843
+ // Hide noisy columns that aren't relevant for a single-metric
1844
+ // top-X list — the streamer picked "top by gifts/tips/cheers"
1845
+ // for a reason. They can re-enable in Settings if they want.
1846
+ showCommands: false,
1847
+ showAlerts: false,
1848
+ showBits: currency === "cheers",
1849
+ showLastSeen: false,
1850
+ showTopActivity: false,
1851
+ showAchievements: false,
1852
+ showAchievementProgress: false,
1853
+ refreshIntervalSeconds: 30,
1759
1854
  itemGap: 8,
1855
+ autoScroll: false,
1760
1856
  // Source-side knobs that don't map cleanly survive under the
1761
- // generic importMeta envelope so the user can re-introspect.
1857
+ // generic importMeta envelope so the user can re-introspect later.
1858
+ // `period` is interesting because viewerprofiles doesn't yet have
1859
+ // a windowed mode — if/when it does, we can promote this.
1762
1860
  importMeta: buildImportMeta(widget, {
1763
1861
  period: v.period,
1764
1862
  offset: v.offset,
@@ -1904,6 +2002,47 @@ function mapHypetrain(widget, ctx) {
1904
2002
  }
1905
2003
  }, ctx);
1906
2004
  }
2005
+ function mapEmoteWall(widget, ctx) {
2006
+ const v = widget.variables ?? {};
2007
+ const imageSize = typeof v.emoteSize === "number" ? v.emoteSize : typeof v.size === "number" ? v.size : 40;
2008
+ const maxItemsToShow = typeof v.maxEmotes === "number" && v.maxEmotes > 0 ? v.maxEmotes : typeof v.maxItems === "number" && v.maxItems > 0 ? v.maxItems : 40;
2009
+ 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;
2010
+ const sites = [];
2011
+ if (v.enableTwitch !== false && v.twitch !== false) sites.push("twitch");
2012
+ if (v.enableBttv !== false && v.bttv !== false) sites.push("bttv");
2013
+ if (v.enableFfz !== false && v.ffz !== false) sites.push("ffz");
2014
+ if (v.enable7tv !== false && v.sevenTv !== false) sites.push("seventv");
2015
+ const animationType = v.direction === "up" ? "rise" : v.direction === "down" ? "fall" : v.direction === "left" || v.direction === "right" ? "slide" : "random";
2016
+ return buildUnit(widget, "emotebox", {
2017
+ content: {
2018
+ version: 1,
2019
+ type: animationType,
2020
+ imageSize,
2021
+ animationTime: duration,
2022
+ duration,
2023
+ maxItemsToShow,
2024
+ delay: typeof v.delay === "number" ? v.delay : 0,
2025
+ sites,
2026
+ background: "transparent",
2027
+ theme: "dark",
2028
+ userLevels: [1, 2, 3, 4, 5, 6, 7, 8, 9],
2029
+ ignoredList: [],
2030
+ excludedAnimations: [],
2031
+ // Emote-wall is chat-driven, not alert-driven — leave alert toggles
2032
+ // off so the widget doesn't double-fire on subscribe/follower events.
2033
+ alertOn: false,
2034
+ alertTriggers: [],
2035
+ alertEmotes: [],
2036
+ alertNumberEmotes: 30,
2037
+ useAnimatedEmotes: true,
2038
+ importMeta: buildImportMeta(widget, {
2039
+ fullscreen: !!v.fullscreen,
2040
+ direction: v.direction
2041
+ })
2042
+ },
2043
+ css: { background: "transparent" }
2044
+ }, ctx);
2045
+ }
1907
2046
  function mapUnsupportedAsText(widget, ctx) {
1908
2047
  return buildUnit(widget, "text", {
1909
2048
  content: {
@@ -2303,15 +2442,16 @@ function dispatch(widget, ctx) {
2303
2442
  return { unit: mapReadout(widget, fallbackVar, ctx), status: "direct", lumiaType: "text" };
2304
2443
  }
2305
2444
  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" };
2445
+ if (t === "se-widget-top-tippers-list") return { unit: mapViewerProfilesLeaderboard(widget, "tips", ctx), status: "direct", lumiaType: "viewerprofiles" };
2446
+ if (t === "se-widget-top-cheerers-list") return { unit: mapViewerProfilesLeaderboard(widget, "cheers", ctx), status: "direct", lumiaType: "viewerprofiles" };
2447
+ if (t === "se-widget-top-gifters-list") return { unit: mapViewerProfilesLeaderboard(widget, "gifts", ctx), status: "direct", lumiaType: "viewerprofiles" };
2309
2448
  if (t === "se-widget-credit-roll") return { unit: mapCredits(widget, ctx), status: "partial", lumiaType: "credits" };
2310
2449
  if (t === "se-widget-twitch-chat") return { unit: mapChatbox(widget, ctx), status: "partial", lumiaType: "chatbox" };
2311
2450
  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
2451
  if (t === "se-widget-countdown") return { unit: mapTimer(widget, ctx), status: "partial", lumiaType: "timer" };
2313
2452
  if (t === "se-widget-current-song") return { unit: mapNowPlaying(widget, ctx), status: "partial", lumiaType: "nowplaying" };
2314
2453
  if (t === "se-widget-kappagen") return { unit: mapKappagen(widget, ctx), status: "partial", lumiaType: "emotealert" };
2454
+ if (t === "se-widget-emote-wall") return { unit: mapEmoteWall(widget, ctx), status: "partial", lumiaType: "emotebox" };
2315
2455
  if (t === "se-widget-giveaway") return { unit: mapGiveaway(widget, ctx), status: "direct", lumiaType: "raffle" };
2316
2456
  if (t === "se-widget-media-share") return { unit: mapMediaShare(widget, ctx), status: "partial", lumiaType: "songrequest" };
2317
2457
  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.29",
4
4
  "author": "Lumia Stream",
5
5
  "license": "ISC",
6
6
  "description": "Lumia UI Kit",