@lumiastream/ui 0.2.7 → 0.2.8-alpha.10

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
@@ -508,7 +508,7 @@ import FormControlLabel from "@mui/material/FormControlLabel";
508
508
  import { forwardRef as forwardRef2 } from "react";
509
509
  import { useController, useFormContext } from "react-hook-form";
510
510
  import classNames4 from "classnames";
511
- import { jsx as jsx5 } from "react/jsx-runtime";
511
+ import { Fragment, jsx as jsx5 } from "react/jsx-runtime";
512
512
  var LSCheckbox = forwardRef2(({ className = "", checked, inputRef, slotProps, ...props }, ref) => {
513
513
  const CheckboxComponent = Checkbox;
514
514
  return /* @__PURE__ */ jsx5(
@@ -539,8 +539,31 @@ function LSCheckboxField({
539
539
  checkboxProps
540
540
  }) {
541
541
  const formContext = useFormContext();
542
- const resolvedControl = control ?? formContext.control;
543
- const { field } = useController({ name, control: resolvedControl });
542
+ const resolvedControl = control ?? formContext?.control;
543
+ if (!resolvedControl) return /* @__PURE__ */ jsx5(Fragment, {});
544
+ return /* @__PURE__ */ jsx5(
545
+ LSCheckboxFieldInner,
546
+ {
547
+ name,
548
+ label,
549
+ control: resolvedControl,
550
+ className,
551
+ disabled,
552
+ labelPlacement,
553
+ checkboxProps
554
+ }
555
+ );
556
+ }
557
+ function LSCheckboxFieldInner({
558
+ name,
559
+ label,
560
+ control,
561
+ className = "",
562
+ disabled,
563
+ labelPlacement = "end",
564
+ checkboxProps
565
+ }) {
566
+ const { field } = useController({ name, control });
544
567
  return /* @__PURE__ */ jsx5(
545
568
  FormControlLabel,
546
569
  {
@@ -1045,7 +1068,7 @@ import Search from "@mui/icons-material/Search";
1045
1068
  import KeyboardArrowDown2 from "@mui/icons-material/KeyboardArrowDown";
1046
1069
  import Tooltip from "@mui/material/Tooltip";
1047
1070
  import {
1048
- Fragment,
1071
+ Fragment as Fragment2,
1049
1072
  createContext,
1050
1073
  forwardRef as forwardRef4,
1051
1074
  useCallback as useCallback2,
@@ -1055,7 +1078,7 @@ import {
1055
1078
  useRef as useRef3,
1056
1079
  useState as useState4
1057
1080
  } from "react";
1058
- import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
1081
+ import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
1059
1082
  import { createElement } from "react";
1060
1083
  var LSVariableInputContext = createContext({});
1061
1084
  var LSVariableInputProvider = ({
@@ -1799,7 +1822,7 @@ var LSVariableInputField = forwardRef4((props, ref) => {
1799
1822
  const fullToken = `${rootToken}.${node.relativePath}`;
1800
1823
  const expanded = node.isExpandable && isPathExpanded(fullToken);
1801
1824
  const { displayValue, fullValue } = getVariableValuePreview(node.value);
1802
- return /* @__PURE__ */ jsxs5(Fragment, { children: [
1825
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
1803
1826
  /* @__PURE__ */ jsxs5(
1804
1827
  "div",
1805
1828
  {
@@ -1938,7 +1961,7 @@ var LSVariableInputField = forwardRef4((props, ref) => {
1938
1961
  const expanded = expandable && isPathExpanded(definition.name);
1939
1962
  const isLast = idx === filteredAllowedVariables.length - 1;
1940
1963
  const rowCls = `ls-variable-picker__row${isLast && !expanded ? " ls-variable-picker__row--no-border" : ""}`;
1941
- return /* @__PURE__ */ jsxs5(Fragment, { children: [
1964
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
1942
1965
  /* @__PURE__ */ jsxs5(
1943
1966
  "div",
1944
1967
  {
@@ -1978,7 +2001,7 @@ var LSVariableInputField = forwardRef4((props, ref) => {
1978
2001
  const tree = systemVariableValueTrees.get(variable.name);
1979
2002
  const expandable = !!tree && tree.length > 0;
1980
2003
  const expanded = expandable && isPathExpanded(rootToken);
1981
- return /* @__PURE__ */ jsxs5(Fragment, { children: [
2004
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
1982
2005
  /* @__PURE__ */ jsxs5(
1983
2006
  "div",
1984
2007
  {
@@ -2028,7 +2051,7 @@ var LSVariableInputField = forwardRef4((props, ref) => {
2028
2051
  ref
2029
2052
  }
2030
2053
  );
2031
- return /* @__PURE__ */ jsxs5(Fragment2, { children: [
2054
+ return /* @__PURE__ */ jsxs5(Fragment3, { children: [
2032
2055
  /* @__PURE__ */ jsx11(
2033
2056
  Popover,
2034
2057
  {
@@ -2212,7 +2235,7 @@ var VariableInputTextField = forwardRef4(
2212
2235
  ...paramsSlotInputProps,
2213
2236
  ...paramsInputProps,
2214
2237
  startAdornment,
2215
- endAdornment: /* @__PURE__ */ jsxs5(Fragment2, { children: [
2238
+ endAdornment: /* @__PURE__ */ jsxs5(Fragment3, { children: [
2216
2239
  endAdornment ?? null,
2217
2240
  showVariableIcon ? /* @__PURE__ */ jsx11(
2218
2241
  Tooltip,
@@ -2269,7 +2292,7 @@ function variableCompletionOptions(context, variables) {
2269
2292
  }
2270
2293
 
2271
2294
  // src/utils/codeMirrorlinterOptions.ts
2272
- import * as globals from "globals";
2295
+ import globals from "globals";
2273
2296
  var browserGlobals = Object.fromEntries(
2274
2297
  Object.entries(globals.browser).map(([key, value]) => [
2275
2298
  key,
@@ -3111,6 +3134,40 @@ function listenerPlaceholderContext(listener) {
3111
3134
  }
3112
3135
  return {};
3113
3136
  }
3137
+ var SE_WIDGET_TYPE_LABELS = {
3138
+ "subscriber-latest": "Latest subscriber",
3139
+ "subscriber-new-latest": "Latest new subscriber",
3140
+ "subscriber-resub-latest": "Latest resubscriber",
3141
+ "subscriber-gifted-latest": "Latest gifted sub",
3142
+ "subscriber-alltime-gifter": "Top all-time gifter",
3143
+ "follower-latest": "Latest follower",
3144
+ "cheer-latest": "Latest cheer",
3145
+ "cheer-total": "Total cheers",
3146
+ "tip-latest": "Latest tip",
3147
+ "tip-total": "Total tips",
3148
+ "se-widget-bit-boss": "Bit Boss",
3149
+ "se-widget-hype-cup": "Hype Cup",
3150
+ "se-widget-custom-event-list": "Custom widget",
3151
+ "se-widget-credit-roll": "Credits",
3152
+ "se-widget-media-share": "Media share",
3153
+ // Imports as an empty slideshow — Lumia has no Fourthwall catalog API, so
3154
+ // the streamer has to add product images by hand. The friendly title makes
3155
+ // that obvious in the layers panel; see REVIEW_REASONS for the full prompt.
3156
+ "se-widget-merch-products-rotator": "Merch products (add images manually)"
3157
+ };
3158
+ function defaultLabelForSEWidget(widget) {
3159
+ const trimmed = typeof widget.name === "string" ? widget.name.trim() : "";
3160
+ if (trimmed) return trimmed;
3161
+ const override = SE_WIDGET_TYPE_LABELS[widget.type];
3162
+ if (override) return override;
3163
+ const stripped = widget.type.replace(/^se-widget-/, "");
3164
+ const words = stripped.split("-").filter(Boolean);
3165
+ if (words.length === 0) return widget.type;
3166
+ const first = words[0];
3167
+ const head = first.charAt(0).toUpperCase() + first.slice(1);
3168
+ const tail = words.slice(1).join(" ");
3169
+ return tail ? `${head} ${tail}` : head;
3170
+ }
3114
3171
  function translateSeText(input, listener) {
3115
3172
  if (!input) return "";
3116
3173
  const ctx = listener ? listenerPlaceholderContext(listener) : {};
@@ -3180,11 +3237,21 @@ function listenerToLumiaVariable(listener) {
3180
3237
  return m[listener] ?? listener.replace(/-/g, "_");
3181
3238
  }
3182
3239
  function seCssToBounds(css, defaults = { width: 600, height: 200 }, canvas) {
3240
+ let widthIsAuto = false;
3241
+ let heightIsAuto = false;
3183
3242
  const toPx = (v, fallback, axis) => {
3184
- if (v == null) return fallback;
3243
+ if (v == null) {
3244
+ if (axis === "width") widthIsAuto = true;
3245
+ else if (axis === "height") heightIsAuto = true;
3246
+ return fallback;
3247
+ }
3185
3248
  if (typeof v === "number") return v;
3186
3249
  const s = v.trim();
3187
- if (s === "auto" || s === "") return fallback;
3250
+ if (s === "auto" || s === "") {
3251
+ if (axis === "width") widthIsAuto = true;
3252
+ else if (axis === "height") heightIsAuto = true;
3253
+ return fallback;
3254
+ }
3188
3255
  if (s.endsWith("%")) {
3189
3256
  const pct = parseFloat(s);
3190
3257
  if (canvas && axis && Number.isFinite(pct)) {
@@ -3198,6 +3265,8 @@ function seCssToBounds(css, defaults = { width: 600, height: 200 }, canvas) {
3198
3265
  };
3199
3266
  const width = toPx(css.width, defaults.width, "width");
3200
3267
  const height = toPx(css.height, defaults.height, "height");
3268
+ const autoWidth = widthIsAuto;
3269
+ const autoHeight = heightIsAuto;
3201
3270
  let x = toPx(css.left, 0, "width");
3202
3271
  let y = toPx(css.top, 0, "height");
3203
3272
  if (typeof css.transform === "string" && css.transform.includes("translate")) {
@@ -3223,7 +3292,9 @@ function seCssToBounds(css, defaults = { width: 600, height: 200 }, canvas) {
3223
3292
  width,
3224
3293
  height,
3225
3294
  opacity: css.opacity ?? 1,
3226
- zIndex: css["z-index"] ?? 0
3295
+ zIndex: css["z-index"] ?? 0,
3296
+ autoWidth,
3297
+ autoHeight
3227
3298
  };
3228
3299
  }
3229
3300
  var LUMIA_DEFAULT_SIZES = {
@@ -3233,7 +3304,11 @@ var LUMIA_DEFAULT_SIZES = {
3233
3304
  audio: { width: 200, height: 200 },
3234
3305
  slideshow: { width: 400, height: 220 },
3235
3306
  alert: { width: 700, height: 600 },
3236
- goal: { width: 740, height: 80 },
3307
+ // 80 was too short for the SE-style slim goal layout (label + bar + "Ends
3308
+ // in" sublabel needs ~125 px of vertical space). Bumped so editor-canvas
3309
+ // rendering (which doesn't honor autoHeight) doesn't clip the sublabel
3310
+ // after import.
3311
+ goal: { width: 740, height: 130 },
3237
3312
  timer: { width: 740, height: 80 },
3238
3313
  chatbox: { width: 375, height: 470 },
3239
3314
  eventlist: { width: 375, height: 470 },
@@ -3243,7 +3318,22 @@ var LUMIA_DEFAULT_SIZES = {
3243
3318
  emotebox: { width: 400, height: 400 },
3244
3319
  nowplaying: { width: 600, height: 250 },
3245
3320
  spotify: { width: 600, height: 250 },
3246
- youtubemusic: { width: 600, height: 250 }
3321
+ youtubemusic: { width: 600, height: 250 },
3322
+ tipjar: { width: 600, height: 700 },
3323
+ // Hype Train: wide-and-short by default to fit a horizontal progress bar.
3324
+ // All four built-in themes (subtle / train / rocket / inferno) render
3325
+ // fine at this aspect — rocket internally uses a vertical gauge but
3326
+ // still works at this footprint. Users can resize per their overlay.
3327
+ hypetrain: { width: 740, height: 140 },
3328
+ // Stream Boss: matches SE's bit-boss default (440×86) for a 1:1 import
3329
+ // experience, lightly padded so Lumia's Card-theme avatar fits without
3330
+ // resizing. Users can swap to Bar-Only and shrink height further.
3331
+ streamboss: { width: 480, height: 100 },
3332
+ // Song Request: SE's media-share default is 480×292. Lumia's now-playing
3333
+ // card + 5-item queue list needs a bit more vertical room, so we add ~80px
3334
+ // for the queue list. Users can hide the queue list and shrink back to
3335
+ // just the card if they want SE's exact footprint.
3336
+ songrequest: { width: 480, height: 380 }
3247
3337
  };
3248
3338
  function translateVariationCondition(seType, seCondition, requirement) {
3249
3339
  if (seType === "gift") return { conditionType: "IS_GIFT", condition: 1 };
@@ -3252,7 +3342,7 @@ function translateVariationCondition(seType, seCondition, requirement) {
3252
3342
  if (seCondition === "ATLEAST" && requirement != null) {
3253
3343
  return { conditionType: "GREATER_NUMBER", condition: requirement };
3254
3344
  }
3255
- return null;
3345
+ return { conditionType: "RANDOM", condition: 100 };
3256
3346
  }
3257
3347
  var LUMIA_LAYOUTS = /* @__PURE__ */ new Set(["column", "row", "onlyImage", "onlyText", "imageOver", "textOver"]);
3258
3348
  function translateLayout(seLayout) {
@@ -3280,7 +3370,13 @@ function buildUnit(widget, lumiaType, moduleExtras) {
3280
3370
  height: bounds.height,
3281
3371
  scale: [1, 1],
3282
3372
  opacity: bounds.opacity,
3283
- zIndex: bounds.zIndex
3373
+ zIndex: bounds.zIndex,
3374
+ // Per-axis flags — only the SE axis that was actually `auto`
3375
+ // collapses in render. The SE cheer goal is the canonical case:
3376
+ // width: "700px" + height: "auto" → numeric width stays at 700,
3377
+ // height collapses to content.
3378
+ ...bounds.autoWidth ? { autoWidth: true } : {},
3379
+ ...bounds.autoHeight ? { autoHeight: true } : {}
3284
3380
  }
3285
3381
  };
3286
3382
  const module = {
@@ -3288,7 +3384,7 @@ function buildUnit(widget, lumiaType, moduleExtras) {
3288
3384
  version: 1,
3289
3385
  settings: {
3290
3386
  type: lumiaType,
3291
- title: widget.name ?? null,
3387
+ title: defaultLabelForSEWidget(widget),
3292
3388
  locked: widget.locked ?? false
3293
3389
  },
3294
3390
  lights: [],
@@ -3302,6 +3398,19 @@ function buildUnit(widget, lumiaType, moduleExtras) {
3302
3398
  }
3303
3399
 
3304
3400
  // src/se-import/mappers/basic.ts
3401
+ function seExtraTextCss(seCss, scrolling) {
3402
+ const extra = {};
3403
+ if (typeof seCss["text-decoration"] === "string") extra.textDecoration = seCss["text-decoration"];
3404
+ if (typeof seCss["text-transform"] === "string") extra.textTransform = seCss["text-transform"];
3405
+ if (seCss["-webkit-text-stroke-color"]) extra["-webkit-text-stroke-color"] = seCss["-webkit-text-stroke-color"];
3406
+ if (seCss["-webkit-text-stroke-width"] != null) extra["-webkit-text-stroke-width"] = seCss["-webkit-text-stroke-width"];
3407
+ if (scrolling?.enabled) {
3408
+ extra.scroll = true;
3409
+ const speed = Number(scrolling.speed);
3410
+ if (Number.isFinite(speed) && speed > 0) extra.scrollSpeed = speed * 600;
3411
+ }
3412
+ return extra;
3413
+ }
3305
3414
  function mapText(widget) {
3306
3415
  const value = translateSeText(widget.text?.value ?? "");
3307
3416
  const seCss = widget.text?.css ?? {};
@@ -3318,7 +3427,8 @@ function mapText(widget) {
3318
3427
  color: seCss["color"] ?? "#ffffff",
3319
3428
  textShadow: seCss["text-shadow"] ?? "rgb(0, 0, 0) 1px 1px 1px",
3320
3429
  lineHeight: seCss["line-height"] ?? 1,
3321
- background: "transparent"
3430
+ background: "transparent",
3431
+ ...seExtraTextCss(seCss, widget.text?.scrolling)
3322
3432
  }
3323
3433
  });
3324
3434
  }
@@ -3340,6 +3450,17 @@ function mapVideo(widget) {
3340
3450
  }
3341
3451
  });
3342
3452
  }
3453
+ var LUMIA_SNOW_WEBM = "https://storage.lumiastream.com/overlays/global/seasonal/snow.webm";
3454
+ function mapSnow(widget) {
3455
+ return buildUnit(widget, "video", {
3456
+ content: {
3457
+ src: LUMIA_SNOW_WEBM,
3458
+ volume: 0,
3459
+ loop: true,
3460
+ muted: true
3461
+ }
3462
+ });
3463
+ }
3343
3464
  function mapReadout(widget, fallbackVar) {
3344
3465
  const explicitListener = widget.variables?.listener;
3345
3466
  const explicit = typeof explicitListener === "string" ? explicitListener : null;
@@ -3360,7 +3481,8 @@ function mapReadout(widget, fallbackVar) {
3360
3481
  color: seCss["color"] ?? "#ffffff",
3361
3482
  textShadow: seCss["text-shadow"] ?? "rgb(0, 0, 0) 1px 1px 1px",
3362
3483
  lineHeight: seCss["line-height"] ?? 1,
3363
- background: "transparent"
3484
+ background: "transparent",
3485
+ ...seExtraTextCss(seCss, widget.text?.scrolling)
3364
3486
  }
3365
3487
  });
3366
3488
  }
@@ -3378,9 +3500,20 @@ var SE_EVENT_TO_LUMIA_ALERT = {
3378
3500
  // LumiaAlertValues / activity.types.ts.
3379
3501
  merch: "fourthwall-shopOrder",
3380
3502
  purchase: "fourthwall-shopOrder",
3381
- charityCampaignDonation: "twitch-charityDonation",
3382
- host: "twitch-raid",
3383
- redemption: "twitch-redemption"
3503
+ charityCampaignDonation: "twitch-charityDonation"
3504
+ // NOTE — two SE events intentionally NOT mapped:
3505
+ // • `host` → previously `twitch-raid`. Twitch deprecated hosting in
3506
+ // late 2022 so the event never fires on the platform anymore. Even
3507
+ // if it did, host payloads don't carry a viewer count (raids do),
3508
+ // so the rendered alert would read "{name} raided with ?? viewers".
3509
+ // Drop the mapping; SE widgets configured with a `host` block fall
3510
+ // through to the unsupported placeholder rather than producing
3511
+ // misleading raid alerts.
3512
+ // • `redemption` → SE's `redemption` event is the legacy SE Store
3513
+ // redemption (their own loyalty-point store purchases), NOT Twitch
3514
+ // channel-point redemptions. Different payload, different semantics.
3515
+ // SE Store is mostly deprecated and seldom used; dropping the
3516
+ // mapping prevents misleading "channel-point redemption" alerts.
3384
3517
  };
3385
3518
  function pickGraphics(g) {
3386
3519
  if (!g) return { type: "image", content: { src: "", loop: false, volume: 1 } };
@@ -3399,25 +3532,48 @@ var SE_DEFAULT_DURATION_BY_EVENT = {
3399
3532
  tip: 5,
3400
3533
  cheer: 5,
3401
3534
  raid: 8,
3402
- host: 8,
3403
3535
  merch: 8,
3404
3536
  purchase: 8,
3405
- charityCampaignDonation: 8,
3406
- redemption: 5
3537
+ charityCampaignDonation: 8
3538
+ // `redemption` intentionally omitted — see SE_EVENT_TO_LUMIA_ALERT above
3539
+ // for why SE Store redemptions don't map to Twitch redemptions.
3407
3540
  };
3541
+ function cssKebabToCamel(input) {
3542
+ const out = {};
3543
+ for (const [k, v] of Object.entries(input)) {
3544
+ const camel = k.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
3545
+ out[camel] = v;
3546
+ }
3547
+ return out;
3548
+ }
3408
3549
  function buildAlertEvent(seEvent, seEventType) {
3409
3550
  if (!seEvent) {
3410
3551
  return { on: false, variations: [] };
3411
3552
  }
3412
3553
  const text = seEvent.text ?? {};
3413
3554
  const css = text.css ?? {};
3414
- const messageCss = css.message ?? {};
3555
+ const rawMessageCss = css.message ?? {};
3556
+ const messageCss = cssKebabToCamel(rawMessageCss);
3415
3557
  const highlight = css.highlights ?? {};
3416
- const variations = (seEvent.variations ?? []).map((variation) => buildAlertVariation(variation, seEventType)).filter((v) => v != null);
3558
+ const shadowEnabled = text.enableShadow !== false;
3559
+ const importedTextShadow = css["text-shadow"] ?? "rgba(0, 0, 0, 0.8) 1px 1px 1px";
3560
+ const textShadow = shadowEnabled ? importedTextShadow : "none";
3561
+ const marginTopRaw = Number(css["margin-top"]);
3562
+ const pushTextUp = Number.isFinite(marginTopRaw) ? -marginTopRaw : 0;
3563
+ const variations = (seEvent.variations ?? []).map((variation) => buildAlertVariation(variation, seEventType, seEvent)).filter((v) => v != null);
3417
3564
  const defaultDuration = (seEventType && SE_DEFAULT_DURATION_BY_EVENT[seEventType]) ?? 8;
3418
3565
  return {
3419
3566
  on: seEvent.enabled !== false,
3420
- theme: "CUSTOM",
3567
+ // CAREFUL: Lumia's `AlertThemes.CUSTOM` enum value is lowercase
3568
+ // `'custom'` (Overlay-UI/.../Alert.types.ts). The AlertBoxHandler
3569
+ // runtime does a strict equality check — `copiedAlert.theme ===
3570
+ // AlertThemes.CUSTOM` — so uppercase `'CUSTOM'` falls through to the
3571
+ // templated (Branded / Card / Simple) code path. That path calls
3572
+ // `replaceModuleVariables` WITHOUT the wrapVariablesWithHtml argument
3573
+ // and the variable highlight span never gets rendered, which is
3574
+ // exactly the "everything is white" bug imported alerts hit. The
3575
+ // lowercase value is load-bearing.
3576
+ theme: "custom",
3421
3577
  themeSettings: {
3422
3578
  primaryColor: null,
3423
3579
  accentColor: null,
@@ -3430,14 +3586,14 @@ function buildAlertEvent(seEvent, seEventType) {
3430
3586
  showBrandIcon: true
3431
3587
  },
3432
3588
  settings: { duration: (seEvent.duration ?? defaultDuration) * 1e3 },
3433
- layout: { value: translateLayout(seEvent.layout), css: {}, customCss: "" },
3589
+ layout: { value: translateLayout(seEvent.layout), css: {}, customCss: "", pushTextUp, pushTextLeft: 0 },
3434
3590
  text: {
3435
3591
  css: {
3436
3592
  fontFamily: css["font-family"] ?? "Roboto",
3437
3593
  fontSize: css["font-size"] ?? 30,
3438
3594
  color: css.color ?? "#ffffff",
3439
3595
  fontWeight: css["font-weight"] ?? "bold",
3440
- textShadow: css["text-shadow"] ?? "rgba(0, 0, 0, 0.8) 1px 1px 1px",
3596
+ textShadow,
3441
3597
  textAlign: css["text-align"] ?? "center"
3442
3598
  },
3443
3599
  messageCss,
@@ -3452,11 +3608,45 @@ function buildAlertEvent(seEvent, seEventType) {
3452
3608
  },
3453
3609
  animations: {
3454
3610
  alertEnterAnimation: seEvent.animation?.in ?? "fadeIn",
3611
+ alertEnterAnimationDelay: 0,
3455
3612
  alertEnterAnimationDuration: (seEvent.animation?.inDuration ?? 1) * 1e3,
3456
3613
  alertExitAnimation: seEvent.animation?.out ?? "fadeOut",
3614
+ alertExitAnimationDelay: 0,
3457
3615
  alertExitAnimationDuration: (seEvent.animation?.outDuration ?? 1) * 1e3,
3458
3616
  textEnterAnimation: text.animation ?? "",
3459
- textEnterAnimationDuration: 1e3
3617
+ textEnterDelay: 0,
3618
+ textEnterAnimationDuration: 1e3,
3619
+ textEnterRepeat: 1,
3620
+ // SE has no separate "text exit" semantics — the text fades out with
3621
+ // the alert. Lumia's AlertBoxHandler reads these fields directly to
3622
+ // schedule `setTextVisible(false)` via
3623
+ // duration - textExitAnimationDuration - textExitOffset
3624
+ // If either is undefined the math evaluates to NaN, setTimeout
3625
+ // fires immediately, and the text becomes visible for one frame
3626
+ // before being hidden — which is exactly the bug that made
3627
+ // imported alerts look like they had "no text at all". Defaulting
3628
+ // to a 1s fade-out that starts 1s before the alert ends matches
3629
+ // SE's perceived behaviour.
3630
+ textExitAnimation: "fadeOut",
3631
+ textExitAnimationDuration: 1e3,
3632
+ textExitOffset: 1e3,
3633
+ // SE wraps variables (`{name}`, `{amount}`, etc.) in a span styled by
3634
+ // `text.css.highlights`. Color comes via `themeSettings.highlightColor`;
3635
+ // the animation lives here. When SE *doesn't* set
3636
+ // `highlights.animation` explicitly, variables visually inherit the
3637
+ // text-level `text.animation` (`pulse`, `bounce`, etc.) because
3638
+ // they're nested inside the animating text. We mirror that fallback so
3639
+ // imports look the same as on SE — without it, common imports (where
3640
+ // only `text.animation` is set) would have a static highlight color
3641
+ // with no pulse, which reads as "the highlight animation didn't
3642
+ // import" even though the color did.
3643
+ highlightTextEnterAnimation: highlight.animation || text.animation || "",
3644
+ highlightTextEnterDelay: 0,
3645
+ highlightTextEnterAnimationDuration: 1e3,
3646
+ highlightTextEnterRepeat: 1,
3647
+ highlightTextExitAnimation: "",
3648
+ highlightTextExitAnimationDuration: 0,
3649
+ highlightTextExitOffset: 0
3460
3650
  },
3461
3651
  tts: {
3462
3652
  enabled: !!seEvent.tts?.enabled,
@@ -3467,10 +3657,12 @@ function buildAlertEvent(seEvent, seEventType) {
3467
3657
  variations
3468
3658
  };
3469
3659
  }
3470
- function buildAlertVariation(v, seEventType) {
3660
+ function buildAlertVariation(v, seEventType, parentSettings) {
3471
3661
  const translated = translateVariationCondition(v.type ?? null, v.condition ?? null, v.requirement ?? null);
3472
3662
  if (!translated) return null;
3473
- const base = buildAlertEvent(v.settings, seEventType);
3663
+ const { variations: _parentVariations, ...parentInheritable } = parentSettings ?? {};
3664
+ const mergedSettings = deepMergeSeSettings(parentSettings ? parentInheritable : void 0, v.settings);
3665
+ const base = buildAlertEvent(mergedSettings, seEventType);
3474
3666
  return {
3475
3667
  name: v.name ?? "Variation",
3476
3668
  on: v.enabled !== false,
@@ -3481,6 +3673,22 @@ function buildAlertVariation(v, seEventType) {
3481
3673
  ...base
3482
3674
  };
3483
3675
  }
3676
+ function deepMergeSeSettings(parent, variation) {
3677
+ if (parent == null) return variation;
3678
+ if (variation == null) return parent;
3679
+ if (typeof parent !== "object" || typeof variation !== "object") return variation;
3680
+ if (Array.isArray(parent) || Array.isArray(variation)) return variation;
3681
+ const out = { ...parent };
3682
+ for (const [k, vv] of Object.entries(variation)) {
3683
+ if (vv === void 0) continue;
3684
+ if (vv && typeof vv === "object" && !Array.isArray(vv) && parent[k] && typeof parent[k] === "object" && !Array.isArray(parent[k])) {
3685
+ out[k] = deepMergeSeSettings(parent[k], vv);
3686
+ } else {
3687
+ out[k] = vv;
3688
+ }
3689
+ }
3690
+ return out;
3691
+ }
3484
3692
  function mapAlertBox(widget, opts = {}) {
3485
3693
  const v = widget.variables ?? {};
3486
3694
  const events = {};
@@ -3509,6 +3717,9 @@ function currentForListener(listener) {
3509
3717
  if (listener.startsWith("subscriber-")) return "{{twitch_total_subscriber_count}}";
3510
3718
  if (listener.startsWith("cheer-")) return "{{twitch_total_bits_count}}";
3511
3719
  if (listener.startsWith("tip-")) return "{{total_donation_amount}}";
3720
+ if (listener === "merch-goal-orders") return "{{fourthwall_total_order_count}}";
3721
+ if (listener === "merch-goal-items") return "{{fourthwall_total_items_count}}";
3722
+ if (listener === "merch-goal-total") return "{{fourthwall_total_order_amount}}";
3512
3723
  if (listener.startsWith("merch-")) return "{{fourthwall_total_order_count}}";
3513
3724
  return "0";
3514
3725
  }
@@ -3518,8 +3729,11 @@ function mapGoal(widget) {
3518
3729
  return buildUnit(widget, "goal", {
3519
3730
  content: {
3520
3731
  version: 1,
3521
- // Matches GoalTypes enum in components/Modules/Views/Goal/types.ts.
3522
- type: "bar",
3732
+ // `slim` reproduces SE's default goal look — label above a thin
3733
+ // track, with `0` / `goalAmount` anchoring the bar's edges and the
3734
+ // live `current` floating at the percentage position. See
3735
+ // components/Modules/Views/Goal/types.ts:GoalTypes.SLIM.
3736
+ type: "slim",
3523
3737
  label: translateSeText(v.title ?? "Goal"),
3524
3738
  subLabel: "",
3525
3739
  // The Goal module renders this template literal with {{current}} + {{goal}} substituted.
@@ -3547,10 +3761,13 @@ function mapGoal(widget) {
3547
3761
  se_simple_design: !!v.simpleDesign
3548
3762
  },
3549
3763
  css: {
3550
- borderRadius: "0px",
3551
- borderStyle: "solid",
3552
- borderWidth: "0px",
3553
- borderColor: "transparent",
3764
+ // `borderRadius` / `borderStyle` / `borderWidth` / `borderColor`
3765
+ // used to be set here too, but the Goal renderer applies its own
3766
+ // border + borderRadius via inline `style={…}` on the .goal-bar
3767
+ // div (see Goal/index.tsx:123,127,155) sourced from
3768
+ // `content.border` and `content.borderRadius`. Writing them to
3769
+ // module.css just plopped useless transparent-0px rules on the
3770
+ // outer .overlay-module wrapper.
3554
3771
  lineHeight: 1,
3555
3772
  fontSize: 30,
3556
3773
  textAlign: "center",
@@ -3609,97 +3826,115 @@ function mapCustom(widget) {
3609
3826
  }
3610
3827
  });
3611
3828
  }
3612
- var SEASONAL_TEMPLATE_HTML = '<canvas id="particles"></canvas>';
3613
- var SEASONAL_TEMPLATE_CSS = `html, body { margin: 0; padding: 0; width: 100%; height: 100%; background: transparent; overflow: hidden; }
3614
- #particles { position: fixed; inset: 0; width: 100%; height: 100%; pointer-events: none; }`;
3615
- var SEASONAL_TEMPLATE_JS = `const SEASONS = {
3616
- snow: { emoji: '\\u2744', count: 80, sizeMin: 14, sizeMax: 28, gravity: 1.0, drift: 0.6, sway: 1.2 },
3617
- halloween: { emoji: '\\ud83d\\udc7b', count: 25, sizeMin: 28, sizeMax: 44, gravity: 0.6, drift: 0.4, sway: 0.8 },
3618
- xmas: { emoji: '\\u2744', count: 120, sizeMin: 16, sizeMax: 32, gravity: 1.1, drift: 0.7, sway: 1.3 },
3619
- valentine: { emoji: '\\u2764\\ufe0f', count: 50, sizeMin: 18, sizeMax: 34, gravity: 0.8, drift: 0.5, sway: 1.0 },
3620
- easter: { emoji: '\\ud83e\\udd5a', count: 30, sizeMin: 22, sizeMax: 38, gravity: 0.9, drift: 0.3, sway: 0.4 },
3621
- worldcup: { emoji: '\\u26bd', count: 25, sizeMin: 24, sizeMax: 40, gravity: 1.1, drift: 0.4, sway: 0.5 },
3622
- };
3623
- const cfg = SEASONS[(window.DATA && window.DATA.season) || 'snow'] || SEASONS.snow;
3624
- const canvas = document.getElementById('particles');
3625
- const ctx = canvas.getContext('2d');
3626
- const dpr = window.devicePixelRatio || 1;
3627
- function resize() {
3628
- canvas.width = window.innerWidth * dpr;
3629
- canvas.height = window.innerHeight * dpr;
3630
- canvas.style.width = window.innerWidth + 'px';
3631
- canvas.style.height = window.innerHeight + 'px';
3632
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
3633
- }
3634
- resize();
3635
- window.addEventListener('resize', resize);
3636
- const particles = [];
3637
- for (let i = 0; i < cfg.count; i++) {
3638
- particles.push({
3639
- x: Math.random() * window.innerWidth,
3640
- y: Math.random() * window.innerHeight - window.innerHeight,
3641
- size: cfg.sizeMin + Math.random() * (cfg.sizeMax - cfg.sizeMin),
3642
- speed: 0.5 + Math.random() * 1.5,
3643
- phase: Math.random() * Math.PI * 2,
3644
- });
3645
- }
3646
- function tick(now) {
3647
- ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
3648
- const t = now / 1000;
3649
- for (const p of particles) {
3650
- p.y += p.speed * cfg.gravity;
3651
- const sway = Math.sin(t + p.phase) * cfg.sway;
3652
- if (p.y > window.innerHeight + 40) {
3653
- p.y = -40;
3654
- p.x = Math.random() * window.innerWidth;
3655
- }
3656
- ctx.font = p.size + 'px serif';
3657
- ctx.fillText(cfg.emoji, p.x + sway * 20, p.y);
3658
- }
3659
- requestAnimationFrame(tick);
3660
- }
3661
- requestAnimationFrame(tick);`;
3662
- function mapSeasonal(widget, season) {
3663
- return buildUnit(widget, "custom", {
3664
- content: {
3665
- codeId: `seasonal-${season}-${widget.id}`,
3666
- html: SEASONAL_TEMPLATE_HTML,
3667
- css: SEASONAL_TEMPLATE_CSS,
3668
- js: SEASONAL_TEMPLATE_JS,
3669
- configs: [
3670
- {
3671
- key: "season",
3672
- label: "Season",
3673
- type: "dropdown",
3674
- value: season,
3675
- options: { snow: "Snow", halloween: "Halloween", xmas: "Christmas", valentine: "Valentine's", easter: "Easter", worldcup: "World Cup" }
3676
- }
3677
- ],
3678
- data: { season },
3679
- flavor: "lumia-seasonal"
3680
- }
3681
- });
3682
- }
3683
3829
 
3684
3830
  // src/se-import/mappers/misc.ts
3831
+ function mapChatboxTheme(seTheme) {
3832
+ const t = (seTheme ?? "").toString().toLowerCase();
3833
+ if (!t || t === "default") return "simple";
3834
+ if (t.includes("flipin") || t.includes("bubble") || t.includes("bubbly")) return "bubbly";
3835
+ if (t.includes("glass") || t.includes("transparent")) return "glass";
3836
+ if (t.includes("boxy") || t.includes("checker")) return "boxy";
3837
+ if (t.includes("basic")) return "basic";
3838
+ return "simple";
3839
+ }
3840
+ function paddingShorthand(message) {
3841
+ const pick = (key) => {
3842
+ const raw = message?.[key];
3843
+ if (raw == null || raw === "") return null;
3844
+ const n = typeof raw === "number" ? raw : parseFloat(String(raw));
3845
+ return Number.isFinite(n) ? n : null;
3846
+ };
3847
+ const t = pick("padding-top");
3848
+ const r = pick("padding-right");
3849
+ const b = pick("padding-bottom");
3850
+ const l = pick("padding-left");
3851
+ if (t == null && r == null && b == null && l == null) return void 0;
3852
+ const px = (n) => `${n ?? 0}px`;
3853
+ return `${px(t)} ${px(r)} ${px(b)} ${px(l)}`;
3854
+ }
3685
3855
  function mapChatbox(widget) {
3686
3856
  const v = widget.variables ?? {};
3687
- const fadeOutSeconds = typeof v.fadeOut === "number" ? v.fadeOut : 0;
3857
+ const textCss = widget.text?.css ?? {};
3858
+ const messageCss = textCss.message ?? {};
3859
+ const fadeOutSeconds = typeof v.fadeOut === "number" ? v.fadeOut : v.fadeOut === true ? 30 : 0;
3860
+ const messagePadding = paddingShorthand(messageCss);
3861
+ const DEFAULT_CHATBOX_FONT_SIZE = 14;
3862
+ const moduleCss = {};
3863
+ if (textCss["font-family"] != null) moduleCss.fontFamily = textCss["font-family"];
3864
+ moduleCss.fontSize = textCss["font-size"] != null ? textCss["font-size"] : DEFAULT_CHATBOX_FONT_SIZE;
3865
+ if (textCss["color"] != null) moduleCss.color = textCss["color"];
3866
+ if (textCss["font-weight"] != null) moduleCss.fontWeight = textCss["font-weight"];
3867
+ if (textCss["text-shadow"] != null) moduleCss.textShadow = textCss["text-shadow"];
3868
+ if (textCss["text-align"] != null) moduleCss.textAlign = textCss["text-align"];
3869
+ if (textCss["line-height"] != null) moduleCss.lineHeight = textCss["line-height"];
3870
+ if (messagePadding) moduleCss.padding = messagePadding;
3871
+ const DEFAULT_CHATBOX_BG = "#1e1e1e";
3872
+ const messageBackground = typeof messageCss["background"] === "string" && messageCss["background"].length > 0 ? messageCss["background"] : DEFAULT_CHATBOX_BG;
3873
+ moduleCss.background = messageBackground;
3874
+ const messageFontFamily = typeof messageCss["font-family"] === "string" && messageCss["font-family"].length > 0 ? messageCss["font-family"] : void 0;
3875
+ const highlightBackground = v.highlight && typeof v.highlight === "object" && typeof v.highlight["background"] === "string" ? v.highlight["background"] : void 0;
3876
+ const accentColor = highlightBackground && highlightBackground.length > 0 ? highlightBackground : void 0;
3688
3877
  return buildUnit(widget, "chatbox", {
3878
+ // Visual styling (font / color / padding etc) lives on module.css, not
3879
+ // content. Lumia's TextStyle settings panel + the chatbox renderer both
3880
+ // read from here.
3881
+ css: moduleCss,
3689
3882
  content: {
3690
- theme: v.theme ?? "default",
3883
+ theme: mapChatboxTheme(v.theme),
3691
3884
  fadeOutAfterDelay: fadeOutSeconds > 0,
3692
3885
  fadeOutDelayTime: fadeOutSeconds,
3886
+ // Defaults Lumia's chatbox renderer + UI expect on every module.
3887
+ // Without these the chatbox silently dropped messages (the
3888
+ // `ignoredStreamingSites` truthy gate), rendered in the wrong
3889
+ // theme (no theme → blank list), or showed missing controls in
3890
+ // settings. `ignoredList` carries SE's per-user ignore list across.
3891
+ ignoredList: Array.isArray(v.ignored) ? v.ignored : [],
3892
+ ignoredStreamingSites: [],
3893
+ horizontal: false,
3894
+ reverseFlow: false,
3895
+ hideAlerts: false,
3896
+ hideAlertMessage: false,
3693
3897
  showAvatar: true,
3694
- // SE `messageDelay` (between-message delay) and `delay` (initial delay) have no
3695
- // Lumia equivalent today — kept as provenance so users can wire them later if
3696
- // Lumia adds the feature.
3898
+ showBadges: true,
3899
+ showEmotes: true,
3900
+ showSiteIcon: true,
3901
+ showTimestamp: false,
3902
+ breakLine: false,
3903
+ hyperClickableLinks: true,
3904
+ clickableChatterProfiles: true,
3905
+ previewMediaInChat: true,
3906
+ maxItemsToShow: 20,
3907
+ removeAfter: 3e4,
3908
+ itemGap: 8,
3909
+ borderRadius: "10px",
3910
+ bgOpacity: 0.4,
3911
+ animations: {
3912
+ enterAnimation: "fadeIn",
3913
+ exitAnimation: "fadeOut",
3914
+ enterAnimationDuration: 1e3,
3915
+ exitAnimationDuration: 1e3,
3916
+ enterAnimationDelay: 0,
3917
+ exitAnimationDelay: 0
3918
+ },
3919
+ // Carry the SE message-level font override and the streamer's
3920
+ // accent color across so the imported chatbox looks right at
3921
+ // first paint. Background lives on `module.css.background`
3922
+ // above — `content.background` was a dead write the renderer
3923
+ // never read.
3924
+ ...messageFontFamily ? { messageFontFamily } : {},
3925
+ ...accentColor ? {
3926
+ chatboxStreamingSite: {
3927
+ twitch: { themeConfig: { primaryColor: accentColor, showUsernameColors: true } }
3928
+ }
3929
+ } : {},
3930
+ // SE-only fields preserved as provenance — no native Lumia equivalent.
3931
+ // `delay` / `messageDelay` (initial + per-message hold), and the
3932
+ // raw highlight block in case a future Lumia feature wants the
3933
+ // fine-grained styling.
3697
3934
  se_message_delay: v.messageDelay,
3698
3935
  se_initial_delay: v.delay,
3699
- // SE `ignored` (per-user ignore) and `highlight` (per-user emphasis) likewise
3700
- // have no Lumia equivalent — preserved as provenance only.
3701
- se_ignored_users: v.ignored,
3702
- se_highlight_users: v.highlight
3936
+ se_highlight_users: v.highlight,
3937
+ se_text_css: textCss
3703
3938
  }
3704
3939
  });
3705
3940
  }
@@ -3828,7 +4063,7 @@ var CREDITS_DEFAULT_EVENTS_TO_SHOW = {
3828
4063
  };
3829
4064
  function mapCredits(widget) {
3830
4065
  const v = widget.variables ?? {};
3831
- const itemGap = typeof v.padding === "number" ? v.padding : void 0;
4066
+ const itemGap = typeof v.padding === "number" ? v.padding : 8;
3832
4067
  const reverseFlow = v.direction === "bottom";
3833
4068
  return buildUnit(widget, "credits", {
3834
4069
  content: {
@@ -3849,13 +4084,38 @@ function mapCredits(widget) {
3849
4084
  }
3850
4085
  });
3851
4086
  }
4087
+ function inferSlideshowMediaType(mime, url) {
4088
+ if (typeof mime === "string") {
4089
+ if (mime.startsWith("video/")) return "video";
4090
+ if (mime.startsWith("image/")) return "image";
4091
+ }
4092
+ const ext = url.split("?")[0].split("#")[0].split(".").pop()?.toLowerCase() ?? "";
4093
+ if (["mp4", "webm", "mov", "m4v", "ogv"].includes(ext)) return "video";
4094
+ return "image";
4095
+ }
4096
+ function nameFromUrl(url) {
4097
+ try {
4098
+ const path = url.split("?")[0].split("#")[0];
4099
+ return decodeURIComponent(path.split("/").pop() ?? url);
4100
+ } catch {
4101
+ return url;
4102
+ }
4103
+ }
3852
4104
  function mapSlideshow(widget) {
3853
4105
  const v = widget.variables ?? {};
3854
- const sources = (v.images ?? []).map((i) => typeof i === "string" ? i : i.src ?? "").filter(Boolean);
4106
+ const items = (v.images ?? []).map((raw) => {
4107
+ const url = typeof raw === "string" ? raw : raw.url ?? raw.src ?? "";
4108
+ if (!url) return null;
4109
+ return {
4110
+ type: inferSlideshowMediaType(typeof raw === "object" ? raw?.type : void 0, url),
4111
+ src: url,
4112
+ name: nameFromUrl(url)
4113
+ };
4114
+ }).filter((item) => item !== null);
3855
4115
  const seAnim = typeof v.animation === "object" && v.animation !== null ? v.animation : {};
3856
4116
  return buildUnit(widget, "slideshow", {
3857
4117
  content: {
3858
- items: sources.map((src) => ({ src })),
4118
+ src: items,
3859
4119
  // `interval` of 0 = "rapid cycle" in SE (no delay between images); Lumia's
3860
4120
  // `imageDuration` is the total ms per image. Distinguish three cases:
3861
4121
  // - undefined → use 5000ms default
@@ -3866,7 +4126,10 @@ function mapSlideshow(widget) {
3866
4126
  loopDelay: 0,
3867
4127
  delay: 0,
3868
4128
  playAudio: !!v.playAudio,
3869
- random: !!v.random,
4129
+ // Lumia's renderer reads `content.shuffle` (Slideshow/index.tsx:21);
4130
+ // mirroring SE's `random` to that name keeps "play in random order"
4131
+ // working out of the box.
4132
+ shuffle: !!v.random,
3870
4133
  animation: {
3871
4134
  enterAnimation: typeof v.animation === "string" ? v.animation : seAnim.in ?? "fadeIn",
3872
4135
  exitAnimation: typeof v.animation === "string" ? v.animation : seAnim.out ?? "fadeOut",
@@ -3982,15 +4245,69 @@ function mapNowPlaying(widget) {
3982
4245
  }
3983
4246
  });
3984
4247
  }
4248
+ function mapMediaShare(widget) {
4249
+ const v = widget.variables ?? {};
4250
+ return buildUnit(widget, "songrequest", {
4251
+ content: {
4252
+ version: 1,
4253
+ // SE always shows the now-playing card; honour its cosmetic toggles
4254
+ // for the rest. `showTitle` defaults true; the others default to
4255
+ // SE's documented behavior (tipper visible, stats hidden).
4256
+ showNowPlayingCard: true,
4257
+ showQueueList: true,
4258
+ showSkipButton: true,
4259
+ showPauseButton: true,
4260
+ showTitle: v.showTitle !== false,
4261
+ showTipper: v.showTipper !== false,
4262
+ showAmount: v.showAmount === true,
4263
+ showStats: v.showStats === true,
4264
+ // SE doesn't have a queue-size knob — pick a sensible default
4265
+ // matching what fits in the SE widget's ~292px tall footprint.
4266
+ maxQueueItemsVisible: 5,
4267
+ // SE color knobs translate directly. Fall back to Lumia's defaults
4268
+ // (purple accent, near-black card background) if SE values are
4269
+ // missing — matches what the module ships with for native creates.
4270
+ accentColor: v.accentColor ?? "#9146ff",
4271
+ primaryColor: v.primaryColor ?? "#1a1a1a",
4272
+ // SE's widget hides the YT player by default; mirror that for
4273
+ // audio-only overlay use.
4274
+ playerVisible: false,
4275
+ defaultVolume: 70,
4276
+ autoAdvanceOnEnd: true,
4277
+ // SE-only knob preserved for round-trip provenance; Lumia ignores
4278
+ // it (LumiaStream auto-advance fires on `durationSeconds + grace`).
4279
+ se_timeLeft: v.timeLeft === true
4280
+ },
4281
+ css: {
4282
+ fontFamily: "Roboto",
4283
+ fontWeight: "bold",
4284
+ color: "#ffffff",
4285
+ textShadow: "rgb(0, 0, 0) 1px 1px 1px",
4286
+ background: "transparent",
4287
+ fontSize: 18
4288
+ }
4289
+ });
4290
+ }
3985
4291
  var SE_KAPPAGEN_EVENT_TO_LUMIA_ALERT = {
3986
4292
  follower: "twitch-follower",
3987
4293
  subscriber: "twitch-subscriber",
3988
4294
  cheer: "twitch-bits",
3989
4295
  raid: "twitch-raid",
3990
- host: "twitch-host",
3991
- tip: "streamelements-donation",
3992
- purchase: "streamlabs-merch",
3993
- embers: "youtube-superchat"
4296
+ tip: "streamelements-donation"
4297
+ // NOTE — three SE events intentionally NOT mapped:
4298
+ // • `host` → previously `twitch-host`, which doesn't exist in
4299
+ // LumiaAlertValues (verified). Twitch deprecated hosting
4300
+ // in 2022 so the event never fires anyway.
4301
+ // • `purchase` → previously `streamlabs-merch`, which is deprecated. The
4302
+ // main Alert Box mapper routes merch/purchase to
4303
+ // `fourthwall-shopOrder`; keep Kappagen consistent by
4304
+ // dropping the legacy SL path rather than splitting
4305
+ // emoji emotion between two competing alert keys.
4306
+ // • `embers` → previously `youtube-superchat`, which is wrong on both
4307
+ // ends: embers were a deprecated Twitch paid-emote
4308
+ // feature unrelated to YouTube super chats.
4309
+ // All three drop silently; the rest of the widget (size, spawn zone,
4310
+ // per-event flags for the remaining events) still imports.
3994
4311
  };
3995
4312
  function mapKappagen(widget) {
3996
4313
  const v = widget.variables ?? {};
@@ -4125,6 +4442,75 @@ function mapGiveaway(widget) {
4125
4442
  }
4126
4443
  });
4127
4444
  }
4445
+ var HYPETRAIN_LISTENER_KEYS = /* @__PURE__ */ new Set([
4446
+ "hypetrain-latest",
4447
+ "hypetrain-level-progress",
4448
+ "hypetrain-level-goal",
4449
+ "hypetrain-total",
4450
+ "hypetrain-latest-top-contributors",
4451
+ // SE alert-name variants some community widgets subscribe to as well.
4452
+ "hypetrain-start",
4453
+ "hypetrain-end",
4454
+ "hypetrain-cooldown-start",
4455
+ "hypetrain-cooldown-end"
4456
+ ]);
4457
+ function isHypetrainCustomWidget(widget) {
4458
+ const enabled = [];
4459
+ const listeners = widget.listeners;
4460
+ if (listeners && typeof listeners === "object" && !Array.isArray(listeners)) {
4461
+ for (const [k, v] of Object.entries(listeners)) {
4462
+ if (v) enabled.push(k);
4463
+ }
4464
+ } else if (Array.isArray(listeners)) {
4465
+ enabled.push(...listeners);
4466
+ }
4467
+ const explicit = widget.listener;
4468
+ if (typeof explicit === "string" && explicit && !enabled.includes(explicit)) {
4469
+ enabled.push(explicit);
4470
+ }
4471
+ if (enabled.length === 0) return false;
4472
+ return enabled.every((listener) => HYPETRAIN_LISTENER_KEYS.has(listener));
4473
+ }
4474
+ function mapHypetrain(widget) {
4475
+ const v = widget.variables ?? {};
4476
+ return buildUnit(widget, "hypetrain", {
4477
+ content: {
4478
+ // Twitch-native is the right default — SE hype-train widgets always
4479
+ // reflected the Twitch native train. Users can flip to 'synthetic'
4480
+ // or 'hybrid' from Settings if they want cross-platform pooling.
4481
+ sourceMode: "twitch-native",
4482
+ sources: null,
4483
+ // default money-only whitelist; ignored in twitch-native anyway
4484
+ customValues: {},
4485
+ pretrainThreshold: 5,
4486
+ pretrainDurationMs: 5 * 60 * 1e3,
4487
+ levelDurationMs: 5 * 60 * 1e3,
4488
+ levelStep: 5,
4489
+ levelThresholds: [],
4490
+ // Pick a moderately energetic default theme — SE hype-train widgets
4491
+ // were never subtle. Users can switch in Settings.
4492
+ theme: "train",
4493
+ primaryColor: typeof v.primaryColor === "string" ? v.primaryColor : "#ff6b00",
4494
+ accentColor: typeof v.accentColor === "string" ? v.accentColor : "#ffd84d",
4495
+ textColor: typeof v.textColor === "string" ? v.textColor : "#ffffff",
4496
+ showLevel: true,
4497
+ showTotal: true,
4498
+ showTimer: true,
4499
+ showTopContributor: true,
4500
+ showLeaderboard: true,
4501
+ leaderboardLimit: 5,
4502
+ leaderboardDurationMs: 30 * 1e3,
4503
+ levelUpSoundUrl: "https://storage.lumiastream.com/overlays/lumia/audio/crowdClap.mp3",
4504
+ trainEndSoundUrl: "https://storage.lumiastream.com/overlays/lumia/audio/crowdClap.mp3",
4505
+ audioVolume: 0.5,
4506
+ // SE config preserved verbatim as provenance — no Lumia equivalent for
4507
+ // most of these knobs (per-widget animation tunables, sound URLs the
4508
+ // streamer might want to migrate manually, etc.). Users can inspect
4509
+ // these via the overlay JSON if they want to recreate a specific look.
4510
+ se_variables: v
4511
+ }
4512
+ });
4513
+ }
4128
4514
  function mapUnsupportedAsText(widget) {
4129
4515
  return buildUnit(widget, "text", {
4130
4516
  content: {
@@ -4142,44 +4528,293 @@ function mapUnsupportedAsText(widget) {
4142
4528
  });
4143
4529
  }
4144
4530
 
4531
+ // src/se-import/mappers/hypecup.ts
4532
+ var SE_TYPE_TO_CATEGORY = {
4533
+ cheer: "bits",
4534
+ sub: "subs",
4535
+ follower: "follows",
4536
+ raid: "raids",
4537
+ tip: "tips"
4538
+ };
4539
+ function liftSESpritesIntoTiers(types) {
4540
+ const out = {};
4541
+ if (!types) return out;
4542
+ for (const [seKey, block] of Object.entries(types)) {
4543
+ const cat = SE_TYPE_TO_CATEGORY[seKey];
4544
+ if (!cat) continue;
4545
+ const variations = block?.variations ?? [];
4546
+ const usable = variations.filter((v) => typeof v?.src === "string" && v.src.length > 0);
4547
+ const flagged = usable.filter((v) => v.default === true);
4548
+ const picked = flagged.length > 0 ? flagged : usable;
4549
+ const seenAmounts = /* @__PURE__ */ new Set();
4550
+ const tiers = [];
4551
+ for (const v of picked) {
4552
+ const amount = typeof v.amount === "number" && v.amount > 0 ? v.amount : 1;
4553
+ if (seenAmounts.has(amount)) continue;
4554
+ seenAmounts.add(amount);
4555
+ tiers.push({
4556
+ minAmount: amount,
4557
+ imageUrl: v.src,
4558
+ scale: typeof v.cheerSize === "number" && v.cheerSize > 0 ? v.cheerSize : 1
4559
+ });
4560
+ }
4561
+ tiers.sort((a, b) => a.minAmount - b.minAmount);
4562
+ if (tiers.length > 0) out[cat] = tiers;
4563
+ }
4564
+ return out;
4565
+ }
4566
+ var LISTENER_TO_CATEGORY = {
4567
+ "follower-latest": "follows",
4568
+ "subscriber-latest": "subs",
4569
+ "tip-latest": "tips",
4570
+ "cheer-latest": "bits",
4571
+ "raid-latest": "raids"
4572
+ };
4573
+ function categoriesFromListeners(listeners) {
4574
+ const events = {
4575
+ tips: false,
4576
+ bits: false,
4577
+ subs: false,
4578
+ resubs: false,
4579
+ gifts: false,
4580
+ superchats: false,
4581
+ superstickers: false,
4582
+ kicks: false,
4583
+ charity: false,
4584
+ follows: false,
4585
+ raids: false,
4586
+ redemptions: false,
4587
+ chat: false
4588
+ };
4589
+ if (!listeners) return events;
4590
+ const onListeners = Array.isArray(listeners) ? listeners : Object.entries(listeners).filter(([, on]) => on === true).map(([k]) => k);
4591
+ for (const l of onListeners) {
4592
+ const cat = LISTENER_TO_CATEGORY[l];
4593
+ if (cat) events[cat] = true;
4594
+ }
4595
+ if (events.subs) events.resubs = true;
4596
+ return events;
4597
+ }
4598
+ function mapHypeCup(widget) {
4599
+ const v = widget.variables ?? {};
4600
+ const widgetWidth = typeof v.width === "number" && v.width > 0 ? v.width : parsePx(widget.css?.width);
4601
+ const widgetHeight = typeof v.height === "number" && v.height > 0 ? v.height : parsePx(widget.css?.height);
4602
+ const cupX = positionToFraction(v.cupPosition?.x, widgetWidth, 0.5);
4603
+ const cupY = positionToFraction(v.cupPosition?.y, widgetHeight, 0.7);
4604
+ const bounce = typeof v.bounce === "number" ? Math.min(1, Math.max(0, v.bounce)) : 0.3;
4605
+ const maxBodies = typeof v.recentEvents === "number" && v.recentEvents > 0 ? Math.min(300, Math.max(10, v.recentEvents)) : 80;
4606
+ const duration = typeof v.duration === "number" && v.duration > 0 ? Math.min(15, Math.max(1, v.duration)) : 3;
4607
+ const sprites = liftSESpritesIntoTiers(v.types);
4608
+ return buildUnit(widget, "tipjar", {
4609
+ content: {
4610
+ version: 1,
4611
+ // Phase 1 ships a single preset; SE's per-cup polygon set isn't lifted
4612
+ // yet (it'd require multi-cup support which we deliberately deferred).
4613
+ // The streamer can swap a custom cup image + wireframe via Settings.
4614
+ preset: "glass-mug",
4615
+ customCup: null,
4616
+ cup: {
4617
+ x: cupX,
4618
+ y: cupY,
4619
+ scale: 1,
4620
+ opacity: 1
4621
+ },
4622
+ physics: {
4623
+ gravity: 1,
4624
+ bounce,
4625
+ friction: 0.06,
4626
+ maxBodies
4627
+ },
4628
+ tokens: {
4629
+ defaultRadius: 26
4630
+ },
4631
+ events: categoriesFromListeners(widget.listeners),
4632
+ chat: {
4633
+ useViewerAvatars: true
4634
+ },
4635
+ display: {
4636
+ showWireframes: !!v.showWireframes,
4637
+ showEventMessages: !!v.textEnabled,
4638
+ messageDuration: duration
4639
+ },
4640
+ sprites,
4641
+ // Phase 2 defaults for goal/break — disabled by import. Streamer
4642
+ // can enable in Settings; SE has no equivalent so nothing to lift.
4643
+ goal: { active: false, currency: "tips", target: 100, current: 0, readoutTemplate: "{{current}} / {{target}}", textColor: "#ffffff", hitAt: null },
4644
+ break: { enabled: false, command: "!breakjar", rebuildDelay: 5e3 },
4645
+ // SE-only knobs that don't have a Lumia equivalent — preserved as
4646
+ // provenance for future phases (per-cup polygons + enter/movement
4647
+ // physics tweaks). `se_types` removed — now consumed into sprites above.
4648
+ se_enter_from: v.enterFrom ?? null,
4649
+ se_prevent_movement: !!v.preventMovement,
4650
+ se_fullscreen: !!v.fullscreen,
4651
+ se_cups: v.cups ?? null
4652
+ },
4653
+ css: {
4654
+ background: "transparent"
4655
+ }
4656
+ });
4657
+ }
4658
+ function clamp01(n) {
4659
+ if (!Number.isFinite(n)) return 0.5;
4660
+ return Math.min(1, Math.max(0, n));
4661
+ }
4662
+ function positionToFraction(raw, widgetDim, fallback) {
4663
+ if (typeof raw !== "number" || !Number.isFinite(raw)) return fallback;
4664
+ if (raw <= 1) return clamp01(raw);
4665
+ if (typeof widgetDim !== "number" || widgetDim <= 0) return fallback;
4666
+ return clamp01(raw / widgetDim);
4667
+ }
4668
+ function parsePx(raw) {
4669
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw;
4670
+ if (typeof raw !== "string") return void 0;
4671
+ const match = raw.match(/^(-?\d+(?:\.\d+)?)/);
4672
+ if (!match) return void 0;
4673
+ const n = Number(match[1]);
4674
+ return Number.isFinite(n) ? n : void 0;
4675
+ }
4676
+
4677
+ // src/se-import/mappers/streamboss.ts
4678
+ var SE_LISTENER_TO_ALERT = {
4679
+ "follower-latest": "twitch-follower",
4680
+ "subscriber-latest": "twitch-subscriber",
4681
+ "tip-latest": "lumiastream-donation",
4682
+ "cheer-latest": "twitch-bits",
4683
+ "raid-latest": "twitch-raid"
4684
+ // `purchase-latest` and `subscriber-gifted-latest` are intentionally absent.
4685
+ // SE rolled gifted subs into `subscriber-latest`; Lumia splits them via
4686
+ // extraSettings.isGift, which the runtime already handles consistently.
4687
+ };
4688
+ var SE_LISTENER_MODE = {
4689
+ "follower-latest": "flat",
4690
+ "subscriber-latest": "flat",
4691
+ "tip-latest": "multiplier",
4692
+ // 100 means "100 dmg per $"
4693
+ "cheer-latest": "multiplier",
4694
+ // 1 means "1 dmg per bit"
4695
+ "raid-latest": "multiplier"
4696
+ // 10 means "10 dmg per viewer"
4697
+ };
4698
+ function listenerIsOn(listeners, key) {
4699
+ if (!listeners) return false;
4700
+ if (Array.isArray(listeners)) return listeners.includes(key);
4701
+ return listeners[key] === true;
4702
+ }
4703
+ function mapStreamBoss(widget) {
4704
+ const v = widget.variables ?? {};
4705
+ const baseHp = typeof v.baseHP === "number" && v.baseHP > 0 ? Math.round(v.baseHP) : 1e3;
4706
+ const seDamage = v.damage ?? {};
4707
+ const damageMap = {};
4708
+ const selectedEvents = [];
4709
+ for (const [seListener, alertKey] of Object.entries(SE_LISTENER_TO_ALERT)) {
4710
+ if (!listenerIsOn(widget.listeners, seListener)) continue;
4711
+ const raw = seDamage[seListener];
4712
+ const value = typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : null;
4713
+ if (value === null) continue;
4714
+ damageMap[alertKey] = { value, mode: SE_LISTENER_MODE[seListener] };
4715
+ selectedEvents.push(alertKey);
4716
+ }
4717
+ const barColor = typeof v.colors?.frame === "string" && v.colors.frame ? v.colors.frame : "#2bff00";
4718
+ const widgetHeight = parsePx2(widget.css?.height);
4719
+ const tooShortForCard = typeof widgetHeight === "number" && widgetHeight < 140;
4720
+ const theme = v.showImage === false || tooShortForCard ? "bar-only" : "card";
4721
+ return buildUnit(widget, "streamboss", {
4722
+ content: {
4723
+ version: 1,
4724
+ theme,
4725
+ initialBossName: "Stream Boss",
4726
+ // SE has no stage cycling — every takeover resets to baseHP. We
4727
+ // honor that by shipping a single-element healthStages array; the
4728
+ // runtime cycles modulo length so a one-element array stays put.
4729
+ healthStages: [baseHp],
4730
+ currentBoss: "Stream Boss",
4731
+ currentBossAvatar: "",
4732
+ currentHP: baseHp,
4733
+ maxHP: baseHp,
4734
+ currentStageIndex: 0,
4735
+ damageMap,
4736
+ // Concrete selectedEvents means "user has opted in to exactly these
4737
+ // events". Empty arrays would disable everything — only happens if
4738
+ // SE had all listeners off, which mirrors what the imported overlay
4739
+ // should do.
4740
+ selectedEvents,
4741
+ bossHealsOnEventTrigger: v.selfHeal !== false,
4742
+ newBossCommand: "!newboss",
4743
+ eventQueueDelayMs: 3e3,
4744
+ defeatedDelayMs: 3e3,
4745
+ showAvatar: v.showImage !== false,
4746
+ showFloatingNumbers: true,
4747
+ showEventMessages: v.showText !== false,
4748
+ barColor,
4749
+ barBackgroundColor: "#333333",
4750
+ attackColor: "#ff007f",
4751
+ healColor: "#00ff00",
4752
+ // Phase 2 defaults — off by default so imports stay close to
4753
+ // SE's behaviour and don't surprise the streamer with new effects.
4754
+ lastResetAt: 0,
4755
+ playSounds: true,
4756
+ audioVolume: 50,
4757
+ damageSounds: [],
4758
+ healSound: "",
4759
+ newBossSound: "",
4760
+ defeatSound: "",
4761
+ megaBossChance: 0,
4762
+ particleScatter: true,
4763
+ showCrown: true,
4764
+ matchBossChatColor: true,
4765
+ bossNameColor: "",
4766
+ botChat: "off",
4767
+ chatAsSelf: false,
4768
+ // SE-only knobs preserved as provenance for future passes. None
4769
+ // have direct Lumia analogs today:
4770
+ // - `right` anchors the boss to the right edge of its bounds;
4771
+ // Lumia uses layer position instead, so this is informational.
4772
+ // - `bossName` controls vertical name position in SE; our themes
4773
+ // don't expose that level of fine-grained tweaking yet.
4774
+ // - `mode` SE's gameplay mode hint ('overkill', etc.).
4775
+ se_right: !!v.right,
4776
+ se_bossNamePosition: typeof v.bossName === "string" ? v.bossName : null,
4777
+ se_mode: typeof v.mode === "string" ? v.mode : null,
4778
+ se_showName: v.showName !== false
4779
+ },
4780
+ css: {
4781
+ background: "transparent"
4782
+ }
4783
+ });
4784
+ }
4785
+ function parsePx2(raw) {
4786
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw;
4787
+ if (typeof raw !== "string") return void 0;
4788
+ const match = raw.match(/^(-?\d+(?:\.\d+)?)/);
4789
+ if (!match) return void 0;
4790
+ const n = Number(match[1]);
4791
+ return Number.isFinite(n) ? n : void 0;
4792
+ }
4793
+
4145
4794
  // src/se-import/feature-flags.ts
4146
4795
  var SE_IMPORT_FLAGS = {
4147
4796
  // (Removed) IMPORT_CUSTOM_EMBED: custom-event-list now routes to native eventlist
4148
4797
  // by default with a one-click "switch to custom HTML" option in the review step.
4149
4798
  // (Removed) IMPORT_MERCH_GOAL: Fourthwall integration provides session/total order
4150
4799
  // counters now (see fourthwall.manager.ts ORDER_PLACED handler); merch goal maps
4151
- // to {{fourthwall_total_order_count}} via the goal mapper.
4152
- // Merch products rotator Lumia has no Fourthwall product-list API yet. The
4153
- // Fourthwall integration is webhook-driven (events flow in) but doesn't fetch
4154
- // the user's shop catalog. Adding it requires a separate Fourthwall API auth
4155
- // flow + product-fetch endpoint + periodic refresh. Until then a static stub
4156
- // is unhelpful, so users are routed to review for AI/skip/marketplace-when-
4157
- // available replacements.
4158
- IMPORT_MERCH_PRODUCTS: false,
4159
- // Seasonal effects (snow/halloween/xmas/valentine/easter/worldcup). The
4160
- // generic canvas-particle template is approximate, not pixel-accurate vs
4161
- // SE's polished versions. Flagging off so users get the option to grab a
4162
- // AI-generated replacement or a curated marketplace seasonal overlay when one
4163
- // is configured.
4164
- IMPORT_SEASONAL: false
4800
+ // to {{fourthwall_total_*}} via the goal mapper (per-listener: orders → order_count,
4801
+ // items items_count, total order_amount).
4802
+ // (Removed) IMPORT_SEASONAL: seasonal widgets now route per-widget into existing
4803
+ // modules (image / slideshow / video) rather than a single approximate canvas-
4804
+ // particle template. See dispatcher.ts SEASONAL_IMAGE_TYPES comment for the
4805
+ // rationale and /SE/05-widget-to-module-mapping.md §D for the current mapping.
4806
+ // (Removed) IMPORT_MERCH_PRODUCTS: the merch-products-rotator now imports as an
4807
+ // empty slideshow with a friendly layer title prompting the user to add product
4808
+ // images manually. This is a degraded but honest UX — better than a placeholder
4809
+ // text layer for the (rare) streamer who uses SE's merch rotator. A Fourthwall
4810
+ // catalog fetcher would unlock fully-automatic import, but that's a multi-day
4811
+ // project gated on a new Fourthwall API auth scope, an endpoint, and a refresh
4812
+ // cadence.
4165
4813
  };
4166
- function isWidgetFlaggedOff(seType) {
4167
- if (seType === "se-widget-merch-products-rotator") return !SE_IMPORT_FLAGS.IMPORT_MERCH_PRODUCTS;
4168
- if (seType === "se-widget-snow" || seType === "se-widget-halloween" || seType === "se-widget-halloween-2019" || seType === "se-widget-xmas" || seType === "se-widget-valentine" || seType === "se-widget-easter" || seType === "se-widget-world-cup") {
4169
- return !SE_IMPORT_FLAGS.IMPORT_SEASONAL;
4170
- }
4814
+ function isWidgetFlaggedOff(_seType) {
4171
4815
  return false;
4172
4816
  }
4173
- var FLAG_OFF_REASONS = {
4174
- "se-widget-merch-products-rotator": "Merch products rotator \u2014 Lumia tracks Fourthwall orders but doesn't fetch the full product catalog yet. Generate a replacement with AI, skip it, or use Marketplace when a curated replacement is configured.",
4175
- "se-widget-snow": "Seasonal effect \u2014 the generic auto-import template is approximate. Generate a replacement with AI, skip it, or use Marketplace when a curated version is configured.",
4176
- "se-widget-halloween": "Seasonal effect \u2014 the generic auto-import template is approximate. Generate a replacement with AI, skip it, or use Marketplace when a curated version is configured.",
4177
- "se-widget-halloween-2019": "Seasonal effect \u2014 the generic auto-import template is approximate. Generate a replacement with AI, skip it, or use Marketplace when a curated version is configured.",
4178
- "se-widget-xmas": "Seasonal effect \u2014 the generic auto-import template is approximate. Generate a replacement with AI, skip it, or use Marketplace when a curated version is configured.",
4179
- "se-widget-valentine": "Seasonal effect \u2014 the generic auto-import template is approximate. Generate a replacement with AI, skip it, or use Marketplace when a curated version is configured.",
4180
- "se-widget-easter": "Seasonal effect \u2014 the generic auto-import template is approximate. Generate a replacement with AI, skip it, or use Marketplace when a curated version is configured.",
4181
- "se-widget-world-cup": "Seasonal effect \u2014 the generic auto-import template is approximate. Generate a replacement with AI, skip it, or use Marketplace when a curated version is configured."
4182
- };
4817
+ var FLAG_OFF_REASONS = {};
4183
4818
 
4184
4819
  // src/se-import/dispatcher.ts
4185
4820
  var READOUT_TYPES = {
@@ -4208,15 +4843,7 @@ var READOUT_TYPES = {
4208
4843
  // twitch_session_chat_count, so we render it as a text layer with the variable inline.
4209
4844
  "se-widget-botcounter": "twitch_session_chat_count"
4210
4845
  };
4211
- var SEASONAL_TYPES = {
4212
- "se-widget-snow": "snow",
4213
- "se-widget-halloween": "halloween",
4214
- "se-widget-halloween-2019": "halloween",
4215
- "se-widget-xmas": "xmas",
4216
- "se-widget-valentine": "valentine",
4217
- "se-widget-easter": "easter",
4218
- "se-widget-world-cup": "worldcup"
4219
- };
4846
+ var SEASONAL_IMAGE_TYPES = /* @__PURE__ */ new Set(["se-widget-halloween", "se-widget-xmas", "se-widget-valentine", "se-widget-easter", "se-widget-world-cup"]);
4220
4847
  var GOAL_TYPES = /* @__PURE__ */ new Set([
4221
4848
  "se-widget-follower-goal",
4222
4849
  "se-widget-subscriber-goal",
@@ -4242,7 +4869,6 @@ function dispatch(widget) {
4242
4869
  if (GOAL_TYPES.has(t)) return { unit: mapGoal(widget), status: "direct", lumiaType: "goal" };
4243
4870
  if (t === "se-widget-alert-box") return { unit: mapAlertBox(widget), status: "direct", lumiaType: "alert" };
4244
4871
  if (t === "se-widget-alert-box-merch") return { unit: mapAlertBox(widget, { onlyEvents: ["merch"] }), status: "direct", lumiaType: "alert" };
4245
- if (t === "se-widget-store-redemptions") return { unit: mapSingleAlert(widget, "twitch-redemption"), status: "direct", lumiaType: "alert" };
4246
4872
  if (t === "se-widget-donor-drive-alert") return { unit: mapSingleAlert(widget, "donordrive-donation"), status: "direct", lumiaType: "alert" };
4247
4873
  if (t === "se-widget-extralife-alert") return { unit: mapSingleAlert(widget, "extralife-donation"), status: "direct", lumiaType: "alert" };
4248
4874
  if (t === "se-widget-tiltify-alert") return { unit: mapSingleAlert(widget, "tiltify-campaignDonation"), status: "direct", lumiaType: "alert" };
@@ -4257,10 +4883,17 @@ function dispatch(widget) {
4257
4883
  if (t === "se-widget-current-song") return { unit: mapNowPlaying(widget), status: "partial", lumiaType: "nowplaying" };
4258
4884
  if (t === "se-widget-kappagen") return { unit: mapKappagen(widget), status: "partial", lumiaType: "emotealert" };
4259
4885
  if (t === "se-widget-giveaway") return { unit: mapGiveaway(widget), status: "direct", lumiaType: "raffle" };
4260
- if (t === "se-widget-custom-event-list") return { unit: mapCustom(widget), status: "partial", lumiaType: "custom" };
4261
- if (t in SEASONAL_TYPES) {
4262
- return { unit: mapSeasonal(widget, SEASONAL_TYPES[t]), status: "direct", lumiaType: "custom" };
4886
+ if (t === "se-widget-media-share") return { unit: mapMediaShare(widget), status: "partial", lumiaType: "songrequest" };
4887
+ if (t === "se-widget-hype-cup") return { unit: mapHypeCup(widget), status: "partial", lumiaType: "tipjar" };
4888
+ if (t === "se-widget-bit-boss") return { unit: mapStreamBoss(widget), status: "partial", lumiaType: "streamboss" };
4889
+ if (t === "se-widget-hype-train") return { unit: mapHypetrain(widget), status: "partial", lumiaType: "hypetrain" };
4890
+ if (t === "se-widget-custom-event-list" && isHypetrainCustomWidget(widget)) {
4891
+ return { unit: mapHypetrain(widget), status: "partial", lumiaType: "hypetrain" };
4263
4892
  }
4893
+ if (t === "se-widget-custom-event-list") return { unit: mapCustom(widget), status: "partial", lumiaType: "custom" };
4894
+ if (t === "se-widget-snow") return { unit: mapSnow(widget), status: "direct", lumiaType: "video" };
4895
+ if (t === "se-widget-halloween-2019") return { unit: mapSlideshow(widget), status: "direct", lumiaType: "slideshow" };
4896
+ if (SEASONAL_IMAGE_TYPES.has(t)) return { unit: mapImage(widget), status: "direct", lumiaType: "image" };
4264
4897
  return { unit: mapUnsupportedAsText(widget), status: "placeholder", lumiaType: "text" };
4265
4898
  }
4266
4899
  function recordCoverage(coverage, result, seType) {
@@ -4447,7 +5080,10 @@ var SE_WIDGET_TO_MARKETPLACE_CANDIDATES = {
4447
5080
  // value for marketplace replacements since AI generation rarely produces
4448
5081
  // satisfying results for these.
4449
5082
  "se-widget-hype-cup": [],
4450
- "se-widget-bit-boss": [],
5083
+ // Note: `se-widget-bit-boss` is auto-mapped onto the native `streamboss`
5084
+ // module by the dispatcher and no longer needs a marketplace fallback.
5085
+ // Re-add a key here if a community curated boss overlay later supersedes
5086
+ // the native module for specific use cases.
4451
5087
  "se-widget-train": [],
4452
5088
  // Medium-difficulty widgets — possible to AI-generate but a curated overlay
4453
5089
  // will usually look better.
@@ -4457,6 +5093,15 @@ var SE_WIDGET_TO_MARKETPLACE_CANDIDATES = {
4457
5093
  // have curated replacements for.
4458
5094
  "se-widget-custom-event-list": [],
4459
5095
  "se-widget-botcounter": [],
5096
+ // Hype Train — the dispatcher already auto-routes `se-widget-hype-train` and
5097
+ // hypetrain-only custom-event-lists onto the native `hypetrain` module. This
5098
+ // entry stays here for two cases the auto-router can't catch:
5099
+ // - Heavily customized hype-train custom widgets where the native module
5100
+ // would lose too much visual fidelity (custom locomotive art, themed
5101
+ // particles, etc.) and the streamer would prefer a curated alternative.
5102
+ // - Community curated overlays that pair the native hypetrain with extra
5103
+ // decorative layers (e.g. a "depot" background, station signage).
5104
+ "se-widget-hype-train": [],
4460
5105
  // Flagged-off in feature-flags.ts — the auto-import is disabled because
4461
5106
  // Lumia has no merch data source. Marketplace overlays here would be
4462
5107
  // curated static merch-style goals/sliders that don't depend on a feed.
@@ -4480,6 +5125,125 @@ function hasMarketplaceCandidates(seWidgetType) {
4480
5125
  return getMarketplaceCandidates(seWidgetType).length > 0;
4481
5126
  }
4482
5127
 
5128
+ // src/se-import/api/seClient.ts
5129
+ var SEAuthError = class extends Error {
5130
+ constructor(code, message) {
5131
+ super(message);
5132
+ this.code = code;
5133
+ this.name = "SEAuthError";
5134
+ }
5135
+ code;
5136
+ };
5137
+ function decodeBase64Url(s) {
5138
+ const b64 = s.replace(/-/g, "+").replace(/_/g, "/");
5139
+ const padding = "===".slice((b64.length + 3) % 4);
5140
+ const padded = b64 + (padding.length < 3 ? padding : "");
5141
+ if (typeof atob === "function") return atob(padded);
5142
+ return Buffer.from(padded, "base64").toString("binary");
5143
+ }
5144
+ function decodeJwtPayload(jwt) {
5145
+ if (typeof jwt !== "string" || !jwt.trim()) {
5146
+ throw new SEAuthError("invalid-jwt", "No token provided.");
5147
+ }
5148
+ const parts = jwt.trim().split(".");
5149
+ if (parts.length !== 3) {
5150
+ throw new SEAuthError("invalid-jwt", "Token is not a valid JWT (expected 3 dot-separated parts).");
5151
+ }
5152
+ let raw;
5153
+ try {
5154
+ raw = decodeBase64Url(parts[1] ?? "");
5155
+ } catch {
5156
+ throw new SEAuthError("invalid-jwt", "Token payload is not valid base64.");
5157
+ }
5158
+ let parsed;
5159
+ try {
5160
+ parsed = JSON.parse(raw);
5161
+ } catch {
5162
+ throw new SEAuthError("invalid-jwt", "Token payload is not valid JSON.");
5163
+ }
5164
+ const claims = parsed;
5165
+ if (typeof claims.channel !== "string" || typeof claims.authToken !== "string") {
5166
+ throw new SEAuthError("invalid-jwt", "Token payload is missing the 'channel' or 'authToken' claim \u2014 not a StreamElements account token.");
5167
+ }
5168
+ if (typeof claims.exp === "number" && claims.exp * 1e3 < Date.now()) {
5169
+ throw new SEAuthError("expired", "This StreamElements token has expired. Generate a new one on the SE Account page.");
5170
+ }
5171
+ return claims;
5172
+ }
5173
+ var SEClient = class _SEClient {
5174
+ constructor(claims) {
5175
+ this.claims = claims;
5176
+ }
5177
+ claims;
5178
+ static fromJwt(jwt) {
5179
+ return new _SEClient(decodeJwtPayload(jwt));
5180
+ }
5181
+ get channelId() {
5182
+ return this.claims.channel;
5183
+ }
5184
+ get provider() {
5185
+ return this.claims.provider;
5186
+ }
5187
+ get providerId() {
5188
+ return this.claims.provider_id;
5189
+ }
5190
+ get userId() {
5191
+ return this.claims.user;
5192
+ }
5193
+ get jti() {
5194
+ return this.claims.jti;
5195
+ }
5196
+ get expiresAt() {
5197
+ return new Date(this.claims.exp * 1e3);
5198
+ }
5199
+ // Convenience for provenance stamping on imported entities. Single source
5200
+ // of truth so every mapper writes the same shape.
5201
+ provenance() {
5202
+ return {
5203
+ jti: this.claims.jti,
5204
+ channelId: this.claims.channel,
5205
+ provider: this.claims.provider,
5206
+ importedAt: (/* @__PURE__ */ new Date()).toISOString()
5207
+ };
5208
+ }
5209
+ async get(path) {
5210
+ const res = await fetch(`https://api.streamelements.com${path}`, {
5211
+ method: "GET",
5212
+ headers: {
5213
+ Authorization: `apikey ${this.claims.authToken}`,
5214
+ Accept: "application/json"
5215
+ }
5216
+ });
5217
+ if (res.status === 401) {
5218
+ throw new SEAuthError("unauthorized", "StreamElements rejected the token. Generate a new one and try again.");
5219
+ }
5220
+ if (res.status === 403) {
5221
+ throw new SEAuthError("forbidden", "Your StreamElements token does not have permission for this resource.");
5222
+ }
5223
+ if (!res.ok) {
5224
+ let detail = "";
5225
+ try {
5226
+ const body = await res.json();
5227
+ if (body?.message) detail = ` \u2014 ${body.message}`;
5228
+ } catch {
5229
+ }
5230
+ throw new Error(`StreamElements API returned HTTP ${res.status}${detail}`);
5231
+ }
5232
+ return await res.json();
5233
+ }
5234
+ };
5235
+ async function fetchUsableChannels(client) {
5236
+ const me = await client.get("/kappa/v2/users/current");
5237
+ return (me.channels ?? []).filter((c) => c.authorized !== false && c.inactive !== true);
5238
+ }
5239
+ async function fetchSEOverlays(client, count = 200) {
5240
+ const res = await client.get(`/kappa/v2/overlays/${client.channelId}?count=${count}`);
5241
+ return res.docs ?? [];
5242
+ }
5243
+ async function fetchSEBootstrapForOverlay(client, overlayId) {
5244
+ return client.get(`/kappa/v2/overlays/${overlayId}/bootstrap?isEditor=false&isMobile=false&isObs=false&isObsLive=false&isXsplit=false`);
5245
+ }
5246
+
4483
5247
  // src/se-import/ui/SEImportWizard.tsx
4484
5248
  import { useCallback as useCallback3, useEffect as useEffect6, useMemo as useMemo5, useRef as useRef4, useState as useState6 } from "react";
4485
5249
 
@@ -4528,7 +5292,7 @@ function MarketplacePicker({ seWidgetType, fetchMarketplaceOverlay, CustomEmbed,
4528
5292
  /* @__PURE__ */ jsx12("code", { children: seWidgetType }),
4529
5293
  " yet. Use AI generation or skip the widget for now."
4530
5294
  ] }),
4531
- /* @__PURE__ */ jsx12("div", { className: "ui-flex-row", style: { marginTop: 8 }, children: /* @__PURE__ */ jsx12("button", { type: "button", className: "ui-button", onClick: onCancel, children: "Close" }) })
5295
+ /* @__PURE__ */ jsx12("div", { className: "ui-flex-row", style: { marginTop: 8 }, children: /* @__PURE__ */ jsx12(LSButton, { type: "button", color: "secondary", onClick: onCancel, label: "Close" }) })
4532
5296
  ] });
4533
5297
  }
4534
5298
  return /* @__PURE__ */ jsxs6("div", { className: "ui-flex-column ui-gap-2", style: { padding: 16, minHeight: 420 }, children: [
@@ -4539,15 +5303,15 @@ function MarketplacePicker({ seWidgetType, fetchMarketplaceOverlay, CustomEmbed,
4539
5303
  " with a layer from a curated Lumia Marketplace overlay."
4540
5304
  ] }),
4541
5305
  /* @__PURE__ */ jsx12("div", { className: "ui-flex-row ui-gap-2", style: { flexWrap: "wrap", marginTop: 4 }, children: candidates.map((c) => /* @__PURE__ */ jsx12(
4542
- "button",
5306
+ LSButton,
4543
5307
  {
4544
5308
  type: "button",
4545
- className: `ui-button ${c.id === activeId ? "ui-button-purple" : ""}`,
5309
+ color: c.id === activeId ? "primary" : "secondary",
4546
5310
  onClick: () => {
4547
5311
  setActiveId(c.id);
4548
5312
  setActiveLayerId(null);
4549
5313
  },
4550
- children: c.loading ? `Loading #${c.id}\u2026` : c.error ? `#${c.id} (error)` : c.overlay?.name ?? `Overlay #${c.id}`
5314
+ label: c.loading ? `Loading #${c.id}\u2026` : c.error ? `#${c.id} (error)` : c.overlay?.name ?? `Overlay #${c.id}`
4551
5315
  },
4552
5316
  c.id
4553
5317
  )) }),
@@ -4562,13 +5326,13 @@ function MarketplacePicker({ seWidgetType, fetchMarketplaceOverlay, CustomEmbed,
4562
5326
  /* @__PURE__ */ jsxs6("div", { style: { flex: "0 0 200px", display: "flex", flexDirection: "column", gap: 4, maxHeight: 320, overflowY: "auto" }, children: [
4563
5327
  /* @__PURE__ */ jsx12("div", { style: { fontSize: 12, fontWeight: 600, opacity: 0.8 }, children: "Layers" }),
4564
5328
  activeLayers.map((l) => /* @__PURE__ */ jsx12(
4565
- "button",
5329
+ LSButton,
4566
5330
  {
4567
5331
  type: "button",
4568
- className: `ui-button ${l.id === activeLayerId ? "ui-button-purple" : ""}`,
5332
+ color: l.id === activeLayerId ? "primary" : "secondary",
4569
5333
  style: { justifyContent: "flex-start", textAlign: "left" },
4570
5334
  onClick: () => setActiveLayerId(l.id),
4571
- children: /* @__PURE__ */ jsxs6("div", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: [
5335
+ label: /* @__PURE__ */ jsxs6("div", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: [
4572
5336
  /* @__PURE__ */ jsx12("div", { style: { fontSize: 12, fontWeight: 600 }, children: l.label }),
4573
5337
  /* @__PURE__ */ jsx12("div", { style: { fontSize: 10, opacity: 0.7 }, children: l.type })
4574
5338
  ] })
@@ -4580,15 +5344,15 @@ function MarketplacePicker({ seWidgetType, fetchMarketplaceOverlay, CustomEmbed,
4580
5344
  /* @__PURE__ */ jsx12("div", { style: { flex: 1, minHeight: 240, background: "rgba(0,0,0,0.3)", borderRadius: 6, overflow: "hidden" }, children: /* @__PURE__ */ jsx12(ActiveLayerPreview, { overlay: activeCandidate.overlay, layerId: activeLayerId, CustomEmbed }) })
4581
5345
  ] }),
4582
5346
  /* @__PURE__ */ jsxs6("div", { className: "ui-flex-row ui-gap-2", style: { marginTop: 12, justifyContent: "flex-end" }, children: [
4583
- /* @__PURE__ */ jsx12("button", { type: "button", className: "ui-button", onClick: onCancel, children: "Cancel" }),
5347
+ /* @__PURE__ */ jsx12(LSButton, { type: "button", color: "secondary", variant: "outlined", onClick: onCancel, label: "Cancel" }),
4584
5348
  /* @__PURE__ */ jsx12(
4585
- "button",
5349
+ LSButton,
4586
5350
  {
4587
5351
  type: "button",
4588
- className: "ui-button ui-button-purple",
5352
+ color: "primary",
4589
5353
  onClick: handleConfirm,
4590
5354
  disabled: !activeCandidate?.overlay || !activeLayerId,
4591
- children: "Use this layer"
5355
+ label: "Use this layer"
4592
5356
  }
4593
5357
  )
4594
5358
  ] })
@@ -4622,51 +5386,154 @@ function ActiveLayerPreview({ overlay, layerId, CustomEmbed }) {
4622
5386
  ] });
4623
5387
  }
4624
5388
 
5389
+ // src/se-import/ui/SEImportWizard.css
5390
+ styleInject('.se-import,\n.se-import * {\n box-sizing: border-box;\n}\n.se-import {\n --se-bg: #17162a;\n --se-surface: #27264a;\n --se-panel: #171b38;\n --se-panel-2: #202449;\n --se-border: rgba(116, 124, 211, 0.24);\n --se-border-strong: rgba(126, 128, 255, 0.58);\n --se-text: #f5f4ff;\n --se-muted: #bab8d2;\n --se-primary: #ff4076;\n --se-purple: #393853;\n --se-green: #06c96f;\n --se-warn: #f7a42b;\n width: 100%;\n height: 100%;\n min-width: min(680px, 100%);\n color: var(--se-text);\n font-family: inherit;\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n.se-import__shell {\n border-radius: 0;\n padding: 28px 48px 24px;\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n}\n.se-import__header {\n flex: 0 0 auto;\n}\nfooter.se-import-actions {\n flex: 0 0 auto;\n}\n.se-import__header {\n display: grid;\n justify-items: center;\n gap: 42px;\n}\n.se-import__title {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n font-size: 20px;\n font-weight: 500;\n}\n.se-import-stepper {\n display: grid;\n width: min(760px, 100%);\n grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));\n}\n.se-import-step {\n position: relative;\n display: grid;\n justify-items: center;\n gap: 12px;\n min-width: 0;\n color: rgba(255, 255, 255, 0.72);\n}\n.se-import-step__label {\n color: var(--se-text);\n font-size: 18px;\n line-height: 1.1;\n}\n.se-import-step__line {\n position: absolute;\n top: 42px;\n left: 50%;\n width: 100%;\n height: 2px;\n background: rgba(255, 255, 255, 0.16);\n}\n.se-import-step:last-child .se-import-step__line {\n display: none;\n}\n.se-import-step__dot {\n z-index: 1;\n display: grid;\n width: 22px;\n height: 22px;\n place-items: center;\n border-radius: 50%;\n background: #47485e;\n color: #fff;\n font-size: 13px;\n font-weight: 700;\n line-height: 1;\n}\n.se-import-step--active .se-import-step__dot {\n background: var(--se-primary);\n}\n.se-import-step--done .se-import-step__dot {\n background: var(--se-green);\n}\n.se-import-step--done .se-import-step__line {\n background: var(--se-green);\n}\n.se-import-step--done + .se-import-step--active .se-import-step__line {\n background:\n linear-gradient(\n 90deg,\n var(--se-green),\n var(--se-primary));\n}\n.se-import__content {\n display: grid;\n place-items: center;\n padding: 64px 0 28px;\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n}\n@media (max-height: 760px) {\n .se-import__shell {\n padding: 18px 32px 16px;\n }\n .se-import__content {\n padding: 32px 0 16px;\n }\n}\n.se-import-panel {\n width: min(100%, 1080px);\n padding: 28px;\n}\n.se-import-panel--narrow {\n width: min(100%, 1140px);\n}\n.se-import-panel--medium {\n width: min(100%, 880px);\n}\n.se-import-panel--wide {\n width: min(100%, 1000px);\n}\n.se-import-panel--confirm {\n width: min(100%, 800px);\n}\n.se-import-panel h2,\n.se-import-panel h3,\n.se-import-panel p {\n margin: 0;\n}\n.se-import-panel h2 {\n font-size: 26px;\n font-weight: 700;\n line-height: 1.15;\n}\n.se-import-panel > p {\n margin-top: 14px;\n color: var(--se-muted);\n font-size: 16px;\n line-height: 1.45;\n}\n.se-import-field {\n margin-top: 34px;\n}\n.se-import-field .mui-ls-input {\n --ls-control-height: 66px;\n}\n.se-import-field .MuiInputLabel-root {\n color: var(--se-text);\n font-weight: 700;\n}\n.se-import-field .MuiInputLabel-root.Mui-focused {\n color: #dfe1ff;\n}\n.se-import-field .MuiInputBase-root {\n background: rgba(28, 32, 66, 0.85);\n box-shadow: inset 0 0 0 3px rgba(255, 255, 255, 0.04);\n font-size: 18px;\n}\n.se-import-field .MuiOutlinedInput-notchedOutline {\n border-color: var(--se-border-strong) !important;\n}\n.se-import-help {\n display: flex;\n gap: 18px;\n margin-top: 30px;\n border: 1px solid var(--se-purple);\n border-radius: 12px;\n padding: 24px;\n color: var(--white2);\n}\n.se-import-help__icon {\n display: grid;\n width: 30px;\n height: 30px;\n flex: 0 0 auto;\n place-items: center;\n border: 1px solid var(--se-purple);\n border-radius: 50%;\n color: var(--white2);\n font-weight: 800;\n}\n.se-import-help__title {\n color: var(--se-text);\n font-size: 18px;\n font-weight: 700;\n}\n.se-import-help ol {\n margin: 16px 0 0;\n padding-left: 18px;\n font-size: 15px;\n line-height: 1.7;\n}\n.se-import-help a {\n color: var(--primary);\n font-weight: 700;\n}\n.se-import-help--info {\n align-items: center;\n}\n.se-import-section-title {\n display: flex;\n gap: 1rem;\n margin-bottom: 1.5rem;\n}\n.se-import-section-title__num {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 46px;\n height: 46px;\n border: 1px solid var(--se-purple);\n border-radius: 50%;\n font-size: 1.5rem;\n font-weight: 800;\n}\n.se-import-section-title__content {\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n.se-import-section-title h3 {\n font-size: 18px;\n font-weight: 800;\n}\n.se-import-section-title p {\n margin-top: 4px;\n color: var(--se-muted);\n font-size: 14px;\n}\n.se-import-grid {\n display: grid;\n gap: 8px;\n margin-top: 10px;\n}\n.se-import-grid--four {\n grid-template-columns: repeat(4, minmax(0, 1fr));\n}\n.se-import-grid--two {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n}\n.se-import-stat {\n min-height: 116px;\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 8px;\n background:\n linear-gradient(\n 135deg,\n rgba(42, 47, 92, 0.9),\n rgba(33, 38, 78, 0.9));\n padding: 24px 22px;\n}\n.se-import-stat--success {\n background:\n linear-gradient(\n 135deg,\n rgba(12, 91, 86, 0.72),\n rgba(32, 65, 76, 0.8));\n border-color: rgba(19, 201, 126, 0.25);\n}\n.se-import-stat--info {\n border-color: rgba(116, 104, 255, 0.8);\n box-shadow: inset 3px 0 0 #6e76ff;\n}\n.se-import-stat--warn,\n.se-import-stat--muted {\n box-shadow: inset 3px 0 0 var(--se-warn);\n}\n.se-import-stat__label {\n color: var(--se-muted);\n font-size: 12px;\n font-weight: 800;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n}\n.se-import-stat__value {\n margin-top: 12px;\n color: #fff;\n font-size: 30px;\n font-weight: 800;\n line-height: 1;\n}\n.se-import-stat__sub {\n margin-top: 8px;\n color: var(--se-muted);\n font-size: 13px;\n}\n.se-import-option {\n display: grid;\n grid-template-columns: auto minmax(0, 1fr) auto auto;\n align-items: center;\n gap: 18px;\n border: 1px solid var(--se-purple);\n border-radius: 12px;\n background:\n linear-gradient(\n 135deg,\n rgba(35, 39, 81, 0.95),\n rgba(31, 35, 73, 0.95));\n box-shadow: inset 3px 0 0 var(--se-primary);\n cursor: pointer;\n padding: 22px;\n margin-bottom: 1rem;\n}\n.se-import-option--disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n.se-import-option__icon {\n display: grid;\n width: 26px;\n height: 26px;\n place-items: center;\n border-radius: 50%;\n background: rgba(255, 255, 255, 0.22);\n color: #d8dbff;\n font-weight: 800;\n}\n.se-import-option__body {\n display: grid;\n gap: 8px;\n min-width: 0;\n}\n.se-import-option__title {\n font-size: 17px;\n font-weight: 800;\n}\n.se-import-option__copy {\n color: var(--se-muted);\n font-size: 14px;\n line-height: 1.45;\n}\n.se-import-option__count {\n color: #fff;\n font-size: 16px;\n}\n.se-import-option input {\n position: absolute;\n opacity: 0;\n pointer-events: none;\n}\n.se-import-toggle {\n position: relative;\n width: 48px;\n height: 28px;\n border-radius: 999px;\n box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14);\n}\n.se-import-toggle::after {\n position: absolute;\n top: 4px;\n left: 4px;\n width: 20px;\n height: 20px;\n border-radius: 50%;\n background: #fff;\n content: "";\n transition: transform 160ms ease;\n}\n.se-import-option input:checked + .se-import-toggle {\n background: var(--primary);\n}\n.se-import-option input:checked + .se-import-toggle::after {\n transform: translateX(20px);\n}\n.se-import-advanced {\n margin-top: 26px;\n border-radius: 10px;\n background: rgba(32, 36, 73, 0.7);\n padding: 24px;\n}\n.se-import-advanced summary {\n cursor: pointer;\n font-size: 18px;\n font-weight: 700;\n}\n.se-import-advanced p {\n margin-top: 10px;\n color: var(--se-muted);\n}\n.se-import-transfer {\n position: relative;\n display: grid;\n width: min(720px, 100%);\n min-height: 190px;\n grid-template-columns: 180px minmax(260px, 1fr) 180px;\n align-items: center;\n gap: 28px;\n margin: 0 auto 60px;\n}\n.se-import-transfer__source {\n position: relative;\n display: grid;\n width: 160px;\n height: 160px;\n place-items: center;\n animation: se-import-source-bounce 1.8s ease-in-out infinite;\n}\n.se-import-transfer__source::before {\n top: -13px;\n left: -13px;\n box-shadow: 155px 0 0 var(--se-primary);\n}\n.se-import-transfer__source::after {\n bottom: -13px;\n left: -13px;\n box-shadow: 155px 0 0 var(--se-primary);\n}\n.se-import-transfer__source img {\n width: 192px;\n height: 192px;\n object-fit: contain;\n}\n.se-import-transfer__source--lumia {\n animation-delay: 0.22s;\n}\n.se-import-transfer__files {\n position: relative;\n height: 130px;\n}\n.se-import-transfer__file {\n position: absolute;\n top: 50%;\n left: -18px;\n width: 56px;\n height: 66px;\n object-fit: contain;\n transform: translateY(-50%);\n filter: drop-shadow(0 10px 12px rgba(8, 9, 25, 0.25));\n animation: se-import-file-fly 2.4s cubic-bezier(0.35, 0, 0.25, 1) infinite;\n}\n.se-import-transfer__file--one {\n animation-delay: 0s;\n}\n.se-import-transfer__file--two {\n animation-delay: 0.38s;\n}\n.se-import-transfer__file--three {\n animation-delay: 0.76s;\n}\n.se-import-transfer__file--four {\n animation-delay: 1.14s;\n}\n@keyframes se-import-source-bounce {\n 0%, 100% {\n transform: translateY(0) scale(1);\n }\n 50% {\n transform: translateY(-8px) scale(1.025);\n }\n}\n@keyframes se-import-file-fly {\n 0% {\n opacity: 0;\n transform: translate(-10px, -50%) scale(0.82) rotate(-8deg);\n }\n 12% {\n opacity: 1;\n }\n 45% {\n transform: translate(145px, calc(-50% - 18px)) scale(1.08) rotate(5deg);\n }\n 82% {\n opacity: 1;\n }\n 100% {\n opacity: 0;\n transform: translate(330px, -50%) scale(0.9) rotate(10deg);\n }\n}\n.se-import-mirror-head {\n display: flex;\n align-items: end;\n justify-content: space-between;\n gap: 20px;\n}\n.se-import-mirror-head p {\n margin-top: 14px;\n color: var(--se-muted);\n font-size: 16px;\n}\n.se-import-mirror-head span {\n flex: 0 0 auto;\n color: var(--se-muted);\n font-weight: 700;\n}\n.se-import-progress {\n width: 100%;\n height: 14px;\n margin-top: 28px;\n overflow: hidden;\n border: 0;\n border-radius: 999px;\n background: rgba(255, 64, 118, 0.1);\n}\n.se-import-progress::-webkit-progress-bar {\n background: rgba(255, 64, 118, 0.1);\n}\n.se-import-progress::-webkit-progress-value {\n border-radius: 999px;\n background:\n linear-gradient(\n 90deg,\n #ff4076,\n #e7295f);\n}\n.se-import-progress::-moz-progress-bar {\n border-radius: 999px;\n background:\n linear-gradient(\n 90deg,\n #ff4076,\n #e7295f);\n}\n.se-import-asset-list {\n max-height: 360px;\n margin-top: 22px;\n overflow-y: auto;\n border-top: 1px solid rgba(255, 255, 255, 0.08);\n}\n.se-import-asset-row {\n display: grid;\n grid-template-columns: 32px minmax(0, 1fr) auto auto;\n align-items: center;\n gap: 14px;\n min-height: 42px;\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n color: var(--se-muted);\n font-size: 13px;\n}\n.se-import-asset-row__icon {\n width: 24px;\n height: 28px;\n justify-self: end;\n object-fit: contain;\n}\n.se-import-asset-row__url {\n overflow: hidden;\n font-family:\n ui-monospace,\n SFMono-Regular,\n Menlo,\n Monaco,\n Consolas,\n monospace;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.se-import-asset-row__state {\n color: #9b8cff;\n font-weight: 800;\n}\n.se-import-asset-row--done .se-import-asset-row__state,\n.se-import-asset-row--reused .se-import-asset-row__state {\n color: var(--se-green);\n}\n.se-import-asset-row--failed .se-import-asset-row__state,\n.se-import-asset-row__error {\n color: #ff8080;\n}\n.se-import-link-button {\n border: 0;\n background: transparent;\n color: #ff9ab6;\n cursor: pointer;\n font: inherit;\n font-weight: 800;\n}\n.se-import-overview {\n margin-top: 28px;\n border: 1px solid var(--se-border);\n border-radius: 12px;\n background: rgba(26, 30, 61, 0.62);\n padding: 28px;\n}\n.se-import-overview h3 {\n margin-bottom: 18px;\n font-size: 18px;\n font-weight: 800;\n}\n.se-import-row {\n display: grid;\n grid-template-columns: 190px minmax(0, 1fr) auto;\n gap: 18px;\n align-items: center;\n min-height: 48px;\n border-top: 1px solid rgba(255, 255, 255, 0.08);\n color: var(--se-muted);\n}\n.se-import-row strong {\n min-width: 0;\n overflow: hidden;\n color: var(--se-text);\n font-weight: 600;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.se-import-row em {\n color: var(--se-muted);\n font-style: normal;\n}\n.se-import-review-card {\n display: flex;\n justify-content: space-between;\n gap: 18px;\n border: 1px solid var(--se-border);\n border-radius: 10px;\n background: rgba(30, 34, 71, 0.8);\n padding: 20px;\n}\n.se-import-review-card__type {\n font-family:\n ui-monospace,\n SFMono-Regular,\n Menlo,\n Monaco,\n Consolas,\n monospace;\n font-weight: 800;\n}\n.se-import-review-card p {\n margin-top: 8px;\n color: var(--se-muted);\n line-height: 1.45;\n}\n.se-import-review-card__status {\n flex: 0 0 auto;\n color: #facc15;\n font-size: 11px;\n font-weight: 800;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n}\n.se-import-review-bulk,\n.se-import-actions--inline {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: flex-end;\n gap: 10px;\n margin-top: 16px;\n}\n.se-import-muted {\n margin-top: 16px;\n color: var(--se-muted);\n font-size: 13px;\n}\n.se-import-generated {\n display: grid;\n gap: 12px;\n margin-top: 18px;\n}\n.se-import-generated__title {\n font-weight: 800;\n}\n.se-import-preview {\n width: 100%;\n height: 260px;\n border: 1px solid var(--se-border);\n border-radius: 8px;\n background: repeating-conic-gradient(#1a1c20 0% 25%, #1d1f24 0% 50%) 50% / 20px 20px;\n}\n.se-import-code {\n margin-top: 16px;\n}\n.se-import-code summary {\n cursor: pointer;\n color: var(--se-muted);\n font-size: 13px;\n}\n.se-import-code pre {\n max-height: 220px;\n margin: 10px 0 0;\n overflow: auto;\n border-radius: 8px;\n background: rgba(0, 0, 0, 0.28);\n padding: 12px;\n color: #d8dcff;\n font-size: 12px;\n}\n.se-import-error {\n margin-top: 18px;\n border: 1px solid rgba(239, 68, 68, 0.45);\n border-radius: 8px;\n background: rgba(220, 38, 38, 0.12);\n padding: 12px;\n color: #fca5a5;\n}\n.se-import-error__title {\n font-weight: 800;\n}\n.se-import-error__hint {\n margin-top: 6px;\n white-space: pre-wrap;\n}\n.se-import-actions {\n display: flex;\n justify-content: flex-end;\n gap: 12px;\n margin-top: 8px;\n}\nfooter.se-import-actions:not(.se-import-actions--inline) {\n background: var(--se-bg, #17162a);\n border-top: 1px solid rgba(255, 255, 255, 0.06);\n margin: 0 -48px -24px;\n padding: 16px 48px;\n}\n@media (max-height: 760px) {\n footer.se-import-actions:not(.se-import-actions--inline) {\n margin: 0 -32px -16px;\n padding: 12px 32px;\n }\n}\n@media (max-width: 900px) {\n .se-import__shell {\n min-height: 0;\n padding: 24px 18px;\n }\n .se-import__content {\n min-height: 0;\n padding-top: 32px;\n }\n .se-import-stepper {\n grid-template-columns: repeat(auto-fit, minmax(70px, 1fr));\n }\n .se-import-step__label {\n font-size: 13px;\n }\n .se-import-grid--four,\n .se-import-grid--two,\n .se-import-transfer {\n grid-template-columns: 1fr;\n }\n .se-import-transfer {\n justify-items: center;\n }\n .se-import-transfer__files {\n width: min(360px, 100%);\n }\n .se-import-option,\n .se-import-row,\n .se-import-asset-row {\n grid-template-columns: 1fr;\n }\n .se-import-mirror-head,\n .se-import-review-card {\n align-items: flex-start;\n flex-direction: column;\n }\n}\n');
5391
+
5392
+ // src/assets/source_se.svg
5393
+ var source_se_default = 'data:image/svg+xml,<svg width="157" height="164" viewBox="0 0 157 164" fill="none" xmlns="http://www.w3.org/2000/svg">%0A<rect x="8.5" y="9.5" width="139" height="145" fill="%23242444" stroke="%23FF4076" stroke-width="3"/>%0A<path d="M87.1229 44.9224L89.0788 48.7235C102.041 53.3686 110.947 65.8756 110.947 79.7241C110.947 97.8858 96.1663 112.658 77.9929 112.658C59.8196 112.658 45.0457 97.8858 45.0457 79.7241C45.0457 65.8684 53.9519 53.3686 66.9143 48.7235L68.8702 44.9296C53.1725 49.0337 42 63.3222 42 79.7241C42 99.5592 58.1524 115.701 78.0001 115.701C97.8479 115.701 114 99.5592 114 79.7241C114 63.3222 102.828 49.0265 87.1229 44.9224Z" fill="url(%23paint0_linear_1424_495)"/>%0A<mask id="mask0_1424_495" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="47" y="48" width="62" height="63">%0A<path d="M77.9986 110.386C94.991 110.386 108.766 96.6196 108.766 79.638C108.766 62.6563 94.991 48.8899 77.9986 48.8899C61.0061 48.8899 47.231 62.6563 47.231 79.638C47.231 96.6196 61.0061 110.386 77.9986 110.386Z" fill="white"/>%0A</mask>%0A<g mask="url(%23mask0_1424_495)">%0A<path d="M108.825 78.9093C108.651 81.2463 105.036 90.9619 100.59 90.9619C98.7853 90.9619 98.3812 88.322 98.0853 86.3602C97.2841 89.4905 91.7629 96.2563 87.6706 96.2563C80.2295 96.2563 78.4974 80.5683 77.9921 78.4766C77.4869 80.561 75.7548 96.2563 68.3137 96.2563C64.2215 96.2563 58.7002 89.4977 57.8991 86.3602C57.6031 88.322 57.2062 90.9619 55.3947 90.9619C50.9487 90.9619 47.3328 81.2463 47.1596 78.9093C46.207 80.3158 46.2214 85.9995 47.636 89.6419C52.3561 101.795 64.1782 110.429 77.9849 110.429C91.7917 110.429 103.614 101.795 108.334 89.6419C109.749 85.9995 109.763 80.3231 108.81 78.9093H108.825Z" fill="url(%23paint1_linear_1424_495)"/>%0A<path d="M77.9988 96.2998C79.2258 98.5497 81.8961 101.918 86.4719 101.918C91.0478 101.918 94.6059 97.2014 95.7968 95.2105C95.4143 96.6097 94.642 99.6032 97.132 99.6032C102.841 99.6032 106.969 90.5798 106.969 90.5798C102.466 102.178 91.1777 110.422 77.9988 110.422C64.8199 110.422 53.532 102.178 49.0283 90.5798C49.0283 90.5798 53.1567 99.6032 58.8656 99.6032C61.3628 99.6032 60.5833 96.6021 60.2008 95.2105C61.3917 97.2014 64.8343 101.918 69.5257 101.918C74.1014 101.918 76.7646 98.5428 77.9988 96.2998Z" fill="url(%23paint2_linear_1424_495)"/>%0A<path d="M85.2523 106.419C82.4448 106.419 80.2579 105.546 77.9989 104.14C75.7399 105.539 73.553 106.419 70.7455 106.419C66.4656 106.419 65.1664 105.157 63.6147 104.003C63.658 105.474 65.1592 107.552 66.7109 108.301C70.1537 109.817 74.0221 110.422 77.9989 110.422C81.9756 110.422 85.8442 109.809 89.2868 108.301C90.8385 107.544 92.3326 105.474 92.3831 104.003C90.8313 105.157 89.5323 106.419 85.2523 106.419Z" fill="url(%23paint3_linear_1424_495)"/>%0A</g>%0A<path d="M73.8428 64.7073L69.4257 74.0622C69.2453 74.4446 69.1804 74.8773 69.2525 75.3029L70.6527 83.7562C70.696 84.0303 71.1002 84.0303 71.1435 83.7562L71.9013 79.1473C72.2116 77.2359 73.2292 75.5048 74.7522 74.3003L73.8428 64.7001V64.7073Z" fill="%23FF4800"/>%0A<path d="M82.1584 64.7073L86.5755 74.0622C86.7559 74.4446 86.8208 74.8773 86.7487 75.3029L85.3485 83.7634C85.3052 84.0376 84.9082 84.0376 84.8577 83.7634L84.0999 79.1473C83.7896 77.2359 82.7719 75.5048 81.249 74.3003L82.1584 64.7001V64.7073Z" fill="%23FF4800"/>%0A<path d="M84.5241 50.0291L78.4687 38.2867C78.2739 37.9044 77.7326 37.9044 77.5305 38.2867L71.4751 50.0219C71.2226 50.5052 71.1287 51.0606 71.2081 51.5943L74.261 73.0451L73.8063 75.0503L75.4158 79.4645C75.4519 79.5655 75.5457 79.6304 75.6468 79.6304H80.3308C80.4319 79.6304 80.5257 79.5655 80.5618 79.4645L82.1712 75.0503L81.7165 73.0451L84.7695 51.5943C84.8489 51.0534 84.7551 50.5052 84.5024 50.0219L84.5241 50.0291Z" fill="url(%23paint4_linear_1424_495)"/>%0A<path d="M77.7905 69.5107L75.6397 74.8265C75.3077 75.6488 75.2427 76.5576 75.4593 77.4159L77.7544 86.605C77.8193 86.8575 78.1802 86.8575 78.2379 86.605L80.5331 77.4159C80.7495 76.5576 80.6846 75.6488 80.3526 74.8265L78.2019 69.5107C78.1225 69.3232 77.8554 69.3232 77.7832 69.5107H77.7905Z" fill="%23FF4800"/>%0A<rect width="15" height="16" fill="%23FF4076"/>%0A<rect y="148" width="15" height="16" fill="%23FF4076"/>%0A<rect x="142" width="15" height="16" fill="%23FF4076"/>%0A<rect x="142" y="148" width="15" height="16" fill="%23FF4076"/>%0A<defs>%0A<linearGradient id="paint0_linear_1424_495" x1="78.0001" y1="24.4742" x2="78.0001" y2="138.609" gradientUnits="userSpaceOnUse">%0A<stop offset="0.3" stop-color="%2300ADFF"/>%0A<stop offset="1" stop-color="%231542FF"/>%0A</linearGradient>%0A<linearGradient id="paint1_linear_1424_495" x1="77.9993" y1="79.7965" x2="77.9993" y2="120.816" gradientUnits="userSpaceOnUse">%0A<stop stop-color="%23FCE3AD"/>%0A<stop offset="0.07" stop-color="%23FCE2A4"/>%0A<stop offset="0.19" stop-color="%23FCE08B"/>%0A<stop offset="0.33" stop-color="%23FCDD64"/>%0A<stop offset="0.48" stop-color="%23FCDA32"/>%0A<stop offset="0.66" stop-color="%23F9BC22"/>%0A<stop offset="1" stop-color="%23F57700"/>%0A</linearGradient>%0A<linearGradient id="paint2_linear_1424_495" x1="77.9988" y1="90.2913" x2="77.9988" y2="113.603" gradientUnits="userSpaceOnUse">%0A<stop stop-color="%23FCD619"/>%0A<stop offset="0.35" stop-color="%23F9B811"/>%0A<stop offset="1" stop-color="%23F57700"/>%0A</linearGradient>%0A<linearGradient id="paint3_linear_1424_495" x1="77.9989" y1="101.947" x2="77.9989" y2="112.694" gradientUnits="userSpaceOnUse">%0A<stop stop-color="%23F57700"/>%0A<stop offset="1" stop-color="%23FF4800"/>%0A</linearGradient>%0A<linearGradient id="paint4_linear_1424_495" x1="77.9996" y1="38.0054" x2="77.9996" y2="92.2167" gradientUnits="userSpaceOnUse">%0A<stop stop-color="%23E4E4E4"/>%0A<stop offset="0.88" stop-color="%23B6B8B8"/>%0A</linearGradient>%0A</defs>%0A</svg>%0A';
5394
+
5395
+ // src/assets/source_lumia.svg
5396
+ var source_lumia_default = 'data:image/svg+xml,<svg width="157" height="164" viewBox="0 0 157 164" fill="none" xmlns="http://www.w3.org/2000/svg">%0A<rect x="8.5" y="9.5" width="139" height="145" fill="%23242444" stroke="%23FF4076" stroke-width="3"/>%0A<path d="M81.877 128.851C81.877 131.843 79.5456 132.572 76.6689 132.572C73.7923 132.572 71.4609 131.843 71.4609 128.851H81.877Z" fill="%23FF4076"/>%0A<path d="M77.3916 32C84.8793 32 92.6433 34.2919 98.5977 39.2305C104.635 44.2381 108.604 51.8052 108.604 61.8105C108.603 73.1268 103.496 81.0627 98.6982 88.5176C98.5049 88.8177 98.3117 89.1179 98.1201 89.417C93.1275 97.1931 88.4496 104.881 88.0957 116.606C88.0422 118.373 86.5954 119.776 84.8281 119.776H73.832L74.0488 120.9H82.7236C84.5288 120.901 85.9921 122.364 85.9922 124.169C85.9922 125.975 84.5288 127.438 82.7236 127.438H71.3496C69.7828 127.438 68.4363 126.326 68.1396 124.788L66.6631 117.126C66.4786 116.168 66.7307 115.178 67.3516 114.426C67.9726 113.674 68.8977 113.238 69.873 113.238H81.7549C82.8573 101.346 87.9524 93.1519 92.6182 85.8848C92.7267 85.7157 92.8346 85.5466 92.9424 85.3789C97.9058 77.6522 102.065 71.1771 102.065 61.8105C102.065 53.7021 98.9254 47.9983 94.4229 44.2637C89.8371 40.46 83.6294 38.5381 77.3916 38.5381C65.8229 38.5382 53.5382 46.8207 53.5381 62.1387C53.5381 70.5737 57.3337 78.2378 61.7773 85.8975C62.3932 86.9591 63.0286 88.0311 63.665 89.1064C65.2574 91.7971 66.8614 94.5084 68.2139 97.1221C68.5532 97.7775 68.8826 98.4382 69.1982 99.1035C69.7167 98.1764 70.2716 97.2596 70.8477 96.3564C73.0295 92.9368 75.7271 89.4027 78.3623 85.9502C78.789 85.3914 79.2138 84.8343 79.6348 84.2803C82.7122 80.2309 85.6068 76.2986 87.7383 72.4648C89.8803 68.6123 91.0742 65.165 91.0742 62.084C91.0742 58.1367 89.6207 55.1129 87.3291 53.0479C85.0029 50.9516 81.5869 49.6475 77.3916 49.6475C70.0529 49.6475 64.5294 55.5289 64.5293 62.084C64.5293 64.4095 65.0263 68.0042 66.2227 70.8975C67.4926 73.9687 68.9334 74.959 70.2285 74.959C71.187 74.9589 71.7005 74.6624 72.085 74.2744C72.5587 73.7963 73.0139 72.9773 73.3662 71.7422C74.0688 69.2783 74.0688 66.3016 74.0684 64.2012V64.0537C74.0686 62.2485 75.5317 60.7852 77.3369 60.7852C79.1422 60.7852 80.6062 62.2485 80.6064 64.0537V65.2021C80.5968 67.3108 80.4956 70.5813 79.6533 73.5352C79.1375 75.3439 78.2704 77.3208 76.7295 78.876C75.0993 80.5211 72.9061 81.497 70.2285 81.4971C64.7429 81.4971 61.6996 77.0691 60.1807 73.3955C58.5882 69.5443 57.9912 65.0667 57.9912 62.084C57.9913 51.8935 66.4665 43.1094 77.3916 43.1094C82.9298 43.1094 87.9897 44.8418 91.7061 48.1904C95.4567 51.5704 97.6123 56.3999 97.6123 62.084C97.6123 66.7738 95.8263 71.3721 93.4521 75.6426C91.0671 79.9317 87.9082 84.2005 84.8408 88.2363C84.4031 88.8123 83.9677 89.3835 83.5361 89.9492C80.8874 93.4208 78.3814 96.705 76.3604 99.873C73.9874 103.593 72.5611 106.741 72.2754 109.389C72.0981 111.031 70.7238 112.283 69.0723 112.307C67.4206 112.33 66.0104 111.118 65.7861 109.481C65.3962 106.635 64.1894 103.57 62.4072 100.126C61.1449 97.6868 59.6908 95.2302 58.1406 92.6113C57.4828 91.4997 56.8073 90.3588 56.1221 89.1777C51.6525 81.4736 47 72.474 47 62.1387C47.0001 42.4332 63.0409 32.0001 77.3916 32Z" fill="%23FF4076"/>%0A<path d="M71.6338 65.5146C72.1863 65.262 72.839 65.5052 73.0918 66.0576C73.3445 66.6102 73.1013 67.2627 72.5488 67.5156L68.7217 69.2666C68.1692 69.5191 67.5155 69.2768 67.2627 68.7246C67.0099 68.1719 67.2531 67.5186 67.8057 67.2656L71.6338 65.5146Z" fill="%23FF4076"/>%0A<path d="M82.208 65.5684C82.4316 65.0036 83.0708 64.7267 83.6357 64.9502L87.5498 66.5C88.1146 66.7236 88.3914 67.3628 88.168 67.9277C87.9445 68.4926 87.3052 68.7691 86.7402 68.5459L82.8262 66.9961C82.2611 66.7726 81.9845 66.1333 82.208 65.5684Z" fill="%23FF4076"/>%0A<path d="M66.9834 60.3623C67.1772 59.7865 67.8011 59.4771 68.377 59.6709L72.3662 61.0127C72.942 61.2065 73.2522 61.8304 73.0586 62.4062C72.8649 62.982 72.2408 63.2921 71.665 63.0986L67.6758 61.7559C67.0999 61.5621 66.7896 60.9382 66.9834 60.3623Z" fill="%23FF4076"/>%0A<path d="M86.4482 58.8301C86.9968 58.5686 87.6533 58.8014 87.915 59.3496C88.1767 59.8979 87.9437 60.5547 87.3955 60.8164L83.5977 62.6299C83.049 62.8917 82.3923 62.6587 82.1309 62.1104C81.8692 61.562 82.1012 60.9053 82.6494 60.6436L86.4482 58.8301Z" fill="%23FF4076"/>%0A<path d="M73.084 53.8389C73.6539 53.6282 74.2874 53.9193 74.498 54.4893L75.958 58.4395C76.1686 59.0093 75.8775 59.6428 75.3076 59.8535C74.7377 60.0642 74.1042 59.7721 73.8936 59.2021L72.4336 55.252C72.2231 54.6822 72.5143 54.0496 73.084 53.8389Z" fill="%23FF4076"/>%0A<path d="M80.2275 54.2715C80.4366 53.701 81.0691 53.4072 81.6396 53.6162C82.2102 53.8253 82.5031 54.4578 82.2939 55.0283L80.8447 58.9824C80.6357 59.5529 80.0041 59.8458 79.4336 59.6367C78.8631 59.4277 78.5702 58.7961 78.7793 58.2256L80.2275 54.2715Z" fill="%23FF4076"/>%0A<rect width="15" height="16" fill="%23FF4076"/>%0A<rect y="148" width="15" height="16" fill="%23FF4076"/>%0A<rect x="142" width="15" height="16" fill="%23FF4076"/>%0A<rect x="142" y="148" width="15" height="16" fill="%23FF4076"/>%0A</svg>%0A';
5397
+
5398
+ // src/assets/audio_file.svg
5399
+ var audio_file_default = 'data:image/svg+xml,<svg width="23" height="29" viewBox="0 0 23 29" fill="none" xmlns="http://www.w3.org/2000/svg">%0A<path fill-rule="evenodd" clip-rule="evenodd" d="M22.7675 8.5178V25.5534C22.7675 26.3064 22.4678 27.0287 21.9341 27.5611C21.4004 28.0936 20.6765 28.3925 19.9217 28.3926H2.84579C2.09106 28.3925 1.36706 28.0936 0.833383 27.5611C0.299704 27.0287 0 26.3064 0 25.5534V2.83914C3.46516e-05 2.08616 0.299694 1.36386 0.833383 0.831425C1.36706 0.298996 2.09106 4.12048e-05 2.84579 0H14.2297L22.7675 8.5178ZM15.5614 14.0434C15.4739 14.0241 15.383 14.0247 15.2956 14.045L9.28308 15.4475C9.1509 15.4784 9.03315 15.5534 8.94878 15.6597C8.86444 15.766 8.81842 15.8978 8.81843 16.0335V21.3474C8.45759 21.2198 8.06549 21.2104 7.6989 21.3203C7.33226 21.4304 7.01003 21.6543 6.77901 21.9596C6.54799 22.2648 6.42011 22.6357 6.4138 23.0184C6.4075 23.4012 6.52298 23.776 6.74378 24.0886C6.96461 24.4013 7.27932 24.6355 7.64215 24.7576C8.00495 24.8797 8.39716 24.8834 8.76206 24.7678C9.12698 24.6522 9.44546 24.4232 9.67178 24.1145C9.8981 23.8057 10.0205 23.433 10.0209 23.0501V18.9157L14.831 17.7934V20.1445C14.47 20.0168 14.0778 20.0074 13.7111 20.1174C13.3444 20.2275 13.0222 20.4514 12.7912 20.7567C12.5603 21.0619 12.4326 21.4328 12.4264 21.8155C12.4201 22.1983 12.5358 22.5734 12.7567 22.8861C12.9777 23.1987 13.2923 23.4328 13.6551 23.5547C14.018 23.6766 14.4105 23.6799 14.7754 23.5641C15.1403 23.4483 15.459 23.2193 15.6851 22.9104C15.9112 22.6015 16.0333 22.2288 16.0335 21.8461C16.0335 21.8032 16.032 21.7605 16.0292 21.7184C16.032 21.694 16.0335 21.6697 16.0335 21.6456V14.6306C16.0335 14.5401 16.013 14.4506 15.9736 14.3691C15.9343 14.2876 15.8769 14.216 15.8061 14.1597L15.8022 14.1569C15.7315 14.1017 15.649 14.0628 15.5614 14.0434ZM12.8068 9.93757H20.633L12.8068 2.12945V9.93757Z" fill="white"/>%0A</svg>%0A';
5400
+
5401
+ // src/assets/img_file.svg
5402
+ var img_file_default = 'data:image/svg+xml,<svg width="23" height="29" viewBox="0 0 23 29" fill="none" xmlns="http://www.w3.org/2000/svg">%0A<path fill-rule="evenodd" clip-rule="evenodd" d="M11.7445 13.2276C13.0784 13.2276 14.0388 13.2283 14.7699 13.3267C15.4894 13.4235 15.9266 13.6082 16.2504 13.9322C16.5743 14.2565 16.7591 14.694 16.8559 15.4135C16.9544 16.1445 16.9554 17.1047 16.9554 18.4385V19.8415C16.9554 21.1754 16.9547 22.1358 16.8563 22.8669C16.7595 23.5864 16.5748 24.0236 16.2508 24.3474C15.9265 24.6713 15.489 24.8561 14.7695 24.9529C14.0385 25.0514 13.0784 25.0524 11.7445 25.0524H10.3415C9.00759 25.0524 8.04722 25.0517 7.31607 24.9533C6.59659 24.8565 6.1594 24.6718 5.83564 24.3478C5.51168 24.0235 5.32694 23.586 5.23007 22.8665C5.13166 22.1355 5.13065 21.1754 5.13065 19.8415V18.4385C5.13065 17.1046 5.13135 16.1442 5.22968 15.4131C5.32646 14.6936 5.51124 14.2564 5.83524 13.9326C6.15948 13.6087 6.597 13.4239 7.31646 13.3271C8.04751 13.2287 9.00765 13.2276 10.3415 13.2276H11.7445ZM8.28842 17.3961C7.82545 17.4347 7.44577 17.667 7.10626 17.9398C6.82149 18.1692 6.50307 18.4799 6.13274 18.8496V19.8415C6.13274 20.2034 6.13329 20.5335 6.1347 20.8314L6.91641 20.0497C7.40027 19.5659 7.72283 19.2453 7.98583 19.0328C8.24538 18.8237 8.35792 18.7979 8.40703 18.7944C8.53557 18.7835 8.66468 18.8079 8.78007 18.8656C8.82286 18.8874 8.91773 18.9527 9.08187 19.2445C9.2474 19.5391 9.42752 19.9572 9.69683 20.5856L9.73402 20.6729L9.74615 20.6995C9.85061 20.9435 9.94729 21.1693 10.044 21.3474C10.1458 21.5332 10.2932 21.7566 10.5416 21.9103C10.8341 22.0915 11.1831 22.1586 11.5221 22.0993C11.8097 22.0481 12.0292 21.8962 12.1927 21.7615C12.3484 21.6324 12.5226 21.4582 12.7106 21.2702L12.7313 21.2495C13.0294 20.9514 13.2231 20.7583 13.3823 20.6244C13.5351 20.4954 13.6068 20.4645 13.6461 20.4525C13.7783 20.4126 13.9194 20.4126 14.0516 20.4525C14.0909 20.4645 14.1626 20.4954 14.3155 20.6244C14.4747 20.7583 14.6682 20.9512 14.9668 21.2499L15.9141 22.1972C15.9464 21.6935 15.9519 21.0675 15.9533 20.2517L15.9392 20.238C15.6656 19.9645 15.4301 19.7285 15.2197 19.551C14.9973 19.3637 14.7552 19.1989 14.4564 19.1091C14.0602 18.9896 13.6375 18.9896 13.2413 19.1091C12.9425 19.1996 12.7011 19.3637 12.478 19.551C12.2676 19.7285 12.0321 19.9649 11.7593 20.2384L11.739 20.258C11.5237 20.4726 11.3971 20.5989 11.2982 20.6796L11.2841 20.6917L11.2759 20.6756C11.2149 20.5634 11.1434 20.3997 11.0234 20.1198L10.9862 20.0329L10.9706 19.9965C10.7209 19.4143 10.5113 18.9245 10.3044 18.5556C10.0904 18.1761 9.82392 17.819 9.40873 17.6114C9.06229 17.4379 8.67451 17.3634 8.28842 17.3961ZM14.1996 14.9312C13.6185 14.9312 13.1474 15.4023 13.1474 15.9834C13.1474 16.5645 13.6185 17.0356 14.1996 17.0356C14.7807 17.0356 15.2518 16.5645 15.2518 15.9834C15.2518 15.4023 14.7807 14.9312 14.1996 14.9312Z" fill="white"/>%0A<path fill-rule="evenodd" clip-rule="evenodd" d="M22.7675 8.5178V25.5534C22.7675 26.3064 22.4678 27.0287 21.9341 27.5611C21.4004 28.0936 20.6765 28.3925 19.9217 28.3926H2.84579C2.09106 28.3925 1.36706 28.0936 0.833383 27.5611C0.299704 27.0287 0 26.3064 0 25.5534V2.83914C3.46516e-05 2.08616 0.299694 1.36386 0.833383 0.831425C1.36706 0.298996 2.09106 4.12048e-05 2.84579 0H14.2297L22.7675 8.5178ZM10.3415 12.426C9.03021 12.426 8.00652 12.4252 7.2096 12.5324C6.40115 12.6413 5.76631 12.868 5.26843 13.3654C4.77051 13.863 4.54419 14.4978 4.43544 15.3062C4.32825 16.1032 4.32897 17.1273 4.32897 18.4385V19.8415C4.32897 21.1528 4.32816 22.1765 4.43544 22.9734C4.54428 23.7819 4.77099 24.4167 5.26843 24.9146C5.766 25.4125 6.40083 25.6388 7.20921 25.7476C8.00621 25.8548 9.03026 25.854 10.3415 25.854H11.7445C13.0558 25.854 14.0795 25.8549 14.8764 25.7476C15.6849 25.6387 16.3197 25.412 16.8176 24.9146C17.3155 24.417 17.5418 23.7822 17.6506 22.9738C17.7578 22.1768 17.757 21.1528 17.757 19.8415V18.4385C17.757 17.1272 17.7578 16.1035 17.6506 15.3066C17.5417 14.4982 17.315 13.8633 16.8176 13.3654C16.32 12.8675 15.6852 12.6412 14.8768 12.5324C14.0798 12.4253 13.0557 12.426 11.7445 12.426H10.3415ZM12.8068 9.93757H20.633L12.8068 2.12945V9.93757Z" fill="white"/>%0A</svg>%0A';
5403
+
5404
+ // src/assets/video_file.svg
5405
+ var video_file_default = 'data:image/svg+xml,<svg width="23" height="29" viewBox="0 0 23 29" fill="none" xmlns="http://www.w3.org/2000/svg">%0A<path fill-rule="evenodd" clip-rule="evenodd" d="M22.7675 8.5178V25.5534C22.7675 26.3064 22.4678 27.0287 21.9341 27.5611C21.4004 28.0936 20.6765 28.3925 19.9217 28.3926H2.84579C2.09106 28.3925 1.36706 28.0936 0.833383 27.5611C0.299704 27.0287 0 26.3064 0 25.5534V2.83914C3.46516e-05 2.08616 0.299694 1.36386 0.833383 0.831425C1.36706 0.298996 2.09106 4.12048e-05 2.84579 0H14.2297L22.7675 8.5178ZM7.89972 14.6224C7.44912 14.6224 7.01703 14.8013 6.69838 15.1199C6.37968 15.4386 6.20046 15.8709 6.20046 16.3216V20.9563C6.20046 21.407 6.37968 21.8393 6.69838 22.158C7.01703 22.4766 7.44912 22.6555 7.89972 22.6556H13.1525C13.6032 22.6556 14.0355 22.4767 14.3542 22.158C14.6729 21.8393 14.8517 21.407 14.8517 20.9563V19.9714L16.9648 21.8531C17.4622 22.296 18.2506 21.9425 18.2506 21.2757V15.7748C18.2506 15.1074 17.4622 14.7545 16.9648 15.1974L14.8517 17.0791V16.3216C14.8517 15.8709 14.6729 15.4386 14.3542 15.1199C14.0355 14.8012 13.6032 14.6224 13.1525 14.6224H7.89972ZM12.8068 9.93757H20.633L12.8068 2.12945V9.93757Z" fill="white"/>%0A</svg>%0A';
5406
+
5407
+ // src/assets/sound.svg
5408
+ var sound_default = 'data:image/svg+xml,<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">%0A<path d="M18.622 3.21699C18.7399 3.31063 18.8351 3.42968 18.9005 3.56526C18.966 3.70084 19 3.84944 19 3.99999V15.667C19 15.707 18.9977 15.7473 18.993 15.788C18.9977 15.858 19 15.9287 19 16C18.9997 16.6368 18.7967 17.2569 18.4206 17.7707C18.0444 18.2844 17.5145 18.6652 16.9076 18.8578C16.3007 19.0505 15.6482 19.045 15.0446 18.8422C14.441 18.6394 13.9176 18.2498 13.5501 17.7298C13.1826 17.2098 12.9901 16.5864 13.0004 15.9497C13.0107 15.313 13.2234 14.6961 13.6076 14.1883C13.9918 13.6805 14.5276 13.3082 15.1375 13.1251C15.7473 12.942 16.3997 12.9577 17 13.17V9.25999L9 11.127V18.003C8.99924 18.6397 8.79595 19.2596 8.41953 19.7731C8.04312 20.2866 7.5131 20.667 6.90616 20.8593C6.29923 21.0516 5.64684 21.0458 5.0434 20.8428C4.43996 20.6398 3.91676 20.2501 3.54948 19.73C3.1822 19.2099 2.98991 18.5865 3.00041 17.9499C3.01091 17.3133 3.22367 16.6966 3.60789 16.1889C3.99212 15.6813 4.5279 15.309 5.13771 15.126C5.74752 14.943 6.39973 14.9588 7 15.171V6.33299C6.99997 6.10722 7.07635 5.88808 7.2167 5.71124C7.35705 5.53439 7.55312 5.41024 7.773 5.35899L17.773 3.02599C17.9183 2.99221 18.0694 2.99139 18.2151 3.02357C18.3608 3.05576 18.4974 3.12013 18.615 3.21199L18.622 3.21699Z" fill="white"/>%0A</svg>%0A';
5409
+
5410
+ // src/assets/img.svg
5411
+ var img_default = 'data:image/svg+xml,<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">%0A<path fill-rule="evenodd" clip-rule="evenodd" d="M3.172 3.17199C2 4.34299 2 6.22899 2 9.99999V14C2 17.771 2 19.657 3.172 20.828C4.344 21.999 6.229 22 10 22H14C17.771 22 19.657 22 20.828 20.828C21.999 19.656 22 17.771 22 14V9.99999C22 7.15999 22 5.38999 21.5 4.18899V17C21.0246 17.0001 20.5538 16.9065 20.1146 16.7246C19.6753 16.5427 19.2762 16.2761 18.94 15.94L18.188 15.188C17.466 14.466 17.106 14.106 16.697 13.954C16.2474 13.7868 15.7526 13.7868 15.303 13.954C14.894 14.106 14.533 14.466 13.813 15.188L13.699 15.301C13.114 15.886 12.821 16.179 12.51 16.233C12.2685 16.2756 12.0197 16.2279 11.811 16.099C11.543 15.933 11.38 15.552 11.053 14.791L11 14.667C10.25 12.917 9.876 12.043 9.222 11.715C8.89266 11.5499 8.52411 11.4789 8.157 11.51C7.428 11.572 6.756 12.245 5.41 13.59L3.5 15.5V2.88699C3.384 2.97299 3.27467 3.06799 3.172 3.17199Z" fill="white"/>%0A<mask id="mask0_1424_377" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="2" y="2" width="20" height="20">%0A<path d="M2 10C2 6.229 2 4.343 3.172 3.172C4.344 2.001 6.229 2 10 2H14C17.771 2 19.657 2 20.828 3.172C21.999 4.344 22 6.229 22 10V14C22 17.771 22 19.657 20.828 20.828C19.656 21.999 17.771 22 14 22H10C6.229 22 4.343 22 3.172 20.828C2.001 19.656 2 17.771 2 14V10Z" fill="white"/>%0A</mask>%0A<g mask="url(%23mask0_1424_377)">%0A<path d="M2 10C2 6.229 2 4.343 3.172 3.172C4.344 2.001 6.229 2 10 2H14C17.771 2 19.657 2 20.828 3.172C21.999 4.344 22 6.229 22 10V14C22 17.771 22 19.657 20.828 20.828C19.656 21.999 17.771 22 14 22H10C6.229 22 4.343 22 3.172 20.828C2.001 19.656 2 17.771 2 14V10Z" stroke="white" stroke-width="4"/>%0A</g>%0A<path d="M15 11C16.1046 11 17 10.1046 17 9C17 7.89543 16.1046 7 15 7C13.8954 7 13 7.89543 13 9C13 10.1046 13.8954 11 15 11Z" fill="white"/>%0A</svg>%0A';
5412
+
5413
+ // src/assets/video.svg
5414
+ var video_default = 'data:image/svg+xml,<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">%0A<path d="M5 5.5C4.27065 5.5 3.57118 5.78973 3.05546 6.30546C2.53973 6.82118 2.25 7.52065 2.25 8.25V15.75C2.25 16.4793 2.53973 17.1788 3.05546 17.6945C3.57118 18.2103 4.27065 18.5 5 18.5H13.5C14.2293 18.5 14.9288 18.2103 15.4445 17.6945C15.9603 17.1788 16.25 16.4793 16.25 15.75V14.156L19.669 17.201C20.474 17.918 21.75 17.346 21.75 16.267V7.365C21.75 6.285 20.474 5.714 19.669 6.431L16.25 9.476V8.25C16.25 7.52065 15.9603 6.82118 15.4445 6.30546C14.9288 5.78973 14.2293 5.5 13.5 5.5H5Z" fill="white"/>%0A</svg>%0A';
5415
+
4625
5416
  // src/se-import/ui/SEImportWizard.tsx
4626
- import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
4627
- function StepDot({ active, done, label, num }) {
4628
- const bg = done ? "#22c55e" : active ? "#6a6ffc" : "var(--ui-bg-elevated, #2a2c31)";
4629
- return /* @__PURE__ */ jsxs7("div", { className: "ui-flex-row", style: { alignItems: "center", gap: 8, opacity: active || done ? 1 : 0.6 }, children: [
4630
- /* @__PURE__ */ jsx13("div", { style: { width: 22, height: 22, borderRadius: "50%", background: bg, color: "#fff", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 11, fontWeight: 700 }, children: done ? "\u2713" : num }),
4631
- /* @__PURE__ */ jsx13("span", { style: { fontSize: 12, fontWeight: active ? 600 : 400 }, children: label })
4632
- ] });
5417
+ import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
5418
+ var CORE_STEPS = [
5419
+ { key: "connect", label: "Connect" },
5420
+ { key: "pick", label: "Overlays" },
5421
+ { key: "url", label: "Load" },
5422
+ { key: "discovery", label: "Discovery" },
5423
+ { key: "options", label: "Options" },
5424
+ { key: "mirror", label: "Assets" },
5425
+ { key: "confirm", label: "Confirm" }
5426
+ ];
5427
+ function stepClass(done, active) {
5428
+ return [
5429
+ "se-import-step",
5430
+ done && "se-import-step--done",
5431
+ active && "se-import-step--active"
5432
+ ].filter(Boolean).join(" ");
4633
5433
  }
4634
- function ProgressBar({ value, max }) {
4635
- const pct = max > 0 ? Math.min(100, Math.round(value / max * 100)) : 0;
4636
- return /* @__PURE__ */ jsx13("div", { style: { height: 6, borderRadius: 3, background: "var(--ui-bg-elevated, #2a2c31)", overflow: "hidden" }, children: /* @__PURE__ */ jsx13("div", { style: { height: "100%", width: `${pct}%`, background: "#6a6ffc", transition: "width 240ms" } }) });
5434
+ function panelClass(extra) {
5435
+ return ["se-import-panel", extra].filter(Boolean).join(" ");
4637
5436
  }
4638
5437
  function ErrorPanel({ message, hint }) {
4639
- return /* @__PURE__ */ jsxs7("div", { style: { padding: 10, borderRadius: 6, background: "rgba(220, 38, 38, 0.12)", border: "1px solid rgba(220, 38, 38, 0.4)", color: "#fca5a5", fontSize: 13 }, children: [
4640
- /* @__PURE__ */ jsx13("div", { style: { fontWeight: 600 }, children: message }),
4641
- hint && /* @__PURE__ */ jsx13("div", { style: { marginTop: 6, whiteSpace: "pre-wrap", opacity: 0.85 }, children: hint })
5438
+ return /* @__PURE__ */ jsxs7("div", { className: "se-import-error", children: [
5439
+ /* @__PURE__ */ jsx13("div", { className: "se-import-error__title", children: message }),
5440
+ hint && /* @__PURE__ */ jsx13("div", { className: "se-import-error__hint", children: hint })
4642
5441
  ] });
4643
5442
  }
4644
- function StatusBadge({ label, count, pct, tone }) {
4645
- const colors = { success: "#22c55e", info: "#6a6ffc", warn: "#f59e0b", muted: "#9ca3af" };
4646
- return /* @__PURE__ */ jsxs7("div", { style: { flex: "1 1 120px", padding: "10px 12px", borderRadius: 6, background: "var(--ui-bg-elevated, #1d1f24)", borderLeft: `3px solid ${colors[tone]}` }, children: [
4647
- /* @__PURE__ */ jsx13("div", { style: { fontSize: 11, textTransform: "uppercase", letterSpacing: 0.5, opacity: 0.7 }, children: label }),
4648
- /* @__PURE__ */ jsx13("div", { style: { fontSize: 22, fontWeight: 700 }, children: count }),
4649
- /* @__PURE__ */ jsxs7("div", { style: { fontSize: 12, opacity: 0.7 }, children: [
4650
- pct,
4651
- "%"
4652
- ] })
5443
+ function Field({
5444
+ label,
5445
+ value,
5446
+ onChange,
5447
+ placeholder,
5448
+ disabled
5449
+ }) {
5450
+ return /* @__PURE__ */ jsx13("div", { className: "se-import-field", children: /* @__PURE__ */ jsx13(
5451
+ LSInput,
5452
+ {
5453
+ label,
5454
+ value,
5455
+ onChange: (_, next) => onChange(String(next ?? "")),
5456
+ placeholder,
5457
+ disabled
5458
+ }
5459
+ ) });
5460
+ }
5461
+ function ProgressBar({ value, max }) {
5462
+ return /* @__PURE__ */ jsx13(
5463
+ "progress",
5464
+ {
5465
+ className: "se-import-progress",
5466
+ value: max > 0 ? value : 0,
5467
+ max: max || 1
5468
+ }
5469
+ );
5470
+ }
5471
+ function StatusCard({
5472
+ label,
5473
+ value,
5474
+ sub,
5475
+ tone = "neutral"
5476
+ }) {
5477
+ return /* @__PURE__ */ jsxs7("div", { className: `se-import-stat se-import-stat--${tone}`, children: [
5478
+ /* @__PURE__ */ jsx13("div", { className: "se-import-stat__label", children: label }),
5479
+ /* @__PURE__ */ jsx13("div", { className: "se-import-stat__value", children: value }),
5480
+ sub && /* @__PURE__ */ jsx13("div", { className: "se-import-stat__sub", children: sub })
4653
5481
  ] });
4654
5482
  }
4655
5483
  function CoverageCards({ coverage }) {
4656
5484
  const byStatus = useMemo5(() => {
4657
5485
  const acc = { direct: 0, partial: 0, template: 0, placeholder: 0 };
4658
- for (const m of coverage.mappings) acc[m.status] += m.count;
5486
+ for (const item of coverage.mappings) acc[item.status] += item.count;
4659
5487
  return acc;
4660
5488
  }, [coverage]);
4661
- const pct = (n) => coverage.totalWidgets > 0 ? Math.round(n / coverage.totalWidgets * 100) : 0;
4662
- return /* @__PURE__ */ jsxs7("div", { className: "ui-flex-row ui-gap-2", style: { flexWrap: "wrap" }, children: [
4663
- /* @__PURE__ */ jsx13(StatusBadge, { label: "Direct", count: byStatus.direct, pct: pct(byStatus.direct), tone: "success" }),
4664
- /* @__PURE__ */ jsx13(StatusBadge, { label: "Partial", count: byStatus.partial, pct: pct(byStatus.partial), tone: "info" }),
4665
- /* @__PURE__ */ jsx13(StatusBadge, { label: "Template", count: byStatus.template, pct: pct(byStatus.template), tone: "warn" }),
4666
- /* @__PURE__ */ jsx13(StatusBadge, { label: "Placeholder", count: byStatus.placeholder, pct: pct(byStatus.placeholder), tone: "muted" })
5489
+ const pct = (value) => coverage.totalWidgets > 0 ? Math.round(value / coverage.totalWidgets * 100) : 0;
5490
+ return /* @__PURE__ */ jsxs7("div", { className: "se-import-grid se-import-grid--four", children: [
5491
+ /* @__PURE__ */ jsx13(
5492
+ StatusCard,
5493
+ {
5494
+ label: "Direct",
5495
+ value: byStatus.direct,
5496
+ sub: `${pct(byStatus.direct)}%`,
5497
+ tone: "success"
5498
+ }
5499
+ ),
5500
+ /* @__PURE__ */ jsx13(
5501
+ StatusCard,
5502
+ {
5503
+ label: "Partial",
5504
+ value: byStatus.partial,
5505
+ sub: `${pct(byStatus.partial)}%`,
5506
+ tone: "info"
5507
+ }
5508
+ ),
5509
+ /* @__PURE__ */ jsx13(
5510
+ StatusCard,
5511
+ {
5512
+ label: "Template",
5513
+ value: byStatus.template,
5514
+ sub: `${pct(byStatus.template)}%`,
5515
+ tone: "warn"
5516
+ }
5517
+ ),
5518
+ /* @__PURE__ */ jsx13(
5519
+ StatusCard,
5520
+ {
5521
+ label: "Placeholder",
5522
+ value: byStatus.placeholder,
5523
+ sub: `${pct(byStatus.placeholder)}%`,
5524
+ tone: "muted"
5525
+ }
5526
+ )
4667
5527
  ] });
4668
5528
  }
4669
- function CustomOverlayPreview({ html, css, js, data, width, height }) {
5529
+ function CustomOverlayPreview({
5530
+ html,
5531
+ css,
5532
+ js,
5533
+ data,
5534
+ width,
5535
+ height
5536
+ }) {
4670
5537
  const srcdoc = useMemo5(() => {
4671
5538
  const safeData = JSON.stringify(data ?? {});
4672
5539
  const safeJs = JSON.stringify(`(async () => { ${js || ""} })()`);
@@ -4682,219 +5549,900 @@ try { eval(${safeJs}); } catch(e) { document.body.insertAdjacentHTML('beforeend'
4682
5549
  "iframe",
4683
5550
  {
4684
5551
  title: "ai-preview",
5552
+ className: "se-import-preview",
4685
5553
  sandbox: "allow-scripts",
4686
5554
  srcDoc: srcdoc,
4687
- style: { width: "100%", height: 260, border: "1px solid var(--ui-border, #2a2c31)", borderRadius: 6, background: "repeating-conic-gradient(#1a1c20 0% 25%, #1d1f24 0% 50%) 50% / 20px 20px" },
4688
5555
  "data-canvas-width": width,
4689
5556
  "data-canvas-height": height
4690
5557
  }
4691
5558
  );
4692
5559
  }
4693
- function StepURL({ url, setUrl, error, busy }) {
4694
- return /* @__PURE__ */ jsxs7("div", { className: "ui-flex-column ui-gap-2", children: [
4695
- /* @__PURE__ */ jsxs7("p", { style: { margin: 0, opacity: 0.85 }, children: [
4696
- "Paste the public ",
4697
- /* @__PURE__ */ jsx13("strong", { children: "preview URL" }),
4698
- " for your StreamElements overlay. The URL contains the access token, so no StreamElements login is required."
4699
- ] }),
4700
- /* @__PURE__ */ jsx13(LSInput, { label: "StreamElements overlay URL", value: url, onChange: (e) => setUrl(e.target.value), placeholder: "https://streamelements.com/overlay/<id>/<token>", disabled: busy }),
4701
- /* @__PURE__ */ jsxs7("details", { children: [
4702
- /* @__PURE__ */ jsx13("summary", { style: { cursor: "pointer", fontWeight: 600, fontSize: 13 }, children: "How to find the overlay URL" }),
4703
- /* @__PURE__ */ jsxs7("ol", { style: { paddingLeft: 18, marginTop: 8, fontSize: 13, opacity: 0.9 }, children: [
4704
- /* @__PURE__ */ jsxs7("li", { children: [
4705
- "Open ",
4706
- /* @__PURE__ */ jsx13("a", { href: "https://streamelements.com/dashboard/overlays", target: "_blank", rel: "noreferrer", children: "streamelements.com/dashboard/overlays" }),
4707
- "."
4708
- ] }),
4709
- /* @__PURE__ */ jsx13("li", { children: "Click the overlay you want to bring over." }),
4710
- /* @__PURE__ */ jsxs7("li", { children: [
4711
- "Click ",
4712
- /* @__PURE__ */ jsx13("strong", { children: "Copy URL" }),
4713
- " in the top-right of the editor."
4714
- ] }),
4715
- /* @__PURE__ */ jsx13("li", { children: "Paste it above." })
4716
- ] })
4717
- ] }),
4718
- error && /* @__PURE__ */ jsx13(ErrorPanel, { message: error.message, hint: error.hint })
4719
- ] });
4720
- }
4721
- function StepDiscovery({ result, assetCount }) {
4722
- const widgets = result.coverage.totalWidgets;
4723
- const reviewCount = result.reviewItems.length;
4724
- const customCount = result.coverage.mappings.find((m) => m.seType === "se-widget-custom-event-list")?.count ?? 0;
4725
- const alertCount = result.coverage.mappings.filter((m) => m.lumiaType === "alert").reduce((a, m) => a + m.count, 0);
4726
- return /* @__PURE__ */ jsxs7("div", { className: "ui-flex-column ui-gap-2", children: [
4727
- /* @__PURE__ */ jsx13("div", { style: { fontWeight: 600, fontSize: 16 }, children: "Here's what we found" }),
4728
- /* @__PURE__ */ jsxs7("div", { className: "ui-flex-row ui-gap-2", style: { flexWrap: "wrap" }, children: [
4729
- /* @__PURE__ */ jsx13(DiscoveryStat, { label: "Total widgets", value: widgets }),
4730
- /* @__PURE__ */ jsx13(DiscoveryStat, { label: "Alerts", value: alertCount }),
4731
- /* @__PURE__ */ jsx13(DiscoveryStat, { label: "Custom widgets", value: customCount, sub: customCount > 0 ? "will need the SE compatibility shim" : void 0 }),
4732
- /* @__PURE__ */ jsx13(DiscoveryStat, { label: "SE-hosted assets", value: assetCount, sub: assetCount > 0 ? "can be mirrored to Lumia storage" : "no migration needed" }),
4733
- /* @__PURE__ */ jsx13(DiscoveryStat, { label: "Need review", value: reviewCount, sub: reviewCount > 0 ? "no native Lumia equivalent yet" : "everything maps cleanly", highlight: reviewCount > 0 })
4734
- ] }),
4735
- /* @__PURE__ */ jsx13("div", { style: { marginTop: 8 }, children: /* @__PURE__ */ jsx13(CoverageCards, { coverage: result.coverage }) })
4736
- ] });
4737
- }
4738
- function DiscoveryStat({ label, value, sub, highlight }) {
4739
- return /* @__PURE__ */ jsxs7("div", { style: { flex: "1 1 160px", padding: "12px 14px", borderRadius: 6, background: "var(--ui-bg-elevated, #1d1f24)", border: highlight ? "1px solid #f59e0b" : "1px solid transparent" }, children: [
4740
- /* @__PURE__ */ jsx13("div", { style: { fontSize: 11, textTransform: "uppercase", letterSpacing: 0.5, opacity: 0.7 }, children: label }),
4741
- /* @__PURE__ */ jsx13("div", { style: { fontSize: 26, fontWeight: 700 }, children: value }),
4742
- sub && /* @__PURE__ */ jsx13("div", { style: { fontSize: 11, opacity: 0.65, marginTop: 4 }, children: sub })
5560
+ function StepHeader({
5561
+ number,
5562
+ title,
5563
+ subtitle
5564
+ }) {
5565
+ return /* @__PURE__ */ jsxs7("div", { className: "se-import-section-title", children: [
5566
+ /* @__PURE__ */ jsx13("div", { className: "se-import-section-title__num", children: number }),
5567
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-section-title__content", children: [
5568
+ /* @__PURE__ */ jsx13("h3", { children: title }),
5569
+ subtitle && /* @__PURE__ */ jsx13("p", { children: subtitle })
5570
+ ] })
4743
5571
  ] });
4744
5572
  }
4745
- function StepOptions({ options, setOptions, assetCount }) {
4746
- return /* @__PURE__ */ jsxs7("div", { className: "ui-flex-column ui-gap-2", children: [
4747
- /* @__PURE__ */ jsx13("div", { style: { fontWeight: 600, fontSize: 16 }, children: "Import options" }),
4748
- /* @__PURE__ */ jsxs7("label", { className: "ui-flex-row", style: { gap: 10, padding: 12, borderRadius: 6, background: "var(--ui-bg-elevated, #1d1f24)", cursor: assetCount === 0 ? "not-allowed" : "pointer", opacity: assetCount === 0 ? 0.5 : 1 }, children: [
4749
- /* @__PURE__ */ jsx13("input", { type: "checkbox", checked: options.mirrorAssets, disabled: assetCount === 0, onChange: (e) => setOptions({ ...options, mirrorAssets: e.target.checked }), style: { marginTop: 2 } }),
5573
+ var JWT_FEATURES = [
5574
+ "All your overlays \u2014 pick one, many, or all",
5575
+ "Live counters (follower / sub / tip / bits goals)",
5576
+ "Chat bot commands",
5577
+ "Loyalty points + watch time",
5578
+ "Recent activity replay",
5579
+ "Bits leaderboard"
5580
+ ];
5581
+ function ModeCard({
5582
+ primary,
5583
+ title,
5584
+ body,
5585
+ caveat,
5586
+ onClick
5587
+ }) {
5588
+ return /* @__PURE__ */ jsxs7(
5589
+ "button",
5590
+ {
5591
+ type: "button",
5592
+ onClick,
5593
+ style: {
5594
+ display: "flex",
5595
+ flexDirection: "column",
5596
+ alignItems: "flex-start",
5597
+ gap: 14,
5598
+ padding: 24,
5599
+ minHeight: 280,
5600
+ borderRadius: 14,
5601
+ border: primary ? "2px solid var(--se-primary)" : "1px solid var(--se-border)",
5602
+ background: primary ? "rgba(255, 64, 118, 0.08)" : "transparent",
5603
+ color: "var(--se-text)",
5604
+ textAlign: "left",
5605
+ cursor: "pointer",
5606
+ transition: "background 120ms, border-color 120ms, transform 120ms"
5607
+ },
5608
+ onMouseEnter: (e) => {
5609
+ e.currentTarget.style.background = primary ? "rgba(255, 64, 118, 0.14)" : "rgba(255, 255, 255, 0.04)";
5610
+ if (!primary)
5611
+ e.currentTarget.style.borderColor = "var(--se-border-strong)";
5612
+ e.currentTarget.style.transform = "translateY(-2px)";
5613
+ },
5614
+ onMouseLeave: (e) => {
5615
+ e.currentTarget.style.background = primary ? "rgba(255, 64, 118, 0.08)" : "transparent";
5616
+ if (!primary)
5617
+ e.currentTarget.style.borderColor = "var(--se-border)";
5618
+ e.currentTarget.style.transform = "translateY(0)";
5619
+ },
5620
+ children: [
5621
+ /* @__PURE__ */ jsx13(
5622
+ "div",
5623
+ {
5624
+ style: {
5625
+ fontSize: 18,
5626
+ fontWeight: 800,
5627
+ color: primary ? "var(--se-primary)" : "var(--se-text)"
5628
+ },
5629
+ children: title
5630
+ }
5631
+ ),
5632
+ /* @__PURE__ */ jsx13("div", { style: { fontSize: 13.5, lineHeight: 1.55, color: "var(--se-text)", width: "100%" }, children: body }),
5633
+ /* @__PURE__ */ jsx13("div", { style: { fontSize: 12, color: "var(--se-muted)", marginTop: "auto" }, children: caveat })
5634
+ ]
5635
+ }
5636
+ );
5637
+ }
5638
+ function StepModePicker({
5639
+ onChooseJwt,
5640
+ onChooseUrl
5641
+ }) {
5642
+ return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--wide"), children: [
5643
+ /* @__PURE__ */ jsx13(
5644
+ StepHeader,
5645
+ {
5646
+ number: 1,
5647
+ title: "What would you like to import?",
5648
+ subtitle: "Pick a starting point. You can change your mind from the Back button on the next step."
5649
+ }
5650
+ ),
5651
+ /* @__PURE__ */ jsxs7(
5652
+ "div",
5653
+ {
5654
+ style: {
5655
+ display: "grid",
5656
+ gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
5657
+ gap: 18,
5658
+ marginTop: 8
5659
+ },
5660
+ children: [
5661
+ /* @__PURE__ */ jsx13(
5662
+ ModeCard,
5663
+ {
5664
+ primary: true,
5665
+ title: "Import everything from StreamElements",
5666
+ body: /* @__PURE__ */ jsxs7(Fragment4, { children: [
5667
+ /* @__PURE__ */ jsx13("div", { style: { marginBottom: 10 }, children: "Connect once and bring over your entire SE setup:" }),
5668
+ /* @__PURE__ */ jsx13(
5669
+ "ul",
5670
+ {
5671
+ style: {
5672
+ margin: 0,
5673
+ paddingLeft: 18,
5674
+ display: "grid",
5675
+ gap: 6,
5676
+ fontSize: 13
5677
+ },
5678
+ children: JWT_FEATURES.map((feat) => /* @__PURE__ */ jsx13("li", { children: feat }, feat))
5679
+ }
5680
+ )
5681
+ ] }),
5682
+ caveat: "Requires pasting your StreamElements JWT. Stored only in memory \u2014 never sent to Lumia.",
5683
+ onClick: onChooseJwt
5684
+ }
5685
+ ),
5686
+ /* @__PURE__ */ jsx13(
5687
+ ModeCard,
5688
+ {
5689
+ title: "Import a single overlay",
5690
+ body: /* @__PURE__ */ jsxs7(Fragment4, { children: [
5691
+ "Paste a single overlay's public preview URL to bring just that overlay over.",
5692
+ /* @__PURE__ */ jsx13("div", { style: { marginTop: 10, color: "var(--se-muted)" }, children: "No login required \u2014 perfect for trying one overlay before deciding whether to migrate your whole account." })
5693
+ ] }),
5694
+ caveat: "The URL contains the access token; treat it like a password.",
5695
+ onClick: onChooseUrl
5696
+ }
5697
+ )
5698
+ ]
5699
+ }
5700
+ )
5701
+ ] });
5702
+ }
5703
+ function StepConnect({
5704
+ jwt,
5705
+ setJwt,
5706
+ error,
5707
+ busy,
5708
+ channels,
5709
+ selectedChannelId,
5710
+ setSelectedChannelId,
5711
+ onUseUrl
5712
+ }) {
5713
+ return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--narrow"), children: [
5714
+ /* @__PURE__ */ jsx13(
5715
+ StepHeader,
5716
+ {
5717
+ number: 1,
5718
+ title: "Connect your StreamElements account",
5719
+ 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."
5720
+ }
5721
+ ),
5722
+ /* @__PURE__ */ jsx13(
5723
+ Field,
5724
+ {
5725
+ label: "StreamElements JWT",
5726
+ value: jwt,
5727
+ onChange: setJwt,
5728
+ placeholder: "eyJhbGciOi...",
5729
+ disabled: busy
5730
+ }
5731
+ ),
5732
+ channels && channels.length > 1 && /* @__PURE__ */ jsxs7("div", { className: "se-import-help se-import-help--info", children: [
5733
+ /* @__PURE__ */ jsx13("div", { className: "se-import-help__icon", children: "i" }),
4750
5734
  /* @__PURE__ */ jsxs7("div", { children: [
4751
- /* @__PURE__ */ jsxs7("div", { style: { fontWeight: 600 }, children: [
4752
- "Mirror StreamElements assets to Lumia storage ",
4753
- assetCount > 0 && /* @__PURE__ */ jsxs7("span", { style: { opacity: 0.7, fontWeight: 400 }, children: [
4754
- "(",
4755
- assetCount,
4756
- " files)"
5735
+ /* @__PURE__ */ jsx13("div", { className: "se-import-help__title", children: "Pick a channel" }),
5736
+ /* @__PURE__ */ jsx13("p", { children: "This account has multiple StreamElements channels. Choose which one to import from." }),
5737
+ /* @__PURE__ */ jsx13("div", { style: { display: "grid", gap: 8, marginTop: 12 }, children: channels.map((c) => /* @__PURE__ */ jsxs7(
5738
+ "label",
5739
+ {
5740
+ style: {
5741
+ display: "flex",
5742
+ alignItems: "center",
5743
+ gap: 10,
5744
+ padding: "8px 12px",
5745
+ border: "1px solid var(--se-border)",
5746
+ borderRadius: 8,
5747
+ background: selectedChannelId === c._id ? "rgba(255, 64, 118, 0.12)" : "transparent",
5748
+ cursor: "pointer"
5749
+ },
5750
+ children: [
5751
+ /* @__PURE__ */ jsx13(
5752
+ "input",
5753
+ {
5754
+ type: "radio",
5755
+ name: "se-channel",
5756
+ checked: selectedChannelId === c._id,
5757
+ onChange: () => setSelectedChannelId(c._id)
5758
+ }
5759
+ ),
5760
+ c.avatar && /* @__PURE__ */ jsx13("img", { src: c.avatar, alt: "", width: 24, height: 24, style: { borderRadius: "50%" } }),
5761
+ /* @__PURE__ */ jsxs7("div", { style: { display: "flex", flexDirection: "column" }, children: [
5762
+ /* @__PURE__ */ jsx13("span", { style: { fontWeight: 700 }, children: c.displayName ?? c.username }),
5763
+ /* @__PURE__ */ jsxs7("span", { style: { fontSize: 12, color: "var(--se-muted)" }, children: [
5764
+ c.provider,
5765
+ " \xB7 ",
5766
+ c.username
5767
+ ] })
5768
+ ] })
5769
+ ]
5770
+ },
5771
+ c._id
5772
+ )) })
5773
+ ] })
5774
+ ] }),
5775
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-help", children: [
5776
+ /* @__PURE__ */ jsx13("div", { className: "se-import-help__icon", children: "i" }),
5777
+ /* @__PURE__ */ jsxs7("div", { children: [
5778
+ /* @__PURE__ */ jsx13("div", { className: "se-import-help__title", children: "How to find your JWT" }),
5779
+ /* @__PURE__ */ jsxs7("ol", { children: [
5780
+ /* @__PURE__ */ jsxs7("li", { children: [
5781
+ "Open",
5782
+ " ",
5783
+ /* @__PURE__ */ jsx13(
5784
+ "a",
5785
+ {
5786
+ href: "https://streamelements.com/dashboard/account/channels",
5787
+ target: "_blank",
5788
+ rel: "noreferrer",
5789
+ children: "streamelements.com/dashboard/account/channels"
5790
+ }
5791
+ ),
5792
+ "."
5793
+ ] }),
5794
+ /* @__PURE__ */ jsxs7("li", { children: [
5795
+ "Click ",
5796
+ /* @__PURE__ */ jsx13("strong", { children: "Show JWT Token" }),
5797
+ " for the channel you want to import."
5798
+ ] }),
5799
+ /* @__PURE__ */ jsx13("li", { children: "Copy the entire token and paste it above." }),
5800
+ /* @__PURE__ */ jsxs7("li", { children: [
5801
+ "When you're done, click ",
5802
+ /* @__PURE__ */ jsx13("strong", { children: "Revoke" }),
5803
+ " on the same page so the token stops working."
4757
5804
  ] })
4758
- ] }),
4759
- /* @__PURE__ */ jsxs7("div", { style: { fontSize: 12, opacity: 0.8, marginTop: 4 }, children: [
4760
- "Downloads every ",
4761
- `cdn.streamelements.com`,
4762
- " URL referenced in the overlay and re-uploads it to your Lumia asset library, then rewrites the URLs. Recommended \u2014 if StreamElements rotates their CDN or removes a file, your overlay keeps working."
4763
5805
  ] })
4764
5806
  ] })
4765
5807
  ] }),
4766
- /* @__PURE__ */ jsx13(LSInput, { label: "Overlay name", value: options.name, onChange: (e) => setOptions({ ...options, name: e.target.value }) })
5808
+ /* @__PURE__ */ jsxs7("div", { style: { marginTop: 16, fontSize: 13, color: "var(--se-muted)" }, children: [
5809
+ "Prefer to import just a single overlay without logging in?",
5810
+ " ",
5811
+ /* @__PURE__ */ jsx13(
5812
+ "a",
5813
+ {
5814
+ href: "#",
5815
+ onClick: (e) => {
5816
+ e.preventDefault();
5817
+ onUseUrl();
5818
+ },
5819
+ style: { color: "var(--se-primary)", fontWeight: 700 },
5820
+ children: "Use a public preview URL instead"
5821
+ }
5822
+ ),
5823
+ "."
5824
+ ] }),
5825
+ error && /* @__PURE__ */ jsx13(ErrorPanel, { message: error.message, hint: error.hint })
4767
5826
  ] });
4768
5827
  }
4769
- function StepMirror({ rows, running, onRetry }) {
4770
- const done = rows.filter((r) => r.state === "done").length;
4771
- const reused = rows.filter((r) => r.state === "reused").length;
4772
- const keptOnCdn = rows.filter((r) => r.state === "kept-on-cdn").length;
4773
- const failed = rows.filter((r) => r.state === "failed").length;
4774
- const summary = running ? `Downloading from StreamElements and uploading to Lumia\u2026 (${done + reused + keptOnCdn}/${rows.length})` : `Finished. ${done} mirrored${reused ? `, ${reused} reused from your library` : ""}${keptOnCdn ? `, ${keptOnCdn} SVG kept on SE CDN` : ""}${failed ? `, ${failed} failed (left pointing at SE).` : "."}`;
4775
- const stateColor = (s) => s === "done" ? "#22c55e" : s === "reused" ? "#60a5fa" : s === "kept-on-cdn" ? "#a78bfa" : s === "failed" ? "#ef4444" : "#6a6ffc";
4776
- const stateLabel = (s) => s === "pending" ? "\u2026" : s === "fetching" ? "Fetching\u2026" : s === "reused" ? "\u21BA reused" : s === "kept-on-cdn" ? "\u2297 kept on SE CDN" : s === "done" ? "\u2713" : "\u2717";
4777
- return /* @__PURE__ */ jsxs7("div", { className: "ui-flex-column ui-gap-2", children: [
4778
- /* @__PURE__ */ jsx13("div", { style: { fontWeight: 600, fontSize: 16 }, children: "Mirroring assets" }),
4779
- /* @__PURE__ */ jsx13("div", { style: { fontSize: 13, opacity: 0.8 }, children: summary }),
4780
- /* @__PURE__ */ jsx13(ProgressBar, { value: done + reused + failed + keptOnCdn, max: rows.length }),
4781
- /* @__PURE__ */ jsx13("div", { className: "ui-flex-column", style: { maxHeight: 320, overflowY: "auto", border: "1px solid var(--ui-border, #2a2c31)", borderRadius: 6 }, children: rows.map((row, i) => /* @__PURE__ */ jsxs7("div", { style: { padding: "8px 12px", borderTop: i === 0 ? "none" : "1px solid var(--ui-border, #2a2c31)", fontSize: 12 }, children: [
4782
- /* @__PURE__ */ jsxs7("div", { className: "ui-flex-row", style: { justifyContent: "space-between", gap: 8, alignItems: "center" }, children: [
4783
- /* @__PURE__ */ jsx13("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1, fontFamily: "ui-monospace, monospace" }, title: row.url, children: row.url }),
4784
- /* @__PURE__ */ jsx13("span", { style: { color: stateColor(row.state), fontWeight: 600 }, children: stateLabel(row.state) }),
4785
- row.state === "failed" && /* @__PURE__ */ jsx13(
4786
- "button",
4787
- {
4788
- type: "button",
4789
- className: "ui-button ui-button-secondary",
4790
- style: { padding: "2px 10px", fontSize: 11, minWidth: 0 },
4791
- onClick: () => onRetry(i),
4792
- title: "Retry this asset",
4793
- children: "Retry"
4794
- }
4795
- )
5828
+ function StepPickOverlay({
5829
+ overlays,
5830
+ loading,
5831
+ error,
5832
+ selectedIds,
5833
+ toggleId,
5834
+ selectAll,
5835
+ clearAll,
5836
+ onRetry
5837
+ }) {
5838
+ const selectedCount = selectedIds.size;
5839
+ const total = overlays?.length ?? 0;
5840
+ const allSelected = total > 0 && selectedCount === total;
5841
+ const hasAnySelected = selectedCount > 0;
5842
+ return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--wide"), children: [
5843
+ /* @__PURE__ */ jsx13(
5844
+ StepHeader,
5845
+ {
5846
+ number: 2,
5847
+ title: "Pick the overlays to import",
5848
+ subtitle: `Select one or more StreamElements overlays from your account. We'll run each one through the discovery \u2192 review \u2192 import pipeline in sequence. ${total > 0 ? `${total} overlay${total === 1 ? "" : "s"} on this channel.` : ""}`
5849
+ }
5850
+ ),
5851
+ loading && /* @__PURE__ */ jsxs7("div", { className: "se-import-help se-import-help--info", children: [
5852
+ /* @__PURE__ */ jsx13("div", { className: "se-import-help__icon", children: "\u2026" }),
5853
+ /* @__PURE__ */ jsx13("div", { children: "Fetching your StreamElements overlays." })
5854
+ ] }),
5855
+ error && /* @__PURE__ */ jsxs7(Fragment4, { children: [
5856
+ /* @__PURE__ */ jsx13(ErrorPanel, { message: error.message, hint: error.hint }),
5857
+ /* @__PURE__ */ jsx13("div", { style: { marginTop: 12 }, children: /* @__PURE__ */ jsx13(LSButton, { type: "button", color: "secondary", onClick: onRetry, label: "Retry" }) })
5858
+ ] }),
5859
+ !loading && !error && overlays && overlays.length === 0 && /* @__PURE__ */ jsxs7("div", { className: "se-import-help se-import-help--info", children: [
5860
+ /* @__PURE__ */ jsx13("div", { className: "se-import-help__icon", children: "i" }),
5861
+ /* @__PURE__ */ jsxs7("div", { children: [
5862
+ /* @__PURE__ */ jsx13("div", { className: "se-import-help__title", children: "No overlays found" }),
5863
+ /* @__PURE__ */ jsx13("p", { children: "This StreamElements account doesn't have any overlays yet. Create one in the SE dashboard, or use the legacy preview URL flow from the Connect step." })
5864
+ ] })
5865
+ ] }),
5866
+ !loading && !error && overlays && overlays.length > 0 && /* @__PURE__ */ jsxs7(Fragment4, { children: [
5867
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-actions se-import-actions--inline", style: { marginTop: 24, marginBottom: 16, justifyContent: "space-between", alignItems: "center", display: "flex" }, children: [
5868
+ /* @__PURE__ */ jsxs7("label", { style: { display: "inline-flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 14, fontWeight: 600 }, children: [
5869
+ /* @__PURE__ */ jsx13(
5870
+ "input",
5871
+ {
5872
+ type: "checkbox",
5873
+ checked: allSelected,
5874
+ ref: (el) => {
5875
+ if (el) el.indeterminate = !allSelected && hasAnySelected;
5876
+ },
5877
+ onChange: () => allSelected ? clearAll() : selectAll()
5878
+ }
5879
+ ),
5880
+ /* @__PURE__ */ jsxs7("span", { children: [
5881
+ allSelected ? "Deselect all" : "Select all",
5882
+ " (",
5883
+ selectedCount,
5884
+ "/",
5885
+ total,
5886
+ ")"
5887
+ ] })
5888
+ ] }),
5889
+ /* @__PURE__ */ jsx13("span", { style: { fontSize: 13, color: "var(--se-muted)" }, children: hasAnySelected ? `${selectedCount} overlay${selectedCount === 1 ? "" : "s"} queued. Each runs through the full import flow in sequence.` : "Click overlays below to select them." })
4796
5890
  ] }),
4797
- row.error && /* @__PURE__ */ jsx13("div", { style: { marginTop: 4, color: "#fca5a5" }, children: row.error })
4798
- ] }, row.url)) })
5891
+ /* @__PURE__ */ jsx13(
5892
+ "div",
5893
+ {
5894
+ className: "se-import-grid",
5895
+ style: {
5896
+ gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))",
5897
+ gap: 12,
5898
+ maxHeight: 480,
5899
+ overflowY: "auto",
5900
+ padding: 4
5901
+ },
5902
+ children: overlays.map((o) => {
5903
+ const selected = selectedIds.has(o._id);
5904
+ return /* @__PURE__ */ jsxs7(
5905
+ "label",
5906
+ {
5907
+ style: {
5908
+ position: "relative",
5909
+ display: "flex",
5910
+ flexDirection: "column",
5911
+ border: selected ? "2px solid var(--se-primary)" : "1px solid var(--se-border)",
5912
+ borderRadius: 10,
5913
+ padding: 10,
5914
+ background: selected ? "rgba(255, 64, 118, 0.08)" : "rgba(28, 32, 66, 0.45)",
5915
+ cursor: "pointer",
5916
+ transition: "border-color 120ms, background 120ms"
5917
+ },
5918
+ children: [
5919
+ /* @__PURE__ */ jsx13(
5920
+ "input",
5921
+ {
5922
+ type: "checkbox",
5923
+ checked: selected,
5924
+ onChange: () => toggleId(o._id),
5925
+ style: { position: "absolute", top: 10, right: 10, accentColor: "var(--se-primary)", zIndex: 1 }
5926
+ }
5927
+ ),
5928
+ o.preview ? /* @__PURE__ */ jsx13(
5929
+ "img",
5930
+ {
5931
+ src: o.preview,
5932
+ alt: "",
5933
+ style: {
5934
+ width: "100%",
5935
+ height: 110,
5936
+ objectFit: "cover",
5937
+ borderRadius: 6,
5938
+ background: "var(--se-panel-2)"
5939
+ },
5940
+ loading: "lazy"
5941
+ }
5942
+ ) : /* @__PURE__ */ jsx13("div", { style: { width: "100%", height: 110, borderRadius: 6, background: "var(--se-panel-2)" } }),
5943
+ /* @__PURE__ */ jsx13("div", { style: { marginTop: 8, fontSize: 14, fontWeight: 700, color: "var(--se-text)", paddingRight: 28 }, children: o.name ?? "Unnamed overlay" }),
5944
+ /* @__PURE__ */ jsxs7("div", { style: { marginTop: 4, fontSize: 11, color: "var(--se-muted)" }, children: [
5945
+ o.type ?? "regular",
5946
+ o.settings?.width && o.settings?.height ? ` \xB7 ${o.settings.width}\xD7${o.settings.height}` : ""
5947
+ ] }),
5948
+ o.updatedAt && /* @__PURE__ */ jsxs7("div", { style: { marginTop: 2, fontSize: 11, color: "var(--se-muted)" }, children: [
5949
+ "Updated ",
5950
+ new Date(o.updatedAt).toLocaleDateString()
5951
+ ] })
5952
+ ]
5953
+ },
5954
+ o._id
5955
+ );
5956
+ })
5957
+ }
5958
+ )
5959
+ ] })
4799
5960
  ] });
4800
5961
  }
4801
- function StepReview({ item, index, total, state, error, generated, canvas, onSkip, onKeep, onGenerate, onAccept, onRetry, onPickMarketplace, onKeepAllPlaceholders, onSkipAll, remainingCount, marketplaceAvailable }) {
4802
- return /* @__PURE__ */ jsxs7("div", { className: "ui-flex-column ui-gap-2", children: [
4803
- /* @__PURE__ */ jsxs7("div", { className: "ui-flex-row", style: { justifyContent: "space-between", alignItems: "center" }, children: [
4804
- /* @__PURE__ */ jsxs7("div", { style: { fontWeight: 600, fontSize: 16 }, children: [
4805
- "Reviewing widget ",
4806
- index + 1,
4807
- " of ",
4808
- total
4809
- ] }),
4810
- /* @__PURE__ */ jsxs7("div", { className: "ui-flex-row ui-gap-2", style: { alignItems: "center" }, children: [
4811
- item.flaggedOff && /* @__PURE__ */ jsx13("div", { style: { fontSize: 10, padding: "2px 8px", borderRadius: 4, background: "rgba(234, 179, 8, 0.2)", color: "#facc15", textTransform: "uppercase", letterSpacing: 0.5, fontWeight: 600 }, children: "Auto-import disabled" }),
4812
- /* @__PURE__ */ jsx13("div", { style: { fontSize: 11, opacity: 0.7, textTransform: "uppercase", letterSpacing: 0.5 }, children: item.status })
5962
+ function StepURL({
5963
+ url,
5964
+ setUrl,
5965
+ error,
5966
+ busy
5967
+ }) {
5968
+ return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--narrow"), children: [
5969
+ /* @__PURE__ */ jsx13(
5970
+ StepHeader,
5971
+ {
5972
+ number: 1,
5973
+ title: "Paste your StreamElements overlay URL",
5974
+ subtitle: "This should be the public preview URL of your overlay. It includes the\n access token."
5975
+ }
5976
+ ),
5977
+ /* @__PURE__ */ jsx13(
5978
+ LSInput,
5979
+ {
5980
+ label: "Overlay URL",
5981
+ value: url,
5982
+ onChange: (_, next) => setUrl(String(next ?? "")),
5983
+ placeholder: "https://streamelements.com/overlay/<id>/<token>",
5984
+ disabled: busy
5985
+ }
5986
+ ),
5987
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-help", children: [
5988
+ /* @__PURE__ */ jsx13("div", { className: "se-import-help__icon", children: "i" }),
5989
+ /* @__PURE__ */ jsxs7("div", { children: [
5990
+ /* @__PURE__ */ jsx13("div", { className: "se-import-help__title", children: "How to find the overlay URL" }),
5991
+ /* @__PURE__ */ jsxs7("ol", { children: [
5992
+ /* @__PURE__ */ jsxs7("li", { children: [
5993
+ "Open",
5994
+ " ",
5995
+ /* @__PURE__ */ jsx13(
5996
+ "a",
5997
+ {
5998
+ href: "https://streamelements.com/dashboard/overlays",
5999
+ target: "_blank",
6000
+ rel: "noreferrer",
6001
+ children: "streamelements.com/dashboard/overlays"
6002
+ }
6003
+ ),
6004
+ "."
6005
+ ] }),
6006
+ /* @__PURE__ */ jsx13("li", { children: "Click the overlay you want to bring over." }),
6007
+ /* @__PURE__ */ jsx13("li", { children: "Click Copy URL in the top right of the editor." }),
6008
+ /* @__PURE__ */ jsx13("li", { children: "Paste it above." })
6009
+ ] })
4813
6010
  ] })
4814
6011
  ] }),
4815
- remainingCount > 1 && /* @__PURE__ */ jsxs7("div", { className: "ui-flex-row ui-gap-2", style: { justifyContent: "flex-end", alignItems: "center", fontSize: 12 }, children: [
4816
- /* @__PURE__ */ jsxs7("span", { style: { opacity: 0.7 }, children: [
6012
+ error && /* @__PURE__ */ jsx13(ErrorPanel, { message: error.message, hint: error.hint })
6013
+ ] });
6014
+ }
6015
+ function StepDiscovery({
6016
+ result,
6017
+ assetCount
6018
+ }) {
6019
+ const widgets = result.coverage.totalWidgets;
6020
+ const reviewCount = result.reviewItems.length;
6021
+ const customCount = result.coverage.mappings.find(
6022
+ (item) => item.seType === "se-widget-custom-event-list"
6023
+ )?.count ?? 0;
6024
+ const alertCount = result.coverage.mappings.filter((item) => item.lumiaType === "alert").reduce((sum, item) => sum + item.count, 0);
6025
+ return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--wide"), children: [
6026
+ /* @__PURE__ */ jsx13(StepHeader, { number: 2, title: "Here's what we found in your overlay" }),
6027
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-grid se-import-grid--four", children: [
6028
+ /* @__PURE__ */ jsx13(StatusCard, { label: "Total widgets", value: widgets }),
6029
+ /* @__PURE__ */ jsx13(StatusCard, { label: "Alerts", value: alertCount }),
6030
+ /* @__PURE__ */ jsx13(
6031
+ StatusCard,
6032
+ {
6033
+ label: "Custom widgets",
6034
+ value: customCount,
6035
+ sub: customCount > 0 ? "uses the SE compatibility shim" : void 0
6036
+ }
6037
+ ),
6038
+ /* @__PURE__ */ jsx13(
6039
+ StatusCard,
6040
+ {
6041
+ label: "SE-hosted assets",
6042
+ value: assetCount,
6043
+ sub: assetCount > 0 ? "can be mirrored to Lumia storage" : "no migration needed"
6044
+ }
6045
+ )
6046
+ ] }),
6047
+ /* @__PURE__ */ jsx13(CoverageCards, { coverage: result.coverage }),
6048
+ /* @__PURE__ */ jsx13("div", { className: "se-import-grid se-import-grid--two", children: /* @__PURE__ */ jsx13(
6049
+ StatusCard,
6050
+ {
6051
+ label: "Need review",
6052
+ value: reviewCount,
6053
+ sub: reviewCount > 0 ? "manual choice needed" : "everything looks good",
6054
+ tone: reviewCount > 0 ? "warn" : "success"
6055
+ }
6056
+ ) })
6057
+ ] });
6058
+ }
6059
+ function StepOptions({
6060
+ options,
6061
+ setOptions,
6062
+ assetCount
6063
+ }) {
6064
+ return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--medium"), children: [
6065
+ /* @__PURE__ */ jsx13(StepHeader, { number: 3, title: "Options", subtitle: "Choose how to handle your assets" }),
6066
+ /* @__PURE__ */ jsxs7(
6067
+ "label",
6068
+ {
6069
+ className: `se-import-option ${assetCount === 0 ? "se-import-option--disabled" : ""}`,
6070
+ children: [
6071
+ /* @__PURE__ */ jsx13("span", { className: "se-import-option__icon", children: "\u21A5" }),
6072
+ /* @__PURE__ */ jsxs7("span", { className: "se-import-option__body", children: [
6073
+ /* @__PURE__ */ jsx13("span", { className: "se-import-option__title", children: "Mirror StreamElements assets to Lumia storage" }),
6074
+ /* @__PURE__ */ jsx13("span", { className: "se-import-option__copy", children: "Downloads every cdn.streamelements.com URL referenced in the overlay, uploads it to your Lumia asset library, then rewrites the URLs." })
6075
+ ] }),
6076
+ /* @__PURE__ */ jsxs7("span", { className: "se-import-option__count", children: [
6077
+ assetCount,
6078
+ " files"
6079
+ ] }),
6080
+ /* @__PURE__ */ jsx13(
6081
+ "input",
6082
+ {
6083
+ type: "checkbox",
6084
+ checked: options.mirrorAssets,
6085
+ disabled: assetCount === 0,
6086
+ onChange: (e) => setOptions({ ...options, mirrorAssets: e.target.checked })
6087
+ }
6088
+ ),
6089
+ /* @__PURE__ */ jsx13("span", { className: "se-import-toggle" })
6090
+ ]
6091
+ }
6092
+ ),
6093
+ /* @__PURE__ */ jsx13(
6094
+ LSInput,
6095
+ {
6096
+ label: "Overlay name",
6097
+ value: options.name,
6098
+ onChange: (e) => setOptions({ ...options, name: e.target.value })
6099
+ }
6100
+ )
6101
+ ] });
6102
+ }
6103
+ function mirrorLabel(state) {
6104
+ if (state === "fetching") return "Fetching";
6105
+ if (state === "done") return "Downloaded";
6106
+ if (state === "reused") return "Reused";
6107
+ if (state === "kept-on-cdn") return "Kept on CDN";
6108
+ if (state === "failed") return "Failed";
6109
+ return "Queued";
6110
+ }
6111
+ function fileTypeIcon(url) {
6112
+ const ext = filenameFromURL(url).split(".").pop()?.toLowerCase();
6113
+ if (ext === "mp3" || ext === "wav" || ext === "ogg") return sound_default;
6114
+ if (ext === "webm" || ext === "mp4" || ext === "mov") return video_default;
6115
+ return img_default;
6116
+ }
6117
+ function AssetTransferAnimation() {
6118
+ return /* @__PURE__ */ jsxs7("div", { className: "se-import-transfer", "aria-hidden": "true", children: [
6119
+ /* @__PURE__ */ jsx13("div", { className: "se-import-transfer__source se-import-transfer__source--se", children: /* @__PURE__ */ jsx13("img", { src: source_se_default, alt: "" }) }),
6120
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-transfer__files", children: [
6121
+ /* @__PURE__ */ jsx13(
6122
+ "img",
6123
+ {
6124
+ className: "se-import-transfer__file se-import-transfer__file--one",
6125
+ src: img_file_default,
6126
+ alt: ""
6127
+ }
6128
+ ),
6129
+ /* @__PURE__ */ jsx13(
6130
+ "img",
6131
+ {
6132
+ className: "se-import-transfer__file se-import-transfer__file--two",
6133
+ src: audio_file_default,
6134
+ alt: ""
6135
+ }
6136
+ ),
6137
+ /* @__PURE__ */ jsx13(
6138
+ "img",
6139
+ {
6140
+ className: "se-import-transfer__file se-import-transfer__file--three",
6141
+ src: img_file_default,
6142
+ alt: ""
6143
+ }
6144
+ ),
6145
+ /* @__PURE__ */ jsx13(
6146
+ "img",
6147
+ {
6148
+ className: "se-import-transfer__file se-import-transfer__file--four",
6149
+ src: video_file_default,
6150
+ alt: ""
6151
+ }
6152
+ )
6153
+ ] }),
6154
+ /* @__PURE__ */ jsx13("div", { className: "se-import-transfer__source se-import-transfer__source--lumia", children: /* @__PURE__ */ jsx13("img", { src: source_lumia_default, alt: "" }) })
6155
+ ] });
6156
+ }
6157
+ function StepMirror({
6158
+ rows,
6159
+ running,
6160
+ onRetry
6161
+ }) {
6162
+ const done = rows.filter((row) => row.state === "done").length;
6163
+ const reused = rows.filter((row) => row.state === "reused").length;
6164
+ const keptOnCdn = rows.filter((row) => row.state === "kept-on-cdn").length;
6165
+ const failed = rows.filter((row) => row.state === "failed").length;
6166
+ const completed = done + reused + keptOnCdn + failed;
6167
+ const summary = running ? `Downloading... (${done + reused + keptOnCdn}/${rows.length})` : `Finished. ${done} mirrored${reused ? `, ${reused} reused` : ""}${keptOnCdn ? `, ${keptOnCdn} kept on CDN` : ""}${failed ? `, ${failed} failed` : ""}.`;
6168
+ return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--wide"), children: [
6169
+ /* @__PURE__ */ jsx13(StepHeader, { number: 4, title: "Mirroring assets", subtitle: "We're downloading your assets from StreamElements and preparing them for import." }),
6170
+ /* @__PURE__ */ jsx13(AssetTransferAnimation, {}),
6171
+ /* @__PURE__ */ jsx13("div", { className: "se-import-mirror-head", children: /* @__PURE__ */ jsx13("span", { children: summary }) }),
6172
+ /* @__PURE__ */ jsx13(ProgressBar, { value: completed, max: rows.length }),
6173
+ /* @__PURE__ */ jsx13("div", { className: "se-import-asset-list", children: rows.map((row, index) => /* @__PURE__ */ jsxs7(
6174
+ "div",
6175
+ {
6176
+ className: `se-import-asset-row se-import-asset-row--${row.state}`,
6177
+ children: [
6178
+ /* @__PURE__ */ jsx13(
6179
+ "img",
6180
+ {
6181
+ className: "se-import-asset-row__icon",
6182
+ src: fileTypeIcon(row.url),
6183
+ alt: ""
6184
+ }
6185
+ ),
6186
+ /* @__PURE__ */ jsx13("span", { className: "se-import-asset-row__url", title: row.url, children: row.url }),
6187
+ /* @__PURE__ */ jsx13("span", { className: "se-import-asset-row__state", children: mirrorLabel(row.state) }),
6188
+ row.state === "failed" && /* @__PURE__ */ jsx13(
6189
+ LSButton,
6190
+ {
6191
+ type: "button",
6192
+ variant: "text",
6193
+ color: "secondary",
6194
+ size: "small",
6195
+ className: "se-import-link-button",
6196
+ onClick: () => onRetry(index),
6197
+ label: "Retry"
6198
+ }
6199
+ ),
6200
+ row.error && /* @__PURE__ */ jsx13("span", { className: "se-import-asset-row__error", children: row.error })
6201
+ ]
6202
+ },
6203
+ row.url
6204
+ )) })
6205
+ ] });
6206
+ }
6207
+ function StepReview({
6208
+ item,
6209
+ index,
6210
+ total,
6211
+ state,
6212
+ error,
6213
+ generated,
6214
+ canvas,
6215
+ onSkip,
6216
+ onKeep,
6217
+ onGenerate,
6218
+ onAccept,
6219
+ onRetry,
6220
+ onPickMarketplace,
6221
+ onKeepAllPlaceholders,
6222
+ onSkipAll,
6223
+ remainingCount,
6224
+ marketplaceAvailable
6225
+ }) {
6226
+ return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--medium"), children: [
6227
+ /* @__PURE__ */ jsx13(
6228
+ StepHeader,
6229
+ {
6230
+ number: 4,
6231
+ title: `Review widget ${index + 1} of ${total}`,
6232
+ subtitle: "Choose how to handle this unsupported StreamElements widget"
6233
+ }
6234
+ ),
6235
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-review-card", children: [
6236
+ /* @__PURE__ */ jsxs7("div", { children: [
6237
+ /* @__PURE__ */ jsx13("div", { className: "se-import-review-card__type", children: item.seWidget.type }),
6238
+ /* @__PURE__ */ jsx13("p", { children: item.reason })
6239
+ ] }),
6240
+ /* @__PURE__ */ jsx13("div", { className: "se-import-review-card__status", children: item.flaggedOff ? "Auto-import disabled" : item.status })
6241
+ ] }),
6242
+ remainingCount > 1 && /* Bulk-action row — shortcut to wrap up the review step when there are still
6243
+ several unactioned widgets and the user doesn't want to walk each one. */
6244
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-review-bulk", children: [
6245
+ /* @__PURE__ */ jsxs7("span", { children: [
4817
6246
  remainingCount,
4818
6247
  " remaining"
4819
6248
  ] }),
4820
- /* @__PURE__ */ jsx13("button", { type: "button", className: "ui-button ui-button-secondary", style: { padding: "4px 10px", fontSize: 12 }, onClick: onKeepAllPlaceholders, title: "Keep every remaining placeholder/template widget as-is (skips flagged-off widgets that should go to marketplace/AI)", children: "Keep all placeholders" }),
4821
- /* @__PURE__ */ jsx13("button", { type: "button", className: "ui-button ui-button-secondary", style: { padding: "4px 10px", fontSize: 12 }, onClick: onSkipAll, title: "Drop every remaining widget entirely", children: "Skip all" })
4822
- ] }),
4823
- /* @__PURE__ */ jsxs7("div", { style: { padding: 12, borderRadius: 6, background: "var(--ui-bg-elevated, #1d1f24)" }, children: [
4824
- /* @__PURE__ */ jsx13("div", { style: { fontFamily: "ui-monospace, monospace", fontWeight: 600, fontSize: 13 }, children: item.seWidget.type }),
4825
- /* @__PURE__ */ jsx13("div", { style: { fontSize: 13, opacity: 0.85, marginTop: 4 }, children: item.reason })
6249
+ /* @__PURE__ */ jsx13(
6250
+ LSButton,
6251
+ {
6252
+ type: "button",
6253
+ color: "secondary",
6254
+ variant: "contained",
6255
+ onClick: onKeepAllPlaceholders,
6256
+ label: "Keep all placeholders"
6257
+ }
6258
+ ),
6259
+ /* @__PURE__ */ jsx13(
6260
+ LSButton,
6261
+ {
6262
+ type: "button",
6263
+ color: "secondary",
6264
+ variant: "contained",
6265
+ onClick: onSkipAll,
6266
+ label: "Skip all"
6267
+ }
6268
+ )
4826
6269
  ] }),
4827
- state === "generating" && /* @__PURE__ */ jsx13("div", { style: { padding: 12, fontSize: 13, opacity: 0.85 }, children: "Generating with AI\u2026 this can take 30\u201390 seconds." }),
4828
- state === "generated" && generated && /* @__PURE__ */ jsxs7(Fragment3, { children: [
4829
- /* @__PURE__ */ jsx13("div", { style: { fontSize: 13, fontWeight: 600, marginTop: 4 }, children: "Preview" }),
6270
+ state === "generating" && /* @__PURE__ */ jsx13("div", { className: "se-import-muted", children: "Generating with AI. This can take 30-90 seconds." }),
6271
+ state === "generated" && generated && /* @__PURE__ */ jsxs7("div", { className: "se-import-generated", children: [
6272
+ /* @__PURE__ */ jsx13("div", { className: "se-import-generated__title", children: "Preview" }),
4830
6273
  (() => {
4831
6274
  const landing = getAILandingBounds(item.seWidget, canvas);
4832
- return /* @__PURE__ */ jsx13(CustomOverlayPreview, { html: generated.html ?? "", css: generated.css ?? "", js: generated.js ?? "", data: generated.data ?? {}, width: landing.width, height: landing.height });
6275
+ return /* @__PURE__ */ jsx13(
6276
+ CustomOverlayPreview,
6277
+ {
6278
+ html: generated.html ?? "",
6279
+ css: generated.css ?? "",
6280
+ js: generated.js ?? "",
6281
+ data: generated.data ?? {},
6282
+ width: landing.width,
6283
+ height: landing.height
6284
+ }
6285
+ );
4833
6286
  })(),
4834
- /* @__PURE__ */ jsxs7("details", { children: [
4835
- /* @__PURE__ */ jsx13("summary", { style: { cursor: "pointer", fontSize: 12, opacity: 0.8 }, children: "Generated code" }),
4836
- /* @__PURE__ */ jsxs7("details", { style: { marginLeft: 12, marginTop: 8 }, children: [
4837
- /* @__PURE__ */ jsx13("summary", { style: { cursor: "pointer", fontSize: 12 }, children: "HTML" }),
4838
- /* @__PURE__ */ jsx13("pre", { style: { fontSize: 11, padding: 8, background: "rgba(0,0,0,0.25)", borderRadius: 4, maxHeight: 160, overflow: "auto" }, children: generated.html || "" })
4839
- ] }),
4840
- /* @__PURE__ */ jsxs7("details", { style: { marginLeft: 12 }, children: [
4841
- /* @__PURE__ */ jsx13("summary", { style: { cursor: "pointer", fontSize: 12 }, children: "CSS" }),
4842
- /* @__PURE__ */ jsx13("pre", { style: { fontSize: 11, padding: 8, background: "rgba(0,0,0,0.25)", borderRadius: 4, maxHeight: 160, overflow: "auto" }, children: generated.css || "" })
4843
- ] }),
4844
- /* @__PURE__ */ jsxs7("details", { style: { marginLeft: 12 }, children: [
4845
- /* @__PURE__ */ jsx13("summary", { style: { cursor: "pointer", fontSize: 12 }, children: "JS" }),
4846
- /* @__PURE__ */ jsx13("pre", { style: { fontSize: 11, padding: 8, background: "rgba(0,0,0,0.25)", borderRadius: 4, maxHeight: 160, overflow: "auto" }, children: generated.js || "" })
4847
- ] })
6287
+ /* @__PURE__ */ jsxs7("details", { className: "se-import-code", children: [
6288
+ /* @__PURE__ */ jsx13("summary", { children: "Generated code" }),
6289
+ /* @__PURE__ */ jsx13("pre", { children: JSON.stringify(
6290
+ {
6291
+ html: generated.html ?? "",
6292
+ css: generated.css ?? "",
6293
+ js: generated.js ?? ""
6294
+ },
6295
+ null,
6296
+ 2
6297
+ ) })
4848
6298
  ] })
4849
6299
  ] }),
4850
6300
  error && /* @__PURE__ */ jsx13(ErrorPanel, { message: error }),
4851
- /* @__PURE__ */ jsxs7("details", { children: [
4852
- /* @__PURE__ */ jsx13("summary", { style: { cursor: "pointer", fontSize: 12, opacity: 0.8 }, children: "Original StreamElements config" }),
4853
- /* @__PURE__ */ jsx13("pre", { style: { marginTop: 6, fontSize: 11, padding: 8, background: "rgba(0,0,0,0.25)", borderRadius: 4, maxHeight: 180, overflow: "auto" }, children: JSON.stringify(item.seWidget.variables ?? {}, null, 2) })
6301
+ /* @__PURE__ */ jsxs7("details", { className: "se-import-code", children: [
6302
+ /* @__PURE__ */ jsx13("summary", { children: "Original StreamElements config" }),
6303
+ /* @__PURE__ */ jsx13("pre", { children: JSON.stringify(item.seWidget.variables ?? {}, null, 2) })
4854
6304
  ] }),
4855
- /* @__PURE__ */ jsx13("div", { className: "ui-flex-row ui-gap-2", style: { marginTop: 8 }, children: state === "generated" ? /* @__PURE__ */ jsxs7(Fragment3, { children: [
4856
- /* @__PURE__ */ jsx13("button", { type: "button", className: "ui-button", onClick: onRetry, children: "Try again" }),
4857
- /* @__PURE__ */ jsx13("button", { type: "button", className: "ui-button", onClick: onSkip, children: "Skip this widget" }),
4858
- /* @__PURE__ */ jsx13("button", { type: "button", className: "ui-button ui-button-purple", onClick: onAccept, children: "Use this and continue" })
4859
- ] }) : /* @__PURE__ */ jsxs7(Fragment3, { children: [
4860
- !item.flaggedOff && /* @__PURE__ */ jsx13("button", { type: "button", className: "ui-button", onClick: onKeep, disabled: state === "generating", children: "Keep placeholder" }),
4861
- /* @__PURE__ */ jsx13("button", { type: "button", className: "ui-button", onClick: onSkip, disabled: state === "generating", children: "Skip" }),
4862
- marketplaceAvailable && /* @__PURE__ */ jsx13("button", { type: "button", className: "ui-button", onClick: onPickMarketplace, disabled: state === "generating", children: "Pick from Marketplace" }),
4863
- /* @__PURE__ */ jsx13("button", { type: "button", className: "ui-button ui-button-purple", onClick: onGenerate, disabled: state === "generating", children: state === "generating" ? "Generating\u2026" : "Generate with AI" })
6305
+ /* @__PURE__ */ jsx13("div", { className: "se-import-actions se-import-actions--inline", children: state === "generated" ? /* @__PURE__ */ jsxs7(Fragment4, { children: [
6306
+ /* @__PURE__ */ jsx13(
6307
+ LSButton,
6308
+ {
6309
+ type: "button",
6310
+ color: "secondary",
6311
+ onClick: onRetry,
6312
+ label: "Try again"
6313
+ }
6314
+ ),
6315
+ /* @__PURE__ */ jsx13(
6316
+ LSButton,
6317
+ {
6318
+ type: "button",
6319
+ color: "secondary",
6320
+ onClick: onSkip,
6321
+ label: "Skip this widget"
6322
+ }
6323
+ ),
6324
+ /* @__PURE__ */ jsx13(
6325
+ LSButton,
6326
+ {
6327
+ type: "button",
6328
+ color: "primary",
6329
+ onClick: onAccept,
6330
+ label: "Use this and continue"
6331
+ }
6332
+ )
6333
+ ] }) : /* @__PURE__ */ jsxs7(Fragment4, { children: [
6334
+ !item.flaggedOff && /* @__PURE__ */ jsx13(
6335
+ LSButton,
6336
+ {
6337
+ type: "button",
6338
+ color: "secondary",
6339
+ onClick: onKeep,
6340
+ disabled: state === "generating",
6341
+ label: "Keep placeholder"
6342
+ }
6343
+ ),
6344
+ /* @__PURE__ */ jsx13(
6345
+ LSButton,
6346
+ {
6347
+ type: "button",
6348
+ color: "secondary",
6349
+ onClick: onSkip,
6350
+ disabled: state === "generating",
6351
+ label: "Skip"
6352
+ }
6353
+ ),
6354
+ marketplaceAvailable && /* @__PURE__ */ jsx13(
6355
+ LSButton,
6356
+ {
6357
+ type: "button",
6358
+ color: "secondary",
6359
+ onClick: onPickMarketplace,
6360
+ disabled: state === "generating",
6361
+ label: "Pick from Marketplace"
6362
+ }
6363
+ ),
6364
+ /* @__PURE__ */ jsx13(
6365
+ LSButton,
6366
+ {
6367
+ type: "button",
6368
+ color: "primary",
6369
+ onClick: onGenerate,
6370
+ disabled: state === "generating",
6371
+ label: state === "generating" ? "Generating..." : "Generate with AI"
6372
+ }
6373
+ )
4864
6374
  ] }) }),
4865
- item.flaggedOff && !marketplaceAvailable && state !== "generated" && /* @__PURE__ */ jsx13("div", { style: { fontSize: 12, opacity: 0.75 }, children: "No curated Marketplace replacement is configured for this widget yet." })
6375
+ item.flaggedOff && !marketplaceAvailable && state !== "generated" && /* @__PURE__ */ jsx13("div", { className: "se-import-muted", children: "No curated Marketplace replacement is configured for this widget yet." })
4866
6376
  ] });
4867
6377
  }
4868
- function StepConfirm({ result, options, mirrorRows, rowStates }) {
4869
- const total = result.overlay.settings.layers.length;
4870
- const mirrored = mirrorRows.filter((r) => r.state === "done" || r.state === "reused" || r.state === "kept-on-cdn").length;
4871
- const mirrorFailed = mirrorRows.filter((r) => r.state === "failed").length;
6378
+ function StepConfirm({
6379
+ result,
6380
+ options,
6381
+ mirrorRows,
6382
+ rowStates
6383
+ }) {
6384
+ const widgets = result.coverage.totalWidgets;
6385
+ const alertCount = result.coverage.mappings.filter((item) => item.lumiaType === "alert").reduce((sum, item) => sum + item.count, 0);
6386
+ const mirrored = mirrorRows.filter(
6387
+ (row) => row.state === "done" || row.state === "reused" || row.state === "kept-on-cdn"
6388
+ ).length;
4872
6389
  const reviewBuckets = Object.values(rowStates).reduce(
4873
- (acc, s) => {
4874
- acc[s.state] = (acc[s.state] ?? 0) + 1;
6390
+ (acc, item) => {
6391
+ acc[item.state] = (acc[item.state] ?? 0) + 1;
4875
6392
  return acc;
4876
6393
  },
4877
6394
  {}
4878
6395
  );
4879
- return /* @__PURE__ */ jsxs7("div", { className: "ui-flex-column ui-gap-2", children: [
4880
- /* @__PURE__ */ jsx13("div", { style: { fontWeight: 600, fontSize: 16 }, children: "Ready to import" }),
4881
- /* @__PURE__ */ jsxs7("div", { className: "ui-flex-column ui-gap-2", style: { padding: 12, borderRadius: 6, background: "var(--ui-bg-elevated, #1d1f24)" }, children: [
4882
- /* @__PURE__ */ jsx13(Row, { label: "Overlay name", value: options.name || result.overlay.name }),
4883
- /* @__PURE__ */ jsx13(Row, { label: "Layers", value: `${total}` }),
4884
- /* @__PURE__ */ jsx13(Row, { label: "Assets mirrored", value: options.mirrorAssets ? `${mirrored} / ${mirrorRows.length}${mirrorFailed ? ` (${mirrorFailed} kept on SE CDN)` : ""}` : "skipped" }),
4885
- /* @__PURE__ */ jsx13(Row, { label: "Generated with AI", value: `${reviewBuckets.generated ?? 0}` }),
4886
- /* @__PURE__ */ jsx13(Row, { label: "Skipped", value: `${reviewBuckets.skipped ?? 0}` }),
4887
- /* @__PURE__ */ jsx13(Row, { label: "Kept as placeholder", value: `${reviewBuckets.kept ?? 0}` })
6396
+ return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--confirm"), children: [
6397
+ /* @__PURE__ */ jsx13(
6398
+ StepHeader,
6399
+ {
6400
+ number: 5,
6401
+ title: "Confirm",
6402
+ subtitle: "Review and confirm your import"
6403
+ }
6404
+ ),
6405
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-overview", children: [
6406
+ /* @__PURE__ */ jsx13("h3", { children: "Overview" }),
6407
+ /* @__PURE__ */ jsx13(Row, { label: "Overlay", value: options.name || result.overlay.name }),
6408
+ /* @__PURE__ */ jsx13(Row, { label: "Widgets", value: `${widgets}` }),
6409
+ /* @__PURE__ */ jsx13(Row, { label: "Alerts", value: `${alertCount}` }),
6410
+ /* @__PURE__ */ jsx13(
6411
+ Row,
6412
+ {
6413
+ label: "Assets to import",
6414
+ value: `${mirrorRows.length}`,
6415
+ meta: options.mirrorAssets ? `Mirrored to Lumia storage (${mirrored})` : "Skipped"
6416
+ }
6417
+ ),
6418
+ /* @__PURE__ */ jsx13(
6419
+ Row,
6420
+ {
6421
+ label: "Import behavior",
6422
+ value: options.mirrorAssets ? "Mirror assets and rewrite URLs" : "Keep original asset URLs"
6423
+ }
6424
+ ),
6425
+ reviewBuckets.generated ? /* @__PURE__ */ jsx13(Row, { label: "Generated with AI", value: `${reviewBuckets.generated}` }) : null,
6426
+ reviewBuckets.skipped ? /* @__PURE__ */ jsx13(Row, { label: "Skipped", value: `${reviewBuckets.skipped}` }) : null,
6427
+ reviewBuckets.kept ? /* @__PURE__ */ jsx13(Row, { label: "Kept as placeholder", value: `${reviewBuckets.kept}` }) : null
4888
6428
  ] })
4889
6429
  ] });
4890
6430
  }
4891
- function Row({ label, value }) {
4892
- return /* @__PURE__ */ jsxs7("div", { className: "ui-flex-row", style: { justifyContent: "space-between" }, children: [
4893
- /* @__PURE__ */ jsx13("span", { style: { opacity: 0.75 }, children: label }),
4894
- /* @__PURE__ */ jsx13("span", { style: { fontWeight: 600 }, children: value })
6431
+ function Row({
6432
+ label,
6433
+ value,
6434
+ meta
6435
+ }) {
6436
+ return /* @__PURE__ */ jsxs7("div", { className: "se-import-row", children: [
6437
+ /* @__PURE__ */ jsx13("span", { children: label }),
6438
+ /* @__PURE__ */ jsx13("strong", { children: value }),
6439
+ meta && /* @__PURE__ */ jsx13("em", { children: meta })
4895
6440
  ] });
4896
6441
  }
4897
- function SEImportWizard({ bindings }) {
6442
+ function SEImportWizard({
6443
+ bindings,
6444
+ initialUrl = ""
6445
+ }) {
4898
6446
  const {
4899
6447
  fetchMarketplaceOverlay,
4900
6448
  generateAICustomCode,
@@ -4908,25 +6456,167 @@ function SEImportWizard({ bindings }) {
4908
6456
  t
4909
6457
  } = bindings;
4910
6458
  const translate = (key, fallback) => t ? t(key) ?? fallback : fallback;
4911
- const [step, setStep] = useState6("url");
4912
- const [url, setUrl] = useState6("");
6459
+ const [step, setStep] = useState6(initialUrl ? "url" : "mode");
6460
+ const [entryMode, setEntryMode] = useState6(
6461
+ initialUrl ? "url" : null
6462
+ );
6463
+ const [jwt, setJwt] = useState6("");
6464
+ const [seClient, setSeClient] = useState6(null);
6465
+ const [channels, setChannels] = useState6(null);
6466
+ const [selectedChannelId, setSelectedChannelId] = useState6(null);
6467
+ const [connectError, setConnectError] = useState6(null);
6468
+ const [connecting, setConnecting] = useState6(false);
6469
+ const [overlayList, setOverlayList] = useState6(null);
6470
+ const [overlaysLoading, setOverlaysLoading] = useState6(false);
6471
+ const [overlaysError, setOverlaysError] = useState6(null);
6472
+ const [selectedOverlayIds, setSelectedOverlayIds] = useState6(/* @__PURE__ */ new Set());
6473
+ const [overlayQueue, setOverlayQueue] = useState6([]);
6474
+ const [currentOverlayIndex, setCurrentOverlayIndex] = useState6(0);
6475
+ const [totalOverlaysToImport, setTotalOverlaysToImport] = useState6(0);
6476
+ const [url, setUrl] = useState6(initialUrl);
4913
6477
  const [loadError, setLoadError] = useState6(null);
4914
6478
  const [loading, setLoading] = useState6(false);
4915
6479
  const [result, setResult] = useState6(null);
4916
- const [originalReviewItems, setOriginalReviewItems] = useState6([]);
4917
- const [options, setOptions] = useState6({ mirrorAssets: true, name: "" });
4918
- const assetUrls = useMemo5(() => result ? findSEAssetURLs(result.overlay) : [], [result]);
6480
+ const [originalReviewItems, setOriginalReviewItems] = useState6(
6481
+ []
6482
+ );
6483
+ const [options, setOptions] = useState6({
6484
+ mirrorAssets: true,
6485
+ name: ""
6486
+ });
4919
6487
  const [mirrorRows, setMirrorRows] = useState6([]);
4920
6488
  const [mirrorRunning, setMirrorRunning] = useState6(false);
4921
- const mirrorStartedRef = useRef4(false);
4922
6489
  const [rowStates, setRowStates] = useState6({});
4923
6490
  const [reviewIndex, setReviewIndex] = useState6(0);
4924
6491
  const [importing, setImporting] = useState6(false);
6492
+ const [marketplacePickerOpen, setMarketplacePickerOpen] = useState6(false);
6493
+ const mirrorStartedRef = useRef4(false);
6494
+ const mirrorAbortRef = useRef4(null);
6495
+ const filenameCacheRef = useRef4(/* @__PURE__ */ new Map());
6496
+ const assetUrls = useMemo5(
6497
+ () => result ? findSEAssetURLs(result.overlay) : [],
6498
+ [result]
6499
+ );
6500
+ const currentItem = originalReviewItems[reviewIndex];
6501
+ const currentRow = currentItem ? rowStates[currentItem.moduleId] ?? { state: "pending" } : void 0;
6502
+ const handleConnect = useCallback3(async () => {
6503
+ setConnectError(null);
6504
+ let client;
6505
+ try {
6506
+ client = SEClient.fromJwt(jwt);
6507
+ } catch (e) {
6508
+ if (e instanceof SEAuthError) {
6509
+ const hint = e.code === "expired" ? "Generate a new JWT on streamelements.com/dashboard/account/channels." : void 0;
6510
+ setConnectError({ message: e.message, hint });
6511
+ } else {
6512
+ setConnectError({
6513
+ message: e instanceof Error ? e.message : "Could not read that token."
6514
+ });
6515
+ }
6516
+ return;
6517
+ }
6518
+ setConnecting(true);
6519
+ try {
6520
+ const usable = await fetchUsableChannels(client);
6521
+ const preferred = usable.find((c) => c._id === client.channelId) ?? usable[0];
6522
+ if (!preferred) {
6523
+ setConnectError({
6524
+ message: "No authorized StreamElements channels found on this account."
6525
+ });
6526
+ return;
6527
+ }
6528
+ setSeClient(client);
6529
+ setChannels(usable);
6530
+ setSelectedChannelId(preferred._id);
6531
+ } catch (e) {
6532
+ if (e instanceof SEAuthError) {
6533
+ const hint = e.code === "unauthorized" ? "Generate a new JWT on streamelements.com/dashboard/account/channels." : void 0;
6534
+ setConnectError({ message: e.message, hint });
6535
+ bindings.onAuthError?.(e);
6536
+ } else {
6537
+ setConnectError({
6538
+ message: e instanceof Error ? e.message : "Could not reach StreamElements."
6539
+ });
6540
+ }
6541
+ } finally {
6542
+ setConnecting(false);
6543
+ }
6544
+ }, [bindings, jwt]);
6545
+ const loadOverlayList = useCallback3(async (client) => {
6546
+ setOverlaysError(null);
6547
+ setOverlaysLoading(true);
6548
+ try {
6549
+ const list = await fetchSEOverlays(client);
6550
+ setOverlayList(list);
6551
+ } catch (e) {
6552
+ if (e instanceof SEAuthError) {
6553
+ setOverlaysError({ message: e.message, hint: e.code === "unauthorized" ? "Generate a new JWT on streamelements.com/dashboard/account/channels." : void 0 });
6554
+ bindings.onAuthError?.(e);
6555
+ } else {
6556
+ setOverlaysError({ message: e instanceof Error ? e.message : "Could not load your StreamElements overlays." });
6557
+ }
6558
+ } finally {
6559
+ setOverlaysLoading(false);
6560
+ }
6561
+ }, [bindings]);
6562
+ const toggleOverlaySelection = useCallback3((id) => {
6563
+ setSelectedOverlayIds((prev) => {
6564
+ const next = new Set(prev);
6565
+ if (next.has(id)) next.delete(id);
6566
+ else next.add(id);
6567
+ return next;
6568
+ });
6569
+ }, []);
6570
+ const selectAllOverlays = useCallback3(() => {
6571
+ setSelectedOverlayIds(new Set((overlayList ?? []).map((o) => o._id)));
6572
+ }, [overlayList]);
6573
+ const clearAllOverlays = useCallback3(() => {
6574
+ setSelectedOverlayIds(/* @__PURE__ */ new Set());
6575
+ }, []);
6576
+ const loadBootstrapAndKickoff = useCallback3(async (client, overlayId) => {
6577
+ setLoadError(null);
6578
+ setLoading(true);
6579
+ try {
6580
+ const bootstrap = await fetchSEBootstrapForOverlay(client, overlayId);
6581
+ if (!isSEBootstrap(bootstrap)) {
6582
+ throw new Error("StreamElements returned a payload that doesn't look like an overlay bootstrap.");
6583
+ }
6584
+ const imported = importSEBootstrap(bootstrap);
6585
+ setResult(imported);
6586
+ setOptions((o) => ({ ...o, name: imported.overlay.name }));
6587
+ setOriginalReviewItems(imported.reviewItems);
6588
+ setRowStates(Object.fromEntries(imported.reviewItems.map((r) => [r.moduleId, { state: "pending" }])));
6589
+ setReviewIndex(0);
6590
+ setMirrorRows([]);
6591
+ mirrorStartedRef.current = false;
6592
+ setStep("discovery");
6593
+ } catch (e) {
6594
+ setLoadError({
6595
+ message: "Could not load the overlay.",
6596
+ hint: e?.message ?? String(e)
6597
+ });
6598
+ } finally {
6599
+ setLoading(false);
6600
+ }
6601
+ }, []);
6602
+ const handlePickConfirm = useCallback3(async () => {
6603
+ if (!seClient) return;
6604
+ const ids = Array.from(selectedOverlayIds);
6605
+ if (ids.length === 0) return;
6606
+ const [first, ...rest] = ids;
6607
+ setOverlayQueue(rest);
6608
+ setTotalOverlaysToImport(ids.length);
6609
+ setCurrentOverlayIndex(1);
6610
+ await loadBootstrapAndKickoff(seClient, first);
6611
+ }, [seClient, selectedOverlayIds, loadBootstrapAndKickoff]);
4925
6612
  const handleLoad = useCallback3(async () => {
4926
6613
  setLoadError(null);
4927
6614
  const parts = extractSEPreviewParts(url);
4928
6615
  if (!parts) {
4929
- setLoadError({ message: "That doesn't look like a StreamElements overlay URL.", hint: "Expected: https://streamelements.com/overlay/<overlayId>/<token>" });
6616
+ setLoadError({
6617
+ message: "That doesn't look like a StreamElements overlay URL.",
6618
+ hint: "Expected: https://streamelements.com/overlay/<overlayId>/<token>"
6619
+ });
4930
6620
  return;
4931
6621
  }
4932
6622
  setLoading(true);
@@ -4934,51 +6624,82 @@ function SEImportWizard({ bindings }) {
4934
6624
  const bootstrap = await fetchSEBootstrap(parts);
4935
6625
  const imported = importSEBootstrap(bootstrap);
4936
6626
  setResult(imported);
4937
- setOptions((o) => ({ ...o, name: imported.overlay.name }));
6627
+ setOptions((previous) => ({ ...previous, name: imported.overlay.name }));
4938
6628
  setOriginalReviewItems(imported.reviewItems);
4939
- setRowStates(Object.fromEntries(imported.reviewItems.map((r) => [r.moduleId, { state: "pending" }])));
6629
+ setRowStates(
6630
+ Object.fromEntries(
6631
+ imported.reviewItems.map((item) => [
6632
+ item.moduleId,
6633
+ { state: "pending" }
6634
+ ])
6635
+ )
6636
+ );
4940
6637
  setReviewIndex(0);
4941
6638
  setStep("discovery");
4942
- } catch (e) {
4943
- setLoadError({ message: "Could not load the overlay.", hint: e?.message });
6639
+ } catch (error) {
6640
+ setLoadError({
6641
+ message: "Could not load the overlay.",
6642
+ hint: error?.message
6643
+ });
4944
6644
  } finally {
4945
6645
  setLoading(false);
4946
6646
  }
4947
6647
  }, [url]);
4948
- const mirrorAbortRef = useRef4(null);
4949
6648
  const processAssetRow = useCallback3(
4950
6649
  async (assetUrl, idx, existingByFilename, signal) => {
4951
6650
  const filename = filenameFromURL(assetUrl);
4952
6651
  const reuseUrl = existingByFilename.get(filename);
4953
6652
  if (reuseUrl) {
4954
- setResult((curr) => curr ? { ...curr, overlay: rewriteAssetURLs(curr.overlay, { [assetUrl]: reuseUrl }) } : curr);
6653
+ setResult(
6654
+ (current) => current ? {
6655
+ ...current,
6656
+ overlay: rewriteAssetURLs(current.overlay, {
6657
+ [assetUrl]: reuseUrl
6658
+ })
6659
+ } : current
6660
+ );
4955
6661
  return { state: "reused", newUrl: reuseUrl };
4956
6662
  }
4957
- if (filename.toLowerCase().endsWith(".svg")) {
6663
+ if (filename.toLowerCase().endsWith(".svg"))
4958
6664
  return { state: "kept-on-cdn", newUrl: assetUrl };
4959
- }
4960
- setMirrorRows((prev) => prev.map((r, i) => i === idx ? { ...r, state: "fetching", error: void 0 } : r));
6665
+ setMirrorRows(
6666
+ (previous) => previous.map(
6667
+ (row, i) => i === idx ? { ...row, state: "fetching", error: void 0 } : row
6668
+ )
6669
+ );
4961
6670
  try {
4962
- const newUrl = await mirrorOneAsset(assetUrl, (file) => uploadAsset(file), signal);
6671
+ const newUrl = await mirrorOneAsset(
6672
+ assetUrl,
6673
+ (file) => uploadAsset(file),
6674
+ signal
6675
+ );
4963
6676
  existingByFilename.set(filename, newUrl);
4964
- setResult((curr) => curr ? { ...curr, overlay: rewriteAssetURLs(curr.overlay, { [assetUrl]: newUrl }) } : curr);
6677
+ setResult(
6678
+ (current) => current ? {
6679
+ ...current,
6680
+ overlay: rewriteAssetURLs(current.overlay, {
6681
+ [assetUrl]: newUrl
6682
+ })
6683
+ } : current
6684
+ );
4965
6685
  return { state: "done", newUrl };
4966
- } catch (err) {
4967
- if (err?.name === "AbortError" || signal?.aborted) {
6686
+ } catch (error) {
6687
+ if (error?.name === "AbortError" || signal?.aborted)
4968
6688
  return { state: "pending" };
4969
- }
4970
- return { state: "failed", error: extractErrorMessage(err) };
6689
+ return { state: "failed", error: extractErrorMessage(error) };
4971
6690
  }
4972
6691
  },
4973
6692
  [uploadAsset]
4974
6693
  );
4975
- const filenameCacheRef = useRef4(/* @__PURE__ */ new Map());
4976
6694
  const runMirror = useCallback3(async () => {
4977
6695
  if (!result) return;
4978
6696
  mirrorAbortRef.current = new AbortController();
4979
6697
  const signal = mirrorAbortRef.current.signal;
4980
6698
  setMirrorRunning(true);
4981
- const initialRows = assetUrls.map((u) => ({ url: u, state: "pending" }));
6699
+ const initialRows = assetUrls.map((assetUrl) => ({
6700
+ url: assetUrl,
6701
+ state: "pending"
6702
+ }));
4982
6703
  setMirrorRows(initialRows);
4983
6704
  const existingByFilename = /* @__PURE__ */ new Map();
4984
6705
  for (const asset of existingAssets ?? []) {
@@ -4987,17 +6708,17 @@ function SEImportWizard({ bindings }) {
4987
6708
  }
4988
6709
  filenameCacheRef.current = existingByFilename;
4989
6710
  for (let i = 0; i < initialRows.length; i += 1) {
4990
- if (signal.aborted) {
4991
- setMirrorRunning(false);
4992
- return;
4993
- }
4994
- const row = initialRows[i];
4995
- const outcome = await processAssetRow(row.url, i, existingByFilename, signal);
4996
- if (signal.aborted) {
4997
- setMirrorRunning(false);
4998
- return;
4999
- }
5000
- setMirrorRows((prev) => prev.map((r, idx) => idx === i ? { ...r, ...outcome } : r));
6711
+ if (signal.aborted) break;
6712
+ const outcome = await processAssetRow(
6713
+ initialRows[i].url,
6714
+ i,
6715
+ existingByFilename,
6716
+ signal
6717
+ );
6718
+ if (signal.aborted) break;
6719
+ setMirrorRows(
6720
+ (previous) => previous.map((row, idx) => idx === i ? { ...row, ...outcome } : row)
6721
+ );
5001
6722
  }
5002
6723
  setMirrorRunning(false);
5003
6724
  }, [assetUrls, existingAssets, processAssetRow, result]);
@@ -5005,8 +6726,15 @@ function SEImportWizard({ bindings }) {
5005
6726
  async (idx) => {
5006
6727
  const rowUrl = mirrorRows[idx]?.url;
5007
6728
  if (!rowUrl) return;
5008
- const outcome = await processAssetRow(rowUrl, idx, filenameCacheRef.current, mirrorAbortRef.current?.signal);
5009
- setMirrorRows((prev) => prev.map((r, i) => i === idx ? { ...r, ...outcome } : r));
6729
+ const outcome = await processAssetRow(
6730
+ rowUrl,
6731
+ idx,
6732
+ filenameCacheRef.current,
6733
+ mirrorAbortRef.current?.signal
6734
+ );
6735
+ setMirrorRows(
6736
+ (previous) => previous.map((row, i) => i === idx ? { ...row, ...outcome } : row)
6737
+ );
5010
6738
  },
5011
6739
  [mirrorRows, processAssetRow]
5012
6740
  );
@@ -5016,15 +6744,28 @@ function SEImportWizard({ bindings }) {
5016
6744
  void runMirror();
5017
6745
  }
5018
6746
  }, [step, runMirror]);
5019
- const currentItem = originalReviewItems[reviewIndex];
5020
- const currentRow = currentItem ? rowStates[currentItem.moduleId] ?? { state: "pending" } : void 0;
6747
+ useEffect6(() => {
6748
+ if (step === "pick" && seClient && overlayList === null && !overlaysLoading) {
6749
+ void loadOverlayList(seClient);
6750
+ }
6751
+ }, [step, seClient, overlayList, overlaysLoading, loadOverlayList]);
5021
6752
  const advanceReview = useCallback3(() => {
5022
- if (reviewIndex + 1 < originalReviewItems.length) setReviewIndex(reviewIndex + 1);
6753
+ if (reviewIndex + 1 < originalReviewItems.length)
6754
+ setReviewIndex(reviewIndex + 1);
5023
6755
  else setStep("confirm");
5024
6756
  }, [originalReviewItems.length, reviewIndex]);
5025
- const setRow = useCallback3((moduleId, patch) => {
5026
- setRowStates((prev) => ({ ...prev, [moduleId]: { ...prev[moduleId] ?? { state: "pending" }, ...patch } }));
5027
- }, []);
6757
+ const setRow = useCallback3(
6758
+ (moduleId, patch) => {
6759
+ setRowStates((previous) => ({
6760
+ ...previous,
6761
+ [moduleId]: {
6762
+ ...previous[moduleId] ?? { state: "pending" },
6763
+ ...patch
6764
+ }
6765
+ }));
6766
+ },
6767
+ []
6768
+ );
5028
6769
  const handleKeep = () => {
5029
6770
  if (!result || !currentItem) return;
5030
6771
  setResult(applyReviewAction(result, currentItem.moduleId, "keep"));
@@ -5041,22 +6782,43 @@ function SEImportWizard({ bindings }) {
5041
6782
  if (!currentItem || !result) return;
5042
6783
  setRow(currentItem.moduleId, { state: "generating", error: void 0 });
5043
6784
  try {
5044
- const canvas = { width: result.overlay.settings.metadata.width, height: result.overlay.settings.metadata.height };
5045
- const generated = await generateAICustomCode(buildAIPromptForSEWidget(currentItem.seWidget, canvas));
6785
+ const canvas = {
6786
+ width: result.overlay.settings.metadata.width,
6787
+ height: result.overlay.settings.metadata.height
6788
+ };
6789
+ const generated = await generateAICustomCode(
6790
+ buildAIPromptForSEWidget(currentItem.seWidget, canvas)
6791
+ );
5046
6792
  setRow(currentItem.moduleId, { state: "generated", generated });
5047
- } catch (e) {
5048
- setRow(currentItem.moduleId, { state: "failed", error: e?.message ?? "AI generation failed." });
6793
+ } catch (error) {
6794
+ setRow(currentItem.moduleId, {
6795
+ state: "failed",
6796
+ error: error?.message ?? "AI generation failed."
6797
+ });
5049
6798
  }
5050
6799
  };
5051
6800
  const handleAcceptGenerated = () => {
5052
6801
  if (!result || !currentItem || !currentRow?.generated) return;
5053
- setResult(applyReviewAction(result, currentItem.moduleId, "ai", currentRow.generated));
6802
+ setResult(
6803
+ applyReviewAction(
6804
+ result,
6805
+ currentItem.moduleId,
6806
+ "ai",
6807
+ currentRow.generated
6808
+ )
6809
+ );
5054
6810
  advanceReview();
5055
6811
  };
5056
- const [marketplacePickerOpen, setMarketplacePickerOpen] = useState6(false);
5057
6812
  const handlePickMarketplace = (transplant) => {
5058
6813
  if (!result || !currentItem) return;
5059
- setResult(applyReviewAction(result, currentItem.moduleId, "marketplace", transplant));
6814
+ setResult(
6815
+ applyReviewAction(
6816
+ result,
6817
+ currentItem.moduleId,
6818
+ "marketplace",
6819
+ transplant
6820
+ )
6821
+ );
5060
6822
  setRow(currentItem.moduleId, { state: "kept" });
5061
6823
  setMarketplacePickerOpen(false);
5062
6824
  advanceReview();
@@ -5066,22 +6828,22 @@ function SEImportWizard({ bindings }) {
5066
6828
  const patch = {};
5067
6829
  for (const item of originalReviewItems) {
5068
6830
  const rowState = rowStates[item.moduleId]?.state ?? "pending";
5069
- if (rowState !== "pending") continue;
5070
- if (item.flaggedOff) continue;
5071
- if (item.status !== "placeholder" && item.status !== "template") continue;
6831
+ if (rowState !== "pending" || item.flaggedOff || item.status !== "placeholder" && item.status !== "template")
6832
+ continue;
5072
6833
  patch[item.moduleId] = { state: "kept" };
5073
6834
  }
5074
- if (Object.keys(patch).length) setRowStates((prev) => ({ ...prev, ...patch }));
5075
- const remainingIndex = originalReviewItems.findIndex((item) => (patch[item.moduleId]?.state ?? rowStates[item.moduleId]?.state ?? "pending") === "pending");
6835
+ if (Object.keys(patch).length)
6836
+ setRowStates((previous) => ({ ...previous, ...patch }));
6837
+ const remainingIndex = originalReviewItems.findIndex(
6838
+ (item) => (patch[item.moduleId]?.state ?? rowStates[item.moduleId]?.state ?? "pending") === "pending"
6839
+ );
5076
6840
  if (remainingIndex >= 0) {
5077
6841
  setReviewIndex(remainingIndex);
5078
6842
  const remainingItem = originalReviewItems[remainingIndex];
5079
- if (remainingItem?.flaggedOff) {
5080
- const actionText = hasMarketplaceCandidates(remainingItem.seWidget.type) ? "Skip, Marketplace, or Generate with AI" : "Skip or Generate with AI";
5081
- notify.warning(`Choose ${actionText} for the flagged-off widget.`);
5082
- } else {
5083
- notify.warning("Review the remaining widget before continuing.");
5084
- }
6843
+ const actionText = remainingItem?.flaggedOff && hasMarketplaceCandidates(remainingItem.seWidget.type) ? "Skip, Marketplace, or Generate with AI" : "Skip or Generate with AI";
6844
+ notify.warning(
6845
+ remainingItem?.flaggedOff ? `Choose ${actionText} for the flagged-off widget.` : "Review the remaining widget before continuing."
6846
+ );
5085
6847
  return;
5086
6848
  }
5087
6849
  setStep("confirm");
@@ -5100,10 +6862,11 @@ function SEImportWizard({ bindings }) {
5100
6862
  setStep("confirm");
5101
6863
  return;
5102
6864
  }
5103
- const nextLayers = (result.overlay.settings.layers ?? []).filter((l) => !toSkip.has(l.id));
6865
+ const nextLayers = (result.overlay.settings.layers ?? []).filter(
6866
+ (layer) => !toSkip.has(layer.id)
6867
+ );
5104
6868
  const modules = { ...result.overlay.settings.modules ?? {} };
5105
6869
  for (const id of toSkip) delete modules[id];
5106
- const reviewItems = result.reviewItems.filter((r) => !toSkip.has(r.moduleId));
5107
6870
  setResult({
5108
6871
  ...result,
5109
6872
  overlay: {
@@ -5111,13 +6874,31 @@ function SEImportWizard({ bindings }) {
5111
6874
  settings: { ...result.overlay.settings, layers: nextLayers, modules },
5112
6875
  layers_count: nextLayers.length
5113
6876
  },
5114
- reviewItems
6877
+ reviewItems: result.reviewItems.filter(
6878
+ (item) => !toSkip.has(item.moduleId)
6879
+ )
5115
6880
  });
5116
- setRowStates((prev) => ({ ...prev, ...patch }));
6881
+ setRowStates((previous) => ({ ...previous, ...patch }));
5117
6882
  setStep("confirm");
5118
6883
  };
5119
6884
  const handleFinalImport = useCallback3(async () => {
5120
- if (!result) return;
6885
+ if (!result || !seClient) {
6886
+ if (!result) return;
6887
+ setImporting(true);
6888
+ try {
6889
+ const body = { name: options.name.trim() || result.overlay.name, description: result.overlay.description, settings: result.overlay.settings };
6890
+ const { uuid } = await saveOverlay(body);
6891
+ notify.success("Imported overlay from StreamElements.");
6892
+ mirrorAbortRef.current?.abort();
6893
+ onClose();
6894
+ if (uuid) onComplete({ overlayId: uuid });
6895
+ } catch (error) {
6896
+ notify.error(`Import failed: ${extractErrorMessage(error)}`);
6897
+ } finally {
6898
+ setImporting(false);
6899
+ }
6900
+ return;
6901
+ }
5121
6902
  setImporting(true);
5122
6903
  try {
5123
6904
  const body = {
@@ -5126,163 +6907,348 @@ function SEImportWizard({ bindings }) {
5126
6907
  settings: result.overlay.settings
5127
6908
  };
5128
6909
  const { uuid } = await saveOverlay(body);
5129
- notify.success(`Imported overlay from StreamElements.`);
6910
+ const hasMore = overlayQueue.length > 0;
6911
+ if (hasMore) {
6912
+ notify.success(`Imported ${currentOverlayIndex} of ${totalOverlaysToImport} overlays from StreamElements.`);
6913
+ const [nextId, ...rest] = overlayQueue;
6914
+ setOverlayQueue(rest);
6915
+ setCurrentOverlayIndex(currentOverlayIndex + 1);
6916
+ setImporting(false);
6917
+ await loadBootstrapAndKickoff(seClient, nextId);
6918
+ return;
6919
+ }
6920
+ notify.success(
6921
+ totalOverlaysToImport > 1 ? `Imported all ${totalOverlaysToImport} overlays from StreamElements.` : "Imported overlay from StreamElements."
6922
+ );
5130
6923
  mirrorAbortRef.current?.abort();
5131
6924
  onClose();
5132
6925
  if (uuid) onComplete({ overlayId: uuid });
5133
- } catch (err) {
5134
- notify.error(`Import failed: ${extractErrorMessage(err)}`);
6926
+ } catch (error) {
6927
+ notify.error(`Import failed: ${extractErrorMessage(error)}`);
5135
6928
  } finally {
5136
6929
  setImporting(false);
5137
6930
  }
5138
- }, [notify, onClose, onComplete, options.name, result, saveOverlay]);
5139
- const stepOrder = ["url", "discovery", "options", "mirror", "review", "confirm"];
6931
+ }, [
6932
+ notify,
6933
+ onClose,
6934
+ onComplete,
6935
+ options.name,
6936
+ result,
6937
+ saveOverlay,
6938
+ seClient,
6939
+ overlayQueue,
6940
+ currentOverlayIndex,
6941
+ totalOverlaysToImport,
6942
+ loadBootstrapAndKickoff
6943
+ ]);
6944
+ const visibleSteps = useMemo5(() => {
6945
+ const showMirrorStep = options.mirrorAssets && (assetUrls.length > 0 || mirrorRows.length > 0 || step === "mirror");
6946
+ if (step === "mode") return [];
6947
+ const onUrlFlow = entryMode === "url" || step === "url";
6948
+ const onJwtFlow = !onUrlFlow;
6949
+ const steps = CORE_STEPS.filter((item) => {
6950
+ if (item.key === "mode") return false;
6951
+ if (item.key === "mirror" && !showMirrorStep) return false;
6952
+ if (item.key === "connect" && !onJwtFlow) return false;
6953
+ if (item.key === "pick" && !onJwtFlow) return false;
6954
+ if (item.key === "url" && !onUrlFlow) return false;
6955
+ return true;
6956
+ });
6957
+ if (originalReviewItems.length > 0)
6958
+ steps.splice(Math.max(steps.length - 1, 0), 0, {
6959
+ key: "review",
6960
+ label: `Review (${originalReviewItems.length})`
6961
+ });
6962
+ return steps;
6963
+ }, [
6964
+ assetUrls.length,
6965
+ entryMode,
6966
+ mirrorRows.length,
6967
+ options.mirrorAssets,
6968
+ originalReviewItems.length,
6969
+ seClient,
6970
+ step
6971
+ ]);
5140
6972
  const goBack = () => {
6973
+ const currentIndex = visibleSteps.findIndex((item) => item.key === step);
5141
6974
  if (step === "review" && reviewIndex > 0) {
5142
6975
  setReviewIndex(reviewIndex - 1);
5143
6976
  return;
5144
6977
  }
5145
- const idx = stepOrder.indexOf(step);
5146
- if (idx <= 0) return;
5147
- let prev = stepOrder[idx - 1];
5148
- if (prev === "mirror" && !options.mirrorAssets) prev = stepOrder[idx - 2];
5149
- if (prev) setStep(prev);
5150
- };
5151
- const goNext = () => {
5152
- if (step === "url") {
5153
- void handleLoad();
5154
- return;
5155
- }
5156
- if (step === "discovery") {
5157
- setStep("options");
6978
+ if (currentIndex === 0 && (step === "connect" || step === "url")) {
6979
+ setStep("mode");
6980
+ setEntryMode(null);
5158
6981
  return;
5159
6982
  }
5160
- if (step === "options") {
5161
- if (options.mirrorAssets && assetUrls.length > 0) setStep("mirror");
5162
- else if (originalReviewItems.length > 0) setStep("review");
5163
- else setStep("confirm");
6983
+ if (currentIndex > 0) setStep(visibleSteps[currentIndex - 1].key);
6984
+ };
6985
+ const goNext = () => {
6986
+ if (step === "connect") {
6987
+ if (!seClient) return void handleConnect();
6988
+ setStep("pick");
5164
6989
  return;
5165
6990
  }
5166
- if (step === "mirror") {
5167
- if (originalReviewItems.length > 0) setStep("review");
5168
- else setStep("confirm");
5169
- return;
6991
+ if (step === "pick") {
6992
+ return void handlePickConfirm();
5170
6993
  }
6994
+ if (step === "url") return void handleLoad();
5171
6995
  if (step === "review") {
5172
- if (currentItem) {
5173
- if (currentRow?.state === "generated") {
5174
- handleAcceptGenerated();
5175
- } else if (currentRow?.state === "pending") {
5176
- if (currentItem.flaggedOff) {
5177
- const actionText = hasMarketplaceCandidates(currentItem.seWidget.type) ? "Skip, Marketplace, or Generate with AI" : "Skip or Generate with AI";
5178
- notify.warning(`Choose ${actionText} for the flagged-off widget.`);
5179
- return;
5180
- }
5181
- handleKeep();
5182
- } else {
5183
- advanceReview();
6996
+ if (!currentItem) return;
6997
+ if (currentRow?.state === "generated") return handleAcceptGenerated();
6998
+ if (currentRow?.state === "pending") {
6999
+ if (currentItem.flaggedOff) {
7000
+ const actionText = hasMarketplaceCandidates(currentItem.seWidget.type) ? "Skip, Marketplace, or Generate with AI" : "Skip or Generate with AI";
7001
+ notify.warning(`Choose ${actionText} for the flagged-off widget.`);
7002
+ return;
5184
7003
  }
7004
+ return handleKeep();
5185
7005
  }
5186
- return;
5187
- }
5188
- if (step === "confirm") {
5189
- void handleFinalImport();
5190
- return;
7006
+ return advanceReview();
5191
7007
  }
7008
+ if (step === "confirm") return void handleFinalImport();
7009
+ const currentIndex = visibleSteps.findIndex((item) => item.key === step);
7010
+ if (currentIndex >= 0)
7011
+ setStep(visibleSteps[currentIndex + 1]?.key ?? "confirm");
5192
7012
  };
5193
7013
  const nextLabel = (() => {
5194
- if (step === "url") return loading ? "Loading\u2026" : "Load overlay";
5195
- if (step === "discovery") return "Continue";
5196
- if (step === "options") return "Continue";
5197
- if (step === "mirror") return mirrorRunning ? "Working\u2026" : "Continue";
5198
- if (step === "review") return currentRow?.state === "generated" ? "Use & continue" : "Continue";
5199
- if (step === "confirm") return importing ? "Importing\u2026" : "Create overlay";
5200
- return "Next";
7014
+ if (step === "connect")
7015
+ return connecting ? "Connecting..." : seClient ? "Continue" : "Connect";
7016
+ if (step === "pick") {
7017
+ if (loading) return "Loading overlay...";
7018
+ const n = selectedOverlayIds.size;
7019
+ return n > 1 ? `Import ${n} overlays` : "Import overlay";
7020
+ }
7021
+ if (step === "url") return loading ? "Loading..." : "Load overlay";
7022
+ if (step === "mirror") return mirrorRunning ? "Working..." : "Continue";
7023
+ if (step === "review")
7024
+ return currentRow?.state === "generated" ? "Use & continue" : "Continue";
7025
+ if (step === "confirm")
7026
+ return importing ? "Importing..." : "Create overlay";
7027
+ return "Continue";
5201
7028
  })();
5202
7029
  const nextDisabled = (() => {
7030
+ if (step === "connect")
7031
+ return connecting || !seClient && !jwt.trim();
7032
+ if (step === "pick")
7033
+ return loading || overlaysLoading || selectedOverlayIds.size === 0;
5203
7034
  if (step === "url") return loading || !url.trim();
5204
7035
  if (step === "mirror") return mirrorRunning;
5205
- if (step === "review") return currentRow?.state === "generating" || currentRow?.state === "pending" && currentItem?.flaggedOff;
7036
+ if (step === "review")
7037
+ return currentRow?.state === "generating" || currentRow?.state === "pending" && currentItem?.flaggedOff;
5206
7038
  if (step === "confirm") return importing;
5207
7039
  return false;
5208
7040
  })();
5209
- const stepperIndex = stepOrder.indexOf(step);
5210
- const visibleSteps = [
5211
- { key: "url", label: "Load" },
5212
- { key: "discovery", label: "Discovery" },
5213
- { key: "options", label: "Options" },
5214
- ...options.mirrorAssets && assetUrls.length > 0 ? [{ key: "mirror", label: "Assets" }] : [],
5215
- ...originalReviewItems.length > 0 ? [{ key: "review", label: `Review (${originalReviewItems.length})` }] : [],
5216
- { key: "confirm", label: "Confirm" }
5217
- ];
5218
- return /* @__PURE__ */ jsxs7("div", { className: "ui-flex-column ui-gap-2", style: { minWidth: 680 }, children: [
5219
- result && /* @__PURE__ */ jsx13("div", { className: "ui-flex-row ui-gap-2", style: { flexWrap: "wrap", paddingBottom: 8, borderBottom: "1px solid var(--ui-border, #2a2c31)" }, children: visibleSteps.map((s, i) => /* @__PURE__ */ jsx13(StepDot, { num: i + 1, label: s.label, active: s.key === step, done: visibleSteps.findIndex((v) => v.key === step) > i }, s.key)) }),
5220
- step === "url" && /* @__PURE__ */ jsx13(StepURL, { url, setUrl, error: loadError, busy: loading }),
5221
- step === "discovery" && result && /* @__PURE__ */ jsx13(StepDiscovery, { result, assetCount: assetUrls.length }),
5222
- step === "options" && /* @__PURE__ */ jsx13(StepOptions, { options, setOptions, assetCount: assetUrls.length }),
5223
- step === "mirror" && /* @__PURE__ */ jsx13(StepMirror, { rows: mirrorRows, running: mirrorRunning, onRetry: retryMirrorRow }),
5224
- step === "review" && currentItem && result && !marketplacePickerOpen && /* @__PURE__ */ jsx13(
5225
- StepReview,
7041
+ const activeIndex = visibleSteps.findIndex((item) => item.key === step);
7042
+ const batchActive = totalOverlaysToImport > 1 && step !== "mode" && step !== "pick" && step !== "connect" && step !== "url";
7043
+ const currentOverlayName = overlayList?.find((o) => o._id === Array.from(selectedOverlayIds).filter((id) => !overlayQueue.includes(id))[currentOverlayIndex - 1])?.name;
7044
+ return /* @__PURE__ */ jsx13("div", { className: "se-import", children: /* @__PURE__ */ jsxs7("div", { className: "se-import__shell", children: [
7045
+ batchActive && /* @__PURE__ */ jsxs7(
7046
+ "div",
5226
7047
  {
5227
- item: currentItem,
5228
- index: reviewIndex,
5229
- total: originalReviewItems.length,
5230
- state: currentRow?.state ?? "pending",
5231
- error: currentRow?.error,
5232
- generated: currentRow?.generated,
5233
- canvas: { width: result.overlay.settings.metadata.width, height: result.overlay.settings.metadata.height },
5234
- onSkip: handleSkip,
5235
- onKeep: handleKeep,
5236
- onGenerate: handleGenerate,
5237
- onAccept: handleAcceptGenerated,
5238
- onRetry: handleGenerate,
5239
- onPickMarketplace: () => setMarketplacePickerOpen(true),
5240
- onKeepAllPlaceholders: handleKeepAllPlaceholders,
5241
- onSkipAll: handleSkipAll,
5242
- remainingCount: originalReviewItems.filter((r) => (rowStates[r.moduleId]?.state ?? "pending") === "pending").length,
5243
- marketplaceAvailable: hasMarketplaceCandidates(currentItem.seWidget.type)
7048
+ style: {
7049
+ marginBottom: 16,
7050
+ padding: "10px 16px",
7051
+ border: "1px solid var(--se-border-strong)",
7052
+ borderRadius: 8,
7053
+ background: "rgba(255, 64, 118, 0.08)",
7054
+ color: "var(--se-text)",
7055
+ fontSize: 13,
7056
+ fontWeight: 600,
7057
+ textAlign: "center"
7058
+ },
7059
+ children: [
7060
+ "Importing overlay ",
7061
+ currentOverlayIndex,
7062
+ " of ",
7063
+ totalOverlaysToImport,
7064
+ currentOverlayName ? ` \xB7 ${currentOverlayName}` : "",
7065
+ overlayQueue.length > 0 ? ` \xB7 ${overlayQueue.length} more queued` : ""
7066
+ ]
5244
7067
  }
5245
7068
  ),
5246
- step === "review" && currentItem && marketplacePickerOpen && /* @__PURE__ */ jsx13(
5247
- MarketplacePicker,
7069
+ visibleSteps.length > 0 && /* @__PURE__ */ jsx13("header", { className: "se-import__header", children: /* @__PURE__ */ jsx13("div", { className: "se-import-stepper", children: visibleSteps.map((item, index) => /* @__PURE__ */ jsxs7(
7070
+ "div",
5248
7071
  {
5249
- seWidgetType: currentItem.seWidget.type,
5250
- fetchMarketplaceOverlay,
5251
- CustomEmbed,
5252
- onPick: handlePickMarketplace,
5253
- onCancel: () => setMarketplacePickerOpen(false)
5254
- }
5255
- ),
5256
- step === "confirm" && result && /* @__PURE__ */ jsx13(StepConfirm, { result, options, mirrorRows, rowStates }),
5257
- /* @__PURE__ */ jsxs7("div", { className: "ui-flex-row ui-gap-2", style: { justifyContent: "flex-end", marginTop: 12 }, children: [
7072
+ className: stepClass(
7073
+ index < activeIndex,
7074
+ index === activeIndex
7075
+ ),
7076
+ children: [
7077
+ /* @__PURE__ */ jsx13("div", { className: "se-import-step__label", children: item.label }),
7078
+ /* @__PURE__ */ jsx13("div", { className: "se-import-step__line" }),
7079
+ /* @__PURE__ */ jsx13("div", { className: "se-import-step__dot", children: index < activeIndex ? "\u2713" : index + 1 })
7080
+ ]
7081
+ },
7082
+ item.key
7083
+ )) }) }),
7084
+ /* @__PURE__ */ jsxs7("main", { className: "se-import__content", children: [
7085
+ step === "mode" && /* @__PURE__ */ jsx13(
7086
+ StepModePicker,
7087
+ {
7088
+ onChooseJwt: () => {
7089
+ setEntryMode("jwt");
7090
+ setStep("connect");
7091
+ },
7092
+ onChooseUrl: () => {
7093
+ setEntryMode("url");
7094
+ setStep("url");
7095
+ }
7096
+ }
7097
+ ),
7098
+ step === "connect" && /* @__PURE__ */ jsx13(
7099
+ StepConnect,
7100
+ {
7101
+ jwt,
7102
+ setJwt,
7103
+ error: connectError,
7104
+ busy: connecting,
7105
+ channels,
7106
+ selectedChannelId,
7107
+ setSelectedChannelId,
7108
+ onUseUrl: () => {
7109
+ setEntryMode("url");
7110
+ setStep("url");
7111
+ }
7112
+ }
7113
+ ),
7114
+ step === "pick" && /* @__PURE__ */ jsx13(
7115
+ StepPickOverlay,
7116
+ {
7117
+ overlays: overlayList,
7118
+ loading: overlaysLoading,
7119
+ error: overlaysError,
7120
+ selectedIds: selectedOverlayIds,
7121
+ toggleId: toggleOverlaySelection,
7122
+ selectAll: selectAllOverlays,
7123
+ clearAll: clearAllOverlays,
7124
+ onRetry: () => seClient && void loadOverlayList(seClient)
7125
+ }
7126
+ ),
7127
+ step === "url" && /* @__PURE__ */ jsx13(
7128
+ StepURL,
7129
+ {
7130
+ url,
7131
+ setUrl,
7132
+ error: loadError,
7133
+ busy: loading
7134
+ }
7135
+ ),
7136
+ step === "discovery" && result && /* @__PURE__ */ jsx13(StepDiscovery, { result, assetCount: assetUrls.length }),
7137
+ step === "options" && /* @__PURE__ */ jsx13(
7138
+ StepOptions,
7139
+ {
7140
+ options,
7141
+ setOptions,
7142
+ assetCount: assetUrls.length
7143
+ }
7144
+ ),
7145
+ step === "mirror" && /* @__PURE__ */ jsx13(
7146
+ StepMirror,
7147
+ {
7148
+ rows: mirrorRows,
7149
+ running: mirrorRunning,
7150
+ onRetry: retryMirrorRow
7151
+ }
7152
+ ),
7153
+ step === "review" && currentItem && result && !marketplacePickerOpen && /* @__PURE__ */ jsx13(
7154
+ StepReview,
7155
+ {
7156
+ item: currentItem,
7157
+ index: reviewIndex,
7158
+ total: originalReviewItems.length,
7159
+ state: currentRow?.state ?? "pending",
7160
+ error: currentRow?.error,
7161
+ generated: currentRow?.generated,
7162
+ canvas: {
7163
+ width: result.overlay.settings.metadata.width,
7164
+ height: result.overlay.settings.metadata.height
7165
+ },
7166
+ onSkip: handleSkip,
7167
+ onKeep: handleKeep,
7168
+ onGenerate: handleGenerate,
7169
+ onAccept: handleAcceptGenerated,
7170
+ onRetry: handleGenerate,
7171
+ onPickMarketplace: () => setMarketplacePickerOpen(true),
7172
+ onKeepAllPlaceholders: handleKeepAllPlaceholders,
7173
+ onSkipAll: handleSkipAll,
7174
+ remainingCount: originalReviewItems.filter(
7175
+ (item) => (rowStates[item.moduleId]?.state ?? "pending") === "pending"
7176
+ ).length,
7177
+ marketplaceAvailable: hasMarketplaceCandidates(
7178
+ currentItem.seWidget.type
7179
+ )
7180
+ }
7181
+ ),
7182
+ step === "review" && currentItem && marketplacePickerOpen && /* @__PURE__ */ jsx13("div", { className: panelClass("se-import-panel--wide"), children: /* @__PURE__ */ jsx13(
7183
+ MarketplacePicker,
7184
+ {
7185
+ seWidgetType: currentItem.seWidget.type,
7186
+ fetchMarketplaceOverlay,
7187
+ CustomEmbed,
7188
+ onPick: handlePickMarketplace,
7189
+ onCancel: () => setMarketplacePickerOpen(false)
7190
+ }
7191
+ ) }),
7192
+ step === "confirm" && result && /* @__PURE__ */ jsx13(
7193
+ StepConfirm,
7194
+ {
7195
+ result,
7196
+ options,
7197
+ mirrorRows,
7198
+ rowStates
7199
+ }
7200
+ )
7201
+ ] }),
7202
+ /* @__PURE__ */ jsxs7("footer", { className: "se-import-actions", children: [
5258
7203
  /* @__PURE__ */ jsx13(
5259
- "button",
7204
+ LSButton,
5260
7205
  {
5261
7206
  type: "button",
5262
- className: "ui-button",
7207
+ color: "secondary",
5263
7208
  onClick: () => {
5264
7209
  mirrorAbortRef.current?.abort();
5265
7210
  onClose();
5266
7211
  },
5267
7212
  disabled: loading || importing,
5268
- children: translate("action.cancel", "Cancel")
7213
+ label: translate("common.cancel", "Cancel")
7214
+ }
7215
+ ),
7216
+ step !== "mode" && /* @__PURE__ */ jsx13(
7217
+ LSButton,
7218
+ {
7219
+ type: "button",
7220
+ color: "secondary",
7221
+ onClick: goBack,
7222
+ disabled: loading || importing || mirrorRunning || currentRow?.state === "generating",
7223
+ label: "Back"
5269
7224
  }
5270
7225
  ),
5271
- stepperIndex > 0 && /* @__PURE__ */ jsx13("button", { type: "button", className: "ui-button", onClick: goBack, disabled: loading || importing || mirrorRunning || currentRow?.state === "generating", children: "Back" }),
5272
- /* @__PURE__ */ jsx13("button", { type: "button", className: "ui-button ui-button-purple", onClick: goNext, disabled: nextDisabled, children: nextLabel })
7226
+ step !== "mode" && /* @__PURE__ */ jsx13(
7227
+ LSButton,
7228
+ {
7229
+ type: "button",
7230
+ color: "primary",
7231
+ onClick: goNext,
7232
+ disabled: nextDisabled,
7233
+ label: nextLabel
7234
+ }
7235
+ )
5273
7236
  ] })
5274
- ] });
7237
+ ] }) });
5275
7238
  }
5276
7239
  function extractErrorMessage(err) {
5277
7240
  if (!err) return "Failed";
5278
- const e = err;
5279
- return e?.response?.data?.message || e?.response?.data?.error || e?.message || "Failed";
7241
+ const error = err;
7242
+ return error?.response?.data?.message || error?.response?.data?.error || error?.message || "Failed";
5280
7243
  }
5281
7244
 
5282
7245
  // src/se-import/index.ts
5283
7246
  var REVIEW_REASONS = {
5284
7247
  "se-widget-bit-boss": "No native Lumia equivalent \u2014 bit boss fight bar with HP/damage states.",
5285
- "se-widget-hype-cup": "No native Lumia equivalent \u2014 Box2D physics stacking animation.",
7248
+ // `se-widget-hype-cup` intentionally omitted it now routes to the native
7249
+ // `tipjar` module (see dispatcher.ts) and lands with `partial` status, so it
7250
+ // never reaches the placeholder-reason fallback. If a future feature flag
7251
+ // disables tipjar auto-import, add an entry to FLAG_OFF_REASONS instead.
5286
7252
  "se-widget-train": "No native Lumia equivalent \u2014 animated subscriber/event train.",
5287
7253
  "se-widget-media-share": "No native Lumia equivalent \u2014 viewer-submitted media player.",
5288
7254
  "se-widget-contest": "No native Lumia equivalent \u2014 SE-specific contest widget.",
@@ -5291,13 +7257,17 @@ var REVIEW_REASONS = {
5291
7257
  "se-widget-top-cheerers-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render cheer rankings.',
5292
7258
  "se-widget-top-gifters-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render gifter rankings.',
5293
7259
  "se-widget-top-tippers-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render tipper rankings.',
5294
- "se-widget-snow": "Seasonal effect \u2014 needs a particle template in Lumia.",
5295
- "se-widget-halloween": "Seasonal effect \u2014 needs a Lumia template.",
5296
- "se-widget-halloween-2019": "Seasonal effect \u2014 needs a Lumia template.",
5297
- "se-widget-xmas": "Seasonal effect \u2014 needs a Lumia template.",
5298
- "se-widget-valentine": "Seasonal effect \u2014 needs a Lumia template.",
5299
- "se-widget-easter": "Seasonal effect \u2014 needs a Lumia template.",
5300
- "se-widget-world-cup": "Seasonal effect \u2014 needs a Lumia template."
7260
+ // Merch products rotator imports as an empty slideshow with the title
7261
+ // "Merch products (add images manually)". Lumia's Fourthwall integration is
7262
+ // webhook-driven (events flow in) but doesn't fetch the user's shop catalog,
7263
+ // so we can't pre-populate the slideshow items. The streamer adds their
7264
+ // product images by hand in the slideshow editor. Status = 'partial' (see
7265
+ // dispatcher.ts), so this string surfaces in the review step.
7266
+ "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."
7267
+ // Seasonal widgets (snow / halloween / halloween-2019 / xmas / valentine / easter
7268
+ // / world-cup) intentionally omitted — they route directly to image/slideshow/
7269
+ // video modules now (see dispatcher.ts SEASONAL_IMAGE_TYPES), so they always
7270
+ // produce a `direct` status and never need a fallback review reason.
5301
7271
  };
5302
7272
  function reasonFor(seType, status, flaggedOff) {
5303
7273
  if (flaggedOff && FLAG_OFF_REASONS[seType]) return FLAG_OFF_REASONS[seType];
@@ -5403,7 +7373,7 @@ function importSEBootstrap(bootstrap) {
5403
7373
  const module = {
5404
7374
  id: lumiaGroupId,
5405
7375
  loaded: true,
5406
- settings: { title: w.name ?? "Group", type: "group" },
7376
+ settings: { title: defaultLabelForSEWidget(w), type: "group" },
5407
7377
  lights: [],
5408
7378
  css: {},
5409
7379
  content: {
@@ -5623,6 +7593,8 @@ export {
5623
7593
  LSVariableInputProvider,
5624
7594
  MEDIA_PREVIEW_USER_LEVEL_VALUES,
5625
7595
  MarketplacePicker,
7596
+ SEAuthError,
7597
+ SEClient,
5626
7598
  SEImportWizard,
5627
7599
  SE_IMPORT_FLAGS,
5628
7600
  SE_WIDGET_TO_MARKETPLACE_CANDIDATES,
@@ -5631,9 +7603,11 @@ export {
5631
7603
  buildBootstrapUrl,
5632
7604
  buildChatMessageContent,
5633
7605
  codeMirrorlinterOptions,
7606
+ decodeJwtPayload,
5634
7607
  extractSEOverlayId,
5635
7608
  extractSEPreviewParts,
5636
7609
  fetchSEBootstrap,
7610
+ fetchUsableChannels,
5637
7611
  filenameFromURL,
5638
7612
  findSEAssetURLs,
5639
7613
  getAILandingBounds,