@scarlett-player/ui 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -108,6 +108,24 @@ that, the Speed sub-panel is 253px tall against a 211px portrait phone player
108
108
  and loses its Back header and its first three speeds to the host's
109
109
  `overflow: hidden`.
110
110
 
111
+ ## Layout
112
+
113
+ `controls` is the order of slots in the bar. The default is:
114
+
115
+ ```typescript
116
+ uiPlugin({
117
+ controls: [
118
+ 'play', 'skip-backward', 'skip-forward', 'volume', 'time',
119
+ 'live-indicator', 'bandwidth-indicator', 'spacer', 'settings',
120
+ 'captions', 'chromecast', 'airplay', 'pip', 'fullscreen',
121
+ ],
122
+ });
123
+ ```
124
+
125
+ Any id a plugin registers through the control registry (`share`, `chapters`,
126
+ `playlist-previous`, `playlist-next`, ...) can be placed in the same list. A
127
+ control whose plugin is not loaded is skipped.
128
+
111
129
  ## Keyboard Shortcuts
112
130
 
113
131
  | Key | Action |
package/dist/index.cjs CHANGED
@@ -33,7 +33,8 @@ __export(index_exports, {
33
33
  resolveFitItems: () => resolveFitItems,
34
34
  styles: () => styles,
35
35
  uiPlugin: () => uiPlugin,
36
- unregisterControl: () => unregisterControl
36
+ unregisterControl: () => unregisterControl,
37
+ unregisterControlsFor: () => unregisterControlsFor
37
38
  });
38
39
  module.exports = __toCommonJS(index_exports);
39
40
  var import_core2 = require("@scarlett-player/core");
@@ -123,6 +124,10 @@ var styles = `
123
124
  ============================================ */
124
125
  .sp-container {
125
126
  position: relative;
127
+ /* Own stacking context: the control bar, menus and overlays all carry
128
+ z-index, and without this they compete with the host page's own layers
129
+ instead of staying inside the player. */
130
+ isolation: isolate;
126
131
  width: 100%;
127
132
  height: 100%;
128
133
  background: #000;
@@ -244,11 +249,13 @@ var styles = `
244
249
  cursor: pointer;
245
250
  z-index: 10;
246
251
  opacity: 0;
252
+ pointer-events: none;
247
253
  transition: opacity 0.25s ease;
248
254
  }
249
255
 
250
256
  .sp-progress-wrapper--visible {
251
257
  opacity: 1;
258
+ pointer-events: auto;
252
259
  }
253
260
 
254
261
  /* Touch: a 20px wrapper is not a 20px target. The control bar is a later
@@ -1371,7 +1378,7 @@ var BigPlayButton = class {
1371
1378
  * must not sit on top of it, and `error` state alone does not say (a
1372
1379
  * dismissed overlay leaves the error behind)
1373
1380
  */
1374
- constructor(api, isOverlayVisible = () => false) {
1381
+ constructor(api, isOverlayVisible) {
1375
1382
  /**
1376
1383
  * Latched on the first `playing`.
1377
1384
  *
@@ -1385,7 +1392,8 @@ var BigPlayButton = class {
1385
1392
  this.start();
1386
1393
  };
1387
1394
  this.api = api;
1388
- this.isOverlayVisible = isOverlayVisible;
1395
+ this.hasOverlayProbe = typeof isOverlayVisible === "function";
1396
+ this.isOverlayVisible = isOverlayVisible ?? (() => false);
1389
1397
  const btn = document.createElement("button");
1390
1398
  btn.className = "sp-big-play";
1391
1399
  btn.setAttribute("type", "button");
@@ -1414,7 +1422,7 @@ var BigPlayButton = class {
1414
1422
  this.hasStarted = true;
1415
1423
  }
1416
1424
  let visible;
1417
- if (error || this.isOverlayVisible()) {
1425
+ if (this.hasOverlayProbe ? this.isOverlayVisible() : Boolean(error)) {
1418
1426
  visible = false;
1419
1427
  } else if (playbackState === "loading") {
1420
1428
  visible = false;
@@ -1840,15 +1848,17 @@ var ProgressBar = class {
1840
1848
  const seekableRange = this.api.getState("seekableRange");
1841
1849
  if (live && seekableRange) {
1842
1850
  const rangeLength = seekableRange.end - seekableRange.start;
1843
- return seekableRange.start + percent * rangeLength;
1851
+ const time2 = seekableRange.start + percent * rangeLength;
1852
+ return Number.isFinite(time2) ? time2 : null;
1844
1853
  }
1845
1854
  const duration = this.api.getState("duration") || 0;
1846
- return percent * duration;
1855
+ const time = percent * duration;
1856
+ return Number.isFinite(time) ? time : null;
1847
1857
  }
1848
1858
  updateTooltip(clientX) {
1849
1859
  const rect = this.el.getBoundingClientRect();
1850
1860
  const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
1851
- const time = this.getTimeFromPosition(clientX);
1861
+ const time = this.getTimeFromPosition(clientX) ?? 0;
1852
1862
  const live = this.api.getState("live");
1853
1863
  const seekableRange = this.api.getState("seekableRange");
1854
1864
  if (live && seekableRange) {
@@ -1883,7 +1893,9 @@ var ProgressBar = class {
1883
1893
  }
1884
1894
  this.lastSeekTime = now;
1885
1895
  const time = this.getTimeFromPosition(clientX);
1886
- video.currentTime = time;
1896
+ if (time !== null && Number.isFinite(time)) {
1897
+ video.currentTime = time;
1898
+ }
1887
1899
  }
1888
1900
  destroy() {
1889
1901
  this.wrapper.removeEventListener("mousedown", this.onMouseDown);
@@ -2114,11 +2126,9 @@ var LiveIndicator = class {
2114
2126
  }
2115
2127
  }
2116
2128
  seekToLive() {
2117
- const video = getVideo(this.api.container);
2118
- if (!video) return;
2119
2129
  const seekableRange = this.api.getState("seekableRange");
2120
2130
  if (seekableRange) {
2121
- video.currentTime = seekableRange.end;
2131
+ this.api.emit("playback:seeking", { time: seekableRange.end });
2122
2132
  }
2123
2133
  }
2124
2134
  destroy() {
@@ -2164,6 +2174,9 @@ var QualityMenu = class {
2164
2174
  const qualities = this.api.getState("qualities") || [];
2165
2175
  const currentQuality = this.api.getState("currentQuality");
2166
2176
  this.el.style.display = qualities.length > 0 ? "" : "none";
2177
+ if (qualities.length === 0 && this.isOpen) {
2178
+ this.close();
2179
+ }
2167
2180
  this.btnLabel.textContent = currentQuality?.label || "Auto";
2168
2181
  const qualitiesJson = JSON.stringify(qualities.map((q) => q.id));
2169
2182
  const currentId = currentQuality?.id || "auto";
@@ -2228,6 +2241,16 @@ var QualityMenu = class {
2228
2241
  this.menu.classList.remove("sp-quality-menu--open");
2229
2242
  this.btn.setAttribute("aria-expanded", "false");
2230
2243
  }
2244
+ /**
2245
+ * Whether the dropdown is open.
2246
+ *
2247
+ * Read by the UI plugin so the control bar's auto-hide waits for it.
2248
+ *
2249
+ * @returns True while the menu is showing
2250
+ */
2251
+ isMenuOpen() {
2252
+ return this.isOpen;
2253
+ }
2231
2254
  destroy() {
2232
2255
  document.removeEventListener("click", this.closeHandler);
2233
2256
  this.el.remove();
@@ -2652,7 +2675,7 @@ var SettingsMenu = class {
2652
2675
  this.el.appendChild(this.panel);
2653
2676
  this.closeHandler = (e) => {
2654
2677
  if (!this.el.contains(e.target)) {
2655
- this.close();
2678
+ this.close(false);
2656
2679
  }
2657
2680
  };
2658
2681
  document.addEventListener("click", this.closeHandler);
@@ -2665,7 +2688,6 @@ var SettingsMenu = class {
2665
2688
  this.showPanel("main");
2666
2689
  } else {
2667
2690
  this.close();
2668
- this.btn.focus();
2669
2691
  }
2670
2692
  return;
2671
2693
  }
@@ -2702,6 +2724,8 @@ var SettingsMenu = class {
2702
2724
  this.updateSpeedActiveStates();
2703
2725
  } else if (this.currentPanel === "captions") {
2704
2726
  this.updateCaptionsActiveStates();
2727
+ } else if (this.currentPanel === "audio") {
2728
+ this.updateAudioActiveStates();
2705
2729
  }
2706
2730
  }
2707
2731
  }
@@ -2716,11 +2740,25 @@ var SettingsMenu = class {
2716
2740
  this.btn.setAttribute("aria-expanded", "true");
2717
2741
  this.focusFirstItem();
2718
2742
  }
2719
- close() {
2743
+ /**
2744
+ * Close the panel.
2745
+ *
2746
+ * Choosing an item tears down the row that had focus, which drops focus to
2747
+ * `<body>` and strands keyboard viewers outside the player entirely. Hand it
2748
+ * back to the button that opened the menu instead.
2749
+ *
2750
+ * @param restoreFocus - Return focus to the gear button. The outside-click
2751
+ * handler passes false, so a click elsewhere is not pulled back into the bar.
2752
+ */
2753
+ close(restoreFocus = true) {
2754
+ const wasOpen = this.isOpen;
2720
2755
  this.isOpen = false;
2721
2756
  this.currentPanel = "main";
2722
2757
  this.panel.classList.remove("sp-settings-panel--open");
2723
2758
  this.btn.setAttribute("aria-expanded", "false");
2759
+ if (wasOpen && restoreFocus) {
2760
+ this.btn.focus();
2761
+ }
2724
2762
  }
2725
2763
  showPanel(panel) {
2726
2764
  this.currentPanel = panel;
@@ -2737,6 +2775,9 @@ var SettingsMenu = class {
2737
2775
  case "captions":
2738
2776
  this.renderCaptionsPanel();
2739
2777
  break;
2778
+ case "audio":
2779
+ this.renderAudioPanel();
2780
+ break;
2740
2781
  }
2741
2782
  this.focusFirstItem();
2742
2783
  }
@@ -2765,6 +2806,16 @@ var SettingsMenu = class {
2765
2806
  );
2766
2807
  this.panel.appendChild(captionsRow);
2767
2808
  }
2809
+ const audioTracks = this.api.getState("audioTracks") || [];
2810
+ if (audioTracks.length > 1) {
2811
+ const currentAudioTrack = this.api.getState("currentAudioTrack");
2812
+ const audioRow = this.createMainRow(
2813
+ "Audio",
2814
+ currentAudioTrack?.label || "Default",
2815
+ () => this.showPanel("audio")
2816
+ );
2817
+ this.panel.appendChild(audioRow);
2818
+ }
2768
2819
  const speedLabel = playbackRate === 1 ? "Normal" : `${playbackRate}x`;
2769
2820
  const speedRow = this.createMainRow(
2770
2821
  "Speed",
@@ -2865,6 +2916,36 @@ var SettingsMenu = class {
2865
2916
  this.panel.appendChild(item);
2866
2917
  }
2867
2918
  }
2919
+ renderAudioPanel() {
2920
+ this.panel.innerHTML = "";
2921
+ this.panel.className = "sp-settings-panel sp-settings-panel--open sp-settings-panel--sub";
2922
+ const header = this.createSubHeader("Audio");
2923
+ this.panel.appendChild(header);
2924
+ const audioTracks = this.api.getState("audioTracks") || [];
2925
+ const currentAudioTrack = this.api.getState("currentAudioTrack");
2926
+ const activeId = currentAudioTrack?.id ?? null;
2927
+ for (const track of audioTracks) {
2928
+ const item = this.createMenuItem(track.label, track.id, track.id === activeId);
2929
+ item.addEventListener("click", (e) => {
2930
+ e.preventDefault();
2931
+ this.selectAudioTrack(track.id);
2932
+ });
2933
+ this.panel.appendChild(item);
2934
+ }
2935
+ }
2936
+ selectAudioTrack(trackId) {
2937
+ this.api.emit("track:audio", { trackId });
2938
+ this.close();
2939
+ }
2940
+ updateAudioActiveStates() {
2941
+ const currentAudioTrack = this.api.getState("currentAudioTrack");
2942
+ const activeId = currentAudioTrack?.id ?? null;
2943
+ const items = this.panel.querySelectorAll(".sp-settings-panel__item");
2944
+ items.forEach((item) => {
2945
+ const id = item.getAttribute("data-id");
2946
+ item.classList.toggle("sp-settings-panel__item--active", id === activeId);
2947
+ });
2948
+ }
2868
2949
  selectCaption(trackId) {
2869
2950
  this.api.emit("track:text", { trackId });
2870
2951
  this.close();
@@ -3254,6 +3335,17 @@ var OverflowTray = class {
3254
3335
  this.panel.classList.remove("sp-overflow-tray--open");
3255
3336
  this.btn.setAttribute("aria-expanded", "false");
3256
3337
  }
3338
+ /**
3339
+ * Whether the strip is open.
3340
+ *
3341
+ * Read by the UI plugin so the control bar's auto-hide waits for it: hiding
3342
+ * the bar would take the open tray with it, mid-use.
3343
+ *
3344
+ * @returns True while the strip is showing
3345
+ */
3346
+ isMenuOpen() {
3347
+ return this.isOpen;
3348
+ }
3257
3349
  /**
3258
3350
  * Show the button only while the tray holds something the viewer can see.
3259
3351
  *
@@ -3293,19 +3385,53 @@ var OverflowTray = class {
3293
3385
  };
3294
3386
 
3295
3387
  // src/control-registry.ts
3296
- var registry = /* @__PURE__ */ new Map();
3388
+ var globalRegistry = /* @__PURE__ */ new Map();
3389
+ var scopedRegistries = /* @__PURE__ */ new Map();
3297
3390
  var listeners = /* @__PURE__ */ new Set();
3298
- function registerControl(id, factory) {
3299
- registry.set(id, factory);
3391
+ function registerControl(id, factory, options = {}) {
3392
+ const { owner } = options;
3393
+ if (owner) {
3394
+ let scoped = scopedRegistries.get(owner);
3395
+ if (!scoped) {
3396
+ scoped = /* @__PURE__ */ new Map();
3397
+ scopedRegistries.set(owner, scoped);
3398
+ }
3399
+ scoped.set(id, factory);
3400
+ } else {
3401
+ globalRegistry.set(id, factory);
3402
+ }
3300
3403
  for (const listener of listeners) {
3301
- listener(id);
3404
+ listener(id, owner);
3302
3405
  }
3303
3406
  }
3304
- function unregisterControl(id) {
3305
- return registry.delete(id);
3407
+ function unregisterControl(id, options = {}) {
3408
+ const { owner } = options;
3409
+ if (!owner) {
3410
+ return globalRegistry.delete(id);
3411
+ }
3412
+ const scoped = scopedRegistries.get(owner);
3413
+ if (!scoped) {
3414
+ return false;
3415
+ }
3416
+ const removed = scoped.delete(id);
3417
+ if (scoped.size === 0) {
3418
+ scopedRegistries.delete(owner);
3419
+ }
3420
+ return removed;
3421
+ }
3422
+ function unregisterControlsFor(owner) {
3423
+ const removed = scopedRegistries.get(owner)?.size ?? 0;
3424
+ scopedRegistries.delete(owner);
3425
+ return removed;
3306
3426
  }
3307
- function getControlFactory(id) {
3308
- return registry.get(id) ?? null;
3427
+ function getControlFactory(id, owner) {
3428
+ if (owner) {
3429
+ const scoped = scopedRegistries.get(owner)?.get(id);
3430
+ if (scoped) {
3431
+ return scoped;
3432
+ }
3433
+ }
3434
+ return globalRegistry.get(id) ?? null;
3309
3435
  }
3310
3436
  function onControlRegistered(listener) {
3311
3437
  listeners.add(listener);
@@ -3314,12 +3440,13 @@ function onControlRegistered(listener) {
3314
3440
  };
3315
3441
  }
3316
3442
  function resetControlRegistry() {
3317
- registry.clear();
3443
+ globalRegistry.clear();
3444
+ scopedRegistries.clear();
3318
3445
  listeners.clear();
3319
3446
  }
3320
3447
 
3321
3448
  // src/version.ts
3322
- var PKG_VERSION = true ? "1.7.1" : "0.0.0-dev";
3449
+ var PKG_VERSION = true ? "1.9.0" : "0.0.0-dev";
3323
3450
 
3324
3451
  // src/index.ts
3325
3452
  var DEFAULT_LAYOUT = [
@@ -3338,7 +3465,19 @@ var DEFAULT_LAYOUT = [
3338
3465
  "pip",
3339
3466
  "fullscreen"
3340
3467
  ];
3468
+ var STYLE_ID = "sp-ui-styles";
3341
3469
  var DEFAULT_HIDE_DELAY = 3e3;
3470
+ var ACTIVATION_KEYS = /* @__PURE__ */ new Set([" ", "Enter"]);
3471
+ var SLIDER_KEYS = /* @__PURE__ */ new Set([
3472
+ "ArrowLeft",
3473
+ "ArrowRight",
3474
+ "ArrowUp",
3475
+ "ArrowDown",
3476
+ "Home",
3477
+ "End",
3478
+ "PageUp",
3479
+ "PageDown"
3480
+ ]);
3342
3481
  var UNMEASURED_CONTROL_WIDTH = 48;
3343
3482
  var FALLBACK_VOLUME_SLIDER_WIDTH = 64;
3344
3483
  var OVERFLOW_BUTTON_WIDTH = 44;
@@ -3355,7 +3494,7 @@ function uiPlugin(config = {}) {
3355
3494
  let bufferingIndicator = null;
3356
3495
  let errorOverlay = null;
3357
3496
  let bigPlayButton = null;
3358
- let styleEl = null;
3497
+ let releaseStyles = null;
3359
3498
  let controls = [];
3360
3499
  let hideTimeout = null;
3361
3500
  let stateUnsubscribe = null;
@@ -3363,6 +3502,7 @@ function uiPlugin(config = {}) {
3363
3502
  let errorUnsubscribe = null;
3364
3503
  let reconnectingUnsubscribe = null;
3365
3504
  let recoveredUnsubscribe = null;
3505
+ let loadedUnsubscribe = null;
3366
3506
  let controlsVisible = true;
3367
3507
  let rafHandle = null;
3368
3508
  let tray = null;
@@ -3374,6 +3514,7 @@ function uiPlugin(config = {}) {
3374
3514
  let volumeSliderWidth = FALLBACK_VOLUME_SLIDER_WIDTH;
3375
3515
  let lastFitSignature = null;
3376
3516
  let fitPending = true;
3517
+ let addedContainerClass = false;
3377
3518
  const layout = config.controls || DEFAULT_LAYOUT;
3378
3519
  const hideDelay = config.hideDelay ?? DEFAULT_HIDE_DELAY;
3379
3520
  const showBigPlayButton = config.bigPlayButton !== false;
@@ -3416,7 +3557,7 @@ function uiPlugin(config = {}) {
3416
3557
  case "spacer":
3417
3558
  return new Spacer();
3418
3559
  default: {
3419
- const factory = getControlFactory(slot);
3560
+ const factory = getControlFactory(slot, api.container);
3420
3561
  if (factory) {
3421
3562
  try {
3422
3563
  return factory(api);
@@ -3643,6 +3784,10 @@ function uiPlugin(config = {}) {
3643
3784
  updateControls();
3644
3785
  });
3645
3786
  };
3787
+ const hasOpenMenu = () => controls.some((control) => {
3788
+ const menu = control;
3789
+ return typeof menu.isMenuOpen === "function" && menu.isMenuOpen();
3790
+ });
3646
3791
  const showControls = () => {
3647
3792
  if (controlsVisible) {
3648
3793
  resetHideTimer();
@@ -3659,6 +3804,10 @@ function uiPlugin(config = {}) {
3659
3804
  const hideControls = () => {
3660
3805
  const paused = api?.getState("paused");
3661
3806
  if (paused) return;
3807
+ if (hasOpenMenu()) {
3808
+ resetHideTimer();
3809
+ return;
3810
+ }
3662
3811
  controlsVisible = false;
3663
3812
  controlBar?.classList.remove("sp-controls--visible");
3664
3813
  controlBar?.classList.add("sp-controls--hidden");
@@ -3676,22 +3825,33 @@ function uiPlugin(config = {}) {
3676
3825
  const handlePointerActivity = (event) => {
3677
3826
  last_pointer_type = event.pointerType;
3678
3827
  };
3679
- const handleInteraction = () => {
3680
- if (last_pointer_type === "touch") {
3828
+ const handleInteraction = (event) => {
3829
+ if (last_pointer_type === "touch" && !isOnControls(event)) {
3681
3830
  const gestures = api?.getPlugin("gestures");
3682
3831
  if (gestures?.ownsTapInteraction()) return;
3683
3832
  }
3684
3833
  showControls();
3685
3834
  };
3835
+ const isOnControls = (event) => {
3836
+ const target = event?.target;
3837
+ if (!(target instanceof Node)) return false;
3838
+ return Boolean(controlBar?.contains(target)) || Boolean(progressBar?.render().contains(target));
3839
+ };
3686
3840
  const handleMouseLeave = () => {
3687
3841
  hideControls();
3688
3842
  };
3689
3843
  const handleKeyDown = (e) => {
3690
3844
  if (!api.container.contains(document.activeElement)) return;
3845
+ if (e.defaultPrevented) return;
3846
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
3691
3847
  const activeEl = document.activeElement;
3692
3848
  if (activeEl instanceof HTMLInputElement || activeEl instanceof HTMLTextAreaElement || activeEl instanceof HTMLSelectElement || activeEl?.isContentEditable) {
3693
3849
  return;
3694
3850
  }
3851
+ const role = activeEl?.getAttribute("role");
3852
+ const isActivatable = activeEl instanceof HTMLButtonElement || role === "button" || role === "menuitem";
3853
+ if (isActivatable && ACTIVATION_KEYS.has(e.key)) return;
3854
+ if (role === "slider" && SLIDER_KEYS.has(e.key)) return;
3695
3855
  const video = api.container.querySelector("video");
3696
3856
  if (!video) return;
3697
3857
  const live = api.getState("live");
@@ -3758,9 +3918,7 @@ function uiPlugin(config = {}) {
3758
3918
  version: PKG_VERSION,
3759
3919
  async init(pluginApi) {
3760
3920
  api = pluginApi;
3761
- styleEl = document.createElement("style");
3762
- styleEl.textContent = styles;
3763
- document.head.appendChild(styleEl);
3921
+ releaseStyles = (0, import_core2.injectSharedStyles)(STYLE_ID, styles);
3764
3922
  if (config.theme) {
3765
3923
  this.setTheme(config.theme);
3766
3924
  }
@@ -3773,6 +3931,10 @@ function uiPlugin(config = {}) {
3773
3931
  if (containerStyle.position === "static") {
3774
3932
  container.style.position = "relative";
3775
3933
  }
3934
+ if (!container.classList.contains("sp-container")) {
3935
+ container.classList.add("sp-container");
3936
+ addedContainerClass = true;
3937
+ }
3776
3938
  const isPlaying = api.getState("playing");
3777
3939
  gradient = document.createElement("div");
3778
3940
  gradient.className = isPlaying ? "sp-gradient" : "sp-gradient sp-gradient--visible";
@@ -3788,13 +3950,20 @@ function uiPlugin(config = {}) {
3788
3950
  if (payload?.fatal) {
3789
3951
  const error = api.getState("error") || payload;
3790
3952
  errorOverlay?.show(error);
3953
+ scheduleUpdate();
3791
3954
  }
3792
3955
  });
3793
3956
  reconnectingUnsubscribe = api.on("error:reconnecting", () => {
3794
3957
  errorOverlay?.showReconnecting();
3958
+ scheduleUpdate();
3795
3959
  });
3796
3960
  recoveredUnsubscribe = api.on("error:recovered", () => {
3797
3961
  errorOverlay?.hide();
3962
+ scheduleUpdate();
3963
+ });
3964
+ loadedUnsubscribe = api.on("media:loaded", () => {
3965
+ errorOverlay?.hide();
3966
+ scheduleUpdate();
3798
3967
  });
3799
3968
  if (showBigPlayButton) {
3800
3969
  bigPlayButton = new BigPlayButton(api, () => errorOverlay?.isVisible() ?? false);
@@ -3819,10 +3988,13 @@ function uiPlugin(config = {}) {
3819
3988
  });
3820
3989
  resizeObserver.observe(container);
3821
3990
  }
3822
- controlRegistryUnsubscribe = onControlRegistered((id) => {
3991
+ controlRegistryUnsubscribe = onControlRegistered((id, owner) => {
3823
3992
  if (!layout.includes(id)) {
3824
3993
  return;
3825
3994
  }
3995
+ if (owner && owner !== api.container) {
3996
+ return;
3997
+ }
3826
3998
  api.logger.debug(`Control "${id}" registered after init, rebuilding control bar`);
3827
3999
  rebuildControlBar();
3828
4000
  });
@@ -3866,6 +4038,8 @@ function uiPlugin(config = {}) {
3866
4038
  reconnectingUnsubscribe = null;
3867
4039
  recoveredUnsubscribe?.();
3868
4040
  recoveredUnsubscribe = null;
4041
+ loadedUnsubscribe?.();
4042
+ loadedUnsubscribe = null;
3869
4043
  if (api?.container) {
3870
4044
  api.container.removeEventListener("pointerdown", handlePointerActivity);
3871
4045
  api.container.removeEventListener("pointermove", handlePointerActivity);
@@ -3895,8 +4069,12 @@ function uiPlugin(config = {}) {
3895
4069
  gradient = null;
3896
4070
  bufferingIndicator?.remove();
3897
4071
  bufferingIndicator = null;
3898
- styleEl?.remove();
3899
- styleEl = null;
4072
+ releaseStyles?.();
4073
+ releaseStyles = null;
4074
+ if (addedContainerClass) {
4075
+ api?.container?.classList.remove("sp-container");
4076
+ addedContainerClass = false;
4077
+ }
3900
4078
  api?.logger.debug("UI controls plugin destroyed");
3901
4079
  },
3902
4080
  // Public API
@@ -3949,5 +4127,6 @@ var index_default = uiPlugin;
3949
4127
  resolveFitItems,
3950
4128
  styles,
3951
4129
  uiPlugin,
3952
- unregisterControl
4130
+ unregisterControl,
4131
+ unregisterControlsFor
3953
4132
  });