@lumiastream/ui 0.6.1 → 0.6.2

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(
@@ -7080,7 +7110,7 @@ async function mirrorOneAsset(url, upload, signal, proxyAssetFetch) {
7080
7110
  const blob = await fetchAssetBlob(url, signal, proxyAssetFetch);
7081
7111
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
7082
7112
  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.`);
7113
+ throw new Error(`Asset is ${(blob.size / (1024 * 1024)).toFixed(1)} MB, over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
7084
7114
  }
7085
7115
  const filename = filenameFromURL(url);
7086
7116
  const type = blob.type || guessMime(filename);
@@ -7102,7 +7132,7 @@ async function fetchAssetBlob(url, signal, proxyAssetFetch) {
7102
7132
  if (!res.ok) throw new Error(`Source returned HTTP ${res.status}`);
7103
7133
  const declared = Number(res.headers.get("content-length"));
7104
7134
  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.`);
7135
+ throw new Error(`Asset is ${(declared / (1024 * 1024)).toFixed(1)} MB, over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
7106
7136
  }
7107
7137
  return res.blob();
7108
7138
  }
@@ -7342,7 +7372,7 @@ function decodeJwtPayload(jwt) {
7342
7372
  }
7343
7373
  const claims = parsed;
7344
7374
  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.");
7375
+ throw new SEAuthError("invalid-jwt", "Token payload is missing the 'channel' or 'authToken' claim. not a StreamElements account token.");
7346
7376
  }
7347
7377
  if (typeof claims.exp === "number" && claims.exp * 1e3 < Date.now()) {
7348
7378
  throw new SEAuthError("expired", "This StreamElements token has expired. Generate a new one on the SE Account page.");
@@ -7351,7 +7381,7 @@ function decodeJwtPayload(jwt) {
7351
7381
  }
7352
7382
  var SEClient = class _SEClient {
7353
7383
  // `jwt` is held alongside the decoded claims so the client can switch auth
7354
- // modes without re-parsing. Defaults to `bearer-jwt` confirmed against
7384
+ // modes without re-parsing. Defaults to `bearer-jwt`. confirmed against
7355
7385
  // the user's diagnostic 2026-05-16: every endpoint the existing 5 helpers
7356
7386
  // hit (users/current, channels/me, overlays, bot/commands, plus
7357
7387
  // sessions/loyalty/bits leaderboard transitively) returns 200, AND
@@ -7439,7 +7469,7 @@ var SEClient = class _SEClient {
7439
7469
  let detail = "";
7440
7470
  try {
7441
7471
  const body = await res.json();
7442
- if (body?.message) detail = ` \u2014 ${body.message}`;
7472
+ if (body?.message) detail = ` - ${body.message}`;
7443
7473
  } catch {
7444
7474
  }
7445
7475
  throw new Error(`StreamElements API returned HTTP ${res.status}${detail}`);
@@ -7481,7 +7511,7 @@ async function fetchElementWidgetInstancePublic(channelId, widgetInstanceId, pub
7481
7511
  let detail = "";
7482
7512
  try {
7483
7513
  const body = JSON.parse(await result.errorBody());
7484
- if (body?.message) detail = ` \u2014 ${body.message}`;
7514
+ if (body?.message) detail = ` - ${body.message}`;
7485
7515
  } catch {
7486
7516
  }
7487
7517
  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 +8519,15 @@ var PLACEHOLDER_WIDGET_TYPES = /* @__PURE__ */ new Set([
8489
8519
  "streamlabels"
8490
8520
  ]);
8491
8521
  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.",
8522
+ donation_ticker: "No native Lumia equivalent yet. imported as a placeholder text layer.",
8523
+ stream_boss: "Lumia has a native streamboss module. set it up directly rather than importing the SL config.",
8524
+ credits: "Lumia has a native credits module. set it up directly rather than importing the SL config.",
8525
+ spin_wheel: "Lumia has a native spinwheel module. set it up directly rather than importing the SL config.",
8526
+ emote_wall: "Lumia has a native emotebox module. set it up directly rather than importing the SL config.",
8527
+ sub_train: "No native Lumia equivalent yet. imported as a placeholder text layer.",
8498
8528
  streamlabels: "Use Lumia template variables (e.g. {{twitch_last_follower}}) inside a Text module instead of importing the SL streamlabels config."
8499
8529
  };
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.";
8530
+ 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
8531
  function stampImportMeta(module, widgetType, role, extra = {}) {
8502
8532
  stampImportMetaMerge(module, {
8503
8533
  source: "streamlabs",
@@ -8655,7 +8685,7 @@ function importStreamlabsWidget(response, widgetType, options = {}) {
8655
8685
  });
8656
8686
  reviewItems.push({
8657
8687
  moduleId: placeholder.module.id,
8658
- // SL doesn't have a single "widget" object the way SE does we
8688
+ // SL doesn't have a single "widget" object the way SE does. we
8659
8689
  // stash the settings snapshot here so the review step can show
8660
8690
  // the raw config the user imported. Cast through unknown because
8661
8691
  // ReviewItem is currently typed against SEWidget; widening the
@@ -8673,7 +8703,7 @@ function importStreamlabsWidget(response, widgetType, options = {}) {
8673
8703
  layers.push(placeholder.layer);
8674
8704
  modules[placeholder.layer.id] = placeholder.module;
8675
8705
  coverage.notes.push(
8676
- `Unknown Streamlabs widget type "${widgetType}" \u2014 imported as a placeholder.`
8706
+ `Unknown Streamlabs widget type "${widgetType}". imported as a placeholder.`
8677
8707
  );
8678
8708
  coverage.mappings.push({
8679
8709
  seType: `streamlabs:${widgetType}`,
@@ -9086,7 +9116,7 @@ function StepHeader({
9086
9116
  ] });
9087
9117
  }
9088
9118
  var JWT_FEATURES = [
9089
- "All your overlays \u2014 pick one, many, or all",
9119
+ "All your overlays: pick one, many, or all",
9090
9120
  "Live counters (follower / sub / tip / bits goals)",
9091
9121
  "Chat bot commands",
9092
9122
  "Loyalty points + watch time",
@@ -9240,7 +9270,7 @@ function StepModePicker({
9240
9270
  }
9241
9271
  )
9242
9272
  ] }),
9243
- caveat: "Requires pasting your StreamElements JWT. Stored only in memory \u2014 never sent to Lumia.",
9273
+ caveat: "Requires pasting your StreamElements JWT. Stored only in memory. Never sent to Lumia.",
9244
9274
  onClick: onChooseJwt
9245
9275
  }
9246
9276
  )
@@ -9265,7 +9295,7 @@ function StepConnect({
9265
9295
  {
9266
9296
  number: 1,
9267
9297
  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."
9298
+ 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
9299
  }
9270
9300
  ),
9271
9301
  /* @__PURE__ */ jsx14(
@@ -11914,15 +11944,15 @@ function importElementOverlay(response, options = {}) {
11914
11944
  // src/se-import/index.ts
11915
11945
  var IMPORT_META_KEY2 = "importMeta";
11916
11946
  var REVIEW_REASONS = {
11917
- "se-widget-bit-boss": "No native Lumia equivalent \u2014 bit boss fight bar with HP/damage states.",
11947
+ "se-widget-bit-boss": "No native Lumia equivalent. bit boss fight bar with HP/damage states.",
11918
11948
  // `se-widget-hype-cup` intentionally omitted — it now routes to the native
11919
11949
  // `tipjar` module (see dispatcher.ts) and lands with `partial` status, so it
11920
11950
  // never reaches the placeholder-reason fallback. If a future feature flag
11921
11951
  // 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.",
11952
+ "se-widget-media-share": "No native Lumia equivalent. viewer-submitted media player.",
11953
+ "se-widget-contest": "No native Lumia equivalent. SE-specific contest widget.",
11954
+ "se-widget-giveaway": "No native Lumia equivalent. SE giveaway service.",
11955
+ "se-widget-botcounter": "No native Lumia equivalent. chatter/message counter.",
11926
11956
  "se-widget-top-cheerers-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render cheer rankings.',
11927
11957
  "se-widget-top-gifters-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render gifter rankings.',
11928
11958
  "se-widget-top-tippers-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render tipper rankings.',
@@ -11932,7 +11962,7 @@ var REVIEW_REASONS = {
11932
11962
  // so we can't pre-populate the slideshow items. The streamer adds their
11933
11963
  // product images by hand in the slideshow editor. Status = 'partial' (see
11934
11964
  // 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."
11965
+ "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
11966
  // Seasonal widgets (snow / halloween / halloween-2019 / xmas / valentine / easter
11937
11967
  // / world-cup) intentionally omitted — they route directly to image/slideshow/
11938
11968
  // video modules now (see dispatcher.ts SEASONAL_IMAGE_TYPES), so they always
@@ -11940,7 +11970,7 @@ var REVIEW_REASONS = {
11940
11970
  };
11941
11971
  function reasonFor(seType, status, flaggedOff) {
11942
11972
  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.");
11973
+ return REVIEW_REASONS[seType] ?? (status === "template" ? "Imported as a generic template stub." : "No native mapping yet. imported as a labelled text placeholder.");
11944
11974
  }
11945
11975
  function buildSEBootstrapDescription(overlayId, sourceUrl) {
11946
11976
  const head = overlayId ? `Imported from StreamElements overlay ${overlayId}` : "Imported from StreamElements";
@@ -11970,7 +12000,7 @@ async function fetchSEBootstrap({
11970
12000
  }) {
11971
12001
  if (!apikey) {
11972
12002
  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."
12003
+ "Missing access token. Use the public preview URL (Overlay menu \u2192 Get URL \u2192 Public link). editor URLs do not include a token."
11974
12004
  );
11975
12005
  }
11976
12006
  const result = await fetchWithProxyFallback(buildBootstrapUrl(overlayId), {
@@ -11981,7 +12011,7 @@ async function fetchSEBootstrap({
11981
12011
  let detail = "";
11982
12012
  try {
11983
12013
  const body = JSON.parse(await result.errorBody());
11984
- detail = body?.message ? ` \u2014 ${body.message}` : "";
12014
+ detail = body?.message ? `. ${body.message}` : "";
11985
12015
  } catch {
11986
12016
  }
11987
12017
  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 +12153,17 @@ function importSEBootstrap(bootstrap, options = {}) {
12123
12153
  }
12124
12154
  if (coverage.mappings.some((m) => m.status === "placeholder")) {
12125
12155
  coverage.notes.push(
12126
- "Some widgets imported as placeholder text layers \u2014 see SE/05-widget-to-module-mapping.md for status."
12156
+ "Some widgets imported as placeholder text layers. see SE/05-widget-to-module-mapping.md for status."
12127
12157
  );
12128
12158
  }
12129
12159
  if (coverage.mappings.some((m) => m.seType === "se-widget-merch-goal")) {
12130
12160
  coverage.notes.push(
12131
- "Merch-goal widgets imported as generic goals \u2014 see SE/03-missing-variables.md \xA78 for the missing data source."
12161
+ "Merch-goal widgets imported as generic goals. see SE/03-missing-variables.md \xA78 for the missing data source."
12132
12162
  );
12133
12163
  }
12134
12164
  if (coverage.mappings.some((m) => m.seType === "se-widget-custom-event-list")) {
12135
12165
  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."
12166
+ "\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
12167
  );
12138
12168
  }
12139
12169
  const overlay = {
@@ -12278,7 +12308,7 @@ function buildAIPromptForSEWidget(widget, canvas) {
12278
12308
  const fullVariables = JSON.stringify(variables, null, 2);
12279
12309
  const truncated = fullVariables.length > VARIABLES_BUDGET;
12280
12310
  const variablesJson = truncated ? `${fullVariables.slice(0, VARIABLES_BUDGET)}
12281
- \u2026[truncated \u2014 ${fullVariables.length - VARIABLES_BUDGET} more bytes elided]` : fullVariables;
12311
+ \u2026[truncated. ${fullVariables.length - VARIABLES_BUDGET} more bytes elided]` : fullVariables;
12282
12312
  return [
12283
12313
  "Recreate a StreamElements overlay widget as a Lumia Stream custom overlay (HTML + CSS + JS).",
12284
12314
  "",
@@ -12286,14 +12316,14 @@ function buildAIPromptForSEWidget(widget, canvas) {
12286
12316
  listener ? `Primary event listener: ${listener}` : "",
12287
12317
  listeners ? `Subscribes to events: ${listeners.join(", ")}` : "",
12288
12318
  `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." : "",
12319
+ truncated ? "Note: the widget config below is truncated. focus on visible/structural fields and ignore tail content you cannot see." : "",
12290
12320
  "",
12291
12321
  "Widget configuration (JSON):",
12292
12322
  "```json",
12293
12323
  variablesJson,
12294
12324
  "```",
12295
12325
  "",
12296
- "Constraints \u2014 your output must run inside a Lumia Custom Overlay sandboxed iframe:",
12326
+ "Constraints. your output must run inside a Lumia Custom Overlay sandboxed iframe:",
12297
12327
  '- Use `window.Overlay.on("alert", handler)` / `window.Overlay.on("chat", handler)` to receive Lumia events.',
12298
12328
  "- Lumia template variables use double-brace syntax: {{username}}, {{amount}}, {{message}}, {{currency}}.",
12299
12329
  "- 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(
@@ -4000,7 +4030,7 @@ async function mirrorOneAsset(url, upload, signal, proxyAssetFetch) {
4000
4030
  const blob = await fetchAssetBlob(url, signal, proxyAssetFetch);
4001
4031
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
4002
4032
  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.`);
4033
+ throw new Error(`Asset is ${(blob.size / (1024 * 1024)).toFixed(1)} MB, over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
4004
4034
  }
4005
4035
  const filename = filenameFromURL(url);
4006
4036
  const type = blob.type || guessMime(filename);
@@ -4022,7 +4052,7 @@ async function fetchAssetBlob(url, signal, proxyAssetFetch) {
4022
4052
  if (!res.ok) throw new Error(`Source returned HTTP ${res.status}`);
4023
4053
  const declared = Number(res.headers.get("content-length"));
4024
4054
  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.`);
4055
+ throw new Error(`Asset is ${(declared / (1024 * 1024)).toFixed(1)} MB, over the ${MAX_MIRROR_ASSET_BYTES / (1024 * 1024)} MB import cap.`);
4026
4056
  }
4027
4057
  return res.blob();
4028
4058
  }
@@ -4262,7 +4292,7 @@ function decodeJwtPayload(jwt) {
4262
4292
  }
4263
4293
  const claims = parsed;
4264
4294
  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.");
4295
+ throw new SEAuthError("invalid-jwt", "Token payload is missing the 'channel' or 'authToken' claim. not a StreamElements account token.");
4266
4296
  }
4267
4297
  if (typeof claims.exp === "number" && claims.exp * 1e3 < Date.now()) {
4268
4298
  throw new SEAuthError("expired", "This StreamElements token has expired. Generate a new one on the SE Account page.");
@@ -4271,7 +4301,7 @@ function decodeJwtPayload(jwt) {
4271
4301
  }
4272
4302
  var SEClient = class _SEClient {
4273
4303
  // `jwt` is held alongside the decoded claims so the client can switch auth
4274
- // modes without re-parsing. Defaults to `bearer-jwt` confirmed against
4304
+ // modes without re-parsing. Defaults to `bearer-jwt`. confirmed against
4275
4305
  // the user's diagnostic 2026-05-16: every endpoint the existing 5 helpers
4276
4306
  // hit (users/current, channels/me, overlays, bot/commands, plus
4277
4307
  // sessions/loyalty/bits leaderboard transitively) returns 200, AND
@@ -4359,7 +4389,7 @@ var SEClient = class _SEClient {
4359
4389
  let detail = "";
4360
4390
  try {
4361
4391
  const body = await res.json();
4362
- if (body?.message) detail = ` \u2014 ${body.message}`;
4392
+ if (body?.message) detail = ` - ${body.message}`;
4363
4393
  } catch {
4364
4394
  }
4365
4395
  throw new Error(`StreamElements API returned HTTP ${res.status}${detail}`);
@@ -4401,7 +4431,7 @@ async function fetchElementWidgetInstancePublic(channelId, widgetInstanceId, pub
4401
4431
  let detail = "";
4402
4432
  try {
4403
4433
  const body = JSON.parse(await result.errorBody());
4404
- if (body?.message) detail = ` \u2014 ${body.message}`;
4434
+ if (body?.message) detail = ` - ${body.message}`;
4405
4435
  } catch {
4406
4436
  }
4407
4437
  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 +7373,15 @@ var PLACEHOLDER_WIDGET_TYPES = /* @__PURE__ */ new Set([
7343
7373
  "streamlabels"
7344
7374
  ]);
7345
7375
  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.",
7376
+ donation_ticker: "No native Lumia equivalent yet. imported as a placeholder text layer.",
7377
+ stream_boss: "Lumia has a native streamboss module. set it up directly rather than importing the SL config.",
7378
+ credits: "Lumia has a native credits module. set it up directly rather than importing the SL config.",
7379
+ spin_wheel: "Lumia has a native spinwheel module. set it up directly rather than importing the SL config.",
7380
+ emote_wall: "Lumia has a native emotebox module. set it up directly rather than importing the SL config.",
7381
+ sub_train: "No native Lumia equivalent yet. imported as a placeholder text layer.",
7352
7382
  streamlabels: "Use Lumia template variables (e.g. {{twitch_last_follower}}) inside a Text module instead of importing the SL streamlabels config."
7353
7383
  };
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.";
7384
+ 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
7385
  function stampImportMeta(module, widgetType, role, extra = {}) {
7356
7386
  stampImportMetaMerge(module, {
7357
7387
  source: "streamlabs",
@@ -7509,7 +7539,7 @@ function importStreamlabsWidget(response, widgetType, options = {}) {
7509
7539
  });
7510
7540
  reviewItems.push({
7511
7541
  moduleId: placeholder.module.id,
7512
- // SL doesn't have a single "widget" object the way SE does we
7542
+ // SL doesn't have a single "widget" object the way SE does. we
7513
7543
  // stash the settings snapshot here so the review step can show
7514
7544
  // the raw config the user imported. Cast through unknown because
7515
7545
  // ReviewItem is currently typed against SEWidget; widening the
@@ -7527,7 +7557,7 @@ function importStreamlabsWidget(response, widgetType, options = {}) {
7527
7557
  layers.push(placeholder.layer);
7528
7558
  modules[placeholder.layer.id] = placeholder.module;
7529
7559
  coverage.notes.push(
7530
- `Unknown Streamlabs widget type "${widgetType}" \u2014 imported as a placeholder.`
7560
+ `Unknown Streamlabs widget type "${widgetType}". imported as a placeholder.`
7531
7561
  );
7532
7562
  coverage.mappings.push({
7533
7563
  seType: `streamlabs:${widgetType}`,
@@ -7962,7 +7992,7 @@ function StepHeader({
7962
7992
  ] });
7963
7993
  }
7964
7994
  var JWT_FEATURES = [
7965
- "All your overlays \u2014 pick one, many, or all",
7995
+ "All your overlays: pick one, many, or all",
7966
7996
  "Live counters (follower / sub / tip / bits goals)",
7967
7997
  "Chat bot commands",
7968
7998
  "Loyalty points + watch time",
@@ -8116,7 +8146,7 @@ function StepModePicker({
8116
8146
  }
8117
8147
  )
8118
8148
  ] }),
8119
- caveat: "Requires pasting your StreamElements JWT. Stored only in memory \u2014 never sent to Lumia.",
8149
+ caveat: "Requires pasting your StreamElements JWT. Stored only in memory. Never sent to Lumia.",
8120
8150
  onClick: onChooseJwt
8121
8151
  }
8122
8152
  )
@@ -8141,7 +8171,7 @@ function StepConnect({
8141
8171
  {
8142
8172
  number: 1,
8143
8173
  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."
8174
+ 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
8175
  }
8146
8176
  ),
8147
8177
  /* @__PURE__ */ jsx14(
@@ -10790,15 +10820,15 @@ function importElementOverlay(response, options = {}) {
10790
10820
  // src/se-import/index.ts
10791
10821
  var IMPORT_META_KEY2 = "importMeta";
10792
10822
  var REVIEW_REASONS = {
10793
- "se-widget-bit-boss": "No native Lumia equivalent \u2014 bit boss fight bar with HP/damage states.",
10823
+ "se-widget-bit-boss": "No native Lumia equivalent. bit boss fight bar with HP/damage states.",
10794
10824
  // `se-widget-hype-cup` intentionally omitted — it now routes to the native
10795
10825
  // `tipjar` module (see dispatcher.ts) and lands with `partial` status, so it
10796
10826
  // never reaches the placeholder-reason fallback. If a future feature flag
10797
10827
  // 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.",
10828
+ "se-widget-media-share": "No native Lumia equivalent. viewer-submitted media player.",
10829
+ "se-widget-contest": "No native Lumia equivalent. SE-specific contest widget.",
10830
+ "se-widget-giveaway": "No native Lumia equivalent. SE giveaway service.",
10831
+ "se-widget-botcounter": "No native Lumia equivalent. chatter/message counter.",
10802
10832
  "se-widget-top-cheerers-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render cheer rankings.',
10803
10833
  "se-widget-top-gifters-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render gifter rankings.',
10804
10834
  "se-widget-top-tippers-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render tipper rankings.',
@@ -10808,7 +10838,7 @@ var REVIEW_REASONS = {
10808
10838
  // so we can't pre-populate the slideshow items. The streamer adds their
10809
10839
  // product images by hand in the slideshow editor. Status = 'partial' (see
10810
10840
  // 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."
10841
+ "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
10842
  // Seasonal widgets (snow / halloween / halloween-2019 / xmas / valentine / easter
10813
10843
  // / world-cup) intentionally omitted — they route directly to image/slideshow/
10814
10844
  // video modules now (see dispatcher.ts SEASONAL_IMAGE_TYPES), so they always
@@ -10816,7 +10846,7 @@ var REVIEW_REASONS = {
10816
10846
  };
10817
10847
  function reasonFor(seType, status, flaggedOff) {
10818
10848
  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.");
10849
+ return REVIEW_REASONS[seType] ?? (status === "template" ? "Imported as a generic template stub." : "No native mapping yet. imported as a labelled text placeholder.");
10820
10850
  }
10821
10851
  function buildSEBootstrapDescription(overlayId, sourceUrl) {
10822
10852
  const head = overlayId ? `Imported from StreamElements overlay ${overlayId}` : "Imported from StreamElements";
@@ -10846,7 +10876,7 @@ async function fetchSEBootstrap({
10846
10876
  }) {
10847
10877
  if (!apikey) {
10848
10878
  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."
10879
+ "Missing access token. Use the public preview URL (Overlay menu \u2192 Get URL \u2192 Public link). editor URLs do not include a token."
10850
10880
  );
10851
10881
  }
10852
10882
  const result = await fetchWithProxyFallback(buildBootstrapUrl(overlayId), {
@@ -10857,7 +10887,7 @@ async function fetchSEBootstrap({
10857
10887
  let detail = "";
10858
10888
  try {
10859
10889
  const body = JSON.parse(await result.errorBody());
10860
- detail = body?.message ? ` \u2014 ${body.message}` : "";
10890
+ detail = body?.message ? `. ${body.message}` : "";
10861
10891
  } catch {
10862
10892
  }
10863
10893
  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 +11029,17 @@ function importSEBootstrap(bootstrap, options = {}) {
10999
11029
  }
11000
11030
  if (coverage.mappings.some((m) => m.status === "placeholder")) {
11001
11031
  coverage.notes.push(
11002
- "Some widgets imported as placeholder text layers \u2014 see SE/05-widget-to-module-mapping.md for status."
11032
+ "Some widgets imported as placeholder text layers. see SE/05-widget-to-module-mapping.md for status."
11003
11033
  );
11004
11034
  }
11005
11035
  if (coverage.mappings.some((m) => m.seType === "se-widget-merch-goal")) {
11006
11036
  coverage.notes.push(
11007
- "Merch-goal widgets imported as generic goals \u2014 see SE/03-missing-variables.md \xA78 for the missing data source."
11037
+ "Merch-goal widgets imported as generic goals. see SE/03-missing-variables.md \xA78 for the missing data source."
11008
11038
  );
11009
11039
  }
11010
11040
  if (coverage.mappings.some((m) => m.seType === "se-widget-custom-event-list")) {
11011
11041
  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."
11042
+ "\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
11043
  );
11014
11044
  }
11015
11045
  const overlay = {
@@ -11154,7 +11184,7 @@ function buildAIPromptForSEWidget(widget, canvas) {
11154
11184
  const fullVariables = JSON.stringify(variables, null, 2);
11155
11185
  const truncated = fullVariables.length > VARIABLES_BUDGET;
11156
11186
  const variablesJson = truncated ? `${fullVariables.slice(0, VARIABLES_BUDGET)}
11157
- \u2026[truncated \u2014 ${fullVariables.length - VARIABLES_BUDGET} more bytes elided]` : fullVariables;
11187
+ \u2026[truncated. ${fullVariables.length - VARIABLES_BUDGET} more bytes elided]` : fullVariables;
11158
11188
  return [
11159
11189
  "Recreate a StreamElements overlay widget as a Lumia Stream custom overlay (HTML + CSS + JS).",
11160
11190
  "",
@@ -11162,14 +11192,14 @@ function buildAIPromptForSEWidget(widget, canvas) {
11162
11192
  listener ? `Primary event listener: ${listener}` : "",
11163
11193
  listeners ? `Subscribes to events: ${listeners.join(", ")}` : "",
11164
11194
  `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." : "",
11195
+ truncated ? "Note: the widget config below is truncated. focus on visible/structural fields and ignore tail content you cannot see." : "",
11166
11196
  "",
11167
11197
  "Widget configuration (JSON):",
11168
11198
  "```json",
11169
11199
  variablesJson,
11170
11200
  "```",
11171
11201
  "",
11172
- "Constraints \u2014 your output must run inside a Lumia Custom Overlay sandboxed iframe:",
11202
+ "Constraints. your output must run inside a Lumia Custom Overlay sandboxed iframe:",
11173
11203
  '- Use `window.Overlay.on("alert", handler)` / `window.Overlay.on("chat", handler)` to receive Lumia events.',
11174
11204
  "- Lumia template variables use double-brace syntax: {{username}}, {{amount}}, {{message}}, {{currency}}.",
11175
11205
  "- 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.2",
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",