@lumiastream/ui 0.6.1 → 0.6.3

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
@@ -4901,6 +4901,29 @@ function listenersToLumiaEvents(listeners, listener) {
4901
4901
  }
4902
4902
  return [...out];
4903
4903
  }
4904
+ var SE_FIRST_PARTY_SCRIPT_RX = /^(?:https?:)?\/\/(?:[^/]+\.)?(?:streamelements\.com|lumiastream\.com)\//i;
4905
+ var EXTERNAL_SCRIPT_SRC_RX = /<script\b[^>]*\bsrc\s*=\s*["']([^"']+)["']/gi;
4906
+ var CHAT_COMMAND_VALUE_RX = /^\s*!\S+/;
4907
+ function hasExternalScriptLoader(html, js) {
4908
+ const haystack = `${html ?? ""}
4909
+ ${js ?? ""}`;
4910
+ EXTERNAL_SCRIPT_SRC_RX.lastIndex = 0;
4911
+ for (const m of haystack.matchAll(EXTERNAL_SCRIPT_SRC_RX)) {
4912
+ const src = m[1]?.trim();
4913
+ if (!src) continue;
4914
+ if (src.startsWith("/") && !src.startsWith("//")) continue;
4915
+ if (SE_FIRST_PARTY_SCRIPT_RX.test(src)) continue;
4916
+ return true;
4917
+ }
4918
+ return false;
4919
+ }
4920
+ function hasChatCommandFieldValue(fieldData) {
4921
+ if (!fieldData) return false;
4922
+ for (const v of Object.values(fieldData)) {
4923
+ if (typeof v === "string" && CHAT_COMMAND_VALUE_RX.test(v)) return true;
4924
+ }
4925
+ return false;
4926
+ }
4904
4927
  function translateConfigs(sourceFields) {
4905
4928
  if (!sourceFields) return [];
4906
4929
  return Object.entries(sourceFields).filter(([key]) => !RESERVED_FIELD_KEYS.has(key)).map(([key, f]) => {
@@ -4953,6 +4976,13 @@ function mapCustom(widget, ctx) {
4953
4976
  widget.listeners,
4954
4977
  widget.listener
4955
4978
  );
4979
+ const externalLoader = hasExternalScriptLoader(v.html, v.js);
4980
+ const wantsChat = externalLoader || hasChatCommandFieldValue(fieldData);
4981
+ if (externalLoader) {
4982
+ if (!eventsList.includes("alert")) eventsList.push("alert");
4983
+ if (!eventsList.includes("alert.trigger")) eventsList.push("alert.trigger");
4984
+ }
4985
+ if (wantsChat && !eventsList.includes("chat")) eventsList.push("chat");
4956
4986
  const data = { ...fieldData ?? {} };
4957
4987
  if (eventsList.length > 0) data.events = eventsList;
4958
4988
  return buildUnit(
@@ -6453,7 +6483,7 @@ function mapHypeCup(widget, ctx) {
6453
6483
  // Phase 1 ships a single preset; SE's per-cup polygon set isn't lifted
6454
6484
  // yet (it'd require multi-cup support which we deliberately deferred).
6455
6485
  // The streamer can swap a custom cup image + wireframe via Settings.
6456
- preset: "glass-mug",
6486
+ preset: "cup-01",
6457
6487
  customCup: null,
6458
6488
  cup: {
6459
6489
  x: cupX,
@@ -6468,7 +6498,7 @@ function mapHypeCup(widget, ctx) {
6468
6498
  maxBodies
6469
6499
  },
6470
6500
  tokens: {
6471
- defaultRadius: 26
6501
+ defaultRadius: 16
6472
6502
  },
6473
6503
  events: categoriesFromListeners(widget.listeners),
6474
6504
  chat: {
@@ -6477,6 +6507,7 @@ function mapHypeCup(widget, ctx) {
6477
6507
  display: {
6478
6508
  showWireframes: !!v.showWireframes,
6479
6509
  showEventMessages: !!v.textEnabled,
6510
+ showPlatformIcons: false,
6480
6511
  messageDuration: duration
6481
6512
  },
6482
6513
  sprites,
@@ -7080,7 +7111,7 @@ async function mirrorOneAsset(url, upload, signal, proxyAssetFetch) {
7080
7111
  const blob = await fetchAssetBlob(url, signal, proxyAssetFetch);
7081
7112
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
7082
7113
  if (blob.size > MAX_MIRROR_ASSET_BYTES) {
7083
- throw new Error(`Asset is ${(blob.size / (1024 * 1024)).toFixed(1)} MB \u2014 over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
7114
+ throw new Error(`Asset is ${(blob.size / (1024 * 1024)).toFixed(1)} MB, over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
7084
7115
  }
7085
7116
  const filename = filenameFromURL(url);
7086
7117
  const type = blob.type || guessMime(filename);
@@ -7102,7 +7133,7 @@ async function fetchAssetBlob(url, signal, proxyAssetFetch) {
7102
7133
  if (!res.ok) throw new Error(`Source returned HTTP ${res.status}`);
7103
7134
  const declared = Number(res.headers.get("content-length"));
7104
7135
  if (Number.isFinite(declared) && declared > MAX_MIRROR_ASSET_BYTES) {
7105
- throw new Error(`Asset is ${(declared / (1024 * 1024)).toFixed(1)} MB \u2014 over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
7136
+ throw new Error(`Asset is ${(declared / (1024 * 1024)).toFixed(1)} MB, over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
7106
7137
  }
7107
7138
  return res.blob();
7108
7139
  }
@@ -7342,7 +7373,7 @@ function decodeJwtPayload(jwt) {
7342
7373
  }
7343
7374
  const claims = parsed;
7344
7375
  if (typeof claims.channel !== "string" || typeof claims.authToken !== "string") {
7345
- throw new SEAuthError("invalid-jwt", "Token payload is missing the 'channel' or 'authToken' claim \u2014 not a StreamElements account token.");
7376
+ throw new SEAuthError("invalid-jwt", "Token payload is missing the 'channel' or 'authToken' claim. not a StreamElements account token.");
7346
7377
  }
7347
7378
  if (typeof claims.exp === "number" && claims.exp * 1e3 < Date.now()) {
7348
7379
  throw new SEAuthError("expired", "This StreamElements token has expired. Generate a new one on the SE Account page.");
@@ -7351,7 +7382,7 @@ function decodeJwtPayload(jwt) {
7351
7382
  }
7352
7383
  var SEClient = class _SEClient {
7353
7384
  // `jwt` is held alongside the decoded claims so the client can switch auth
7354
- // modes without re-parsing. Defaults to `bearer-jwt` confirmed against
7385
+ // modes without re-parsing. Defaults to `bearer-jwt`. confirmed against
7355
7386
  // the user's diagnostic 2026-05-16: every endpoint the existing 5 helpers
7356
7387
  // hit (users/current, channels/me, overlays, bot/commands, plus
7357
7388
  // sessions/loyalty/bits leaderboard transitively) returns 200, AND
@@ -7439,7 +7470,7 @@ var SEClient = class _SEClient {
7439
7470
  let detail = "";
7440
7471
  try {
7441
7472
  const body = await res.json();
7442
- if (body?.message) detail = ` \u2014 ${body.message}`;
7473
+ if (body?.message) detail = ` - ${body.message}`;
7443
7474
  } catch {
7444
7475
  }
7445
7476
  throw new Error(`StreamElements API returned HTTP ${res.status}${detail}`);
@@ -7481,7 +7512,7 @@ async function fetchElementWidgetInstancePublic(channelId, widgetInstanceId, pub
7481
7512
  let detail = "";
7482
7513
  try {
7483
7514
  const body = JSON.parse(await result.errorBody());
7484
- if (body?.message) detail = ` \u2014 ${body.message}`;
7515
+ if (body?.message) detail = ` - ${body.message}`;
7485
7516
  } catch {
7486
7517
  }
7487
7518
  const reason = result.status === 401 || result.status === 403 ? "StreamElements rejected the widget share token. The share link may have been revoked or made private." : result.status === 404 ? "Element widget not found. Double-check the share URL." : `HTTP ${result.status}`;
@@ -8489,15 +8520,15 @@ var PLACEHOLDER_WIDGET_TYPES = /* @__PURE__ */ new Set([
8489
8520
  "streamlabels"
8490
8521
  ]);
8491
8522
  var PLACEHOLDER_REASONS = {
8492
- donation_ticker: "No native Lumia equivalent yet \u2014 imported as a placeholder text layer.",
8493
- stream_boss: "Lumia has a native streamboss module \u2014 set it up directly rather than importing the SL config.",
8494
- credits: "Lumia has a native credits module \u2014 set it up directly rather than importing the SL config.",
8495
- spin_wheel: "Lumia has a native spinwheel module \u2014 set it up directly rather than importing the SL config.",
8496
- emote_wall: "Lumia has a native emotebox module \u2014 set it up directly rather than importing the SL config.",
8497
- sub_train: "No native Lumia equivalent yet \u2014 imported as a placeholder text layer.",
8523
+ donation_ticker: "No native Lumia equivalent yet. imported as a placeholder text layer.",
8524
+ stream_boss: "Lumia has a native streamboss module. set it up directly rather than importing the SL config.",
8525
+ credits: "Lumia has a native credits module. set it up directly rather than importing the SL config.",
8526
+ spin_wheel: "Lumia has a native spinwheel module. set it up directly rather than importing the SL config.",
8527
+ emote_wall: "Lumia has a native emotebox module. set it up directly rather than importing the SL config.",
8528
+ sub_train: "No native Lumia equivalent yet. imported as a placeholder text layer.",
8498
8529
  streamlabels: "Use Lumia template variables (e.g. {{twitch_last_follower}}) inside a Text module instead of importing the SL streamlabels config."
8499
8530
  };
8500
- var CUSTOM_HTML_REVIEW_REASON = "The Streamlabs widget used custom HTML/CSS/JS that Lumia cannot host. The native Lumia module is the right starting point \u2014 recreate the styling there. The original SL settings are stashed under module.content.importMeta for reference.";
8531
+ var CUSTOM_HTML_REVIEW_REASON = "The Streamlabs widget used custom HTML/CSS/JS that Lumia cannot host. The native Lumia module is the right starting point. recreate the styling there. The original SL settings are stashed under module.content.importMeta for reference.";
8501
8532
  function stampImportMeta(module, widgetType, role, extra = {}) {
8502
8533
  stampImportMetaMerge(module, {
8503
8534
  source: "streamlabs",
@@ -8655,7 +8686,7 @@ function importStreamlabsWidget(response, widgetType, options = {}) {
8655
8686
  });
8656
8687
  reviewItems.push({
8657
8688
  moduleId: placeholder.module.id,
8658
- // SL doesn't have a single "widget" object the way SE does we
8689
+ // SL doesn't have a single "widget" object the way SE does. we
8659
8690
  // stash the settings snapshot here so the review step can show
8660
8691
  // the raw config the user imported. Cast through unknown because
8661
8692
  // ReviewItem is currently typed against SEWidget; widening the
@@ -8673,7 +8704,7 @@ function importStreamlabsWidget(response, widgetType, options = {}) {
8673
8704
  layers.push(placeholder.layer);
8674
8705
  modules[placeholder.layer.id] = placeholder.module;
8675
8706
  coverage.notes.push(
8676
- `Unknown Streamlabs widget type "${widgetType}" \u2014 imported as a placeholder.`
8707
+ `Unknown Streamlabs widget type "${widgetType}". imported as a placeholder.`
8677
8708
  );
8678
8709
  coverage.mappings.push({
8679
8710
  seType: `streamlabs:${widgetType}`,
@@ -9086,7 +9117,7 @@ function StepHeader({
9086
9117
  ] });
9087
9118
  }
9088
9119
  var JWT_FEATURES = [
9089
- "All your overlays \u2014 pick one, many, or all",
9120
+ "All your overlays: pick one, many, or all",
9090
9121
  "Live counters (follower / sub / tip / bits goals)",
9091
9122
  "Chat bot commands",
9092
9123
  "Loyalty points + watch time",
@@ -9240,7 +9271,7 @@ function StepModePicker({
9240
9271
  }
9241
9272
  )
9242
9273
  ] }),
9243
- caveat: "Requires pasting your StreamElements JWT. Stored only in memory \u2014 never sent to Lumia.",
9274
+ caveat: "Requires pasting your StreamElements JWT. Stored only in memory. Never sent to Lumia.",
9244
9275
  onClick: onChooseJwt
9245
9276
  }
9246
9277
  )
@@ -9265,7 +9296,7 @@ function StepConnect({
9265
9296
  {
9266
9297
  number: 1,
9267
9298
  title: "Connect your StreamElements account",
9268
- subtitle: "Paste your StreamElements JWT to import overlays, live counters, chat commands, loyalty points, watch time, and recent events. The token is read in your browser only \u2014 Lumia never sends it to our servers."
9299
+ subtitle: "Paste your StreamElements JWT to import overlays, live counters, chat commands, loyalty points, watch time, and recent events. The token is read in your browser only. Lumia never sends it to our servers."
9269
9300
  }
9270
9301
  ),
9271
9302
  /* @__PURE__ */ jsx14(
@@ -11914,15 +11945,15 @@ function importElementOverlay(response, options = {}) {
11914
11945
  // src/se-import/index.ts
11915
11946
  var IMPORT_META_KEY2 = "importMeta";
11916
11947
  var REVIEW_REASONS = {
11917
- "se-widget-bit-boss": "No native Lumia equivalent \u2014 bit boss fight bar with HP/damage states.",
11948
+ "se-widget-bit-boss": "No native Lumia equivalent. bit boss fight bar with HP/damage states.",
11918
11949
  // `se-widget-hype-cup` intentionally omitted — it now routes to the native
11919
11950
  // `tipjar` module (see dispatcher.ts) and lands with `partial` status, so it
11920
11951
  // never reaches the placeholder-reason fallback. If a future feature flag
11921
11952
  // disables tipjar auto-import, add an entry to FLAG_OFF_REASONS instead.
11922
- "se-widget-media-share": "No native Lumia equivalent \u2014 viewer-submitted media player.",
11923
- "se-widget-contest": "No native Lumia equivalent \u2014 SE-specific contest widget.",
11924
- "se-widget-giveaway": "No native Lumia equivalent \u2014 SE giveaway service.",
11925
- "se-widget-botcounter": "No native Lumia equivalent \u2014 chatter/message counter.",
11953
+ "se-widget-media-share": "No native Lumia equivalent. viewer-submitted media player.",
11954
+ "se-widget-contest": "No native Lumia equivalent. SE-specific contest widget.",
11955
+ "se-widget-giveaway": "No native Lumia equivalent. SE giveaway service.",
11956
+ "se-widget-botcounter": "No native Lumia equivalent. chatter/message counter.",
11926
11957
  "se-widget-top-cheerers-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render cheer rankings.',
11927
11958
  "se-widget-top-gifters-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render gifter rankings.',
11928
11959
  "se-widget-top-tippers-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render tipper rankings.',
@@ -11932,7 +11963,7 @@ var REVIEW_REASONS = {
11932
11963
  // so we can't pre-populate the slideshow items. The streamer adds their
11933
11964
  // product images by hand in the slideshow editor. Status = 'partial' (see
11934
11965
  // dispatcher.ts), so this string surfaces in the review step.
11935
- "se-widget-merch-products-rotator": "Imported as an empty slideshow. Lumia doesn't fetch your Fourthwall product catalog yet, so add your product images manually in the slideshow editor \u2014 the layer is pre-positioned and styled to match your SE widget."
11966
+ "se-widget-merch-products-rotator": "Imported as an empty slideshow. Lumia doesn't fetch your Fourthwall product catalog yet, so add your product images manually in the slideshow editor. the layer is pre-positioned and styled to match your SE widget."
11936
11967
  // Seasonal widgets (snow / halloween / halloween-2019 / xmas / valentine / easter
11937
11968
  // / world-cup) intentionally omitted — they route directly to image/slideshow/
11938
11969
  // video modules now (see dispatcher.ts SEASONAL_IMAGE_TYPES), so they always
@@ -11940,7 +11971,7 @@ var REVIEW_REASONS = {
11940
11971
  };
11941
11972
  function reasonFor(seType, status, flaggedOff) {
11942
11973
  if (flaggedOff && FLAG_OFF_REASONS[seType]) return FLAG_OFF_REASONS[seType];
11943
- return REVIEW_REASONS[seType] ?? (status === "template" ? "Imported as a generic template stub." : "No native mapping yet \u2014 imported as a labelled text placeholder.");
11974
+ return REVIEW_REASONS[seType] ?? (status === "template" ? "Imported as a generic template stub." : "No native mapping yet. imported as a labelled text placeholder.");
11944
11975
  }
11945
11976
  function buildSEBootstrapDescription(overlayId, sourceUrl) {
11946
11977
  const head = overlayId ? `Imported from StreamElements overlay ${overlayId}` : "Imported from StreamElements";
@@ -11970,7 +12001,7 @@ async function fetchSEBootstrap({
11970
12001
  }) {
11971
12002
  if (!apikey) {
11972
12003
  throw new Error(
11973
- "Missing access token. Use the public preview URL (Overlay menu \u2192 Get URL \u2192 Public link) \u2014 editor URLs do not include a token."
12004
+ "Missing access token. Use the public preview URL (Overlay menu \u2192 Get URL \u2192 Public link). editor URLs do not include a token."
11974
12005
  );
11975
12006
  }
11976
12007
  const result = await fetchWithProxyFallback(buildBootstrapUrl(overlayId), {
@@ -11981,7 +12012,7 @@ async function fetchSEBootstrap({
11981
12012
  let detail = "";
11982
12013
  try {
11983
12014
  const body = JSON.parse(await result.errorBody());
11984
- detail = body?.message ? ` \u2014 ${body.message}` : "";
12015
+ detail = body?.message ? `. ${body.message}` : "";
11985
12016
  } catch {
11986
12017
  }
11987
12018
  const reason = result.status === 401 || result.status === 403 ? "StreamElements rejected the URL token. The preview link may have been revoked." : result.status === 404 ? "Overlay not found. Double-check the URL." : `HTTP ${result.status}`;
@@ -12123,17 +12154,17 @@ function importSEBootstrap(bootstrap, options = {}) {
12123
12154
  }
12124
12155
  if (coverage.mappings.some((m) => m.status === "placeholder")) {
12125
12156
  coverage.notes.push(
12126
- "Some widgets imported as placeholder text layers \u2014 see SE/05-widget-to-module-mapping.md for status."
12157
+ "Some widgets imported as placeholder text layers. see SE/05-widget-to-module-mapping.md for status."
12127
12158
  );
12128
12159
  }
12129
12160
  if (coverage.mappings.some((m) => m.seType === "se-widget-merch-goal")) {
12130
12161
  coverage.notes.push(
12131
- "Merch-goal widgets imported as generic goals \u2014 see SE/03-missing-variables.md \xA78 for the missing data source."
12162
+ "Merch-goal widgets imported as generic goals. see SE/03-missing-variables.md \xA78 for the missing data source."
12132
12163
  );
12133
12164
  }
12134
12165
  if (coverage.mappings.some((m) => m.seType === "se-widget-custom-event-list")) {
12135
12166
  coverage.notes.push(
12136
- "\u26A0 Custom widgets are experimental \u2014 they run under the SE compatibility shim (Embed.tsx) which handles most SE APIs but may not cover every edge case. Verify each custom layer renders correctly in the editor."
12167
+ "\u26A0 Custom widgets are experimental. they run under the SE compatibility shim (Embed.tsx) which handles most SE APIs but may not cover every edge case. Verify each custom layer renders correctly in the editor."
12137
12168
  );
12138
12169
  }
12139
12170
  const overlay = {
@@ -12278,7 +12309,7 @@ function buildAIPromptForSEWidget(widget, canvas) {
12278
12309
  const fullVariables = JSON.stringify(variables, null, 2);
12279
12310
  const truncated = fullVariables.length > VARIABLES_BUDGET;
12280
12311
  const variablesJson = truncated ? `${fullVariables.slice(0, VARIABLES_BUDGET)}
12281
- \u2026[truncated \u2014 ${fullVariables.length - VARIABLES_BUDGET} more bytes elided]` : fullVariables;
12312
+ \u2026[truncated. ${fullVariables.length - VARIABLES_BUDGET} more bytes elided]` : fullVariables;
12282
12313
  return [
12283
12314
  "Recreate a StreamElements overlay widget as a Lumia Stream custom overlay (HTML + CSS + JS).",
12284
12315
  "",
@@ -12286,14 +12317,14 @@ function buildAIPromptForSEWidget(widget, canvas) {
12286
12317
  listener ? `Primary event listener: ${listener}` : "",
12287
12318
  listeners ? `Subscribes to events: ${listeners.join(", ")}` : "",
12288
12319
  `Canvas: ${landing.width} \xD7 ${landing.height}`,
12289
- truncated ? "Note: the widget config below is truncated \u2014 focus on visible/structural fields and ignore tail content you cannot see." : "",
12320
+ truncated ? "Note: the widget config below is truncated. focus on visible/structural fields and ignore tail content you cannot see." : "",
12290
12321
  "",
12291
12322
  "Widget configuration (JSON):",
12292
12323
  "```json",
12293
12324
  variablesJson,
12294
12325
  "```",
12295
12326
  "",
12296
- "Constraints \u2014 your output must run inside a Lumia Custom Overlay sandboxed iframe:",
12327
+ "Constraints. your output must run inside a Lumia Custom Overlay sandboxed iframe:",
12297
12328
  '- Use `window.Overlay.on("alert", handler)` / `window.Overlay.on("chat", handler)` to receive Lumia events.',
12298
12329
  "- Lumia template variables use double-brace syntax: {{username}}, {{amount}}, {{message}}, {{currency}}.",
12299
12330
  "- The `window.DATA` object holds user-configurable values (mirrors your `configs`).",
@@ -183,7 +183,7 @@ declare function fetchWithProxyFallback(url: string, options?: FetchWithProxyOpt
183
183
 
184
184
  /**
185
185
  * Distinct importable CDN URLs referenced anywhere inside the overlay's
186
- * settings. Covers both StreamElements and Streamlabs source hosts see
186
+ * settings. Covers both StreamElements and Streamlabs source hosts, see
187
187
  * IMPORTABLE_CDN_REGEX above for the full host list.
188
188
  *
189
189
  * Name kept as `findSEAssetURLs` for back-compat with existing callers; the
@@ -211,7 +211,7 @@ interface AssetMirrorProgress {
211
211
  *
212
212
  * The host call site is responsible for retries and progress reporting;
213
213
  * this function is intentionally single-shot. Pass `signal` (AbortSignal)
214
- * to abort the source-side `fetch` when the modal closes mid-mirror the
214
+ * to abort the source-side `fetch` when the modal closes mid-mirror, the
215
215
  * upload itself is consumer-defined and the caller decides whether/how to
216
216
  * short-circuit it. The proxy path doesn't take a signal because the
217
217
  * host-provided fetch shape varies.
package/dist/se-import.js CHANGED
@@ -1821,6 +1821,29 @@ function listenersToLumiaEvents(listeners, listener) {
1821
1821
  }
1822
1822
  return [...out];
1823
1823
  }
1824
+ var SE_FIRST_PARTY_SCRIPT_RX = /^(?:https?:)?\/\/(?:[^/]+\.)?(?:streamelements\.com|lumiastream\.com)\//i;
1825
+ var EXTERNAL_SCRIPT_SRC_RX = /<script\b[^>]*\bsrc\s*=\s*["']([^"']+)["']/gi;
1826
+ var CHAT_COMMAND_VALUE_RX = /^\s*!\S+/;
1827
+ function hasExternalScriptLoader(html, js) {
1828
+ const haystack = `${html ?? ""}
1829
+ ${js ?? ""}`;
1830
+ EXTERNAL_SCRIPT_SRC_RX.lastIndex = 0;
1831
+ for (const m of haystack.matchAll(EXTERNAL_SCRIPT_SRC_RX)) {
1832
+ const src = m[1]?.trim();
1833
+ if (!src) continue;
1834
+ if (src.startsWith("/") && !src.startsWith("//")) continue;
1835
+ if (SE_FIRST_PARTY_SCRIPT_RX.test(src)) continue;
1836
+ return true;
1837
+ }
1838
+ return false;
1839
+ }
1840
+ function hasChatCommandFieldValue(fieldData) {
1841
+ if (!fieldData) return false;
1842
+ for (const v of Object.values(fieldData)) {
1843
+ if (typeof v === "string" && CHAT_COMMAND_VALUE_RX.test(v)) return true;
1844
+ }
1845
+ return false;
1846
+ }
1824
1847
  function translateConfigs(sourceFields) {
1825
1848
  if (!sourceFields) return [];
1826
1849
  return Object.entries(sourceFields).filter(([key]) => !RESERVED_FIELD_KEYS.has(key)).map(([key, f]) => {
@@ -1873,6 +1896,13 @@ function mapCustom(widget, ctx) {
1873
1896
  widget.listeners,
1874
1897
  widget.listener
1875
1898
  );
1899
+ const externalLoader = hasExternalScriptLoader(v.html, v.js);
1900
+ const wantsChat = externalLoader || hasChatCommandFieldValue(fieldData);
1901
+ if (externalLoader) {
1902
+ if (!eventsList.includes("alert")) eventsList.push("alert");
1903
+ if (!eventsList.includes("alert.trigger")) eventsList.push("alert.trigger");
1904
+ }
1905
+ if (wantsChat && !eventsList.includes("chat")) eventsList.push("chat");
1876
1906
  const data = { ...fieldData ?? {} };
1877
1907
  if (eventsList.length > 0) data.events = eventsList;
1878
1908
  return buildUnit(
@@ -3373,7 +3403,7 @@ function mapHypeCup(widget, ctx) {
3373
3403
  // Phase 1 ships a single preset; SE's per-cup polygon set isn't lifted
3374
3404
  // yet (it'd require multi-cup support which we deliberately deferred).
3375
3405
  // The streamer can swap a custom cup image + wireframe via Settings.
3376
- preset: "glass-mug",
3406
+ preset: "cup-01",
3377
3407
  customCup: null,
3378
3408
  cup: {
3379
3409
  x: cupX,
@@ -3388,7 +3418,7 @@ function mapHypeCup(widget, ctx) {
3388
3418
  maxBodies
3389
3419
  },
3390
3420
  tokens: {
3391
- defaultRadius: 26
3421
+ defaultRadius: 16
3392
3422
  },
3393
3423
  events: categoriesFromListeners(widget.listeners),
3394
3424
  chat: {
@@ -3397,6 +3427,7 @@ function mapHypeCup(widget, ctx) {
3397
3427
  display: {
3398
3428
  showWireframes: !!v.showWireframes,
3399
3429
  showEventMessages: !!v.textEnabled,
3430
+ showPlatformIcons: false,
3400
3431
  messageDuration: duration
3401
3432
  },
3402
3433
  sprites,
@@ -4000,7 +4031,7 @@ async function mirrorOneAsset(url, upload, signal, proxyAssetFetch) {
4000
4031
  const blob = await fetchAssetBlob(url, signal, proxyAssetFetch);
4001
4032
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
4002
4033
  if (blob.size > MAX_MIRROR_ASSET_BYTES) {
4003
- throw new Error(`Asset is ${(blob.size / (1024 * 1024)).toFixed(1)} MB \u2014 over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
4034
+ throw new Error(`Asset is ${(blob.size / (1024 * 1024)).toFixed(1)} MB, over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
4004
4035
  }
4005
4036
  const filename = filenameFromURL(url);
4006
4037
  const type = blob.type || guessMime(filename);
@@ -4022,7 +4053,7 @@ async function fetchAssetBlob(url, signal, proxyAssetFetch) {
4022
4053
  if (!res.ok) throw new Error(`Source returned HTTP ${res.status}`);
4023
4054
  const declared = Number(res.headers.get("content-length"));
4024
4055
  if (Number.isFinite(declared) && declared > MAX_MIRROR_ASSET_BYTES) {
4025
- throw new Error(`Asset is ${(declared / (1024 * 1024)).toFixed(1)} MB \u2014 over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
4056
+ throw new Error(`Asset is ${(declared / (1024 * 1024)).toFixed(1)} MB, over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
4026
4057
  }
4027
4058
  return res.blob();
4028
4059
  }
@@ -4262,7 +4293,7 @@ function decodeJwtPayload(jwt) {
4262
4293
  }
4263
4294
  const claims = parsed;
4264
4295
  if (typeof claims.channel !== "string" || typeof claims.authToken !== "string") {
4265
- throw new SEAuthError("invalid-jwt", "Token payload is missing the 'channel' or 'authToken' claim \u2014 not a StreamElements account token.");
4296
+ throw new SEAuthError("invalid-jwt", "Token payload is missing the 'channel' or 'authToken' claim. not a StreamElements account token.");
4266
4297
  }
4267
4298
  if (typeof claims.exp === "number" && claims.exp * 1e3 < Date.now()) {
4268
4299
  throw new SEAuthError("expired", "This StreamElements token has expired. Generate a new one on the SE Account page.");
@@ -4271,7 +4302,7 @@ function decodeJwtPayload(jwt) {
4271
4302
  }
4272
4303
  var SEClient = class _SEClient {
4273
4304
  // `jwt` is held alongside the decoded claims so the client can switch auth
4274
- // modes without re-parsing. Defaults to `bearer-jwt` confirmed against
4305
+ // modes without re-parsing. Defaults to `bearer-jwt`. confirmed against
4275
4306
  // the user's diagnostic 2026-05-16: every endpoint the existing 5 helpers
4276
4307
  // hit (users/current, channels/me, overlays, bot/commands, plus
4277
4308
  // sessions/loyalty/bits leaderboard transitively) returns 200, AND
@@ -4359,7 +4390,7 @@ var SEClient = class _SEClient {
4359
4390
  let detail = "";
4360
4391
  try {
4361
4392
  const body = await res.json();
4362
- if (body?.message) detail = ` \u2014 ${body.message}`;
4393
+ if (body?.message) detail = ` - ${body.message}`;
4363
4394
  } catch {
4364
4395
  }
4365
4396
  throw new Error(`StreamElements API returned HTTP ${res.status}${detail}`);
@@ -4401,7 +4432,7 @@ async function fetchElementWidgetInstancePublic(channelId, widgetInstanceId, pub
4401
4432
  let detail = "";
4402
4433
  try {
4403
4434
  const body = JSON.parse(await result.errorBody());
4404
- if (body?.message) detail = ` \u2014 ${body.message}`;
4435
+ if (body?.message) detail = ` - ${body.message}`;
4405
4436
  } catch {
4406
4437
  }
4407
4438
  const reason = result.status === 401 || result.status === 403 ? "StreamElements rejected the widget share token. The share link may have been revoked or made private." : result.status === 404 ? "Element widget not found. Double-check the share URL." : `HTTP ${result.status}`;
@@ -7343,15 +7374,15 @@ var PLACEHOLDER_WIDGET_TYPES = /* @__PURE__ */ new Set([
7343
7374
  "streamlabels"
7344
7375
  ]);
7345
7376
  var PLACEHOLDER_REASONS = {
7346
- donation_ticker: "No native Lumia equivalent yet \u2014 imported as a placeholder text layer.",
7347
- stream_boss: "Lumia has a native streamboss module \u2014 set it up directly rather than importing the SL config.",
7348
- credits: "Lumia has a native credits module \u2014 set it up directly rather than importing the SL config.",
7349
- spin_wheel: "Lumia has a native spinwheel module \u2014 set it up directly rather than importing the SL config.",
7350
- emote_wall: "Lumia has a native emotebox module \u2014 set it up directly rather than importing the SL config.",
7351
- sub_train: "No native Lumia equivalent yet \u2014 imported as a placeholder text layer.",
7377
+ donation_ticker: "No native Lumia equivalent yet. imported as a placeholder text layer.",
7378
+ stream_boss: "Lumia has a native streamboss module. set it up directly rather than importing the SL config.",
7379
+ credits: "Lumia has a native credits module. set it up directly rather than importing the SL config.",
7380
+ spin_wheel: "Lumia has a native spinwheel module. set it up directly rather than importing the SL config.",
7381
+ emote_wall: "Lumia has a native emotebox module. set it up directly rather than importing the SL config.",
7382
+ sub_train: "No native Lumia equivalent yet. imported as a placeholder text layer.",
7352
7383
  streamlabels: "Use Lumia template variables (e.g. {{twitch_last_follower}}) inside a Text module instead of importing the SL streamlabels config."
7353
7384
  };
7354
- var CUSTOM_HTML_REVIEW_REASON = "The Streamlabs widget used custom HTML/CSS/JS that Lumia cannot host. The native Lumia module is the right starting point \u2014 recreate the styling there. The original SL settings are stashed under module.content.importMeta for reference.";
7385
+ var CUSTOM_HTML_REVIEW_REASON = "The Streamlabs widget used custom HTML/CSS/JS that Lumia cannot host. The native Lumia module is the right starting point. recreate the styling there. The original SL settings are stashed under module.content.importMeta for reference.";
7355
7386
  function stampImportMeta(module, widgetType, role, extra = {}) {
7356
7387
  stampImportMetaMerge(module, {
7357
7388
  source: "streamlabs",
@@ -7509,7 +7540,7 @@ function importStreamlabsWidget(response, widgetType, options = {}) {
7509
7540
  });
7510
7541
  reviewItems.push({
7511
7542
  moduleId: placeholder.module.id,
7512
- // SL doesn't have a single "widget" object the way SE does we
7543
+ // SL doesn't have a single "widget" object the way SE does. we
7513
7544
  // stash the settings snapshot here so the review step can show
7514
7545
  // the raw config the user imported. Cast through unknown because
7515
7546
  // ReviewItem is currently typed against SEWidget; widening the
@@ -7527,7 +7558,7 @@ function importStreamlabsWidget(response, widgetType, options = {}) {
7527
7558
  layers.push(placeholder.layer);
7528
7559
  modules[placeholder.layer.id] = placeholder.module;
7529
7560
  coverage.notes.push(
7530
- `Unknown Streamlabs widget type "${widgetType}" \u2014 imported as a placeholder.`
7561
+ `Unknown Streamlabs widget type "${widgetType}". imported as a placeholder.`
7531
7562
  );
7532
7563
  coverage.mappings.push({
7533
7564
  seType: `streamlabs:${widgetType}`,
@@ -7962,7 +7993,7 @@ function StepHeader({
7962
7993
  ] });
7963
7994
  }
7964
7995
  var JWT_FEATURES = [
7965
- "All your overlays \u2014 pick one, many, or all",
7996
+ "All your overlays: pick one, many, or all",
7966
7997
  "Live counters (follower / sub / tip / bits goals)",
7967
7998
  "Chat bot commands",
7968
7999
  "Loyalty points + watch time",
@@ -8116,7 +8147,7 @@ function StepModePicker({
8116
8147
  }
8117
8148
  )
8118
8149
  ] }),
8119
- caveat: "Requires pasting your StreamElements JWT. Stored only in memory \u2014 never sent to Lumia.",
8150
+ caveat: "Requires pasting your StreamElements JWT. Stored only in memory. Never sent to Lumia.",
8120
8151
  onClick: onChooseJwt
8121
8152
  }
8122
8153
  )
@@ -8141,7 +8172,7 @@ function StepConnect({
8141
8172
  {
8142
8173
  number: 1,
8143
8174
  title: "Connect your StreamElements account",
8144
- subtitle: "Paste your StreamElements JWT to import overlays, live counters, chat commands, loyalty points, watch time, and recent events. The token is read in your browser only \u2014 Lumia never sends it to our servers."
8175
+ subtitle: "Paste your StreamElements JWT to import overlays, live counters, chat commands, loyalty points, watch time, and recent events. The token is read in your browser only. Lumia never sends it to our servers."
8145
8176
  }
8146
8177
  ),
8147
8178
  /* @__PURE__ */ jsx14(
@@ -10790,15 +10821,15 @@ function importElementOverlay(response, options = {}) {
10790
10821
  // src/se-import/index.ts
10791
10822
  var IMPORT_META_KEY2 = "importMeta";
10792
10823
  var REVIEW_REASONS = {
10793
- "se-widget-bit-boss": "No native Lumia equivalent \u2014 bit boss fight bar with HP/damage states.",
10824
+ "se-widget-bit-boss": "No native Lumia equivalent. bit boss fight bar with HP/damage states.",
10794
10825
  // `se-widget-hype-cup` intentionally omitted — it now routes to the native
10795
10826
  // `tipjar` module (see dispatcher.ts) and lands with `partial` status, so it
10796
10827
  // never reaches the placeholder-reason fallback. If a future feature flag
10797
10828
  // disables tipjar auto-import, add an entry to FLAG_OFF_REASONS instead.
10798
- "se-widget-media-share": "No native Lumia equivalent \u2014 viewer-submitted media player.",
10799
- "se-widget-contest": "No native Lumia equivalent \u2014 SE-specific contest widget.",
10800
- "se-widget-giveaway": "No native Lumia equivalent \u2014 SE giveaway service.",
10801
- "se-widget-botcounter": "No native Lumia equivalent \u2014 chatter/message counter.",
10829
+ "se-widget-media-share": "No native Lumia equivalent. viewer-submitted media player.",
10830
+ "se-widget-contest": "No native Lumia equivalent. SE-specific contest widget.",
10831
+ "se-widget-giveaway": "No native Lumia equivalent. SE giveaway service.",
10832
+ "se-widget-botcounter": "No native Lumia equivalent. chatter/message counter.",
10802
10833
  "se-widget-top-cheerers-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render cheer rankings.',
10803
10834
  "se-widget-top-gifters-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render gifter rankings.',
10804
10835
  "se-widget-top-tippers-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render tipper rankings.',
@@ -10808,7 +10839,7 @@ var REVIEW_REASONS = {
10808
10839
  // so we can't pre-populate the slideshow items. The streamer adds their
10809
10840
  // product images by hand in the slideshow editor. Status = 'partial' (see
10810
10841
  // dispatcher.ts), so this string surfaces in the review step.
10811
- "se-widget-merch-products-rotator": "Imported as an empty slideshow. Lumia doesn't fetch your Fourthwall product catalog yet, so add your product images manually in the slideshow editor \u2014 the layer is pre-positioned and styled to match your SE widget."
10842
+ "se-widget-merch-products-rotator": "Imported as an empty slideshow. Lumia doesn't fetch your Fourthwall product catalog yet, so add your product images manually in the slideshow editor. the layer is pre-positioned and styled to match your SE widget."
10812
10843
  // Seasonal widgets (snow / halloween / halloween-2019 / xmas / valentine / easter
10813
10844
  // / world-cup) intentionally omitted — they route directly to image/slideshow/
10814
10845
  // video modules now (see dispatcher.ts SEASONAL_IMAGE_TYPES), so they always
@@ -10816,7 +10847,7 @@ var REVIEW_REASONS = {
10816
10847
  };
10817
10848
  function reasonFor(seType, status, flaggedOff) {
10818
10849
  if (flaggedOff && FLAG_OFF_REASONS[seType]) return FLAG_OFF_REASONS[seType];
10819
- return REVIEW_REASONS[seType] ?? (status === "template" ? "Imported as a generic template stub." : "No native mapping yet \u2014 imported as a labelled text placeholder.");
10850
+ return REVIEW_REASONS[seType] ?? (status === "template" ? "Imported as a generic template stub." : "No native mapping yet. imported as a labelled text placeholder.");
10820
10851
  }
10821
10852
  function buildSEBootstrapDescription(overlayId, sourceUrl) {
10822
10853
  const head = overlayId ? `Imported from StreamElements overlay ${overlayId}` : "Imported from StreamElements";
@@ -10846,7 +10877,7 @@ async function fetchSEBootstrap({
10846
10877
  }) {
10847
10878
  if (!apikey) {
10848
10879
  throw new Error(
10849
- "Missing access token. Use the public preview URL (Overlay menu \u2192 Get URL \u2192 Public link) \u2014 editor URLs do not include a token."
10880
+ "Missing access token. Use the public preview URL (Overlay menu \u2192 Get URL \u2192 Public link). editor URLs do not include a token."
10850
10881
  );
10851
10882
  }
10852
10883
  const result = await fetchWithProxyFallback(buildBootstrapUrl(overlayId), {
@@ -10857,7 +10888,7 @@ async function fetchSEBootstrap({
10857
10888
  let detail = "";
10858
10889
  try {
10859
10890
  const body = JSON.parse(await result.errorBody());
10860
- detail = body?.message ? ` \u2014 ${body.message}` : "";
10891
+ detail = body?.message ? `. ${body.message}` : "";
10861
10892
  } catch {
10862
10893
  }
10863
10894
  const reason = result.status === 401 || result.status === 403 ? "StreamElements rejected the URL token. The preview link may have been revoked." : result.status === 404 ? "Overlay not found. Double-check the URL." : `HTTP ${result.status}`;
@@ -10999,17 +11030,17 @@ function importSEBootstrap(bootstrap, options = {}) {
10999
11030
  }
11000
11031
  if (coverage.mappings.some((m) => m.status === "placeholder")) {
11001
11032
  coverage.notes.push(
11002
- "Some widgets imported as placeholder text layers \u2014 see SE/05-widget-to-module-mapping.md for status."
11033
+ "Some widgets imported as placeholder text layers. see SE/05-widget-to-module-mapping.md for status."
11003
11034
  );
11004
11035
  }
11005
11036
  if (coverage.mappings.some((m) => m.seType === "se-widget-merch-goal")) {
11006
11037
  coverage.notes.push(
11007
- "Merch-goal widgets imported as generic goals \u2014 see SE/03-missing-variables.md \xA78 for the missing data source."
11038
+ "Merch-goal widgets imported as generic goals. see SE/03-missing-variables.md \xA78 for the missing data source."
11008
11039
  );
11009
11040
  }
11010
11041
  if (coverage.mappings.some((m) => m.seType === "se-widget-custom-event-list")) {
11011
11042
  coverage.notes.push(
11012
- "\u26A0 Custom widgets are experimental \u2014 they run under the SE compatibility shim (Embed.tsx) which handles most SE APIs but may not cover every edge case. Verify each custom layer renders correctly in the editor."
11043
+ "\u26A0 Custom widgets are experimental. they run under the SE compatibility shim (Embed.tsx) which handles most SE APIs but may not cover every edge case. Verify each custom layer renders correctly in the editor."
11013
11044
  );
11014
11045
  }
11015
11046
  const overlay = {
@@ -11154,7 +11185,7 @@ function buildAIPromptForSEWidget(widget, canvas) {
11154
11185
  const fullVariables = JSON.stringify(variables, null, 2);
11155
11186
  const truncated = fullVariables.length > VARIABLES_BUDGET;
11156
11187
  const variablesJson = truncated ? `${fullVariables.slice(0, VARIABLES_BUDGET)}
11157
- \u2026[truncated \u2014 ${fullVariables.length - VARIABLES_BUDGET} more bytes elided]` : fullVariables;
11188
+ \u2026[truncated. ${fullVariables.length - VARIABLES_BUDGET} more bytes elided]` : fullVariables;
11158
11189
  return [
11159
11190
  "Recreate a StreamElements overlay widget as a Lumia Stream custom overlay (HTML + CSS + JS).",
11160
11191
  "",
@@ -11162,14 +11193,14 @@ function buildAIPromptForSEWidget(widget, canvas) {
11162
11193
  listener ? `Primary event listener: ${listener}` : "",
11163
11194
  listeners ? `Subscribes to events: ${listeners.join(", ")}` : "",
11164
11195
  `Canvas: ${landing.width} \xD7 ${landing.height}`,
11165
- truncated ? "Note: the widget config below is truncated \u2014 focus on visible/structural fields and ignore tail content you cannot see." : "",
11196
+ truncated ? "Note: the widget config below is truncated. focus on visible/structural fields and ignore tail content you cannot see." : "",
11166
11197
  "",
11167
11198
  "Widget configuration (JSON):",
11168
11199
  "```json",
11169
11200
  variablesJson,
11170
11201
  "```",
11171
11202
  "",
11172
- "Constraints \u2014 your output must run inside a Lumia Custom Overlay sandboxed iframe:",
11203
+ "Constraints. your output must run inside a Lumia Custom Overlay sandboxed iframe:",
11173
11204
  '- Use `window.Overlay.on("alert", handler)` / `window.Overlay.on("chat", handler)` to receive Lumia events.',
11174
11205
  "- Lumia template variables use double-brace syntax: {{username}}, {{amount}}, {{message}}, {{currency}}.",
11175
11206
  "- The `window.DATA` object holds user-configurable values (mirrors your `configs`).",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumiastream/ui",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "author": "Lumia Stream",
5
5
  "license": "ISC",
6
6
  "description": "Lumia UI Kit",
@@ -128,7 +128,7 @@
128
128
  "vitest": "^4.1.6"
129
129
  },
130
130
  "dependencies": {
131
- "@lumiastream/lumia-translations": "1.16.0",
131
+ "@lumiastream/lumia-translations": "1.16.1",
132
132
  "@lumiastream/lumia-types": "^3.3.7-alpha.2",
133
133
  "classnames": "^2.5.1",
134
134
  "globals": "^17.4.0",