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

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
@@ -3226,9 +3226,17 @@ var YOUTUBE_CONTEXTS = {
3226
3226
  "subscriber-week": { amount: "youtube_week_member_count", count: "youtube_week_member_count" },
3227
3227
  "subscriber-month": { amount: "youtube_month_member_count", count: "youtube_month_member_count" }
3228
3228
  };
3229
+ var RECENT_TO_LATEST_LISTENER = {
3230
+ "follower-recent": "follower-latest",
3231
+ "subscriber-recent": "subscriber-latest",
3232
+ "cheer-recent": "cheer-latest",
3233
+ "tip-recent": "tip-latest",
3234
+ "raid-recent": "raid-latest"
3235
+ };
3229
3236
  function listenerPlaceholderContext(listener, provider = "twitch") {
3237
+ const resolved = RECENT_TO_LATEST_LISTENER[listener] ?? listener;
3230
3238
  const platformTable = provider === "kick" ? KICK_CONTEXTS : provider === "youtube" ? YOUTUBE_CONTEXTS : null;
3231
- return platformTable?.[listener] ?? TWITCH_CONTEXTS[listener] ?? {};
3239
+ return platformTable?.[resolved] ?? TWITCH_CONTEXTS[resolved] ?? {};
3232
3240
  }
3233
3241
  var SE_WIDGET_TYPE_LABELS = {
3234
3242
  "subscriber-latest": "Latest subscriber",
@@ -3313,8 +3321,9 @@ var YOUTUBE_LISTENER_VARS = {
3313
3321
  };
3314
3322
  function listenerToLumiaVariable(listener, provider = "twitch") {
3315
3323
  if (typeof listener !== "string" || !listener) return "username";
3316
- if (provider === "kick" && KICK_LISTENER_VARS[listener]) return KICK_LISTENER_VARS[listener];
3317
- if (provider === "youtube" && YOUTUBE_LISTENER_VARS[listener]) return YOUTUBE_LISTENER_VARS[listener];
3324
+ const resolvedListener = RECENT_TO_LATEST_LISTENER[listener] ?? listener;
3325
+ if (provider === "kick" && KICK_LISTENER_VARS[resolvedListener]) return KICK_LISTENER_VARS[resolvedListener];
3326
+ if (provider === "youtube" && YOUTUBE_LISTENER_VARS[resolvedListener]) return YOUTUBE_LISTENER_VARS[resolvedListener];
3318
3327
  const m = {
3319
3328
  // Latest events
3320
3329
  "follower-latest": "twitch_last_follower",
@@ -3371,7 +3380,7 @@ function listenerToLumiaVariable(listener, provider = "twitch") {
3371
3380
  "cheer-alltime-top-donation": "twitch_alltime_top_cheerer_amount",
3372
3381
  "cheer-alltime-top-donator": "twitch_alltime_top_cheerer"
3373
3382
  };
3374
- return m[listener] ?? listener.replace(/-/g, "_");
3383
+ return m[resolvedListener] ?? resolvedListener.replace(/-/g, "_");
3375
3384
  }
3376
3385
  function seCssToBounds(css, defaults = { width: 600, height: 200 }, canvas) {
3377
3386
  let widthIsAuto = false;
@@ -3502,6 +3511,25 @@ function seLayoutUsesPushText(seLayout) {
3502
3511
 
3503
3512
  // src/se-import/mappers/build.ts
3504
3513
  import { nanoid } from "nanoid";
3514
+ function mapSeTextCssToModuleCss(widget, defaults = {}) {
3515
+ const seTextCss = widget.text?.css ?? {};
3516
+ const asStr = (v, fallback) => typeof v === "string" && v.length > 0 ? v : fallback;
3517
+ const asNumOrStr = (v, fallback) => {
3518
+ if (typeof v === "number" && Number.isFinite(v)) return v;
3519
+ if (typeof v === "string" && v.length > 0) return v;
3520
+ return fallback;
3521
+ };
3522
+ return {
3523
+ lineHeight: asNumOrStr(seTextCss["line-height"], defaults.lineHeight ?? 1),
3524
+ fontSize: asNumOrStr(seTextCss["font-size"], defaults.fontSize ?? 24),
3525
+ textAlign: asStr(seTextCss["text-align"], defaults.textAlign ?? "center"),
3526
+ fontFamily: asStr(seTextCss["font-family"], defaults.fontFamily ?? "Roboto"),
3527
+ fontWeight: asStr(seTextCss["font-weight"], defaults.fontWeight ?? "bold"),
3528
+ color: asStr(seTextCss.color, defaults.color ?? "#ffffff"),
3529
+ textShadow: asStr(seTextCss["text-shadow"], defaults.textShadow ?? "rgb(0, 0, 0) 1px 1px 1px"),
3530
+ background: defaults.background ?? "transparent"
3531
+ };
3532
+ }
3505
3533
  function buildUnit(widget, lumiaType, moduleExtras, ctx) {
3506
3534
  const id = nanoid();
3507
3535
  const defaults = LUMIA_DEFAULT_SIZES[lumiaType] ?? { width: 400, height: 400 };
@@ -3998,7 +4026,6 @@ function mapGoal(widget, ctx) {
3998
4026
  const v = widget.variables ?? {};
3999
4027
  const listener = v.goalListener ?? (typeof widget.listener === "string" ? widget.listener : null);
4000
4028
  const charity = charityGoalVariables(widget.type);
4001
- const seTextCss = widget.text?.css ?? {};
4002
4029
  return buildUnit(widget, "goal", {
4003
4030
  content: {
4004
4031
  version: 1,
@@ -4041,23 +4068,16 @@ function mapGoal(widget, ctx) {
4041
4068
  simpleDesign: !!v.simpleDesign
4042
4069
  })
4043
4070
  },
4044
- css: {
4045
- // `borderRadius` / `borderStyle` / `borderWidth` / `borderColor`
4046
- // used to be set here too, but the Goal renderer applies its own
4047
- // border + borderRadius via inline `style={…}` on the .goal-bar
4048
- // div (see Goal/index.tsx:123,127,155) sourced from
4049
- // `content.border` and `content.borderRadius`. Writing them to
4050
- // module.css just plopped useless transparent-0px rules on the
4051
- // outer .overlay-module wrapper.
4052
- lineHeight: seTextCss["line-height"] ?? 1,
4053
- fontSize: seTextCss["font-size"] ?? 30,
4054
- textAlign: seTextCss["text-align"] ?? "center",
4055
- fontFamily: seTextCss["font-family"] ?? "Roboto",
4056
- fontWeight: seTextCss["font-weight"] ?? "bold",
4057
- color: seTextCss.color ?? "#ffffff",
4058
- textShadow: seTextCss["text-shadow"] ?? "rgb(0, 0, 0) 1px 1px 1px",
4059
- background: "transparent"
4060
- }
4071
+ // `borderRadius` / `borderStyle` / `borderWidth` / `borderColor`
4072
+ // used to be set on `css` too, but the Goal renderer applies its
4073
+ // own border + borderRadius via inline `style={…}` on the .goal-bar
4074
+ // div (see Goal/index.tsx:123,127,155) sourced from
4075
+ // `content.border` and `content.borderRadius`. Writing them to
4076
+ // module.css just plopped useless transparent-0px rules on the
4077
+ // outer .overlay-module wrapper. SE charity goals commonly use
4078
+ // 24-30px Nunito/Roboto — fontSize default of 30 mirrors the
4079
+ // previous hardcoded fallback.
4080
+ css: mapSeTextCssToModuleCss(widget, { fontSize: 30 })
4061
4081
  }, ctx);
4062
4082
  }
4063
4083
 
@@ -4331,6 +4351,9 @@ function mapEventList(widget, ctx) {
4331
4351
  const filters = computeListenerFilters(widget.listeners, widget.listener);
4332
4352
  const maxItems = v.visibleEvents ?? v.visibleItems ?? 5;
4333
4353
  return buildUnit(widget, "eventlist", {
4354
+ // SE event-list widgets style per-event lines via widget.text.css; the
4355
+ // Lumia eventlist reads font/size/color from module.css.
4356
+ css: mapSeTextCssToModuleCss(widget, { fontSize: 18 }),
4334
4357
  content: {
4335
4358
  theme: v.theme ?? "simple",
4336
4359
  direction: v.direction ?? "top",
@@ -4542,16 +4565,11 @@ function mapCredits(widget, ctx) {
4542
4565
  amount: v.amount
4543
4566
  })
4544
4567
  },
4545
- css: {
4546
- fontSize: 30,
4547
- lineHeight: 1,
4548
- fontFamily: "Roboto",
4549
- fontWeight: "normal",
4550
- color: "#ffffff",
4551
- textShadow: "",
4552
- textAlign: "center",
4553
- background: "transparent"
4554
- }
4568
+ // Credits renders scrolling section headers + per-event lines; the
4569
+ // renderer reads font/size/weight/color from module.css. Pull from SE's
4570
+ // text.css so an imported credits widget keeps the streamer's chosen
4571
+ // font and weight — defaults match the prior hardcoded fallbacks.
4572
+ css: mapSeTextCssToModuleCss(widget, { fontSize: 30, fontWeight: "normal", textShadow: "" })
4555
4573
  }, ctx);
4556
4574
  }
4557
4575
  function inferSlideshowMediaType(mime, url) {
@@ -4678,21 +4696,10 @@ function mapTimer(widget, ctx) {
4678
4696
  if (v.seconds !== false) clockBits.push("{{seconds}}");
4679
4697
  if (clockBits.length) parts.push(clockBits.join(":"));
4680
4698
  }
4681
- const seTextCss = widget.text?.css ?? {};
4682
- const asStr = (v2, fallback) => typeof v2 === "string" && v2.length > 0 ? v2 : fallback;
4683
- const asNum = (v2, fallback) => typeof v2 === "number" && Number.isFinite(v2) ? v2 : typeof v2 === "string" && v2.length > 0 ? v2 : fallback;
4684
- const moduleCss = {
4685
- lineHeight: asNum(seTextCss["line-height"], 1),
4686
- fontSize: asNum(seTextCss["font-size"], 24),
4687
- textAlign: asStr(seTextCss["text-align"], "center"),
4688
- fontFamily: asStr(seTextCss["font-family"], "Roboto"),
4689
- fontWeight: asStr(seTextCss["font-weight"], "bold"),
4690
- color: asStr(seTextCss.color, "#ffffff"),
4691
- textShadow: asStr(seTextCss["text-shadow"], "rgb(0, 0, 0) 1px 1px 1px"),
4692
- background: "transparent"
4693
- };
4694
4699
  return buildUnit(widget, "timer", {
4695
- css: moduleCss,
4700
+ // Carry the streamer's SE text styling (font/size/color/shadow) so an
4701
+ // imported countdown doesn't render in Lumia's default Roboto/24px.
4702
+ css: mapSeTextCssToModuleCss(widget),
4696
4703
  content: {
4697
4704
  version: 1,
4698
4705
  type: "countdown",
@@ -4780,14 +4787,10 @@ function mapMediaShare(widget, ctx) {
4780
4787
  // `durationSeconds + grace`).
4781
4788
  importMeta: buildImportMeta(widget, { timeLeft: v.timeLeft === true })
4782
4789
  },
4783
- css: {
4784
- fontFamily: "Roboto",
4785
- fontWeight: "bold",
4786
- color: "#ffffff",
4787
- textShadow: "rgb(0, 0, 0) 1px 1px 1px",
4788
- background: "transparent",
4789
- fontSize: 18
4790
- }
4790
+ // Lumia songrequest renders the queue list as styled text; pull SE's
4791
+ // widget.text.css so font/size/color carry across (defaults match the
4792
+ // previous hardcoded fallbacks for this widget specifically).
4793
+ css: mapSeTextCssToModuleCss(widget, { fontSize: 18 })
4791
4794
  }, ctx);
4792
4795
  }
4793
4796
  function mapKappagen(widget, ctx) {
@@ -4855,6 +4858,9 @@ function mapLeaderboardFromVariables(widget, currency, ctx) {
4855
4858
  const window = v.period === "weekly" ? "week" : v.period === "monthly" ? "month" : "alltime";
4856
4859
  const title = currency === "tips" ? "Top Tippers" : currency === "cheers" ? "Top Cheerers" : "Top Gifters";
4857
4860
  return buildUnit(widget, "loyaltyleaderboard", {
4861
+ // Carry SE text styling so the leaderboard list keeps the streamer's
4862
+ // chosen font / size / color.
4863
+ css: mapSeTextCssToModuleCss(widget, { fontSize: 18 }),
4858
4864
  content: {
4859
4865
  source: "variables",
4860
4866
  currency,
@@ -4881,6 +4887,8 @@ function mapGiveaway(widget, ctx) {
4881
4887
  const primary = v.colors?.primary ?? "#393853";
4882
4888
  const accent = v.colors?.accent ?? "#FF4076";
4883
4889
  return buildUnit(widget, "raffle", {
4890
+ // Raffle renders entrants/winner text via module.css.
4891
+ css: mapSeTextCssToModuleCss(widget),
4884
4892
  content: {
4885
4893
  version: 1,
4886
4894
  type: "basic",
@@ -4965,6 +4973,9 @@ function isHypetrainCustomWidget(widget) {
4965
4973
  function mapHypetrain(widget, ctx) {
4966
4974
  const v = widget.variables ?? {};
4967
4975
  return buildUnit(widget, "hypetrain", {
4976
+ // Hypetrain renders level / progress / contributor text — carry SE
4977
+ // styling so an imported hype-train widget keeps its font + accent.
4978
+ css: mapSeTextCssToModuleCss(widget),
4968
4979
  content: {
4969
4980
  // Twitch-native is the right default — SE hype-train widgets always
4970
4981
  // reflected the Twitch native train. Users can flip to 'synthetic'
@@ -5357,6 +5368,23 @@ var GOAL_TYPES = /* @__PURE__ */ new Set([
5357
5368
  "se-widget-merch-goal"
5358
5369
  ]);
5359
5370
  var EVENTLIST_TYPES = /* @__PURE__ */ new Set(["se-widget-event-list", "se-widget-follower-recent", "se-widget-subscriber-recent", "se-widget-cheer-recent", "se-widget-tip-recent"]);
5371
+ var SCROLLING_RECENT_TYPES = /* @__PURE__ */ new Set([
5372
+ "se-widget-follower-recent",
5373
+ "se-widget-subscriber-recent",
5374
+ "se-widget-cheer-recent",
5375
+ "se-widget-tip-recent"
5376
+ ]);
5377
+ var RECENT_TO_LATEST_FALLBACK_VAR = {
5378
+ "se-widget-follower-recent": "twitch_last_follower",
5379
+ "se-widget-subscriber-recent": "twitch_last_subscriber",
5380
+ "se-widget-cheer-recent": "twitch_last_bit",
5381
+ "se-widget-tip-recent": "latest_donator"
5382
+ };
5383
+ function isScrollingRecentWidget(widget) {
5384
+ if (!SCROLLING_RECENT_TYPES.has(widget.type)) return false;
5385
+ const scrolling = widget.text?.scrolling;
5386
+ return !!(scrolling && typeof scrolling === "object" && scrolling.enabled === true);
5387
+ }
5360
5388
  function dispatch(widget, ctx) {
5361
5389
  const t = widget.type;
5362
5390
  console.log("[SE-IMPORT] dispatch widget", {
@@ -5385,6 +5413,10 @@ function dispatch(widget, ctx) {
5385
5413
  if (t === "se-widget-donor-drive-alert") return { unit: mapSingleAlert(widget, "donordrive-donation", ctx), status: "direct", lumiaType: "alert" };
5386
5414
  if (t === "se-widget-extralife-alert") return { unit: mapSingleAlert(widget, "extralife-donation", ctx), status: "direct", lumiaType: "alert" };
5387
5415
  if (t === "se-widget-tiltify-alert") return { unit: mapSingleAlert(widget, "tiltify-campaignDonation", ctx), status: "direct", lumiaType: "alert" };
5416
+ if (isScrollingRecentWidget(widget)) {
5417
+ const fallbackVar = RECENT_TO_LATEST_FALLBACK_VAR[t] ?? "username";
5418
+ return { unit: mapReadout(widget, fallbackVar, ctx), status: "direct", lumiaType: "text" };
5419
+ }
5388
5420
  if (EVENTLIST_TYPES.has(t)) return { unit: mapEventList(widget, ctx), status: "direct", lumiaType: "eventlist" };
5389
5421
  if (t === "se-widget-top-tippers-list") return { unit: mapLeaderboardFromVariables(widget, "tips", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
5390
5422
  if (t === "se-widget-top-cheerers-list") return { unit: mapLeaderboardFromVariables(widget, "cheers", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
package/dist/se-import.js CHANGED
@@ -111,9 +111,17 @@ var YOUTUBE_CONTEXTS = {
111
111
  "subscriber-week": { amount: "youtube_week_member_count", count: "youtube_week_member_count" },
112
112
  "subscriber-month": { amount: "youtube_month_member_count", count: "youtube_month_member_count" }
113
113
  };
114
+ var RECENT_TO_LATEST_LISTENER = {
115
+ "follower-recent": "follower-latest",
116
+ "subscriber-recent": "subscriber-latest",
117
+ "cheer-recent": "cheer-latest",
118
+ "tip-recent": "tip-latest",
119
+ "raid-recent": "raid-latest"
120
+ };
114
121
  function listenerPlaceholderContext(listener, provider = "twitch") {
122
+ const resolved = RECENT_TO_LATEST_LISTENER[listener] ?? listener;
115
123
  const platformTable = provider === "kick" ? KICK_CONTEXTS : provider === "youtube" ? YOUTUBE_CONTEXTS : null;
116
- return platformTable?.[listener] ?? TWITCH_CONTEXTS[listener] ?? {};
124
+ return platformTable?.[resolved] ?? TWITCH_CONTEXTS[resolved] ?? {};
117
125
  }
118
126
  var SE_WIDGET_TYPE_LABELS = {
119
127
  "subscriber-latest": "Latest subscriber",
@@ -198,8 +206,9 @@ var YOUTUBE_LISTENER_VARS = {
198
206
  };
199
207
  function listenerToLumiaVariable(listener, provider = "twitch") {
200
208
  if (typeof listener !== "string" || !listener) return "username";
201
- if (provider === "kick" && KICK_LISTENER_VARS[listener]) return KICK_LISTENER_VARS[listener];
202
- if (provider === "youtube" && YOUTUBE_LISTENER_VARS[listener]) return YOUTUBE_LISTENER_VARS[listener];
209
+ const resolvedListener = RECENT_TO_LATEST_LISTENER[listener] ?? listener;
210
+ if (provider === "kick" && KICK_LISTENER_VARS[resolvedListener]) return KICK_LISTENER_VARS[resolvedListener];
211
+ if (provider === "youtube" && YOUTUBE_LISTENER_VARS[resolvedListener]) return YOUTUBE_LISTENER_VARS[resolvedListener];
203
212
  const m = {
204
213
  // Latest events
205
214
  "follower-latest": "twitch_last_follower",
@@ -256,7 +265,7 @@ function listenerToLumiaVariable(listener, provider = "twitch") {
256
265
  "cheer-alltime-top-donation": "twitch_alltime_top_cheerer_amount",
257
266
  "cheer-alltime-top-donator": "twitch_alltime_top_cheerer"
258
267
  };
259
- return m[listener] ?? listener.replace(/-/g, "_");
268
+ return m[resolvedListener] ?? resolvedListener.replace(/-/g, "_");
260
269
  }
261
270
  function seCssToBounds(css, defaults = { width: 600, height: 200 }, canvas) {
262
271
  let widthIsAuto = false;
@@ -387,6 +396,25 @@ function seLayoutUsesPushText(seLayout) {
387
396
 
388
397
  // src/se-import/mappers/build.ts
389
398
  import { nanoid } from "nanoid";
399
+ function mapSeTextCssToModuleCss(widget, defaults = {}) {
400
+ const seTextCss = widget.text?.css ?? {};
401
+ const asStr = (v, fallback) => typeof v === "string" && v.length > 0 ? v : fallback;
402
+ const asNumOrStr = (v, fallback) => {
403
+ if (typeof v === "number" && Number.isFinite(v)) return v;
404
+ if (typeof v === "string" && v.length > 0) return v;
405
+ return fallback;
406
+ };
407
+ return {
408
+ lineHeight: asNumOrStr(seTextCss["line-height"], defaults.lineHeight ?? 1),
409
+ fontSize: asNumOrStr(seTextCss["font-size"], defaults.fontSize ?? 24),
410
+ textAlign: asStr(seTextCss["text-align"], defaults.textAlign ?? "center"),
411
+ fontFamily: asStr(seTextCss["font-family"], defaults.fontFamily ?? "Roboto"),
412
+ fontWeight: asStr(seTextCss["font-weight"], defaults.fontWeight ?? "bold"),
413
+ color: asStr(seTextCss.color, defaults.color ?? "#ffffff"),
414
+ textShadow: asStr(seTextCss["text-shadow"], defaults.textShadow ?? "rgb(0, 0, 0) 1px 1px 1px"),
415
+ background: defaults.background ?? "transparent"
416
+ };
417
+ }
390
418
  function buildUnit(widget, lumiaType, moduleExtras, ctx) {
391
419
  const id = nanoid();
392
420
  const defaults = LUMIA_DEFAULT_SIZES[lumiaType] ?? { width: 400, height: 400 };
@@ -883,7 +911,6 @@ function mapGoal(widget, ctx) {
883
911
  const v = widget.variables ?? {};
884
912
  const listener = v.goalListener ?? (typeof widget.listener === "string" ? widget.listener : null);
885
913
  const charity = charityGoalVariables(widget.type);
886
- const seTextCss = widget.text?.css ?? {};
887
914
  return buildUnit(widget, "goal", {
888
915
  content: {
889
916
  version: 1,
@@ -926,23 +953,16 @@ function mapGoal(widget, ctx) {
926
953
  simpleDesign: !!v.simpleDesign
927
954
  })
928
955
  },
929
- css: {
930
- // `borderRadius` / `borderStyle` / `borderWidth` / `borderColor`
931
- // used to be set here too, but the Goal renderer applies its own
932
- // border + borderRadius via inline `style={…}` on the .goal-bar
933
- // div (see Goal/index.tsx:123,127,155) sourced from
934
- // `content.border` and `content.borderRadius`. Writing them to
935
- // module.css just plopped useless transparent-0px rules on the
936
- // outer .overlay-module wrapper.
937
- lineHeight: seTextCss["line-height"] ?? 1,
938
- fontSize: seTextCss["font-size"] ?? 30,
939
- textAlign: seTextCss["text-align"] ?? "center",
940
- fontFamily: seTextCss["font-family"] ?? "Roboto",
941
- fontWeight: seTextCss["font-weight"] ?? "bold",
942
- color: seTextCss.color ?? "#ffffff",
943
- textShadow: seTextCss["text-shadow"] ?? "rgb(0, 0, 0) 1px 1px 1px",
944
- background: "transparent"
945
- }
956
+ // `borderRadius` / `borderStyle` / `borderWidth` / `borderColor`
957
+ // used to be set on `css` too, but the Goal renderer applies its
958
+ // own border + borderRadius via inline `style={…}` on the .goal-bar
959
+ // div (see Goal/index.tsx:123,127,155) sourced from
960
+ // `content.border` and `content.borderRadius`. Writing them to
961
+ // module.css just plopped useless transparent-0px rules on the
962
+ // outer .overlay-module wrapper. SE charity goals commonly use
963
+ // 24-30px Nunito/Roboto — fontSize default of 30 mirrors the
964
+ // previous hardcoded fallback.
965
+ css: mapSeTextCssToModuleCss(widget, { fontSize: 30 })
946
966
  }, ctx);
947
967
  }
948
968
 
@@ -1216,6 +1236,9 @@ function mapEventList(widget, ctx) {
1216
1236
  const filters = computeListenerFilters(widget.listeners, widget.listener);
1217
1237
  const maxItems = v.visibleEvents ?? v.visibleItems ?? 5;
1218
1238
  return buildUnit(widget, "eventlist", {
1239
+ // SE event-list widgets style per-event lines via widget.text.css; the
1240
+ // Lumia eventlist reads font/size/color from module.css.
1241
+ css: mapSeTextCssToModuleCss(widget, { fontSize: 18 }),
1219
1242
  content: {
1220
1243
  theme: v.theme ?? "simple",
1221
1244
  direction: v.direction ?? "top",
@@ -1427,16 +1450,11 @@ function mapCredits(widget, ctx) {
1427
1450
  amount: v.amount
1428
1451
  })
1429
1452
  },
1430
- css: {
1431
- fontSize: 30,
1432
- lineHeight: 1,
1433
- fontFamily: "Roboto",
1434
- fontWeight: "normal",
1435
- color: "#ffffff",
1436
- textShadow: "",
1437
- textAlign: "center",
1438
- background: "transparent"
1439
- }
1453
+ // Credits renders scrolling section headers + per-event lines; the
1454
+ // renderer reads font/size/weight/color from module.css. Pull from SE's
1455
+ // text.css so an imported credits widget keeps the streamer's chosen
1456
+ // font and weight — defaults match the prior hardcoded fallbacks.
1457
+ css: mapSeTextCssToModuleCss(widget, { fontSize: 30, fontWeight: "normal", textShadow: "" })
1440
1458
  }, ctx);
1441
1459
  }
1442
1460
  function inferSlideshowMediaType(mime, url) {
@@ -1563,21 +1581,10 @@ function mapTimer(widget, ctx) {
1563
1581
  if (v.seconds !== false) clockBits.push("{{seconds}}");
1564
1582
  if (clockBits.length) parts.push(clockBits.join(":"));
1565
1583
  }
1566
- const seTextCss = widget.text?.css ?? {};
1567
- const asStr = (v2, fallback) => typeof v2 === "string" && v2.length > 0 ? v2 : fallback;
1568
- const asNum = (v2, fallback) => typeof v2 === "number" && Number.isFinite(v2) ? v2 : typeof v2 === "string" && v2.length > 0 ? v2 : fallback;
1569
- const moduleCss = {
1570
- lineHeight: asNum(seTextCss["line-height"], 1),
1571
- fontSize: asNum(seTextCss["font-size"], 24),
1572
- textAlign: asStr(seTextCss["text-align"], "center"),
1573
- fontFamily: asStr(seTextCss["font-family"], "Roboto"),
1574
- fontWeight: asStr(seTextCss["font-weight"], "bold"),
1575
- color: asStr(seTextCss.color, "#ffffff"),
1576
- textShadow: asStr(seTextCss["text-shadow"], "rgb(0, 0, 0) 1px 1px 1px"),
1577
- background: "transparent"
1578
- };
1579
1584
  return buildUnit(widget, "timer", {
1580
- css: moduleCss,
1585
+ // Carry the streamer's SE text styling (font/size/color/shadow) so an
1586
+ // imported countdown doesn't render in Lumia's default Roboto/24px.
1587
+ css: mapSeTextCssToModuleCss(widget),
1581
1588
  content: {
1582
1589
  version: 1,
1583
1590
  type: "countdown",
@@ -1665,14 +1672,10 @@ function mapMediaShare(widget, ctx) {
1665
1672
  // `durationSeconds + grace`).
1666
1673
  importMeta: buildImportMeta(widget, { timeLeft: v.timeLeft === true })
1667
1674
  },
1668
- css: {
1669
- fontFamily: "Roboto",
1670
- fontWeight: "bold",
1671
- color: "#ffffff",
1672
- textShadow: "rgb(0, 0, 0) 1px 1px 1px",
1673
- background: "transparent",
1674
- fontSize: 18
1675
- }
1675
+ // Lumia songrequest renders the queue list as styled text; pull SE's
1676
+ // widget.text.css so font/size/color carry across (defaults match the
1677
+ // previous hardcoded fallbacks for this widget specifically).
1678
+ css: mapSeTextCssToModuleCss(widget, { fontSize: 18 })
1676
1679
  }, ctx);
1677
1680
  }
1678
1681
  function mapKappagen(widget, ctx) {
@@ -1740,6 +1743,9 @@ function mapLeaderboardFromVariables(widget, currency, ctx) {
1740
1743
  const window = v.period === "weekly" ? "week" : v.period === "monthly" ? "month" : "alltime";
1741
1744
  const title = currency === "tips" ? "Top Tippers" : currency === "cheers" ? "Top Cheerers" : "Top Gifters";
1742
1745
  return buildUnit(widget, "loyaltyleaderboard", {
1746
+ // Carry SE text styling so the leaderboard list keeps the streamer's
1747
+ // chosen font / size / color.
1748
+ css: mapSeTextCssToModuleCss(widget, { fontSize: 18 }),
1743
1749
  content: {
1744
1750
  source: "variables",
1745
1751
  currency,
@@ -1766,6 +1772,8 @@ function mapGiveaway(widget, ctx) {
1766
1772
  const primary = v.colors?.primary ?? "#393853";
1767
1773
  const accent = v.colors?.accent ?? "#FF4076";
1768
1774
  return buildUnit(widget, "raffle", {
1775
+ // Raffle renders entrants/winner text via module.css.
1776
+ css: mapSeTextCssToModuleCss(widget),
1769
1777
  content: {
1770
1778
  version: 1,
1771
1779
  type: "basic",
@@ -1850,6 +1858,9 @@ function isHypetrainCustomWidget(widget) {
1850
1858
  function mapHypetrain(widget, ctx) {
1851
1859
  const v = widget.variables ?? {};
1852
1860
  return buildUnit(widget, "hypetrain", {
1861
+ // Hypetrain renders level / progress / contributor text — carry SE
1862
+ // styling so an imported hype-train widget keeps its font + accent.
1863
+ css: mapSeTextCssToModuleCss(widget),
1853
1864
  content: {
1854
1865
  // Twitch-native is the right default — SE hype-train widgets always
1855
1866
  // reflected the Twitch native train. Users can flip to 'synthetic'
@@ -2242,6 +2253,23 @@ var GOAL_TYPES = /* @__PURE__ */ new Set([
2242
2253
  "se-widget-merch-goal"
2243
2254
  ]);
2244
2255
  var EVENTLIST_TYPES = /* @__PURE__ */ new Set(["se-widget-event-list", "se-widget-follower-recent", "se-widget-subscriber-recent", "se-widget-cheer-recent", "se-widget-tip-recent"]);
2256
+ var SCROLLING_RECENT_TYPES = /* @__PURE__ */ new Set([
2257
+ "se-widget-follower-recent",
2258
+ "se-widget-subscriber-recent",
2259
+ "se-widget-cheer-recent",
2260
+ "se-widget-tip-recent"
2261
+ ]);
2262
+ var RECENT_TO_LATEST_FALLBACK_VAR = {
2263
+ "se-widget-follower-recent": "twitch_last_follower",
2264
+ "se-widget-subscriber-recent": "twitch_last_subscriber",
2265
+ "se-widget-cheer-recent": "twitch_last_bit",
2266
+ "se-widget-tip-recent": "latest_donator"
2267
+ };
2268
+ function isScrollingRecentWidget(widget) {
2269
+ if (!SCROLLING_RECENT_TYPES.has(widget.type)) return false;
2270
+ const scrolling = widget.text?.scrolling;
2271
+ return !!(scrolling && typeof scrolling === "object" && scrolling.enabled === true);
2272
+ }
2245
2273
  function dispatch(widget, ctx) {
2246
2274
  const t = widget.type;
2247
2275
  console.log("[SE-IMPORT] dispatch widget", {
@@ -2270,6 +2298,10 @@ function dispatch(widget, ctx) {
2270
2298
  if (t === "se-widget-donor-drive-alert") return { unit: mapSingleAlert(widget, "donordrive-donation", ctx), status: "direct", lumiaType: "alert" };
2271
2299
  if (t === "se-widget-extralife-alert") return { unit: mapSingleAlert(widget, "extralife-donation", ctx), status: "direct", lumiaType: "alert" };
2272
2300
  if (t === "se-widget-tiltify-alert") return { unit: mapSingleAlert(widget, "tiltify-campaignDonation", ctx), status: "direct", lumiaType: "alert" };
2301
+ if (isScrollingRecentWidget(widget)) {
2302
+ const fallbackVar = RECENT_TO_LATEST_FALLBACK_VAR[t] ?? "username";
2303
+ return { unit: mapReadout(widget, fallbackVar, ctx), status: "direct", lumiaType: "text" };
2304
+ }
2273
2305
  if (EVENTLIST_TYPES.has(t)) return { unit: mapEventList(widget, ctx), status: "direct", lumiaType: "eventlist" };
2274
2306
  if (t === "se-widget-top-tippers-list") return { unit: mapLeaderboardFromVariables(widget, "tips", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
2275
2307
  if (t === "se-widget-top-cheerers-list") return { unit: mapLeaderboardFromVariables(widget, "cheers", ctx), status: "direct", lumiaType: "loyaltyleaderboard" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumiastream/ui",
3
- "version": "0.2.8-alpha.25",
3
+ "version": "0.2.8-alpha.27",
4
4
  "author": "Lumia Stream",
5
5
  "license": "ISC",
6
6
  "description": "Lumia UI Kit",