@lumiastream/ui 0.2.7 → 0.2.8-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +64 -8
  2. package/dist/index.js +2145 -502
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3111,6 +3111,40 @@ function listenerPlaceholderContext(listener) {
3111
3111
  }
3112
3112
  return {};
3113
3113
  }
3114
+ var SE_WIDGET_TYPE_LABELS = {
3115
+ "subscriber-latest": "Latest subscriber",
3116
+ "subscriber-new-latest": "Latest new subscriber",
3117
+ "subscriber-resub-latest": "Latest resubscriber",
3118
+ "subscriber-gifted-latest": "Latest gifted sub",
3119
+ "subscriber-alltime-gifter": "Top all-time gifter",
3120
+ "follower-latest": "Latest follower",
3121
+ "cheer-latest": "Latest cheer",
3122
+ "cheer-total": "Total cheers",
3123
+ "tip-latest": "Latest tip",
3124
+ "tip-total": "Total tips",
3125
+ "se-widget-bit-boss": "Bit Boss",
3126
+ "se-widget-hype-cup": "Hype Cup",
3127
+ "se-widget-custom-event-list": "Custom widget",
3128
+ "se-widget-credit-roll": "Credits",
3129
+ "se-widget-media-share": "Media share",
3130
+ // Imports as an empty slideshow — Lumia has no Fourthwall catalog API, so
3131
+ // the streamer has to add product images by hand. The friendly title makes
3132
+ // that obvious in the layers panel; see REVIEW_REASONS for the full prompt.
3133
+ "se-widget-merch-products-rotator": "Merch products (add images manually)"
3134
+ };
3135
+ function defaultLabelForSEWidget(widget) {
3136
+ const trimmed = typeof widget.name === "string" ? widget.name.trim() : "";
3137
+ if (trimmed) return trimmed;
3138
+ const override = SE_WIDGET_TYPE_LABELS[widget.type];
3139
+ if (override) return override;
3140
+ const stripped = widget.type.replace(/^se-widget-/, "");
3141
+ const words = stripped.split("-").filter(Boolean);
3142
+ if (words.length === 0) return widget.type;
3143
+ const first = words[0];
3144
+ const head = first.charAt(0).toUpperCase() + first.slice(1);
3145
+ const tail = words.slice(1).join(" ");
3146
+ return tail ? `${head} ${tail}` : head;
3147
+ }
3114
3148
  function translateSeText(input, listener) {
3115
3149
  if (!input) return "";
3116
3150
  const ctx = listener ? listenerPlaceholderContext(listener) : {};
@@ -3243,7 +3277,22 @@ var LUMIA_DEFAULT_SIZES = {
3243
3277
  emotebox: { width: 400, height: 400 },
3244
3278
  nowplaying: { width: 600, height: 250 },
3245
3279
  spotify: { width: 600, height: 250 },
3246
- youtubemusic: { width: 600, height: 250 }
3280
+ youtubemusic: { width: 600, height: 250 },
3281
+ tipjar: { width: 600, height: 700 },
3282
+ // Hype Train: wide-and-short by default to fit a horizontal progress bar.
3283
+ // All four built-in themes (subtle / train / rocket / inferno) render
3284
+ // fine at this aspect — rocket internally uses a vertical gauge but
3285
+ // still works at this footprint. Users can resize per their overlay.
3286
+ hypetrain: { width: 740, height: 140 },
3287
+ // Stream Boss: matches SE's bit-boss default (440×86) for a 1:1 import
3288
+ // experience, lightly padded so Lumia's Card-theme avatar fits without
3289
+ // resizing. Users can swap to Bar-Only and shrink height further.
3290
+ streamboss: { width: 480, height: 100 },
3291
+ // Song Request: SE's media-share default is 480×292. Lumia's now-playing
3292
+ // card + 5-item queue list needs a bit more vertical room, so we add ~80px
3293
+ // for the queue list. Users can hide the queue list and shrink back to
3294
+ // just the card if they want SE's exact footprint.
3295
+ songrequest: { width: 480, height: 380 }
3247
3296
  };
3248
3297
  function translateVariationCondition(seType, seCondition, requirement) {
3249
3298
  if (seType === "gift") return { conditionType: "IS_GIFT", condition: 1 };
@@ -3288,7 +3337,7 @@ function buildUnit(widget, lumiaType, moduleExtras) {
3288
3337
  version: 1,
3289
3338
  settings: {
3290
3339
  type: lumiaType,
3291
- title: widget.name ?? null,
3340
+ title: defaultLabelForSEWidget(widget),
3292
3341
  locked: widget.locked ?? false
3293
3342
  },
3294
3343
  lights: [],
@@ -3340,6 +3389,17 @@ function mapVideo(widget) {
3340
3389
  }
3341
3390
  });
3342
3391
  }
3392
+ var LUMIA_SNOW_WEBM = "https://storage.lumiastream.com/overlays/global/seasonal/snow.webm";
3393
+ function mapSnow(widget) {
3394
+ return buildUnit(widget, "video", {
3395
+ content: {
3396
+ src: LUMIA_SNOW_WEBM,
3397
+ volume: 0,
3398
+ loop: true,
3399
+ muted: true
3400
+ }
3401
+ });
3402
+ }
3343
3403
  function mapReadout(widget, fallbackVar) {
3344
3404
  const explicitListener = widget.variables?.listener;
3345
3405
  const explicit = typeof explicitListener === "string" ? explicitListener : null;
@@ -3509,6 +3569,9 @@ function currentForListener(listener) {
3509
3569
  if (listener.startsWith("subscriber-")) return "{{twitch_total_subscriber_count}}";
3510
3570
  if (listener.startsWith("cheer-")) return "{{twitch_total_bits_count}}";
3511
3571
  if (listener.startsWith("tip-")) return "{{total_donation_amount}}";
3572
+ if (listener === "merch-goal-orders") return "{{fourthwall_total_order_count}}";
3573
+ if (listener === "merch-goal-items") return "{{fourthwall_total_items_count}}";
3574
+ if (listener === "merch-goal-total") return "{{fourthwall_total_order_amount}}";
3512
3575
  if (listener.startsWith("merch-")) return "{{fourthwall_total_order_count}}";
3513
3576
  return "0";
3514
3577
  }
@@ -3609,77 +3672,6 @@ function mapCustom(widget) {
3609
3672
  }
3610
3673
  });
3611
3674
  }
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
3675
 
3684
3676
  // src/se-import/mappers/misc.ts
3685
3677
  function mapChatbox(widget) {
@@ -3982,6 +3974,49 @@ function mapNowPlaying(widget) {
3982
3974
  }
3983
3975
  });
3984
3976
  }
3977
+ function mapMediaShare(widget) {
3978
+ const v = widget.variables ?? {};
3979
+ return buildUnit(widget, "songrequest", {
3980
+ content: {
3981
+ version: 1,
3982
+ // SE always shows the now-playing card; honour its cosmetic toggles
3983
+ // for the rest. `showTitle` defaults true; the others default to
3984
+ // SE's documented behavior (tipper visible, stats hidden).
3985
+ showNowPlayingCard: true,
3986
+ showQueueList: true,
3987
+ showSkipButton: true,
3988
+ showPauseButton: true,
3989
+ showTitle: v.showTitle !== false,
3990
+ showTipper: v.showTipper !== false,
3991
+ showAmount: v.showAmount === true,
3992
+ showStats: v.showStats === true,
3993
+ // SE doesn't have a queue-size knob — pick a sensible default
3994
+ // matching what fits in the SE widget's ~292px tall footprint.
3995
+ maxQueueItemsVisible: 5,
3996
+ // SE color knobs translate directly. Fall back to Lumia's defaults
3997
+ // (purple accent, near-black card background) if SE values are
3998
+ // missing — matches what the module ships with for native creates.
3999
+ accentColor: v.accentColor ?? "#9146ff",
4000
+ primaryColor: v.primaryColor ?? "#1a1a1a",
4001
+ // SE's widget hides the YT player by default; mirror that for
4002
+ // audio-only overlay use.
4003
+ playerVisible: false,
4004
+ defaultVolume: 70,
4005
+ autoAdvanceOnEnd: true,
4006
+ // SE-only knob preserved for round-trip provenance; Lumia ignores
4007
+ // it (LumiaStream auto-advance fires on `durationSeconds + grace`).
4008
+ se_timeLeft: v.timeLeft === true
4009
+ },
4010
+ css: {
4011
+ fontFamily: "Roboto",
4012
+ fontWeight: "bold",
4013
+ color: "#ffffff",
4014
+ textShadow: "rgb(0, 0, 0) 1px 1px 1px",
4015
+ background: "transparent",
4016
+ fontSize: 18
4017
+ }
4018
+ });
4019
+ }
3985
4020
  var SE_KAPPAGEN_EVENT_TO_LUMIA_ALERT = {
3986
4021
  follower: "twitch-follower",
3987
4022
  subscriber: "twitch-subscriber",
@@ -4125,6 +4160,75 @@ function mapGiveaway(widget) {
4125
4160
  }
4126
4161
  });
4127
4162
  }
4163
+ var HYPETRAIN_LISTENER_KEYS = /* @__PURE__ */ new Set([
4164
+ "hypetrain-latest",
4165
+ "hypetrain-level-progress",
4166
+ "hypetrain-level-goal",
4167
+ "hypetrain-total",
4168
+ "hypetrain-latest-top-contributors",
4169
+ // SE alert-name variants some community widgets subscribe to as well.
4170
+ "hypetrain-start",
4171
+ "hypetrain-end",
4172
+ "hypetrain-cooldown-start",
4173
+ "hypetrain-cooldown-end"
4174
+ ]);
4175
+ function isHypetrainCustomWidget(widget) {
4176
+ const enabled = [];
4177
+ const listeners = widget.listeners;
4178
+ if (listeners && typeof listeners === "object" && !Array.isArray(listeners)) {
4179
+ for (const [k, v] of Object.entries(listeners)) {
4180
+ if (v) enabled.push(k);
4181
+ }
4182
+ } else if (Array.isArray(listeners)) {
4183
+ enabled.push(...listeners);
4184
+ }
4185
+ const explicit = widget.listener;
4186
+ if (typeof explicit === "string" && explicit && !enabled.includes(explicit)) {
4187
+ enabled.push(explicit);
4188
+ }
4189
+ if (enabled.length === 0) return false;
4190
+ return enabled.every((listener) => HYPETRAIN_LISTENER_KEYS.has(listener));
4191
+ }
4192
+ function mapHypetrain(widget) {
4193
+ const v = widget.variables ?? {};
4194
+ return buildUnit(widget, "hypetrain", {
4195
+ content: {
4196
+ // Twitch-native is the right default — SE hype-train widgets always
4197
+ // reflected the Twitch native train. Users can flip to 'synthetic'
4198
+ // or 'hybrid' from Settings if they want cross-platform pooling.
4199
+ sourceMode: "twitch-native",
4200
+ sources: null,
4201
+ // default money-only whitelist; ignored in twitch-native anyway
4202
+ customValues: {},
4203
+ pretrainThreshold: 5,
4204
+ pretrainDurationMs: 5 * 60 * 1e3,
4205
+ levelDurationMs: 5 * 60 * 1e3,
4206
+ levelStep: 5,
4207
+ levelThresholds: [],
4208
+ // Pick a moderately energetic default theme — SE hype-train widgets
4209
+ // were never subtle. Users can switch in Settings.
4210
+ theme: "train",
4211
+ primaryColor: typeof v.primaryColor === "string" ? v.primaryColor : "#ff6b00",
4212
+ accentColor: typeof v.accentColor === "string" ? v.accentColor : "#ffd84d",
4213
+ textColor: typeof v.textColor === "string" ? v.textColor : "#ffffff",
4214
+ showLevel: true,
4215
+ showTotal: true,
4216
+ showTimer: true,
4217
+ showTopContributor: true,
4218
+ showLeaderboard: true,
4219
+ leaderboardLimit: 5,
4220
+ leaderboardDurationMs: 30 * 1e3,
4221
+ levelUpSoundUrl: "https://storage.lumiastream.com/overlays/lumia/audio/crowdClap.mp3",
4222
+ trainEndSoundUrl: "https://storage.lumiastream.com/overlays/lumia/audio/crowdClap.mp3",
4223
+ audioVolume: 0.5,
4224
+ // SE config preserved verbatim as provenance — no Lumia equivalent for
4225
+ // most of these knobs (per-widget animation tunables, sound URLs the
4226
+ // streamer might want to migrate manually, etc.). Users can inspect
4227
+ // these via the overlay JSON if they want to recreate a specific look.
4228
+ se_variables: v
4229
+ }
4230
+ });
4231
+ }
4128
4232
  function mapUnsupportedAsText(widget) {
4129
4233
  return buildUnit(widget, "text", {
4130
4234
  content: {
@@ -4142,44 +4246,258 @@ function mapUnsupportedAsText(widget) {
4142
4246
  });
4143
4247
  }
4144
4248
 
4249
+ // src/se-import/mappers/hypecup.ts
4250
+ var SE_TYPE_TO_CATEGORY = {
4251
+ cheer: "bits",
4252
+ sub: "subs",
4253
+ follower: "follows",
4254
+ raid: "raids",
4255
+ tip: "tips"
4256
+ };
4257
+ function liftSESpritesIntoTiers(types) {
4258
+ const out = {};
4259
+ if (!types) return out;
4260
+ for (const [seKey, block] of Object.entries(types)) {
4261
+ const cat = SE_TYPE_TO_CATEGORY[seKey];
4262
+ if (!cat) continue;
4263
+ const variations = block?.variations ?? [];
4264
+ const tiers = variations.filter((v) => typeof v?.src === "string" && v.src.length > 0).map((v) => ({
4265
+ minAmount: typeof v.amount === "number" && v.amount > 0 ? v.amount : 1,
4266
+ imageUrl: v.src,
4267
+ scale: typeof v.cheerSize === "number" && v.cheerSize > 0 ? v.cheerSize : 1
4268
+ }));
4269
+ if (tiers.length > 0) out[cat] = tiers;
4270
+ }
4271
+ return out;
4272
+ }
4273
+ var LISTENER_TO_CATEGORY = {
4274
+ "follower-latest": "follows",
4275
+ "subscriber-latest": "subs",
4276
+ "tip-latest": "tips",
4277
+ "cheer-latest": "bits",
4278
+ "raid-latest": "raids"
4279
+ };
4280
+ function categoriesFromListeners(listeners) {
4281
+ const events = {
4282
+ tips: false,
4283
+ bits: false,
4284
+ subs: false,
4285
+ resubs: false,
4286
+ gifts: false,
4287
+ superchats: false,
4288
+ superstickers: false,
4289
+ kicks: false,
4290
+ charity: false,
4291
+ follows: false,
4292
+ raids: false,
4293
+ redemptions: false,
4294
+ chat: false
4295
+ };
4296
+ if (!listeners) return events;
4297
+ const onListeners = Array.isArray(listeners) ? listeners : Object.entries(listeners).filter(([, on]) => on === true).map(([k]) => k);
4298
+ for (const l of onListeners) {
4299
+ const cat = LISTENER_TO_CATEGORY[l];
4300
+ if (cat) events[cat] = true;
4301
+ }
4302
+ if (events.subs) events.resubs = true;
4303
+ return events;
4304
+ }
4305
+ function mapHypeCup(widget) {
4306
+ const v = widget.variables ?? {};
4307
+ const cupX = typeof v.cupPosition?.x === "number" ? clamp01(v.cupPosition.x) : 0.5;
4308
+ const cupY = typeof v.cupPosition?.y === "number" ? clamp01(v.cupPosition.y) : 0.7;
4309
+ const bounce = typeof v.bounce === "number" ? Math.min(1, Math.max(0, v.bounce)) : 0.3;
4310
+ const maxBodies = typeof v.recentEvents === "number" && v.recentEvents > 0 ? Math.min(300, Math.max(10, v.recentEvents)) : 80;
4311
+ const duration = typeof v.duration === "number" && v.duration > 0 ? Math.min(15, Math.max(1, v.duration)) : 3;
4312
+ const sprites = liftSESpritesIntoTiers(v.types);
4313
+ return buildUnit(widget, "tipjar", {
4314
+ content: {
4315
+ version: 1,
4316
+ // Phase 1 ships a single preset; SE's per-cup polygon set isn't lifted
4317
+ // yet (it'd require multi-cup support which we deliberately deferred).
4318
+ // The streamer can swap a custom cup image + wireframe via Settings.
4319
+ preset: "glass-mug",
4320
+ customCup: null,
4321
+ cup: {
4322
+ x: cupX,
4323
+ y: cupY,
4324
+ scale: 1,
4325
+ opacity: 1
4326
+ },
4327
+ physics: {
4328
+ gravity: 1,
4329
+ bounce,
4330
+ friction: 0.06,
4331
+ maxBodies
4332
+ },
4333
+ tokens: {
4334
+ defaultRadius: 26
4335
+ },
4336
+ events: categoriesFromListeners(widget.listeners),
4337
+ chat: {
4338
+ useViewerAvatars: true
4339
+ },
4340
+ display: {
4341
+ showWireframes: !!v.showWireframes,
4342
+ showEventMessages: !!v.textEnabled,
4343
+ messageDuration: duration
4344
+ },
4345
+ sprites,
4346
+ // Phase 2 defaults for goal/break — disabled by import. Streamer
4347
+ // can enable in Settings; SE has no equivalent so nothing to lift.
4348
+ goal: { active: false, currency: "tips", target: 100, current: 0, readoutTemplate: "{{current}} / {{target}}", textColor: "#ffffff", hitAt: null },
4349
+ break: { enabled: false, command: "!breakjar", rebuildDelay: 5e3 },
4350
+ // SE-only knobs that don't have a Lumia equivalent — preserved as
4351
+ // provenance for future phases (per-cup polygons + enter/movement
4352
+ // physics tweaks). `se_types` removed — now consumed into sprites above.
4353
+ se_enter_from: v.enterFrom ?? null,
4354
+ se_prevent_movement: !!v.preventMovement,
4355
+ se_fullscreen: !!v.fullscreen,
4356
+ se_cups: v.cups ?? null
4357
+ },
4358
+ css: {
4359
+ background: "transparent"
4360
+ }
4361
+ });
4362
+ }
4363
+ function clamp01(n) {
4364
+ if (!Number.isFinite(n)) return 0.5;
4365
+ return Math.min(1, Math.max(0, n));
4366
+ }
4367
+
4368
+ // src/se-import/mappers/streamboss.ts
4369
+ var SE_LISTENER_TO_ALERT = {
4370
+ "follower-latest": "twitch-follower",
4371
+ "subscriber-latest": "twitch-subscriber",
4372
+ "tip-latest": "lumiastream-donation",
4373
+ "cheer-latest": "twitch-bits",
4374
+ "raid-latest": "twitch-raid"
4375
+ // `purchase-latest` and `subscriber-gifted-latest` are intentionally absent.
4376
+ // SE rolled gifted subs into `subscriber-latest`; Lumia splits them via
4377
+ // extraSettings.isGift, which the runtime already handles consistently.
4378
+ };
4379
+ var SE_LISTENER_MODE = {
4380
+ "follower-latest": "flat",
4381
+ "subscriber-latest": "flat",
4382
+ "tip-latest": "multiplier",
4383
+ // 100 means "100 dmg per $"
4384
+ "cheer-latest": "multiplier",
4385
+ // 1 means "1 dmg per bit"
4386
+ "raid-latest": "multiplier"
4387
+ // 10 means "10 dmg per viewer"
4388
+ };
4389
+ function listenerIsOn(listeners, key) {
4390
+ if (!listeners) return false;
4391
+ if (Array.isArray(listeners)) return listeners.includes(key);
4392
+ return listeners[key] === true;
4393
+ }
4394
+ function mapStreamBoss(widget) {
4395
+ const v = widget.variables ?? {};
4396
+ const baseHp = typeof v.baseHP === "number" && v.baseHP > 0 ? Math.round(v.baseHP) : 1e3;
4397
+ const seDamage = v.damage ?? {};
4398
+ const damageMap = {};
4399
+ const selectedEvents = [];
4400
+ for (const [seListener, alertKey] of Object.entries(SE_LISTENER_TO_ALERT)) {
4401
+ if (!listenerIsOn(widget.listeners, seListener)) continue;
4402
+ const raw = seDamage[seListener];
4403
+ const value = typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : null;
4404
+ if (value === null) continue;
4405
+ damageMap[alertKey] = { value, mode: SE_LISTENER_MODE[seListener] };
4406
+ selectedEvents.push(alertKey);
4407
+ }
4408
+ const barColor = typeof v.colors?.frame === "string" && v.colors.frame ? v.colors.frame : "#2bff00";
4409
+ return buildUnit(widget, "streamboss", {
4410
+ content: {
4411
+ version: 1,
4412
+ // Theme: SE bit-boss with `showImage: true` reads as the Card layout
4413
+ // (avatar + name + bar). When images are hidden it's effectively
4414
+ // our Bar-Only theme.
4415
+ theme: v.showImage === false ? "bar-only" : "card",
4416
+ initialBossName: "Stream Boss",
4417
+ // SE has no stage cycling — every takeover resets to baseHP. We
4418
+ // honor that by shipping a single-element healthStages array; the
4419
+ // runtime cycles modulo length so a one-element array stays put.
4420
+ healthStages: [baseHp],
4421
+ currentBoss: "Stream Boss",
4422
+ currentBossAvatar: "",
4423
+ currentHP: baseHp,
4424
+ maxHP: baseHp,
4425
+ currentStageIndex: 0,
4426
+ damageMap,
4427
+ // Concrete selectedEvents means "user has opted in to exactly these
4428
+ // events". Empty arrays would disable everything — only happens if
4429
+ // SE had all listeners off, which mirrors what the imported overlay
4430
+ // should do.
4431
+ selectedEvents,
4432
+ bossHealsOnEventTrigger: v.selfHeal !== false,
4433
+ newBossCommand: "!newboss",
4434
+ eventQueueDelayMs: 3e3,
4435
+ defeatedDelayMs: 3e3,
4436
+ showAvatar: v.showImage !== false,
4437
+ showFloatingNumbers: true,
4438
+ showEventMessages: v.showText !== false,
4439
+ barColor,
4440
+ barBackgroundColor: "#333333",
4441
+ attackColor: "#ff007f",
4442
+ healColor: "#00ff00",
4443
+ // Phase 2 defaults — off by default so imports stay close to
4444
+ // SE's behaviour and don't surprise the streamer with new effects.
4445
+ lastResetAt: 0,
4446
+ playSounds: true,
4447
+ audioVolume: 50,
4448
+ damageSounds: [],
4449
+ healSound: "",
4450
+ newBossSound: "",
4451
+ defeatSound: "",
4452
+ megaBossChance: 0,
4453
+ particleScatter: true,
4454
+ showCrown: true,
4455
+ matchBossChatColor: true,
4456
+ bossNameColor: "",
4457
+ botChat: "off",
4458
+ chatAsSelf: false,
4459
+ // SE-only knobs preserved as provenance for future passes. None
4460
+ // have direct Lumia analogs today:
4461
+ // - `right` anchors the boss to the right edge of its bounds;
4462
+ // Lumia uses layer position instead, so this is informational.
4463
+ // - `bossName` controls vertical name position in SE; our themes
4464
+ // don't expose that level of fine-grained tweaking yet.
4465
+ // - `mode` SE's gameplay mode hint ('overkill', etc.).
4466
+ se_right: !!v.right,
4467
+ se_bossNamePosition: typeof v.bossName === "string" ? v.bossName : null,
4468
+ se_mode: typeof v.mode === "string" ? v.mode : null,
4469
+ se_showName: v.showName !== false
4470
+ },
4471
+ css: {
4472
+ background: "transparent"
4473
+ }
4474
+ });
4475
+ }
4476
+
4145
4477
  // src/se-import/feature-flags.ts
4146
4478
  var SE_IMPORT_FLAGS = {
4147
4479
  // (Removed) IMPORT_CUSTOM_EMBED: custom-event-list now routes to native eventlist
4148
4480
  // by default with a one-click "switch to custom HTML" option in the review step.
4149
4481
  // (Removed) IMPORT_MERCH_GOAL: Fourthwall integration provides session/total order
4150
4482
  // 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
4483
+ // to {{fourthwall_total_*}} via the goal mapper (per-listener: orders → order_count,
4484
+ // items items_count, total order_amount).
4485
+ // (Removed) IMPORT_SEASONAL: seasonal widgets now route per-widget into existing
4486
+ // modules (image / slideshow / video) rather than a single approximate canvas-
4487
+ // particle template. See dispatcher.ts SEASONAL_IMAGE_TYPES comment for the
4488
+ // rationale and /SE/05-widget-to-module-mapping.md §D for the current mapping.
4489
+ // (Removed) IMPORT_MERCH_PRODUCTS: the merch-products-rotator now imports as an
4490
+ // empty slideshow with a friendly layer title prompting the user to add product
4491
+ // images manually. This is a degraded but honest UX — better than a placeholder
4492
+ // text layer for the (rare) streamer who uses SE's merch rotator. A Fourthwall
4493
+ // catalog fetcher would unlock fully-automatic import, but that's a multi-day
4494
+ // project gated on a new Fourthwall API auth scope, an endpoint, and a refresh
4495
+ // cadence.
4165
4496
  };
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
- }
4497
+ function isWidgetFlaggedOff(_seType) {
4171
4498
  return false;
4172
4499
  }
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
- };
4500
+ var FLAG_OFF_REASONS = {};
4183
4501
 
4184
4502
  // src/se-import/dispatcher.ts
4185
4503
  var READOUT_TYPES = {
@@ -4208,15 +4526,7 @@ var READOUT_TYPES = {
4208
4526
  // twitch_session_chat_count, so we render it as a text layer with the variable inline.
4209
4527
  "se-widget-botcounter": "twitch_session_chat_count"
4210
4528
  };
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
- };
4529
+ var SEASONAL_IMAGE_TYPES = /* @__PURE__ */ new Set(["se-widget-halloween", "se-widget-xmas", "se-widget-valentine", "se-widget-easter", "se-widget-world-cup"]);
4220
4530
  var GOAL_TYPES = /* @__PURE__ */ new Set([
4221
4531
  "se-widget-follower-goal",
4222
4532
  "se-widget-subscriber-goal",
@@ -4257,10 +4567,17 @@ function dispatch(widget) {
4257
4567
  if (t === "se-widget-current-song") return { unit: mapNowPlaying(widget), status: "partial", lumiaType: "nowplaying" };
4258
4568
  if (t === "se-widget-kappagen") return { unit: mapKappagen(widget), status: "partial", lumiaType: "emotealert" };
4259
4569
  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" };
4570
+ if (t === "se-widget-media-share") return { unit: mapMediaShare(widget), status: "partial", lumiaType: "songrequest" };
4571
+ if (t === "se-widget-hype-cup") return { unit: mapHypeCup(widget), status: "partial", lumiaType: "tipjar" };
4572
+ if (t === "se-widget-bit-boss") return { unit: mapStreamBoss(widget), status: "partial", lumiaType: "streamboss" };
4573
+ if (t === "se-widget-hype-train") return { unit: mapHypetrain(widget), status: "partial", lumiaType: "hypetrain" };
4574
+ if (t === "se-widget-custom-event-list" && isHypetrainCustomWidget(widget)) {
4575
+ return { unit: mapHypetrain(widget), status: "partial", lumiaType: "hypetrain" };
4263
4576
  }
4577
+ if (t === "se-widget-custom-event-list") return { unit: mapCustom(widget), status: "partial", lumiaType: "custom" };
4578
+ if (t === "se-widget-snow") return { unit: mapSnow(widget), status: "direct", lumiaType: "video" };
4579
+ if (t === "se-widget-halloween-2019") return { unit: mapSlideshow(widget), status: "direct", lumiaType: "slideshow" };
4580
+ if (SEASONAL_IMAGE_TYPES.has(t)) return { unit: mapImage(widget), status: "direct", lumiaType: "image" };
4264
4581
  return { unit: mapUnsupportedAsText(widget), status: "placeholder", lumiaType: "text" };
4265
4582
  }
4266
4583
  function recordCoverage(coverage, result, seType) {
@@ -4447,7 +4764,10 @@ var SE_WIDGET_TO_MARKETPLACE_CANDIDATES = {
4447
4764
  // value for marketplace replacements since AI generation rarely produces
4448
4765
  // satisfying results for these.
4449
4766
  "se-widget-hype-cup": [],
4450
- "se-widget-bit-boss": [],
4767
+ // Note: `se-widget-bit-boss` is auto-mapped onto the native `streamboss`
4768
+ // module by the dispatcher and no longer needs a marketplace fallback.
4769
+ // Re-add a key here if a community curated boss overlay later supersedes
4770
+ // the native module for specific use cases.
4451
4771
  "se-widget-train": [],
4452
4772
  // Medium-difficulty widgets — possible to AI-generate but a curated overlay
4453
4773
  // will usually look better.
@@ -4457,6 +4777,15 @@ var SE_WIDGET_TO_MARKETPLACE_CANDIDATES = {
4457
4777
  // have curated replacements for.
4458
4778
  "se-widget-custom-event-list": [],
4459
4779
  "se-widget-botcounter": [],
4780
+ // Hype Train — the dispatcher already auto-routes `se-widget-hype-train` and
4781
+ // hypetrain-only custom-event-lists onto the native `hypetrain` module. This
4782
+ // entry stays here for two cases the auto-router can't catch:
4783
+ // - Heavily customized hype-train custom widgets where the native module
4784
+ // would lose too much visual fidelity (custom locomotive art, themed
4785
+ // particles, etc.) and the streamer would prefer a curated alternative.
4786
+ // - Community curated overlays that pair the native hypetrain with extra
4787
+ // decorative layers (e.g. a "depot" background, station signage).
4788
+ "se-widget-hype-train": [],
4460
4789
  // Flagged-off in feature-flags.ts — the auto-import is disabled because
4461
4790
  // Lumia has no merch data source. Marketplace overlays here would be
4462
4791
  // curated static merch-style goals/sliders that don't depend on a feed.
@@ -4480,6 +4809,123 @@ function hasMarketplaceCandidates(seWidgetType) {
4480
4809
  return getMarketplaceCandidates(seWidgetType).length > 0;
4481
4810
  }
4482
4811
 
4812
+ // src/se-import/api/seClient.ts
4813
+ var SEAuthError = class extends Error {
4814
+ constructor(code, message) {
4815
+ super(message);
4816
+ this.code = code;
4817
+ this.name = "SEAuthError";
4818
+ }
4819
+ };
4820
+ function decodeBase64Url(s) {
4821
+ const b64 = s.replace(/-/g, "+").replace(/_/g, "/");
4822
+ const padding = "===".slice((b64.length + 3) % 4);
4823
+ const padded = b64 + (padding.length < 3 ? padding : "");
4824
+ if (typeof atob === "function") return atob(padded);
4825
+ return Buffer.from(padded, "base64").toString("binary");
4826
+ }
4827
+ function decodeJwtPayload(jwt) {
4828
+ if (typeof jwt !== "string" || !jwt.trim()) {
4829
+ throw new SEAuthError("invalid-jwt", "No token provided.");
4830
+ }
4831
+ const parts = jwt.trim().split(".");
4832
+ if (parts.length !== 3) {
4833
+ throw new SEAuthError("invalid-jwt", "Token is not a valid JWT (expected 3 dot-separated parts).");
4834
+ }
4835
+ let raw;
4836
+ try {
4837
+ raw = decodeBase64Url(parts[1] ?? "");
4838
+ } catch {
4839
+ throw new SEAuthError("invalid-jwt", "Token payload is not valid base64.");
4840
+ }
4841
+ let parsed;
4842
+ try {
4843
+ parsed = JSON.parse(raw);
4844
+ } catch {
4845
+ throw new SEAuthError("invalid-jwt", "Token payload is not valid JSON.");
4846
+ }
4847
+ const claims = parsed;
4848
+ if (typeof claims.channel !== "string" || typeof claims.authToken !== "string") {
4849
+ throw new SEAuthError("invalid-jwt", "Token payload is missing the 'channel' or 'authToken' claim \u2014 not a StreamElements account token.");
4850
+ }
4851
+ if (typeof claims.exp === "number" && claims.exp * 1e3 < Date.now()) {
4852
+ throw new SEAuthError("expired", "This StreamElements token has expired. Generate a new one on the SE Account page.");
4853
+ }
4854
+ return claims;
4855
+ }
4856
+ var SEClient = class _SEClient {
4857
+ constructor(claims) {
4858
+ this.claims = claims;
4859
+ }
4860
+ static fromJwt(jwt) {
4861
+ return new _SEClient(decodeJwtPayload(jwt));
4862
+ }
4863
+ get channelId() {
4864
+ return this.claims.channel;
4865
+ }
4866
+ get provider() {
4867
+ return this.claims.provider;
4868
+ }
4869
+ get providerId() {
4870
+ return this.claims.provider_id;
4871
+ }
4872
+ get userId() {
4873
+ return this.claims.user;
4874
+ }
4875
+ get jti() {
4876
+ return this.claims.jti;
4877
+ }
4878
+ get expiresAt() {
4879
+ return new Date(this.claims.exp * 1e3);
4880
+ }
4881
+ // Convenience for provenance stamping on imported entities. Single source
4882
+ // of truth so every mapper writes the same shape.
4883
+ provenance() {
4884
+ return {
4885
+ jti: this.claims.jti,
4886
+ channelId: this.claims.channel,
4887
+ provider: this.claims.provider,
4888
+ importedAt: (/* @__PURE__ */ new Date()).toISOString()
4889
+ };
4890
+ }
4891
+ async get(path) {
4892
+ const res = await fetch(`https://api.streamelements.com${path}`, {
4893
+ method: "GET",
4894
+ headers: {
4895
+ Authorization: `apikey ${this.claims.authToken}`,
4896
+ Accept: "application/json"
4897
+ }
4898
+ });
4899
+ if (res.status === 401) {
4900
+ throw new SEAuthError("unauthorized", "StreamElements rejected the token. Generate a new one and try again.");
4901
+ }
4902
+ if (res.status === 403) {
4903
+ throw new SEAuthError("forbidden", "Your StreamElements token does not have permission for this resource.");
4904
+ }
4905
+ if (!res.ok) {
4906
+ let detail = "";
4907
+ try {
4908
+ const body = await res.json();
4909
+ if (body?.message) detail = ` \u2014 ${body.message}`;
4910
+ } catch {
4911
+ }
4912
+ throw new Error(`StreamElements API returned HTTP ${res.status}${detail}`);
4913
+ }
4914
+ return await res.json();
4915
+ }
4916
+ };
4917
+ async function fetchUsableChannels(client) {
4918
+ const me = await client.get("/kappa/v2/users/current");
4919
+ return (me.channels ?? []).filter((c) => c.authorized !== false && c.inactive !== true);
4920
+ }
4921
+ async function fetchSEOverlays(client, count = 200) {
4922
+ const res = await client.get(`/kappa/v2/overlays/${client.channelId}?count=${count}`);
4923
+ return res.docs ?? [];
4924
+ }
4925
+ async function fetchSEBootstrapForOverlay(client, overlayId) {
4926
+ return client.get(`/kappa/v2/overlays/${overlayId}/bootstrap?isEditor=false&isMobile=false&isObs=false&isObsLive=false&isXsplit=false`);
4927
+ }
4928
+
4483
4929
  // src/se-import/ui/SEImportWizard.tsx
4484
4930
  import { useCallback as useCallback3, useEffect as useEffect6, useMemo as useMemo5, useRef as useRef4, useState as useState6 } from "react";
4485
4931
 
@@ -4528,7 +4974,7 @@ function MarketplacePicker({ seWidgetType, fetchMarketplaceOverlay, CustomEmbed,
4528
4974
  /* @__PURE__ */ jsx12("code", { children: seWidgetType }),
4529
4975
  " yet. Use AI generation or skip the widget for now."
4530
4976
  ] }),
4531
- /* @__PURE__ */ jsx12("div", { className: "ui-flex-row", style: { marginTop: 8 }, children: /* @__PURE__ */ jsx12("button", { type: "button", className: "ui-button", onClick: onCancel, children: "Close" }) })
4977
+ /* @__PURE__ */ jsx12("div", { className: "ui-flex-row", style: { marginTop: 8 }, children: /* @__PURE__ */ jsx12(LSButton, { type: "button", color: "secondary", onClick: onCancel, label: "Close" }) })
4532
4978
  ] });
4533
4979
  }
4534
4980
  return /* @__PURE__ */ jsxs6("div", { className: "ui-flex-column ui-gap-2", style: { padding: 16, minHeight: 420 }, children: [
@@ -4539,15 +4985,15 @@ function MarketplacePicker({ seWidgetType, fetchMarketplaceOverlay, CustomEmbed,
4539
4985
  " with a layer from a curated Lumia Marketplace overlay."
4540
4986
  ] }),
4541
4987
  /* @__PURE__ */ jsx12("div", { className: "ui-flex-row ui-gap-2", style: { flexWrap: "wrap", marginTop: 4 }, children: candidates.map((c) => /* @__PURE__ */ jsx12(
4542
- "button",
4988
+ LSButton,
4543
4989
  {
4544
4990
  type: "button",
4545
- className: `ui-button ${c.id === activeId ? "ui-button-purple" : ""}`,
4991
+ color: c.id === activeId ? "primary" : "secondary",
4546
4992
  onClick: () => {
4547
4993
  setActiveId(c.id);
4548
4994
  setActiveLayerId(null);
4549
4995
  },
4550
- children: c.loading ? `Loading #${c.id}\u2026` : c.error ? `#${c.id} (error)` : c.overlay?.name ?? `Overlay #${c.id}`
4996
+ label: c.loading ? `Loading #${c.id}\u2026` : c.error ? `#${c.id} (error)` : c.overlay?.name ?? `Overlay #${c.id}`
4551
4997
  },
4552
4998
  c.id
4553
4999
  )) }),
@@ -4562,13 +5008,13 @@ function MarketplacePicker({ seWidgetType, fetchMarketplaceOverlay, CustomEmbed,
4562
5008
  /* @__PURE__ */ jsxs6("div", { style: { flex: "0 0 200px", display: "flex", flexDirection: "column", gap: 4, maxHeight: 320, overflowY: "auto" }, children: [
4563
5009
  /* @__PURE__ */ jsx12("div", { style: { fontSize: 12, fontWeight: 600, opacity: 0.8 }, children: "Layers" }),
4564
5010
  activeLayers.map((l) => /* @__PURE__ */ jsx12(
4565
- "button",
5011
+ LSButton,
4566
5012
  {
4567
5013
  type: "button",
4568
- className: `ui-button ${l.id === activeLayerId ? "ui-button-purple" : ""}`,
5014
+ color: l.id === activeLayerId ? "primary" : "secondary",
4569
5015
  style: { justifyContent: "flex-start", textAlign: "left" },
4570
5016
  onClick: () => setActiveLayerId(l.id),
4571
- children: /* @__PURE__ */ jsxs6("div", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: [
5017
+ label: /* @__PURE__ */ jsxs6("div", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: [
4572
5018
  /* @__PURE__ */ jsx12("div", { style: { fontSize: 12, fontWeight: 600 }, children: l.label }),
4573
5019
  /* @__PURE__ */ jsx12("div", { style: { fontSize: 10, opacity: 0.7 }, children: l.type })
4574
5020
  ] })
@@ -4580,15 +5026,15 @@ function MarketplacePicker({ seWidgetType, fetchMarketplaceOverlay, CustomEmbed,
4580
5026
  /* @__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
5027
  ] }),
4582
5028
  /* @__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" }),
5029
+ /* @__PURE__ */ jsx12(LSButton, { type: "button", color: "secondary", variant: "outlined", onClick: onCancel, label: "Cancel" }),
4584
5030
  /* @__PURE__ */ jsx12(
4585
- "button",
5031
+ LSButton,
4586
5032
  {
4587
5033
  type: "button",
4588
- className: "ui-button ui-button-purple",
5034
+ color: "primary",
4589
5035
  onClick: handleConfirm,
4590
5036
  disabled: !activeCandidate?.overlay || !activeLayerId,
4591
- children: "Use this layer"
5037
+ label: "Use this layer"
4592
5038
  }
4593
5039
  )
4594
5040
  ] })
@@ -4622,51 +5068,141 @@ function ActiveLayerPreview({ overlay, layerId, CustomEmbed }) {
4622
5068
  ] });
4623
5069
  }
4624
5070
 
5071
+ // src/se-import/ui/SEImportWizard.css
5072
+ 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 min-width: min(680px, 100%);\n color: var(--se-text);\n font-family: inherit;\n}\n.se-import__shell {\n border-radius: 0;\n padding: 28px 48px 24px;\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 min-height: 470px;\n place-items: center;\n padding: 64px 0 28px;\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}\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');
5073
+
4625
5074
  // src/se-import/ui/SEImportWizard.tsx
4626
5075
  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
- ] });
5076
+ var sourceSEIcon = new URL("../../assets/source_se.svg", import.meta.url).href;
5077
+ var sourceLumiaIcon = new URL(
5078
+ "../../assets/source_lumia.svg",
5079
+ import.meta.url
5080
+ ).href;
5081
+ var audioFileIcon = new URL("../../assets/audio_file.svg", import.meta.url).href;
5082
+ var imageFileIcon = new URL("../../assets/img_file.svg", import.meta.url).href;
5083
+ var videoFileIcon = new URL("../../assets/video_file.svg", import.meta.url).href;
5084
+ var audioIcon = new URL("../../assets/sound.svg", import.meta.url).href;
5085
+ var imageIcon = new URL("../../assets/img.svg", import.meta.url).href;
5086
+ var videoIcon = new URL("../../assets/video.svg", import.meta.url).href;
5087
+ var CORE_STEPS = [
5088
+ { key: "connect", label: "Connect" },
5089
+ { key: "pick", label: "Overlays" },
5090
+ { key: "url", label: "Load" },
5091
+ { key: "discovery", label: "Discovery" },
5092
+ { key: "options", label: "Options" },
5093
+ { key: "mirror", label: "Assets" },
5094
+ { key: "confirm", label: "Confirm" }
5095
+ ];
5096
+ function stepClass(done, active) {
5097
+ return [
5098
+ "se-import-step",
5099
+ done && "se-import-step--done",
5100
+ active && "se-import-step--active"
5101
+ ].filter(Boolean).join(" ");
4633
5102
  }
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" } }) });
5103
+ function panelClass(extra) {
5104
+ return ["se-import-panel", extra].filter(Boolean).join(" ");
4637
5105
  }
4638
5106
  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 })
5107
+ return /* @__PURE__ */ jsxs7("div", { className: "se-import-error", children: [
5108
+ /* @__PURE__ */ jsx13("div", { className: "se-import-error__title", children: message }),
5109
+ hint && /* @__PURE__ */ jsx13("div", { className: "se-import-error__hint", children: hint })
4642
5110
  ] });
4643
5111
  }
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
- ] })
5112
+ function Field({
5113
+ label,
5114
+ value,
5115
+ onChange,
5116
+ placeholder,
5117
+ disabled
5118
+ }) {
5119
+ return /* @__PURE__ */ jsx13("div", { className: "se-import-field", children: /* @__PURE__ */ jsx13(
5120
+ LSInput,
5121
+ {
5122
+ label,
5123
+ value,
5124
+ onChange: (_, next) => onChange(String(next ?? "")),
5125
+ placeholder,
5126
+ disabled
5127
+ }
5128
+ ) });
5129
+ }
5130
+ function ProgressBar({ value, max }) {
5131
+ return /* @__PURE__ */ jsx13(
5132
+ "progress",
5133
+ {
5134
+ className: "se-import-progress",
5135
+ value: max > 0 ? value : 0,
5136
+ max: max || 1
5137
+ }
5138
+ );
5139
+ }
5140
+ function StatusCard({
5141
+ label,
5142
+ value,
5143
+ sub,
5144
+ tone = "neutral"
5145
+ }) {
5146
+ return /* @__PURE__ */ jsxs7("div", { className: `se-import-stat se-import-stat--${tone}`, children: [
5147
+ /* @__PURE__ */ jsx13("div", { className: "se-import-stat__label", children: label }),
5148
+ /* @__PURE__ */ jsx13("div", { className: "se-import-stat__value", children: value }),
5149
+ sub && /* @__PURE__ */ jsx13("div", { className: "se-import-stat__sub", children: sub })
4653
5150
  ] });
4654
5151
  }
4655
5152
  function CoverageCards({ coverage }) {
4656
5153
  const byStatus = useMemo5(() => {
4657
5154
  const acc = { direct: 0, partial: 0, template: 0, placeholder: 0 };
4658
- for (const m of coverage.mappings) acc[m.status] += m.count;
5155
+ for (const item of coverage.mappings) acc[item.status] += item.count;
4659
5156
  return acc;
4660
5157
  }, [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" })
5158
+ const pct = (value) => coverage.totalWidgets > 0 ? Math.round(value / coverage.totalWidgets * 100) : 0;
5159
+ return /* @__PURE__ */ jsxs7("div", { className: "se-import-grid se-import-grid--four", children: [
5160
+ /* @__PURE__ */ jsx13(
5161
+ StatusCard,
5162
+ {
5163
+ label: "Direct",
5164
+ value: byStatus.direct,
5165
+ sub: `${pct(byStatus.direct)}%`,
5166
+ tone: "success"
5167
+ }
5168
+ ),
5169
+ /* @__PURE__ */ jsx13(
5170
+ StatusCard,
5171
+ {
5172
+ label: "Partial",
5173
+ value: byStatus.partial,
5174
+ sub: `${pct(byStatus.partial)}%`,
5175
+ tone: "info"
5176
+ }
5177
+ ),
5178
+ /* @__PURE__ */ jsx13(
5179
+ StatusCard,
5180
+ {
5181
+ label: "Template",
5182
+ value: byStatus.template,
5183
+ sub: `${pct(byStatus.template)}%`,
5184
+ tone: "warn"
5185
+ }
5186
+ ),
5187
+ /* @__PURE__ */ jsx13(
5188
+ StatusCard,
5189
+ {
5190
+ label: "Placeholder",
5191
+ value: byStatus.placeholder,
5192
+ sub: `${pct(byStatus.placeholder)}%`,
5193
+ tone: "muted"
5194
+ }
5195
+ )
4667
5196
  ] });
4668
5197
  }
4669
- function CustomOverlayPreview({ html, css, js, data, width, height }) {
5198
+ function CustomOverlayPreview({
5199
+ html,
5200
+ css,
5201
+ js,
5202
+ data,
5203
+ width,
5204
+ height
5205
+ }) {
4670
5206
  const srcdoc = useMemo5(() => {
4671
5207
  const safeData = JSON.stringify(data ?? {});
4672
5208
  const safeJs = JSON.stringify(`(async () => { ${js || ""} })()`);
@@ -4682,219 +5218,900 @@ try { eval(${safeJs}); } catch(e) { document.body.insertAdjacentHTML('beforeend'
4682
5218
  "iframe",
4683
5219
  {
4684
5220
  title: "ai-preview",
5221
+ className: "se-import-preview",
4685
5222
  sandbox: "allow-scripts",
4686
5223
  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
5224
  "data-canvas-width": width,
4689
5225
  "data-canvas-height": height
4690
5226
  }
4691
5227
  );
4692
5228
  }
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 })
5229
+ function StepHeader({
5230
+ number,
5231
+ title,
5232
+ subtitle
5233
+ }) {
5234
+ return /* @__PURE__ */ jsxs7("div", { className: "se-import-section-title", children: [
5235
+ /* @__PURE__ */ jsx13("div", { className: "se-import-section-title__num", children: number }),
5236
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-section-title__content", children: [
5237
+ /* @__PURE__ */ jsx13("h3", { children: title }),
5238
+ subtitle && /* @__PURE__ */ jsx13("p", { children: subtitle })
5239
+ ] })
4719
5240
  ] });
4720
5241
  }
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
- ] });
5242
+ var JWT_FEATURES = [
5243
+ "All your overlays \u2014 pick one, many, or all",
5244
+ "Live counters (follower / sub / tip / bits goals)",
5245
+ "Chat bot commands",
5246
+ "Loyalty points + watch time",
5247
+ "Recent activity replay",
5248
+ "Bits leaderboard"
5249
+ ];
5250
+ function ModeCard({
5251
+ primary,
5252
+ title,
5253
+ body,
5254
+ caveat,
5255
+ onClick
5256
+ }) {
5257
+ return /* @__PURE__ */ jsxs7(
5258
+ "button",
5259
+ {
5260
+ type: "button",
5261
+ onClick,
5262
+ style: {
5263
+ display: "flex",
5264
+ flexDirection: "column",
5265
+ alignItems: "flex-start",
5266
+ gap: 14,
5267
+ padding: 24,
5268
+ minHeight: 280,
5269
+ borderRadius: 14,
5270
+ border: primary ? "2px solid var(--se-primary)" : "1px solid var(--se-border)",
5271
+ background: primary ? "rgba(255, 64, 118, 0.08)" : "transparent",
5272
+ color: "var(--se-text)",
5273
+ textAlign: "left",
5274
+ cursor: "pointer",
5275
+ transition: "background 120ms, border-color 120ms, transform 120ms"
5276
+ },
5277
+ onMouseEnter: (e) => {
5278
+ e.currentTarget.style.background = primary ? "rgba(255, 64, 118, 0.14)" : "rgba(255, 255, 255, 0.04)";
5279
+ if (!primary)
5280
+ e.currentTarget.style.borderColor = "var(--se-border-strong)";
5281
+ e.currentTarget.style.transform = "translateY(-2px)";
5282
+ },
5283
+ onMouseLeave: (e) => {
5284
+ e.currentTarget.style.background = primary ? "rgba(255, 64, 118, 0.08)" : "transparent";
5285
+ if (!primary)
5286
+ e.currentTarget.style.borderColor = "var(--se-border)";
5287
+ e.currentTarget.style.transform = "translateY(0)";
5288
+ },
5289
+ children: [
5290
+ /* @__PURE__ */ jsx13(
5291
+ "div",
5292
+ {
5293
+ style: {
5294
+ fontSize: 18,
5295
+ fontWeight: 800,
5296
+ color: primary ? "var(--se-primary)" : "var(--se-text)"
5297
+ },
5298
+ children: title
5299
+ }
5300
+ ),
5301
+ /* @__PURE__ */ jsx13("div", { style: { fontSize: 13.5, lineHeight: 1.55, color: "var(--se-text)", width: "100%" }, children: body }),
5302
+ /* @__PURE__ */ jsx13("div", { style: { fontSize: 12, color: "var(--se-muted)", marginTop: "auto" }, children: caveat })
5303
+ ]
5304
+ }
5305
+ );
4737
5306
  }
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 })
5307
+ function StepModePicker({
5308
+ onChooseJwt,
5309
+ onChooseUrl
5310
+ }) {
5311
+ return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--wide"), children: [
5312
+ /* @__PURE__ */ jsx13(
5313
+ StepHeader,
5314
+ {
5315
+ number: 1,
5316
+ title: "What would you like to import?",
5317
+ subtitle: "Pick a starting point. You can change your mind from the Back button on the next step."
5318
+ }
5319
+ ),
5320
+ /* @__PURE__ */ jsxs7(
5321
+ "div",
5322
+ {
5323
+ style: {
5324
+ display: "grid",
5325
+ gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
5326
+ gap: 18,
5327
+ marginTop: 8
5328
+ },
5329
+ children: [
5330
+ /* @__PURE__ */ jsx13(
5331
+ ModeCard,
5332
+ {
5333
+ primary: true,
5334
+ title: "Import everything from StreamElements",
5335
+ body: /* @__PURE__ */ jsxs7(Fragment3, { children: [
5336
+ /* @__PURE__ */ jsx13("div", { style: { marginBottom: 10 }, children: "Connect once and bring over your entire SE setup:" }),
5337
+ /* @__PURE__ */ jsx13(
5338
+ "ul",
5339
+ {
5340
+ style: {
5341
+ margin: 0,
5342
+ paddingLeft: 18,
5343
+ display: "grid",
5344
+ gap: 6,
5345
+ fontSize: 13
5346
+ },
5347
+ children: JWT_FEATURES.map((feat) => /* @__PURE__ */ jsx13("li", { children: feat }, feat))
5348
+ }
5349
+ )
5350
+ ] }),
5351
+ caveat: "Requires pasting your StreamElements JWT. Stored only in memory \u2014 never sent to Lumia.",
5352
+ onClick: onChooseJwt
5353
+ }
5354
+ ),
5355
+ /* @__PURE__ */ jsx13(
5356
+ ModeCard,
5357
+ {
5358
+ title: "Import a single overlay",
5359
+ body: /* @__PURE__ */ jsxs7(Fragment3, { children: [
5360
+ "Paste a single overlay's public preview URL to bring just that overlay over.",
5361
+ /* @__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." })
5362
+ ] }),
5363
+ caveat: "The URL contains the access token; treat it like a password.",
5364
+ onClick: onChooseUrl
5365
+ }
5366
+ )
5367
+ ]
5368
+ }
5369
+ )
4743
5370
  ] });
4744
5371
  }
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 } }),
5372
+ function StepConnect({
5373
+ jwt,
5374
+ setJwt,
5375
+ error,
5376
+ busy,
5377
+ channels,
5378
+ selectedChannelId,
5379
+ setSelectedChannelId,
5380
+ onUseUrl
5381
+ }) {
5382
+ return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--narrow"), children: [
5383
+ /* @__PURE__ */ jsx13(
5384
+ StepHeader,
5385
+ {
5386
+ number: 1,
5387
+ title: "Connect your StreamElements account",
5388
+ 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."
5389
+ }
5390
+ ),
5391
+ /* @__PURE__ */ jsx13(
5392
+ Field,
5393
+ {
5394
+ label: "StreamElements JWT",
5395
+ value: jwt,
5396
+ onChange: setJwt,
5397
+ placeholder: "eyJhbGciOi...",
5398
+ disabled: busy
5399
+ }
5400
+ ),
5401
+ channels && channels.length > 1 && /* @__PURE__ */ jsxs7("div", { className: "se-import-help se-import-help--info", children: [
5402
+ /* @__PURE__ */ jsx13("div", { className: "se-import-help__icon", children: "i" }),
4750
5403
  /* @__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)"
5404
+ /* @__PURE__ */ jsx13("div", { className: "se-import-help__title", children: "Pick a channel" }),
5405
+ /* @__PURE__ */ jsx13("p", { children: "This account has multiple StreamElements channels. Choose which one to import from." }),
5406
+ /* @__PURE__ */ jsx13("div", { style: { display: "grid", gap: 8, marginTop: 12 }, children: channels.map((c) => /* @__PURE__ */ jsxs7(
5407
+ "label",
5408
+ {
5409
+ style: {
5410
+ display: "flex",
5411
+ alignItems: "center",
5412
+ gap: 10,
5413
+ padding: "8px 12px",
5414
+ border: "1px solid var(--se-border)",
5415
+ borderRadius: 8,
5416
+ background: selectedChannelId === c._id ? "rgba(255, 64, 118, 0.12)" : "transparent",
5417
+ cursor: "pointer"
5418
+ },
5419
+ children: [
5420
+ /* @__PURE__ */ jsx13(
5421
+ "input",
5422
+ {
5423
+ type: "radio",
5424
+ name: "se-channel",
5425
+ checked: selectedChannelId === c._id,
5426
+ onChange: () => setSelectedChannelId(c._id)
5427
+ }
5428
+ ),
5429
+ c.avatar && /* @__PURE__ */ jsx13("img", { src: c.avatar, alt: "", width: 24, height: 24, style: { borderRadius: "50%" } }),
5430
+ /* @__PURE__ */ jsxs7("div", { style: { display: "flex", flexDirection: "column" }, children: [
5431
+ /* @__PURE__ */ jsx13("span", { style: { fontWeight: 700 }, children: c.displayName ?? c.username }),
5432
+ /* @__PURE__ */ jsxs7("span", { style: { fontSize: 12, color: "var(--se-muted)" }, children: [
5433
+ c.provider,
5434
+ " \xB7 ",
5435
+ c.username
5436
+ ] })
5437
+ ] })
5438
+ ]
5439
+ },
5440
+ c._id
5441
+ )) })
5442
+ ] })
5443
+ ] }),
5444
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-help", children: [
5445
+ /* @__PURE__ */ jsx13("div", { className: "se-import-help__icon", children: "i" }),
5446
+ /* @__PURE__ */ jsxs7("div", { children: [
5447
+ /* @__PURE__ */ jsx13("div", { className: "se-import-help__title", children: "How to find your JWT" }),
5448
+ /* @__PURE__ */ jsxs7("ol", { children: [
5449
+ /* @__PURE__ */ jsxs7("li", { children: [
5450
+ "Open",
5451
+ " ",
5452
+ /* @__PURE__ */ jsx13(
5453
+ "a",
5454
+ {
5455
+ href: "https://streamelements.com/dashboard/account/channels",
5456
+ target: "_blank",
5457
+ rel: "noreferrer",
5458
+ children: "streamelements.com/dashboard/account/channels"
5459
+ }
5460
+ ),
5461
+ "."
5462
+ ] }),
5463
+ /* @__PURE__ */ jsxs7("li", { children: [
5464
+ "Click ",
5465
+ /* @__PURE__ */ jsx13("strong", { children: "Show JWT Token" }),
5466
+ " for the channel you want to import."
5467
+ ] }),
5468
+ /* @__PURE__ */ jsx13("li", { children: "Copy the entire token and paste it above." }),
5469
+ /* @__PURE__ */ jsxs7("li", { children: [
5470
+ "When you're done, click ",
5471
+ /* @__PURE__ */ jsx13("strong", { children: "Revoke" }),
5472
+ " on the same page so the token stops working."
4757
5473
  ] })
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
5474
  ] })
4764
5475
  ] })
4765
5476
  ] }),
4766
- /* @__PURE__ */ jsx13(LSInput, { label: "Overlay name", value: options.name, onChange: (e) => setOptions({ ...options, name: e.target.value }) })
4767
- ] });
4768
- }
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
- )
4796
- ] }),
4797
- row.error && /* @__PURE__ */ jsx13("div", { style: { marginTop: 4, color: "#fca5a5" }, children: row.error })
4798
- ] }, row.url)) })
5477
+ /* @__PURE__ */ jsxs7("div", { style: { marginTop: 16, fontSize: 13, color: "var(--se-muted)" }, children: [
5478
+ "Prefer to import just a single overlay without logging in?",
5479
+ " ",
5480
+ /* @__PURE__ */ jsx13(
5481
+ "a",
5482
+ {
5483
+ href: "#",
5484
+ onClick: (e) => {
5485
+ e.preventDefault();
5486
+ onUseUrl();
5487
+ },
5488
+ style: { color: "var(--se-primary)", fontWeight: 700 },
5489
+ children: "Use a public preview URL instead"
5490
+ }
5491
+ ),
5492
+ "."
5493
+ ] }),
5494
+ error && /* @__PURE__ */ jsx13(ErrorPanel, { message: error.message, hint: error.hint })
4799
5495
  ] });
4800
5496
  }
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 })
4813
- ] })
5497
+ function StepPickOverlay({
5498
+ overlays,
5499
+ loading,
5500
+ error,
5501
+ selectedIds,
5502
+ toggleId,
5503
+ selectAll,
5504
+ clearAll,
5505
+ onRetry
5506
+ }) {
5507
+ const selectedCount = selectedIds.size;
5508
+ const total = overlays?.length ?? 0;
5509
+ const allSelected = total > 0 && selectedCount === total;
5510
+ const hasAnySelected = selectedCount > 0;
5511
+ return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--wide"), children: [
5512
+ /* @__PURE__ */ jsx13(
5513
+ StepHeader,
5514
+ {
5515
+ number: 2,
5516
+ title: "Pick the overlays to import",
5517
+ 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.` : ""}`
5518
+ }
5519
+ ),
5520
+ loading && /* @__PURE__ */ jsxs7("div", { className: "se-import-help se-import-help--info", children: [
5521
+ /* @__PURE__ */ jsx13("div", { className: "se-import-help__icon", children: "\u2026" }),
5522
+ /* @__PURE__ */ jsx13("div", { children: "Fetching your StreamElements overlays." })
5523
+ ] }),
5524
+ error && /* @__PURE__ */ jsxs7(Fragment3, { children: [
5525
+ /* @__PURE__ */ jsx13(ErrorPanel, { message: error.message, hint: error.hint }),
5526
+ /* @__PURE__ */ jsx13("div", { style: { marginTop: 12 }, children: /* @__PURE__ */ jsx13(LSButton, { type: "button", color: "secondary", onClick: onRetry, label: "Retry" }) })
5527
+ ] }),
5528
+ !loading && !error && overlays && overlays.length === 0 && /* @__PURE__ */ jsxs7("div", { className: "se-import-help se-import-help--info", children: [
5529
+ /* @__PURE__ */ jsx13("div", { className: "se-import-help__icon", children: "i" }),
5530
+ /* @__PURE__ */ jsxs7("div", { children: [
5531
+ /* @__PURE__ */ jsx13("div", { className: "se-import-help__title", children: "No overlays found" }),
5532
+ /* @__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." })
5533
+ ] })
5534
+ ] }),
5535
+ !loading && !error && overlays && overlays.length > 0 && /* @__PURE__ */ jsxs7(Fragment3, { children: [
5536
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-actions se-import-actions--inline", style: { marginTop: 24, marginBottom: 16, justifyContent: "space-between", alignItems: "center", display: "flex" }, children: [
5537
+ /* @__PURE__ */ jsxs7("label", { style: { display: "inline-flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 14, fontWeight: 600 }, children: [
5538
+ /* @__PURE__ */ jsx13(
5539
+ "input",
5540
+ {
5541
+ type: "checkbox",
5542
+ checked: allSelected,
5543
+ ref: (el) => {
5544
+ if (el) el.indeterminate = !allSelected && hasAnySelected;
5545
+ },
5546
+ onChange: () => allSelected ? clearAll() : selectAll()
5547
+ }
5548
+ ),
5549
+ /* @__PURE__ */ jsxs7("span", { children: [
5550
+ allSelected ? "Deselect all" : "Select all",
5551
+ " (",
5552
+ selectedCount,
5553
+ "/",
5554
+ total,
5555
+ ")"
5556
+ ] })
5557
+ ] }),
5558
+ /* @__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." })
5559
+ ] }),
5560
+ /* @__PURE__ */ jsx13(
5561
+ "div",
5562
+ {
5563
+ className: "se-import-grid",
5564
+ style: {
5565
+ gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))",
5566
+ gap: 12,
5567
+ maxHeight: 480,
5568
+ overflowY: "auto",
5569
+ padding: 4
5570
+ },
5571
+ children: overlays.map((o) => {
5572
+ const selected = selectedIds.has(o._id);
5573
+ return /* @__PURE__ */ jsxs7(
5574
+ "label",
5575
+ {
5576
+ style: {
5577
+ position: "relative",
5578
+ display: "flex",
5579
+ flexDirection: "column",
5580
+ border: selected ? "2px solid var(--se-primary)" : "1px solid var(--se-border)",
5581
+ borderRadius: 10,
5582
+ padding: 10,
5583
+ background: selected ? "rgba(255, 64, 118, 0.08)" : "rgba(28, 32, 66, 0.45)",
5584
+ cursor: "pointer",
5585
+ transition: "border-color 120ms, background 120ms"
5586
+ },
5587
+ children: [
5588
+ /* @__PURE__ */ jsx13(
5589
+ "input",
5590
+ {
5591
+ type: "checkbox",
5592
+ checked: selected,
5593
+ onChange: () => toggleId(o._id),
5594
+ style: { position: "absolute", top: 10, right: 10, accentColor: "var(--se-primary)", zIndex: 1 }
5595
+ }
5596
+ ),
5597
+ o.preview ? /* @__PURE__ */ jsx13(
5598
+ "img",
5599
+ {
5600
+ src: o.preview,
5601
+ alt: "",
5602
+ style: {
5603
+ width: "100%",
5604
+ height: 110,
5605
+ objectFit: "cover",
5606
+ borderRadius: 6,
5607
+ background: "var(--se-panel-2)"
5608
+ },
5609
+ loading: "lazy"
5610
+ }
5611
+ ) : /* @__PURE__ */ jsx13("div", { style: { width: "100%", height: 110, borderRadius: 6, background: "var(--se-panel-2)" } }),
5612
+ /* @__PURE__ */ jsx13("div", { style: { marginTop: 8, fontSize: 14, fontWeight: 700, color: "var(--se-text)", paddingRight: 28 }, children: o.name ?? "Unnamed overlay" }),
5613
+ /* @__PURE__ */ jsxs7("div", { style: { marginTop: 4, fontSize: 11, color: "var(--se-muted)" }, children: [
5614
+ o.type ?? "regular",
5615
+ o.settings?.width && o.settings?.height ? ` \xB7 ${o.settings.width}\xD7${o.settings.height}` : ""
5616
+ ] }),
5617
+ o.updatedAt && /* @__PURE__ */ jsxs7("div", { style: { marginTop: 2, fontSize: 11, color: "var(--se-muted)" }, children: [
5618
+ "Updated ",
5619
+ new Date(o.updatedAt).toLocaleDateString()
5620
+ ] })
5621
+ ]
5622
+ },
5623
+ o._id
5624
+ );
5625
+ })
5626
+ }
5627
+ )
5628
+ ] })
5629
+ ] });
5630
+ }
5631
+ function StepURL({
5632
+ url,
5633
+ setUrl,
5634
+ error,
5635
+ busy
5636
+ }) {
5637
+ return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--narrow"), children: [
5638
+ /* @__PURE__ */ jsx13(
5639
+ StepHeader,
5640
+ {
5641
+ number: 1,
5642
+ title: "Paste your StreamElements overlay URL",
5643
+ subtitle: "This should be the public preview URL of your overlay. It includes the\n access token."
5644
+ }
5645
+ ),
5646
+ /* @__PURE__ */ jsx13(
5647
+ LSInput,
5648
+ {
5649
+ label: "Overlay URL",
5650
+ value: url,
5651
+ onChange: (_, next) => setUrl(String(next ?? "")),
5652
+ placeholder: "https://streamelements.com/overlay/<id>/<token>",
5653
+ disabled: busy
5654
+ }
5655
+ ),
5656
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-help", children: [
5657
+ /* @__PURE__ */ jsx13("div", { className: "se-import-help__icon", children: "i" }),
5658
+ /* @__PURE__ */ jsxs7("div", { children: [
5659
+ /* @__PURE__ */ jsx13("div", { className: "se-import-help__title", children: "How to find the overlay URL" }),
5660
+ /* @__PURE__ */ jsxs7("ol", { children: [
5661
+ /* @__PURE__ */ jsxs7("li", { children: [
5662
+ "Open",
5663
+ " ",
5664
+ /* @__PURE__ */ jsx13(
5665
+ "a",
5666
+ {
5667
+ href: "https://streamelements.com/dashboard/overlays",
5668
+ target: "_blank",
5669
+ rel: "noreferrer",
5670
+ children: "streamelements.com/dashboard/overlays"
5671
+ }
5672
+ ),
5673
+ "."
5674
+ ] }),
5675
+ /* @__PURE__ */ jsx13("li", { children: "Click the overlay you want to bring over." }),
5676
+ /* @__PURE__ */ jsx13("li", { children: "Click Copy URL in the top right of the editor." }),
5677
+ /* @__PURE__ */ jsx13("li", { children: "Paste it above." })
5678
+ ] })
5679
+ ] })
5680
+ ] }),
5681
+ error && /* @__PURE__ */ jsx13(ErrorPanel, { message: error.message, hint: error.hint })
5682
+ ] });
5683
+ }
5684
+ function StepDiscovery({
5685
+ result,
5686
+ assetCount
5687
+ }) {
5688
+ const widgets = result.coverage.totalWidgets;
5689
+ const reviewCount = result.reviewItems.length;
5690
+ const customCount = result.coverage.mappings.find(
5691
+ (item) => item.seType === "se-widget-custom-event-list"
5692
+ )?.count ?? 0;
5693
+ const alertCount = result.coverage.mappings.filter((item) => item.lumiaType === "alert").reduce((sum, item) => sum + item.count, 0);
5694
+ return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--wide"), children: [
5695
+ /* @__PURE__ */ jsx13(StepHeader, { number: 2, title: "Here's what we found in your overlay" }),
5696
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-grid se-import-grid--four", children: [
5697
+ /* @__PURE__ */ jsx13(StatusCard, { label: "Total widgets", value: widgets }),
5698
+ /* @__PURE__ */ jsx13(StatusCard, { label: "Alerts", value: alertCount }),
5699
+ /* @__PURE__ */ jsx13(
5700
+ StatusCard,
5701
+ {
5702
+ label: "Custom widgets",
5703
+ value: customCount,
5704
+ sub: customCount > 0 ? "uses the SE compatibility shim" : void 0
5705
+ }
5706
+ ),
5707
+ /* @__PURE__ */ jsx13(
5708
+ StatusCard,
5709
+ {
5710
+ label: "SE-hosted assets",
5711
+ value: assetCount,
5712
+ sub: assetCount > 0 ? "can be mirrored to Lumia storage" : "no migration needed"
5713
+ }
5714
+ )
5715
+ ] }),
5716
+ /* @__PURE__ */ jsx13(CoverageCards, { coverage: result.coverage }),
5717
+ /* @__PURE__ */ jsx13("div", { className: "se-import-grid se-import-grid--two", children: /* @__PURE__ */ jsx13(
5718
+ StatusCard,
5719
+ {
5720
+ label: "Need review",
5721
+ value: reviewCount,
5722
+ sub: reviewCount > 0 ? "manual choice needed" : "everything looks good",
5723
+ tone: reviewCount > 0 ? "warn" : "success"
5724
+ }
5725
+ ) })
5726
+ ] });
5727
+ }
5728
+ function StepOptions({
5729
+ options,
5730
+ setOptions,
5731
+ assetCount
5732
+ }) {
5733
+ return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--medium"), children: [
5734
+ /* @__PURE__ */ jsx13(StepHeader, { number: 3, title: "Options", subtitle: "Choose how to handle your assets" }),
5735
+ /* @__PURE__ */ jsxs7(
5736
+ "label",
5737
+ {
5738
+ className: `se-import-option ${assetCount === 0 ? "se-import-option--disabled" : ""}`,
5739
+ children: [
5740
+ /* @__PURE__ */ jsx13("span", { className: "se-import-option__icon", children: "\u21A5" }),
5741
+ /* @__PURE__ */ jsxs7("span", { className: "se-import-option__body", children: [
5742
+ /* @__PURE__ */ jsx13("span", { className: "se-import-option__title", children: "Mirror StreamElements assets to Lumia storage" }),
5743
+ /* @__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." })
5744
+ ] }),
5745
+ /* @__PURE__ */ jsxs7("span", { className: "se-import-option__count", children: [
5746
+ assetCount,
5747
+ " files"
5748
+ ] }),
5749
+ /* @__PURE__ */ jsx13(
5750
+ "input",
5751
+ {
5752
+ type: "checkbox",
5753
+ checked: options.mirrorAssets,
5754
+ disabled: assetCount === 0,
5755
+ onChange: (e) => setOptions({ ...options, mirrorAssets: e.target.checked })
5756
+ }
5757
+ ),
5758
+ /* @__PURE__ */ jsx13("span", { className: "se-import-toggle" })
5759
+ ]
5760
+ }
5761
+ ),
5762
+ /* @__PURE__ */ jsx13(
5763
+ LSInput,
5764
+ {
5765
+ label: "Overlay name",
5766
+ value: options.name,
5767
+ onChange: (e) => setOptions({ ...options, name: e.target.value })
5768
+ }
5769
+ )
5770
+ ] });
5771
+ }
5772
+ function mirrorLabel(state) {
5773
+ if (state === "fetching") return "Fetching";
5774
+ if (state === "done") return "Downloaded";
5775
+ if (state === "reused") return "Reused";
5776
+ if (state === "kept-on-cdn") return "Kept on CDN";
5777
+ if (state === "failed") return "Failed";
5778
+ return "Queued";
5779
+ }
5780
+ function fileTypeIcon(url) {
5781
+ const ext = filenameFromURL(url).split(".").pop()?.toLowerCase();
5782
+ if (ext === "mp3" || ext === "wav" || ext === "ogg") return audioIcon;
5783
+ if (ext === "webm" || ext === "mp4" || ext === "mov") return videoIcon;
5784
+ return imageIcon;
5785
+ }
5786
+ function AssetTransferAnimation() {
5787
+ return /* @__PURE__ */ jsxs7("div", { className: "se-import-transfer", "aria-hidden": "true", children: [
5788
+ /* @__PURE__ */ jsx13("div", { className: "se-import-transfer__source se-import-transfer__source--se", children: /* @__PURE__ */ jsx13("img", { src: sourceSEIcon, alt: "" }) }),
5789
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-transfer__files", children: [
5790
+ /* @__PURE__ */ jsx13(
5791
+ "img",
5792
+ {
5793
+ className: "se-import-transfer__file se-import-transfer__file--one",
5794
+ src: imageFileIcon,
5795
+ alt: ""
5796
+ }
5797
+ ),
5798
+ /* @__PURE__ */ jsx13(
5799
+ "img",
5800
+ {
5801
+ className: "se-import-transfer__file se-import-transfer__file--two",
5802
+ src: audioFileIcon,
5803
+ alt: ""
5804
+ }
5805
+ ),
5806
+ /* @__PURE__ */ jsx13(
5807
+ "img",
5808
+ {
5809
+ className: "se-import-transfer__file se-import-transfer__file--three",
5810
+ src: imageFileIcon,
5811
+ alt: ""
5812
+ }
5813
+ ),
5814
+ /* @__PURE__ */ jsx13(
5815
+ "img",
5816
+ {
5817
+ className: "se-import-transfer__file se-import-transfer__file--four",
5818
+ src: videoFileIcon,
5819
+ alt: ""
5820
+ }
5821
+ )
5822
+ ] }),
5823
+ /* @__PURE__ */ jsx13("div", { className: "se-import-transfer__source se-import-transfer__source--lumia", children: /* @__PURE__ */ jsx13("img", { src: sourceLumiaIcon, alt: "" }) })
5824
+ ] });
5825
+ }
5826
+ function StepMirror({
5827
+ rows,
5828
+ running,
5829
+ onRetry
5830
+ }) {
5831
+ const done = rows.filter((row) => row.state === "done").length;
5832
+ const reused = rows.filter((row) => row.state === "reused").length;
5833
+ const keptOnCdn = rows.filter((row) => row.state === "kept-on-cdn").length;
5834
+ const failed = rows.filter((row) => row.state === "failed").length;
5835
+ const completed = done + reused + keptOnCdn + failed;
5836
+ const summary = running ? `Downloading... (${done + reused + keptOnCdn}/${rows.length})` : `Finished. ${done} mirrored${reused ? `, ${reused} reused` : ""}${keptOnCdn ? `, ${keptOnCdn} kept on CDN` : ""}${failed ? `, ${failed} failed` : ""}.`;
5837
+ return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--wide"), children: [
5838
+ /* @__PURE__ */ jsx13(StepHeader, { number: 4, title: "Mirroring assets", subtitle: "We're downloading your assets from StreamElements and preparing them for import." }),
5839
+ /* @__PURE__ */ jsx13(AssetTransferAnimation, {}),
5840
+ /* @__PURE__ */ jsx13("div", { className: "se-import-mirror-head", children: /* @__PURE__ */ jsx13("span", { children: summary }) }),
5841
+ /* @__PURE__ */ jsx13(ProgressBar, { value: completed, max: rows.length }),
5842
+ /* @__PURE__ */ jsx13("div", { className: "se-import-asset-list", children: rows.map((row, index) => /* @__PURE__ */ jsxs7(
5843
+ "div",
5844
+ {
5845
+ className: `se-import-asset-row se-import-asset-row--${row.state}`,
5846
+ children: [
5847
+ /* @__PURE__ */ jsx13(
5848
+ "img",
5849
+ {
5850
+ className: "se-import-asset-row__icon",
5851
+ src: fileTypeIcon(row.url),
5852
+ alt: ""
5853
+ }
5854
+ ),
5855
+ /* @__PURE__ */ jsx13("span", { className: "se-import-asset-row__url", title: row.url, children: row.url }),
5856
+ /* @__PURE__ */ jsx13("span", { className: "se-import-asset-row__state", children: mirrorLabel(row.state) }),
5857
+ row.state === "failed" && /* @__PURE__ */ jsx13(
5858
+ LSButton,
5859
+ {
5860
+ type: "button",
5861
+ variant: "text",
5862
+ color: "secondary",
5863
+ size: "small",
5864
+ className: "se-import-link-button",
5865
+ onClick: () => onRetry(index),
5866
+ label: "Retry"
5867
+ }
5868
+ ),
5869
+ row.error && /* @__PURE__ */ jsx13("span", { className: "se-import-asset-row__error", children: row.error })
5870
+ ]
5871
+ },
5872
+ row.url
5873
+ )) })
5874
+ ] });
5875
+ }
5876
+ function StepReview({
5877
+ item,
5878
+ index,
5879
+ total,
5880
+ state,
5881
+ error,
5882
+ generated,
5883
+ canvas,
5884
+ onSkip,
5885
+ onKeep,
5886
+ onGenerate,
5887
+ onAccept,
5888
+ onRetry,
5889
+ onPickMarketplace,
5890
+ onKeepAllPlaceholders,
5891
+ onSkipAll,
5892
+ remainingCount,
5893
+ marketplaceAvailable
5894
+ }) {
5895
+ return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--medium"), children: [
5896
+ /* @__PURE__ */ jsx13(
5897
+ StepHeader,
5898
+ {
5899
+ number: 4,
5900
+ title: `Review widget ${index + 1} of ${total}`,
5901
+ subtitle: "Choose how to handle this unsupported StreamElements widget"
5902
+ }
5903
+ ),
5904
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-review-card", children: [
5905
+ /* @__PURE__ */ jsxs7("div", { children: [
5906
+ /* @__PURE__ */ jsx13("div", { className: "se-import-review-card__type", children: item.seWidget.type }),
5907
+ /* @__PURE__ */ jsx13("p", { children: item.reason })
5908
+ ] }),
5909
+ /* @__PURE__ */ jsx13("div", { className: "se-import-review-card__status", children: item.flaggedOff ? "Auto-import disabled" : item.status })
4814
5910
  ] }),
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: [
5911
+ remainingCount > 1 && /* Bulk-action row shortcut to wrap up the review step when there are still
5912
+ several unactioned widgets and the user doesn't want to walk each one. */
5913
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-review-bulk", children: [
5914
+ /* @__PURE__ */ jsxs7("span", { children: [
4817
5915
  remainingCount,
4818
5916
  " remaining"
4819
5917
  ] }),
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 })
5918
+ /* @__PURE__ */ jsx13(
5919
+ LSButton,
5920
+ {
5921
+ type: "button",
5922
+ color: "secondary",
5923
+ variant: "contained",
5924
+ onClick: onKeepAllPlaceholders,
5925
+ label: "Keep all placeholders"
5926
+ }
5927
+ ),
5928
+ /* @__PURE__ */ jsx13(
5929
+ LSButton,
5930
+ {
5931
+ type: "button",
5932
+ color: "secondary",
5933
+ variant: "contained",
5934
+ onClick: onSkipAll,
5935
+ label: "Skip all"
5936
+ }
5937
+ )
4826
5938
  ] }),
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" }),
5939
+ state === "generating" && /* @__PURE__ */ jsx13("div", { className: "se-import-muted", children: "Generating with AI. This can take 30-90 seconds." }),
5940
+ state === "generated" && generated && /* @__PURE__ */ jsxs7("div", { className: "se-import-generated", children: [
5941
+ /* @__PURE__ */ jsx13("div", { className: "se-import-generated__title", children: "Preview" }),
4830
5942
  (() => {
4831
5943
  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 });
5944
+ return /* @__PURE__ */ jsx13(
5945
+ CustomOverlayPreview,
5946
+ {
5947
+ html: generated.html ?? "",
5948
+ css: generated.css ?? "",
5949
+ js: generated.js ?? "",
5950
+ data: generated.data ?? {},
5951
+ width: landing.width,
5952
+ height: landing.height
5953
+ }
5954
+ );
4833
5955
  })(),
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
- ] })
5956
+ /* @__PURE__ */ jsxs7("details", { className: "se-import-code", children: [
5957
+ /* @__PURE__ */ jsx13("summary", { children: "Generated code" }),
5958
+ /* @__PURE__ */ jsx13("pre", { children: JSON.stringify(
5959
+ {
5960
+ html: generated.html ?? "",
5961
+ css: generated.css ?? "",
5962
+ js: generated.js ?? ""
5963
+ },
5964
+ null,
5965
+ 2
5966
+ ) })
4848
5967
  ] })
4849
5968
  ] }),
4850
5969
  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) })
5970
+ /* @__PURE__ */ jsxs7("details", { className: "se-import-code", children: [
5971
+ /* @__PURE__ */ jsx13("summary", { children: "Original StreamElements config" }),
5972
+ /* @__PURE__ */ jsx13("pre", { children: JSON.stringify(item.seWidget.variables ?? {}, null, 2) })
4854
5973
  ] }),
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" })
5974
+ /* @__PURE__ */ jsx13("div", { className: "se-import-actions se-import-actions--inline", children: state === "generated" ? /* @__PURE__ */ jsxs7(Fragment3, { children: [
5975
+ /* @__PURE__ */ jsx13(
5976
+ LSButton,
5977
+ {
5978
+ type: "button",
5979
+ color: "secondary",
5980
+ onClick: onRetry,
5981
+ label: "Try again"
5982
+ }
5983
+ ),
5984
+ /* @__PURE__ */ jsx13(
5985
+ LSButton,
5986
+ {
5987
+ type: "button",
5988
+ color: "secondary",
5989
+ onClick: onSkip,
5990
+ label: "Skip this widget"
5991
+ }
5992
+ ),
5993
+ /* @__PURE__ */ jsx13(
5994
+ LSButton,
5995
+ {
5996
+ type: "button",
5997
+ color: "primary",
5998
+ onClick: onAccept,
5999
+ label: "Use this and continue"
6000
+ }
6001
+ )
4859
6002
  ] }) : /* @__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" })
6003
+ !item.flaggedOff && /* @__PURE__ */ jsx13(
6004
+ LSButton,
6005
+ {
6006
+ type: "button",
6007
+ color: "secondary",
6008
+ onClick: onKeep,
6009
+ disabled: state === "generating",
6010
+ label: "Keep placeholder"
6011
+ }
6012
+ ),
6013
+ /* @__PURE__ */ jsx13(
6014
+ LSButton,
6015
+ {
6016
+ type: "button",
6017
+ color: "secondary",
6018
+ onClick: onSkip,
6019
+ disabled: state === "generating",
6020
+ label: "Skip"
6021
+ }
6022
+ ),
6023
+ marketplaceAvailable && /* @__PURE__ */ jsx13(
6024
+ LSButton,
6025
+ {
6026
+ type: "button",
6027
+ color: "secondary",
6028
+ onClick: onPickMarketplace,
6029
+ disabled: state === "generating",
6030
+ label: "Pick from Marketplace"
6031
+ }
6032
+ ),
6033
+ /* @__PURE__ */ jsx13(
6034
+ LSButton,
6035
+ {
6036
+ type: "button",
6037
+ color: "primary",
6038
+ onClick: onGenerate,
6039
+ disabled: state === "generating",
6040
+ label: state === "generating" ? "Generating..." : "Generate with AI"
6041
+ }
6042
+ )
4864
6043
  ] }) }),
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." })
6044
+ item.flaggedOff && !marketplaceAvailable && state !== "generated" && /* @__PURE__ */ jsx13("div", { className: "se-import-muted", children: "No curated Marketplace replacement is configured for this widget yet." })
4866
6045
  ] });
4867
6046
  }
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;
6047
+ function StepConfirm({
6048
+ result,
6049
+ options,
6050
+ mirrorRows,
6051
+ rowStates
6052
+ }) {
6053
+ const widgets = result.coverage.totalWidgets;
6054
+ const alertCount = result.coverage.mappings.filter((item) => item.lumiaType === "alert").reduce((sum, item) => sum + item.count, 0);
6055
+ const mirrored = mirrorRows.filter(
6056
+ (row) => row.state === "done" || row.state === "reused" || row.state === "kept-on-cdn"
6057
+ ).length;
4872
6058
  const reviewBuckets = Object.values(rowStates).reduce(
4873
- (acc, s) => {
4874
- acc[s.state] = (acc[s.state] ?? 0) + 1;
6059
+ (acc, item) => {
6060
+ acc[item.state] = (acc[item.state] ?? 0) + 1;
4875
6061
  return acc;
4876
6062
  },
4877
6063
  {}
4878
6064
  );
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}` })
6065
+ return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--confirm"), children: [
6066
+ /* @__PURE__ */ jsx13(
6067
+ StepHeader,
6068
+ {
6069
+ number: 5,
6070
+ title: "Confirm",
6071
+ subtitle: "Review and confirm your import"
6072
+ }
6073
+ ),
6074
+ /* @__PURE__ */ jsxs7("div", { className: "se-import-overview", children: [
6075
+ /* @__PURE__ */ jsx13("h3", { children: "Overview" }),
6076
+ /* @__PURE__ */ jsx13(Row, { label: "Overlay", value: options.name || result.overlay.name }),
6077
+ /* @__PURE__ */ jsx13(Row, { label: "Widgets", value: `${widgets}` }),
6078
+ /* @__PURE__ */ jsx13(Row, { label: "Alerts", value: `${alertCount}` }),
6079
+ /* @__PURE__ */ jsx13(
6080
+ Row,
6081
+ {
6082
+ label: "Assets to import",
6083
+ value: `${mirrorRows.length}`,
6084
+ meta: options.mirrorAssets ? `Mirrored to Lumia storage (${mirrored})` : "Skipped"
6085
+ }
6086
+ ),
6087
+ /* @__PURE__ */ jsx13(
6088
+ Row,
6089
+ {
6090
+ label: "Import behavior",
6091
+ value: options.mirrorAssets ? "Mirror assets and rewrite URLs" : "Keep original asset URLs"
6092
+ }
6093
+ ),
6094
+ reviewBuckets.generated ? /* @__PURE__ */ jsx13(Row, { label: "Generated with AI", value: `${reviewBuckets.generated}` }) : null,
6095
+ reviewBuckets.skipped ? /* @__PURE__ */ jsx13(Row, { label: "Skipped", value: `${reviewBuckets.skipped}` }) : null,
6096
+ reviewBuckets.kept ? /* @__PURE__ */ jsx13(Row, { label: "Kept as placeholder", value: `${reviewBuckets.kept}` }) : null
4888
6097
  ] })
4889
6098
  ] });
4890
6099
  }
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 })
6100
+ function Row({
6101
+ label,
6102
+ value,
6103
+ meta
6104
+ }) {
6105
+ return /* @__PURE__ */ jsxs7("div", { className: "se-import-row", children: [
6106
+ /* @__PURE__ */ jsx13("span", { children: label }),
6107
+ /* @__PURE__ */ jsx13("strong", { children: value }),
6108
+ meta && /* @__PURE__ */ jsx13("em", { children: meta })
4895
6109
  ] });
4896
6110
  }
4897
- function SEImportWizard({ bindings }) {
6111
+ function SEImportWizard({
6112
+ bindings,
6113
+ initialUrl = ""
6114
+ }) {
4898
6115
  const {
4899
6116
  fetchMarketplaceOverlay,
4900
6117
  generateAICustomCode,
@@ -4908,25 +6125,167 @@ function SEImportWizard({ bindings }) {
4908
6125
  t
4909
6126
  } = bindings;
4910
6127
  const translate = (key, fallback) => t ? t(key) ?? fallback : fallback;
4911
- const [step, setStep] = useState6("url");
4912
- const [url, setUrl] = useState6("");
6128
+ const [step, setStep] = useState6(initialUrl ? "url" : "mode");
6129
+ const [entryMode, setEntryMode] = useState6(
6130
+ initialUrl ? "url" : null
6131
+ );
6132
+ const [jwt, setJwt] = useState6("");
6133
+ const [seClient, setSeClient] = useState6(null);
6134
+ const [channels, setChannels] = useState6(null);
6135
+ const [selectedChannelId, setSelectedChannelId] = useState6(null);
6136
+ const [connectError, setConnectError] = useState6(null);
6137
+ const [connecting, setConnecting] = useState6(false);
6138
+ const [overlayList, setOverlayList] = useState6(null);
6139
+ const [overlaysLoading, setOverlaysLoading] = useState6(false);
6140
+ const [overlaysError, setOverlaysError] = useState6(null);
6141
+ const [selectedOverlayIds, setSelectedOverlayIds] = useState6(/* @__PURE__ */ new Set());
6142
+ const [overlayQueue, setOverlayQueue] = useState6([]);
6143
+ const [currentOverlayIndex, setCurrentOverlayIndex] = useState6(0);
6144
+ const [totalOverlaysToImport, setTotalOverlaysToImport] = useState6(0);
6145
+ const [url, setUrl] = useState6(initialUrl);
4913
6146
  const [loadError, setLoadError] = useState6(null);
4914
6147
  const [loading, setLoading] = useState6(false);
4915
6148
  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]);
6149
+ const [originalReviewItems, setOriginalReviewItems] = useState6(
6150
+ []
6151
+ );
6152
+ const [options, setOptions] = useState6({
6153
+ mirrorAssets: true,
6154
+ name: ""
6155
+ });
4919
6156
  const [mirrorRows, setMirrorRows] = useState6([]);
4920
6157
  const [mirrorRunning, setMirrorRunning] = useState6(false);
4921
- const mirrorStartedRef = useRef4(false);
4922
6158
  const [rowStates, setRowStates] = useState6({});
4923
6159
  const [reviewIndex, setReviewIndex] = useState6(0);
4924
6160
  const [importing, setImporting] = useState6(false);
6161
+ const [marketplacePickerOpen, setMarketplacePickerOpen] = useState6(false);
6162
+ const mirrorStartedRef = useRef4(false);
6163
+ const mirrorAbortRef = useRef4(null);
6164
+ const filenameCacheRef = useRef4(/* @__PURE__ */ new Map());
6165
+ const assetUrls = useMemo5(
6166
+ () => result ? findSEAssetURLs(result.overlay) : [],
6167
+ [result]
6168
+ );
6169
+ const currentItem = originalReviewItems[reviewIndex];
6170
+ const currentRow = currentItem ? rowStates[currentItem.moduleId] ?? { state: "pending" } : void 0;
6171
+ const handleConnect = useCallback3(async () => {
6172
+ setConnectError(null);
6173
+ let client;
6174
+ try {
6175
+ client = SEClient.fromJwt(jwt);
6176
+ } catch (e) {
6177
+ if (e instanceof SEAuthError) {
6178
+ const hint = e.code === "expired" ? "Generate a new JWT on streamelements.com/dashboard/account/channels." : void 0;
6179
+ setConnectError({ message: e.message, hint });
6180
+ } else {
6181
+ setConnectError({
6182
+ message: e instanceof Error ? e.message : "Could not read that token."
6183
+ });
6184
+ }
6185
+ return;
6186
+ }
6187
+ setConnecting(true);
6188
+ try {
6189
+ const usable = await fetchUsableChannels(client);
6190
+ const preferred = usable.find((c) => c._id === client.channelId) ?? usable[0];
6191
+ if (!preferred) {
6192
+ setConnectError({
6193
+ message: "No authorized StreamElements channels found on this account."
6194
+ });
6195
+ return;
6196
+ }
6197
+ setSeClient(client);
6198
+ setChannels(usable);
6199
+ setSelectedChannelId(preferred._id);
6200
+ } catch (e) {
6201
+ if (e instanceof SEAuthError) {
6202
+ const hint = e.code === "unauthorized" ? "Generate a new JWT on streamelements.com/dashboard/account/channels." : void 0;
6203
+ setConnectError({ message: e.message, hint });
6204
+ bindings.onAuthError?.(e);
6205
+ } else {
6206
+ setConnectError({
6207
+ message: e instanceof Error ? e.message : "Could not reach StreamElements."
6208
+ });
6209
+ }
6210
+ } finally {
6211
+ setConnecting(false);
6212
+ }
6213
+ }, [bindings, jwt]);
6214
+ const loadOverlayList = useCallback3(async (client) => {
6215
+ setOverlaysError(null);
6216
+ setOverlaysLoading(true);
6217
+ try {
6218
+ const list = await fetchSEOverlays(client);
6219
+ setOverlayList(list);
6220
+ } catch (e) {
6221
+ if (e instanceof SEAuthError) {
6222
+ setOverlaysError({ message: e.message, hint: e.code === "unauthorized" ? "Generate a new JWT on streamelements.com/dashboard/account/channels." : void 0 });
6223
+ bindings.onAuthError?.(e);
6224
+ } else {
6225
+ setOverlaysError({ message: e instanceof Error ? e.message : "Could not load your StreamElements overlays." });
6226
+ }
6227
+ } finally {
6228
+ setOverlaysLoading(false);
6229
+ }
6230
+ }, [bindings]);
6231
+ const toggleOverlaySelection = useCallback3((id) => {
6232
+ setSelectedOverlayIds((prev) => {
6233
+ const next = new Set(prev);
6234
+ if (next.has(id)) next.delete(id);
6235
+ else next.add(id);
6236
+ return next;
6237
+ });
6238
+ }, []);
6239
+ const selectAllOverlays = useCallback3(() => {
6240
+ setSelectedOverlayIds(new Set((overlayList ?? []).map((o) => o._id)));
6241
+ }, [overlayList]);
6242
+ const clearAllOverlays = useCallback3(() => {
6243
+ setSelectedOverlayIds(/* @__PURE__ */ new Set());
6244
+ }, []);
6245
+ const loadBootstrapAndKickoff = useCallback3(async (client, overlayId) => {
6246
+ setLoadError(null);
6247
+ setLoading(true);
6248
+ try {
6249
+ const bootstrap = await fetchSEBootstrapForOverlay(client, overlayId);
6250
+ if (!isSEBootstrap(bootstrap)) {
6251
+ throw new Error("StreamElements returned a payload that doesn't look like an overlay bootstrap.");
6252
+ }
6253
+ const imported = importSEBootstrap(bootstrap);
6254
+ setResult(imported);
6255
+ setOptions((o) => ({ ...o, name: imported.overlay.name }));
6256
+ setOriginalReviewItems(imported.reviewItems);
6257
+ setRowStates(Object.fromEntries(imported.reviewItems.map((r) => [r.moduleId, { state: "pending" }])));
6258
+ setReviewIndex(0);
6259
+ setMirrorRows([]);
6260
+ mirrorStartedRef.current = false;
6261
+ setStep("discovery");
6262
+ } catch (e) {
6263
+ setLoadError({
6264
+ message: "Could not load the overlay.",
6265
+ hint: e?.message ?? String(e)
6266
+ });
6267
+ } finally {
6268
+ setLoading(false);
6269
+ }
6270
+ }, []);
6271
+ const handlePickConfirm = useCallback3(async () => {
6272
+ if (!seClient) return;
6273
+ const ids = Array.from(selectedOverlayIds);
6274
+ if (ids.length === 0) return;
6275
+ const [first, ...rest] = ids;
6276
+ setOverlayQueue(rest);
6277
+ setTotalOverlaysToImport(ids.length);
6278
+ setCurrentOverlayIndex(1);
6279
+ await loadBootstrapAndKickoff(seClient, first);
6280
+ }, [seClient, selectedOverlayIds, loadBootstrapAndKickoff]);
4925
6281
  const handleLoad = useCallback3(async () => {
4926
6282
  setLoadError(null);
4927
6283
  const parts = extractSEPreviewParts(url);
4928
6284
  if (!parts) {
4929
- setLoadError({ message: "That doesn't look like a StreamElements overlay URL.", hint: "Expected: https://streamelements.com/overlay/<overlayId>/<token>" });
6285
+ setLoadError({
6286
+ message: "That doesn't look like a StreamElements overlay URL.",
6287
+ hint: "Expected: https://streamelements.com/overlay/<overlayId>/<token>"
6288
+ });
4930
6289
  return;
4931
6290
  }
4932
6291
  setLoading(true);
@@ -4934,51 +6293,82 @@ function SEImportWizard({ bindings }) {
4934
6293
  const bootstrap = await fetchSEBootstrap(parts);
4935
6294
  const imported = importSEBootstrap(bootstrap);
4936
6295
  setResult(imported);
4937
- setOptions((o) => ({ ...o, name: imported.overlay.name }));
6296
+ setOptions((previous) => ({ ...previous, name: imported.overlay.name }));
4938
6297
  setOriginalReviewItems(imported.reviewItems);
4939
- setRowStates(Object.fromEntries(imported.reviewItems.map((r) => [r.moduleId, { state: "pending" }])));
6298
+ setRowStates(
6299
+ Object.fromEntries(
6300
+ imported.reviewItems.map((item) => [
6301
+ item.moduleId,
6302
+ { state: "pending" }
6303
+ ])
6304
+ )
6305
+ );
4940
6306
  setReviewIndex(0);
4941
6307
  setStep("discovery");
4942
- } catch (e) {
4943
- setLoadError({ message: "Could not load the overlay.", hint: e?.message });
6308
+ } catch (error) {
6309
+ setLoadError({
6310
+ message: "Could not load the overlay.",
6311
+ hint: error?.message
6312
+ });
4944
6313
  } finally {
4945
6314
  setLoading(false);
4946
6315
  }
4947
6316
  }, [url]);
4948
- const mirrorAbortRef = useRef4(null);
4949
6317
  const processAssetRow = useCallback3(
4950
6318
  async (assetUrl, idx, existingByFilename, signal) => {
4951
6319
  const filename = filenameFromURL(assetUrl);
4952
6320
  const reuseUrl = existingByFilename.get(filename);
4953
6321
  if (reuseUrl) {
4954
- setResult((curr) => curr ? { ...curr, overlay: rewriteAssetURLs(curr.overlay, { [assetUrl]: reuseUrl }) } : curr);
6322
+ setResult(
6323
+ (current) => current ? {
6324
+ ...current,
6325
+ overlay: rewriteAssetURLs(current.overlay, {
6326
+ [assetUrl]: reuseUrl
6327
+ })
6328
+ } : current
6329
+ );
4955
6330
  return { state: "reused", newUrl: reuseUrl };
4956
6331
  }
4957
- if (filename.toLowerCase().endsWith(".svg")) {
6332
+ if (filename.toLowerCase().endsWith(".svg"))
4958
6333
  return { state: "kept-on-cdn", newUrl: assetUrl };
4959
- }
4960
- setMirrorRows((prev) => prev.map((r, i) => i === idx ? { ...r, state: "fetching", error: void 0 } : r));
6334
+ setMirrorRows(
6335
+ (previous) => previous.map(
6336
+ (row, i) => i === idx ? { ...row, state: "fetching", error: void 0 } : row
6337
+ )
6338
+ );
4961
6339
  try {
4962
- const newUrl = await mirrorOneAsset(assetUrl, (file) => uploadAsset(file), signal);
6340
+ const newUrl = await mirrorOneAsset(
6341
+ assetUrl,
6342
+ (file) => uploadAsset(file),
6343
+ signal
6344
+ );
4963
6345
  existingByFilename.set(filename, newUrl);
4964
- setResult((curr) => curr ? { ...curr, overlay: rewriteAssetURLs(curr.overlay, { [assetUrl]: newUrl }) } : curr);
6346
+ setResult(
6347
+ (current) => current ? {
6348
+ ...current,
6349
+ overlay: rewriteAssetURLs(current.overlay, {
6350
+ [assetUrl]: newUrl
6351
+ })
6352
+ } : current
6353
+ );
4965
6354
  return { state: "done", newUrl };
4966
- } catch (err) {
4967
- if (err?.name === "AbortError" || signal?.aborted) {
6355
+ } catch (error) {
6356
+ if (error?.name === "AbortError" || signal?.aborted)
4968
6357
  return { state: "pending" };
4969
- }
4970
- return { state: "failed", error: extractErrorMessage(err) };
6358
+ return { state: "failed", error: extractErrorMessage(error) };
4971
6359
  }
4972
6360
  },
4973
6361
  [uploadAsset]
4974
6362
  );
4975
- const filenameCacheRef = useRef4(/* @__PURE__ */ new Map());
4976
6363
  const runMirror = useCallback3(async () => {
4977
6364
  if (!result) return;
4978
6365
  mirrorAbortRef.current = new AbortController();
4979
6366
  const signal = mirrorAbortRef.current.signal;
4980
6367
  setMirrorRunning(true);
4981
- const initialRows = assetUrls.map((u) => ({ url: u, state: "pending" }));
6368
+ const initialRows = assetUrls.map((assetUrl) => ({
6369
+ url: assetUrl,
6370
+ state: "pending"
6371
+ }));
4982
6372
  setMirrorRows(initialRows);
4983
6373
  const existingByFilename = /* @__PURE__ */ new Map();
4984
6374
  for (const asset of existingAssets ?? []) {
@@ -4987,17 +6377,17 @@ function SEImportWizard({ bindings }) {
4987
6377
  }
4988
6378
  filenameCacheRef.current = existingByFilename;
4989
6379
  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));
6380
+ if (signal.aborted) break;
6381
+ const outcome = await processAssetRow(
6382
+ initialRows[i].url,
6383
+ i,
6384
+ existingByFilename,
6385
+ signal
6386
+ );
6387
+ if (signal.aborted) break;
6388
+ setMirrorRows(
6389
+ (previous) => previous.map((row, idx) => idx === i ? { ...row, ...outcome } : row)
6390
+ );
5001
6391
  }
5002
6392
  setMirrorRunning(false);
5003
6393
  }, [assetUrls, existingAssets, processAssetRow, result]);
@@ -5005,8 +6395,15 @@ function SEImportWizard({ bindings }) {
5005
6395
  async (idx) => {
5006
6396
  const rowUrl = mirrorRows[idx]?.url;
5007
6397
  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));
6398
+ const outcome = await processAssetRow(
6399
+ rowUrl,
6400
+ idx,
6401
+ filenameCacheRef.current,
6402
+ mirrorAbortRef.current?.signal
6403
+ );
6404
+ setMirrorRows(
6405
+ (previous) => previous.map((row, i) => i === idx ? { ...row, ...outcome } : row)
6406
+ );
5010
6407
  },
5011
6408
  [mirrorRows, processAssetRow]
5012
6409
  );
@@ -5016,15 +6413,28 @@ function SEImportWizard({ bindings }) {
5016
6413
  void runMirror();
5017
6414
  }
5018
6415
  }, [step, runMirror]);
5019
- const currentItem = originalReviewItems[reviewIndex];
5020
- const currentRow = currentItem ? rowStates[currentItem.moduleId] ?? { state: "pending" } : void 0;
6416
+ useEffect6(() => {
6417
+ if (step === "pick" && seClient && overlayList === null && !overlaysLoading) {
6418
+ void loadOverlayList(seClient);
6419
+ }
6420
+ }, [step, seClient, overlayList, overlaysLoading, loadOverlayList]);
5021
6421
  const advanceReview = useCallback3(() => {
5022
- if (reviewIndex + 1 < originalReviewItems.length) setReviewIndex(reviewIndex + 1);
6422
+ if (reviewIndex + 1 < originalReviewItems.length)
6423
+ setReviewIndex(reviewIndex + 1);
5023
6424
  else setStep("confirm");
5024
6425
  }, [originalReviewItems.length, reviewIndex]);
5025
- const setRow = useCallback3((moduleId, patch) => {
5026
- setRowStates((prev) => ({ ...prev, [moduleId]: { ...prev[moduleId] ?? { state: "pending" }, ...patch } }));
5027
- }, []);
6426
+ const setRow = useCallback3(
6427
+ (moduleId, patch) => {
6428
+ setRowStates((previous) => ({
6429
+ ...previous,
6430
+ [moduleId]: {
6431
+ ...previous[moduleId] ?? { state: "pending" },
6432
+ ...patch
6433
+ }
6434
+ }));
6435
+ },
6436
+ []
6437
+ );
5028
6438
  const handleKeep = () => {
5029
6439
  if (!result || !currentItem) return;
5030
6440
  setResult(applyReviewAction(result, currentItem.moduleId, "keep"));
@@ -5041,22 +6451,43 @@ function SEImportWizard({ bindings }) {
5041
6451
  if (!currentItem || !result) return;
5042
6452
  setRow(currentItem.moduleId, { state: "generating", error: void 0 });
5043
6453
  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));
6454
+ const canvas = {
6455
+ width: result.overlay.settings.metadata.width,
6456
+ height: result.overlay.settings.metadata.height
6457
+ };
6458
+ const generated = await generateAICustomCode(
6459
+ buildAIPromptForSEWidget(currentItem.seWidget, canvas)
6460
+ );
5046
6461
  setRow(currentItem.moduleId, { state: "generated", generated });
5047
- } catch (e) {
5048
- setRow(currentItem.moduleId, { state: "failed", error: e?.message ?? "AI generation failed." });
6462
+ } catch (error) {
6463
+ setRow(currentItem.moduleId, {
6464
+ state: "failed",
6465
+ error: error?.message ?? "AI generation failed."
6466
+ });
5049
6467
  }
5050
6468
  };
5051
6469
  const handleAcceptGenerated = () => {
5052
6470
  if (!result || !currentItem || !currentRow?.generated) return;
5053
- setResult(applyReviewAction(result, currentItem.moduleId, "ai", currentRow.generated));
6471
+ setResult(
6472
+ applyReviewAction(
6473
+ result,
6474
+ currentItem.moduleId,
6475
+ "ai",
6476
+ currentRow.generated
6477
+ )
6478
+ );
5054
6479
  advanceReview();
5055
6480
  };
5056
- const [marketplacePickerOpen, setMarketplacePickerOpen] = useState6(false);
5057
6481
  const handlePickMarketplace = (transplant) => {
5058
6482
  if (!result || !currentItem) return;
5059
- setResult(applyReviewAction(result, currentItem.moduleId, "marketplace", transplant));
6483
+ setResult(
6484
+ applyReviewAction(
6485
+ result,
6486
+ currentItem.moduleId,
6487
+ "marketplace",
6488
+ transplant
6489
+ )
6490
+ );
5060
6491
  setRow(currentItem.moduleId, { state: "kept" });
5061
6492
  setMarketplacePickerOpen(false);
5062
6493
  advanceReview();
@@ -5066,22 +6497,22 @@ function SEImportWizard({ bindings }) {
5066
6497
  const patch = {};
5067
6498
  for (const item of originalReviewItems) {
5068
6499
  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;
6500
+ if (rowState !== "pending" || item.flaggedOff || item.status !== "placeholder" && item.status !== "template")
6501
+ continue;
5072
6502
  patch[item.moduleId] = { state: "kept" };
5073
6503
  }
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");
6504
+ if (Object.keys(patch).length)
6505
+ setRowStates((previous) => ({ ...previous, ...patch }));
6506
+ const remainingIndex = originalReviewItems.findIndex(
6507
+ (item) => (patch[item.moduleId]?.state ?? rowStates[item.moduleId]?.state ?? "pending") === "pending"
6508
+ );
5076
6509
  if (remainingIndex >= 0) {
5077
6510
  setReviewIndex(remainingIndex);
5078
6511
  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
- }
6512
+ const actionText = remainingItem?.flaggedOff && hasMarketplaceCandidates(remainingItem.seWidget.type) ? "Skip, Marketplace, or Generate with AI" : "Skip or Generate with AI";
6513
+ notify.warning(
6514
+ remainingItem?.flaggedOff ? `Choose ${actionText} for the flagged-off widget.` : "Review the remaining widget before continuing."
6515
+ );
5085
6516
  return;
5086
6517
  }
5087
6518
  setStep("confirm");
@@ -5100,10 +6531,11 @@ function SEImportWizard({ bindings }) {
5100
6531
  setStep("confirm");
5101
6532
  return;
5102
6533
  }
5103
- const nextLayers = (result.overlay.settings.layers ?? []).filter((l) => !toSkip.has(l.id));
6534
+ const nextLayers = (result.overlay.settings.layers ?? []).filter(
6535
+ (layer) => !toSkip.has(layer.id)
6536
+ );
5104
6537
  const modules = { ...result.overlay.settings.modules ?? {} };
5105
6538
  for (const id of toSkip) delete modules[id];
5106
- const reviewItems = result.reviewItems.filter((r) => !toSkip.has(r.moduleId));
5107
6539
  setResult({
5108
6540
  ...result,
5109
6541
  overlay: {
@@ -5111,13 +6543,31 @@ function SEImportWizard({ bindings }) {
5111
6543
  settings: { ...result.overlay.settings, layers: nextLayers, modules },
5112
6544
  layers_count: nextLayers.length
5113
6545
  },
5114
- reviewItems
6546
+ reviewItems: result.reviewItems.filter(
6547
+ (item) => !toSkip.has(item.moduleId)
6548
+ )
5115
6549
  });
5116
- setRowStates((prev) => ({ ...prev, ...patch }));
6550
+ setRowStates((previous) => ({ ...previous, ...patch }));
5117
6551
  setStep("confirm");
5118
6552
  };
5119
6553
  const handleFinalImport = useCallback3(async () => {
5120
- if (!result) return;
6554
+ if (!result || !seClient) {
6555
+ if (!result) return;
6556
+ setImporting(true);
6557
+ try {
6558
+ const body = { name: options.name.trim() || result.overlay.name, description: result.overlay.description, settings: result.overlay.settings };
6559
+ const { uuid } = await saveOverlay(body);
6560
+ notify.success("Imported overlay from StreamElements.");
6561
+ mirrorAbortRef.current?.abort();
6562
+ onClose();
6563
+ if (uuid) onComplete({ overlayId: uuid });
6564
+ } catch (error) {
6565
+ notify.error(`Import failed: ${extractErrorMessage(error)}`);
6566
+ } finally {
6567
+ setImporting(false);
6568
+ }
6569
+ return;
6570
+ }
5121
6571
  setImporting(true);
5122
6572
  try {
5123
6573
  const body = {
@@ -5126,163 +6576,348 @@ function SEImportWizard({ bindings }) {
5126
6576
  settings: result.overlay.settings
5127
6577
  };
5128
6578
  const { uuid } = await saveOverlay(body);
5129
- notify.success(`Imported overlay from StreamElements.`);
6579
+ const hasMore = overlayQueue.length > 0;
6580
+ if (hasMore) {
6581
+ notify.success(`Imported ${currentOverlayIndex} of ${totalOverlaysToImport} overlays from StreamElements.`);
6582
+ const [nextId, ...rest] = overlayQueue;
6583
+ setOverlayQueue(rest);
6584
+ setCurrentOverlayIndex(currentOverlayIndex + 1);
6585
+ setImporting(false);
6586
+ await loadBootstrapAndKickoff(seClient, nextId);
6587
+ return;
6588
+ }
6589
+ notify.success(
6590
+ totalOverlaysToImport > 1 ? `Imported all ${totalOverlaysToImport} overlays from StreamElements.` : "Imported overlay from StreamElements."
6591
+ );
5130
6592
  mirrorAbortRef.current?.abort();
5131
6593
  onClose();
5132
6594
  if (uuid) onComplete({ overlayId: uuid });
5133
- } catch (err) {
5134
- notify.error(`Import failed: ${extractErrorMessage(err)}`);
6595
+ } catch (error) {
6596
+ notify.error(`Import failed: ${extractErrorMessage(error)}`);
5135
6597
  } finally {
5136
6598
  setImporting(false);
5137
6599
  }
5138
- }, [notify, onClose, onComplete, options.name, result, saveOverlay]);
5139
- const stepOrder = ["url", "discovery", "options", "mirror", "review", "confirm"];
6600
+ }, [
6601
+ notify,
6602
+ onClose,
6603
+ onComplete,
6604
+ options.name,
6605
+ result,
6606
+ saveOverlay,
6607
+ seClient,
6608
+ overlayQueue,
6609
+ currentOverlayIndex,
6610
+ totalOverlaysToImport,
6611
+ loadBootstrapAndKickoff
6612
+ ]);
6613
+ const visibleSteps = useMemo5(() => {
6614
+ const showMirrorStep = options.mirrorAssets && (assetUrls.length > 0 || mirrorRows.length > 0 || step === "mirror");
6615
+ if (step === "mode") return [];
6616
+ const onUrlFlow = entryMode === "url" || step === "url";
6617
+ const onJwtFlow = !onUrlFlow;
6618
+ const steps = CORE_STEPS.filter((item) => {
6619
+ if (item.key === "mode") return false;
6620
+ if (item.key === "mirror" && !showMirrorStep) return false;
6621
+ if (item.key === "connect" && !onJwtFlow) return false;
6622
+ if (item.key === "pick" && !onJwtFlow) return false;
6623
+ if (item.key === "url" && !onUrlFlow) return false;
6624
+ return true;
6625
+ });
6626
+ if (originalReviewItems.length > 0)
6627
+ steps.splice(Math.max(steps.length - 1, 0), 0, {
6628
+ key: "review",
6629
+ label: `Review (${originalReviewItems.length})`
6630
+ });
6631
+ return steps;
6632
+ }, [
6633
+ assetUrls.length,
6634
+ entryMode,
6635
+ mirrorRows.length,
6636
+ options.mirrorAssets,
6637
+ originalReviewItems.length,
6638
+ seClient,
6639
+ step
6640
+ ]);
5140
6641
  const goBack = () => {
6642
+ const currentIndex = visibleSteps.findIndex((item) => item.key === step);
5141
6643
  if (step === "review" && reviewIndex > 0) {
5142
6644
  setReviewIndex(reviewIndex - 1);
5143
6645
  return;
5144
6646
  }
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");
6647
+ if (currentIndex === 0 && (step === "connect" || step === "url")) {
6648
+ setStep("mode");
6649
+ setEntryMode(null);
5158
6650
  return;
5159
6651
  }
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");
6652
+ if (currentIndex > 0) setStep(visibleSteps[currentIndex - 1].key);
6653
+ };
6654
+ const goNext = () => {
6655
+ if (step === "connect") {
6656
+ if (!seClient) return void handleConnect();
6657
+ setStep("pick");
5164
6658
  return;
5165
6659
  }
5166
- if (step === "mirror") {
5167
- if (originalReviewItems.length > 0) setStep("review");
5168
- else setStep("confirm");
5169
- return;
6660
+ if (step === "pick") {
6661
+ return void handlePickConfirm();
5170
6662
  }
6663
+ if (step === "url") return void handleLoad();
5171
6664
  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();
6665
+ if (!currentItem) return;
6666
+ if (currentRow?.state === "generated") return handleAcceptGenerated();
6667
+ if (currentRow?.state === "pending") {
6668
+ if (currentItem.flaggedOff) {
6669
+ const actionText = hasMarketplaceCandidates(currentItem.seWidget.type) ? "Skip, Marketplace, or Generate with AI" : "Skip or Generate with AI";
6670
+ notify.warning(`Choose ${actionText} for the flagged-off widget.`);
6671
+ return;
5184
6672
  }
6673
+ return handleKeep();
5185
6674
  }
5186
- return;
5187
- }
5188
- if (step === "confirm") {
5189
- void handleFinalImport();
5190
- return;
6675
+ return advanceReview();
5191
6676
  }
6677
+ if (step === "confirm") return void handleFinalImport();
6678
+ const currentIndex = visibleSteps.findIndex((item) => item.key === step);
6679
+ if (currentIndex >= 0)
6680
+ setStep(visibleSteps[currentIndex + 1]?.key ?? "confirm");
5192
6681
  };
5193
6682
  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";
6683
+ if (step === "connect")
6684
+ return connecting ? "Connecting..." : seClient ? "Continue" : "Connect";
6685
+ if (step === "pick") {
6686
+ if (loading) return "Loading overlay...";
6687
+ const n = selectedOverlayIds.size;
6688
+ return n > 1 ? `Import ${n} overlays` : "Import overlay";
6689
+ }
6690
+ if (step === "url") return loading ? "Loading..." : "Load overlay";
6691
+ if (step === "mirror") return mirrorRunning ? "Working..." : "Continue";
6692
+ if (step === "review")
6693
+ return currentRow?.state === "generated" ? "Use & continue" : "Continue";
6694
+ if (step === "confirm")
6695
+ return importing ? "Importing..." : "Create overlay";
6696
+ return "Continue";
5201
6697
  })();
5202
6698
  const nextDisabled = (() => {
6699
+ if (step === "connect")
6700
+ return connecting || !seClient && !jwt.trim();
6701
+ if (step === "pick")
6702
+ return loading || overlaysLoading || selectedOverlayIds.size === 0;
5203
6703
  if (step === "url") return loading || !url.trim();
5204
6704
  if (step === "mirror") return mirrorRunning;
5205
- if (step === "review") return currentRow?.state === "generating" || currentRow?.state === "pending" && currentItem?.flaggedOff;
6705
+ if (step === "review")
6706
+ return currentRow?.state === "generating" || currentRow?.state === "pending" && currentItem?.flaggedOff;
5206
6707
  if (step === "confirm") return importing;
5207
6708
  return false;
5208
6709
  })();
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,
6710
+ const activeIndex = visibleSteps.findIndex((item) => item.key === step);
6711
+ const batchActive = totalOverlaysToImport > 1 && step !== "mode" && step !== "pick" && step !== "connect" && step !== "url";
6712
+ const currentOverlayName = overlayList?.find((o) => o._id === Array.from(selectedOverlayIds).filter((id) => !overlayQueue.includes(id))[currentOverlayIndex - 1])?.name;
6713
+ return /* @__PURE__ */ jsx13("div", { className: "se-import", children: /* @__PURE__ */ jsxs7("div", { className: "se-import__shell", children: [
6714
+ batchActive && /* @__PURE__ */ jsxs7(
6715
+ "div",
5226
6716
  {
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)
6717
+ style: {
6718
+ marginBottom: 16,
6719
+ padding: "10px 16px",
6720
+ border: "1px solid var(--se-border-strong)",
6721
+ borderRadius: 8,
6722
+ background: "rgba(255, 64, 118, 0.08)",
6723
+ color: "var(--se-text)",
6724
+ fontSize: 13,
6725
+ fontWeight: 600,
6726
+ textAlign: "center"
6727
+ },
6728
+ children: [
6729
+ "Importing overlay ",
6730
+ currentOverlayIndex,
6731
+ " of ",
6732
+ totalOverlaysToImport,
6733
+ currentOverlayName ? ` \xB7 ${currentOverlayName}` : "",
6734
+ overlayQueue.length > 0 ? ` \xB7 ${overlayQueue.length} more queued` : ""
6735
+ ]
5244
6736
  }
5245
6737
  ),
5246
- step === "review" && currentItem && marketplacePickerOpen && /* @__PURE__ */ jsx13(
5247
- MarketplacePicker,
6738
+ 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(
6739
+ "div",
5248
6740
  {
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: [
6741
+ className: stepClass(
6742
+ index < activeIndex,
6743
+ index === activeIndex
6744
+ ),
6745
+ children: [
6746
+ /* @__PURE__ */ jsx13("div", { className: "se-import-step__label", children: item.label }),
6747
+ /* @__PURE__ */ jsx13("div", { className: "se-import-step__line" }),
6748
+ /* @__PURE__ */ jsx13("div", { className: "se-import-step__dot", children: index < activeIndex ? "\u2713" : index + 1 })
6749
+ ]
6750
+ },
6751
+ item.key
6752
+ )) }) }),
6753
+ /* @__PURE__ */ jsxs7("main", { className: "se-import__content", children: [
6754
+ step === "mode" && /* @__PURE__ */ jsx13(
6755
+ StepModePicker,
6756
+ {
6757
+ onChooseJwt: () => {
6758
+ setEntryMode("jwt");
6759
+ setStep("connect");
6760
+ },
6761
+ onChooseUrl: () => {
6762
+ setEntryMode("url");
6763
+ setStep("url");
6764
+ }
6765
+ }
6766
+ ),
6767
+ step === "connect" && /* @__PURE__ */ jsx13(
6768
+ StepConnect,
6769
+ {
6770
+ jwt,
6771
+ setJwt,
6772
+ error: connectError,
6773
+ busy: connecting,
6774
+ channels,
6775
+ selectedChannelId,
6776
+ setSelectedChannelId,
6777
+ onUseUrl: () => {
6778
+ setEntryMode("url");
6779
+ setStep("url");
6780
+ }
6781
+ }
6782
+ ),
6783
+ step === "pick" && /* @__PURE__ */ jsx13(
6784
+ StepPickOverlay,
6785
+ {
6786
+ overlays: overlayList,
6787
+ loading: overlaysLoading,
6788
+ error: overlaysError,
6789
+ selectedIds: selectedOverlayIds,
6790
+ toggleId: toggleOverlaySelection,
6791
+ selectAll: selectAllOverlays,
6792
+ clearAll: clearAllOverlays,
6793
+ onRetry: () => seClient && void loadOverlayList(seClient)
6794
+ }
6795
+ ),
6796
+ step === "url" && /* @__PURE__ */ jsx13(
6797
+ StepURL,
6798
+ {
6799
+ url,
6800
+ setUrl,
6801
+ error: loadError,
6802
+ busy: loading
6803
+ }
6804
+ ),
6805
+ step === "discovery" && result && /* @__PURE__ */ jsx13(StepDiscovery, { result, assetCount: assetUrls.length }),
6806
+ step === "options" && /* @__PURE__ */ jsx13(
6807
+ StepOptions,
6808
+ {
6809
+ options,
6810
+ setOptions,
6811
+ assetCount: assetUrls.length
6812
+ }
6813
+ ),
6814
+ step === "mirror" && /* @__PURE__ */ jsx13(
6815
+ StepMirror,
6816
+ {
6817
+ rows: mirrorRows,
6818
+ running: mirrorRunning,
6819
+ onRetry: retryMirrorRow
6820
+ }
6821
+ ),
6822
+ step === "review" && currentItem && result && !marketplacePickerOpen && /* @__PURE__ */ jsx13(
6823
+ StepReview,
6824
+ {
6825
+ item: currentItem,
6826
+ index: reviewIndex,
6827
+ total: originalReviewItems.length,
6828
+ state: currentRow?.state ?? "pending",
6829
+ error: currentRow?.error,
6830
+ generated: currentRow?.generated,
6831
+ canvas: {
6832
+ width: result.overlay.settings.metadata.width,
6833
+ height: result.overlay.settings.metadata.height
6834
+ },
6835
+ onSkip: handleSkip,
6836
+ onKeep: handleKeep,
6837
+ onGenerate: handleGenerate,
6838
+ onAccept: handleAcceptGenerated,
6839
+ onRetry: handleGenerate,
6840
+ onPickMarketplace: () => setMarketplacePickerOpen(true),
6841
+ onKeepAllPlaceholders: handleKeepAllPlaceholders,
6842
+ onSkipAll: handleSkipAll,
6843
+ remainingCount: originalReviewItems.filter(
6844
+ (item) => (rowStates[item.moduleId]?.state ?? "pending") === "pending"
6845
+ ).length,
6846
+ marketplaceAvailable: hasMarketplaceCandidates(
6847
+ currentItem.seWidget.type
6848
+ )
6849
+ }
6850
+ ),
6851
+ step === "review" && currentItem && marketplacePickerOpen && /* @__PURE__ */ jsx13("div", { className: panelClass("se-import-panel--wide"), children: /* @__PURE__ */ jsx13(
6852
+ MarketplacePicker,
6853
+ {
6854
+ seWidgetType: currentItem.seWidget.type,
6855
+ fetchMarketplaceOverlay,
6856
+ CustomEmbed,
6857
+ onPick: handlePickMarketplace,
6858
+ onCancel: () => setMarketplacePickerOpen(false)
6859
+ }
6860
+ ) }),
6861
+ step === "confirm" && result && /* @__PURE__ */ jsx13(
6862
+ StepConfirm,
6863
+ {
6864
+ result,
6865
+ options,
6866
+ mirrorRows,
6867
+ rowStates
6868
+ }
6869
+ )
6870
+ ] }),
6871
+ /* @__PURE__ */ jsxs7("footer", { className: "se-import-actions", children: [
5258
6872
  /* @__PURE__ */ jsx13(
5259
- "button",
6873
+ LSButton,
5260
6874
  {
5261
6875
  type: "button",
5262
- className: "ui-button",
6876
+ color: "secondary",
5263
6877
  onClick: () => {
5264
6878
  mirrorAbortRef.current?.abort();
5265
6879
  onClose();
5266
6880
  },
5267
6881
  disabled: loading || importing,
5268
- children: translate("action.cancel", "Cancel")
6882
+ label: translate("action.cancel", "Cancel")
6883
+ }
6884
+ ),
6885
+ step !== "mode" && /* @__PURE__ */ jsx13(
6886
+ LSButton,
6887
+ {
6888
+ type: "button",
6889
+ color: "secondary",
6890
+ onClick: goBack,
6891
+ disabled: loading || importing || mirrorRunning || currentRow?.state === "generating",
6892
+ label: "Back"
5269
6893
  }
5270
6894
  ),
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 })
6895
+ step !== "mode" && /* @__PURE__ */ jsx13(
6896
+ LSButton,
6897
+ {
6898
+ type: "button",
6899
+ color: "primary",
6900
+ onClick: goNext,
6901
+ disabled: nextDisabled,
6902
+ label: nextLabel
6903
+ }
6904
+ )
5273
6905
  ] })
5274
- ] });
6906
+ ] }) });
5275
6907
  }
5276
6908
  function extractErrorMessage(err) {
5277
6909
  if (!err) return "Failed";
5278
- const e = err;
5279
- return e?.response?.data?.message || e?.response?.data?.error || e?.message || "Failed";
6910
+ const error = err;
6911
+ return error?.response?.data?.message || error?.response?.data?.error || error?.message || "Failed";
5280
6912
  }
5281
6913
 
5282
6914
  // src/se-import/index.ts
5283
6915
  var REVIEW_REASONS = {
5284
6916
  "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.",
6917
+ // `se-widget-hype-cup` intentionally omitted it now routes to the native
6918
+ // `tipjar` module (see dispatcher.ts) and lands with `partial` status, so it
6919
+ // never reaches the placeholder-reason fallback. If a future feature flag
6920
+ // disables tipjar auto-import, add an entry to FLAG_OFF_REASONS instead.
5286
6921
  "se-widget-train": "No native Lumia equivalent \u2014 animated subscriber/event train.",
5287
6922
  "se-widget-media-share": "No native Lumia equivalent \u2014 viewer-submitted media player.",
5288
6923
  "se-widget-contest": "No native Lumia equivalent \u2014 SE-specific contest widget.",
@@ -5291,13 +6926,17 @@ var REVIEW_REASONS = {
5291
6926
  "se-widget-top-cheerers-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render cheer rankings.',
5292
6927
  "se-widget-top-gifters-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render gifter rankings.',
5293
6928
  "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."
6929
+ // Merch products rotator imports as an empty slideshow with the title
6930
+ // "Merch products (add images manually)". Lumia's Fourthwall integration is
6931
+ // webhook-driven (events flow in) but doesn't fetch the user's shop catalog,
6932
+ // so we can't pre-populate the slideshow items. The streamer adds their
6933
+ // product images by hand in the slideshow editor. Status = 'partial' (see
6934
+ // dispatcher.ts), so this string surfaces in the review step.
6935
+ "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."
6936
+ // Seasonal widgets (snow / halloween / halloween-2019 / xmas / valentine / easter
6937
+ // / world-cup) intentionally omitted — they route directly to image/slideshow/
6938
+ // video modules now (see dispatcher.ts SEASONAL_IMAGE_TYPES), so they always
6939
+ // produce a `direct` status and never need a fallback review reason.
5301
6940
  };
5302
6941
  function reasonFor(seType, status, flaggedOff) {
5303
6942
  if (flaggedOff && FLAG_OFF_REASONS[seType]) return FLAG_OFF_REASONS[seType];
@@ -5403,7 +7042,7 @@ function importSEBootstrap(bootstrap) {
5403
7042
  const module = {
5404
7043
  id: lumiaGroupId,
5405
7044
  loaded: true,
5406
- settings: { title: w.name ?? "Group", type: "group" },
7045
+ settings: { title: defaultLabelForSEWidget(w), type: "group" },
5407
7046
  lights: [],
5408
7047
  css: {},
5409
7048
  content: {
@@ -5623,6 +7262,8 @@ export {
5623
7262
  LSVariableInputProvider,
5624
7263
  MEDIA_PREVIEW_USER_LEVEL_VALUES,
5625
7264
  MarketplacePicker,
7265
+ SEAuthError,
7266
+ SEClient,
5626
7267
  SEImportWizard,
5627
7268
  SE_IMPORT_FLAGS,
5628
7269
  SE_WIDGET_TO_MARKETPLACE_CANDIDATES,
@@ -5631,9 +7272,11 @@ export {
5631
7272
  buildBootstrapUrl,
5632
7273
  buildChatMessageContent,
5633
7274
  codeMirrorlinterOptions,
7275
+ decodeJwtPayload,
5634
7276
  extractSEOverlayId,
5635
7277
  extractSEPreviewParts,
5636
7278
  fetchSEBootstrap,
7279
+ fetchUsableChannels,
5637
7280
  filenameFromURL,
5638
7281
  findSEAssetURLs,
5639
7282
  getAILandingBounds,