@scarlett-player/ui 1.0.2 → 1.2.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
@@ -30,9 +30,14 @@ const player = new ScarlettPlayer({
30
30
 
31
31
  - Play/pause, seek, volume controls
32
32
  - Fullscreen toggle
33
+ - Picture-in-Picture toggle (disabled until media metadata is loaded; hidden
34
+ when the browser has no PiP support; Safari webkit presentation mode
35
+ supported)
33
36
  - Quality selector
34
37
  - Progress bar with buffering indicator
35
38
  - Time display (current / duration)
39
+ - Error overlay with viewer-friendly copy per error code, a Try Again action,
40
+ and a reconnecting state while the player self-heals
36
41
  - Keyboard shortcuts
37
42
  - Customizable theming
38
43
  - Auto-hide controls
package/dist/index.cjs CHANGED
@@ -712,6 +712,17 @@ var styles = `
712
712
  fill: currentColor;
713
713
  }
714
714
 
715
+ /* Reconnecting: pulse the icon so the overlay reads as active work,
716
+ not a dead-end error */
717
+ .sp-error-overlay--reconnecting .sp-error-overlay__icon {
718
+ animation: sp-reconnect-pulse 1.2s ease-in-out infinite;
719
+ }
720
+
721
+ @keyframes sp-reconnect-pulse {
722
+ 0%, 100% { opacity: 0.4; }
723
+ 50% { opacity: 1; }
724
+ }
725
+
715
726
  .sp-error-overlay__message {
716
727
  color: rgba(255, 255, 255, 0.9);
717
728
  font-size: 15px;
@@ -918,12 +929,24 @@ function createButton(className, label, icon) {
918
929
  "aria-label": label,
919
930
  type: "button"
920
931
  });
921
- btn.innerHTML = icon;
932
+ setHTML(btn, icon);
922
933
  return btn;
923
934
  }
924
935
  function getVideo(container) {
925
936
  return container.querySelector("video");
926
937
  }
938
+ var lastHTML = /* @__PURE__ */ new WeakMap();
939
+ function setHTML(el, html) {
940
+ if (lastHTML.get(el) === html) return false;
941
+ el.innerHTML = html;
942
+ lastHTML.set(el, html);
943
+ return true;
944
+ }
945
+ function setAttr(el, name, value) {
946
+ if (el.getAttribute(name) === value) return false;
947
+ el.setAttribute(name, value);
948
+ return true;
949
+ }
927
950
 
928
951
  // src/utils/format.ts
929
952
  function formatTime(seconds) {
@@ -978,8 +1001,8 @@ var PlayButton = class {
978
1001
  icon = icons.play;
979
1002
  label = "Play";
980
1003
  }
981
- this.el.innerHTML = icon;
982
- this.el.setAttribute("aria-label", label);
1004
+ setHTML(this.el, icon);
1005
+ setAttr(this.el, "aria-label", label);
983
1006
  }
984
1007
  toggle() {
985
1008
  const video = getVideo(this.api.container);
@@ -1483,13 +1506,16 @@ var VolumeControl = class {
1483
1506
  icon = icons.volumeHigh;
1484
1507
  label = "Mute";
1485
1508
  }
1486
- this.btn.innerHTML = icon;
1487
- this.btn.setAttribute("aria-label", label);
1509
+ setHTML(this.btn, icon);
1510
+ setAttr(this.btn, "aria-label", label);
1488
1511
  const displayVolume = muted ? 0 : volume;
1489
- this.level.style.width = `${displayVolume * 100}%`;
1512
+ const width = `${displayVolume * 100}%`;
1513
+ if (this.level.style.width !== width) {
1514
+ this.level.style.width = width;
1515
+ }
1490
1516
  const volumePercent = Math.round(displayVolume * 100);
1491
- this.slider.setAttribute("aria-valuenow", String(volumePercent));
1492
- this.slider.setAttribute("aria-valuetext", `${volumePercent}%`);
1517
+ setAttr(this.slider, "aria-valuenow", String(volumePercent));
1518
+ setAttr(this.slider, "aria-valuetext", `${volumePercent}%`);
1493
1519
  }
1494
1520
  toggleMute() {
1495
1521
  const video = getVideo(this.api.container);
@@ -1726,11 +1752,11 @@ var CastButton = class {
1726
1752
  this.el.classList.toggle("sp-cast--active", !!active);
1727
1753
  this.el.classList.toggle("sp-cast--unavailable", !available && !active);
1728
1754
  if (active) {
1729
- this.el.innerHTML = icons.chromecastConnected;
1730
- this.el.setAttribute("aria-label", "Stop casting");
1755
+ setHTML(this.el, icons.chromecastConnected);
1756
+ setAttr(this.el, "aria-label", "Stop casting");
1731
1757
  } else {
1732
- this.el.innerHTML = icons.chromecast;
1733
- this.el.setAttribute("aria-label", available ? "Cast" : "No Cast devices found");
1758
+ setHTML(this.el, icons.chromecast);
1759
+ setAttr(this.el, "aria-label", available ? "Cast" : "No Cast devices found");
1734
1760
  }
1735
1761
  } else {
1736
1762
  const active = this.api.getState("airplayActive");
@@ -1738,7 +1764,7 @@ var CastButton = class {
1738
1764
  this.el.disabled = false;
1739
1765
  this.el.classList.toggle("sp-cast--active", !!active);
1740
1766
  this.el.classList.remove("sp-cast--unavailable");
1741
- this.el.setAttribute("aria-label", active ? "Stop AirPlay" : "AirPlay");
1767
+ setAttr(this.el, "aria-label", active ? "Stop AirPlay" : "AirPlay");
1742
1768
  }
1743
1769
  }
1744
1770
  handleClick() {
@@ -1776,25 +1802,38 @@ var CastButton = class {
1776
1802
  var PipButton = class {
1777
1803
  constructor(api) {
1778
1804
  this.clickHandler = () => {
1779
- this.toggle();
1805
+ void this.toggle().catch(() => {
1806
+ });
1780
1807
  };
1781
1808
  this.api = api;
1782
- const video = document.createElement("video");
1783
- this.supported = "pictureInPictureEnabled" in document || "webkitSetPresentationMode" in video;
1809
+ const probe = document.createElement("video");
1810
+ this.supported = "pictureInPictureEnabled" in document || "webkitSetPresentationMode" in probe;
1784
1811
  this.el = createButton("sp-pip", "Picture-in-Picture", icons.pip);
1785
1812
  this.el.addEventListener("click", this.clickHandler);
1786
1813
  if (!this.supported) {
1787
1814
  this.el.style.display = "none";
1815
+ } else {
1816
+ this.el.disabled = true;
1817
+ this.el.setAttribute("aria-disabled", "true");
1788
1818
  }
1789
1819
  }
1790
1820
  render() {
1791
1821
  return this.el;
1792
1822
  }
1823
+ /** Whether the media element is ready to enter PiP (metadata loaded). */
1824
+ isMediaReady() {
1825
+ const video = getVideo(this.api.container);
1826
+ return !!video && video.readyState >= HTMLMediaElement.HAVE_METADATA;
1827
+ }
1793
1828
  update() {
1794
1829
  if (!this.supported) return;
1795
- const pip = this.api.getState("pip");
1796
- this.el.setAttribute("aria-label", pip ? "Exit Picture-in-Picture" : "Picture-in-Picture");
1797
- this.el.classList.toggle("sp-pip--active", !!pip);
1830
+ const pip = !!this.api.getState("pip");
1831
+ const enabled = pip || this.isMediaReady();
1832
+ this.el.disabled = !enabled;
1833
+ setAttr(this.el, "aria-disabled", String(!enabled));
1834
+ setHTML(this.el, pip ? icons.exitPip : icons.pip);
1835
+ setAttr(this.el, "aria-label", pip ? "Exit Picture-in-Picture" : "Picture-in-Picture");
1836
+ this.el.classList.toggle("sp-pip--active", pip);
1798
1837
  }
1799
1838
  async toggle() {
1800
1839
  const video = getVideo(this.api.container);
@@ -1802,8 +1841,14 @@ var PipButton = class {
1802
1841
  this.api.logger.warn("PiP: video element not found");
1803
1842
  return;
1804
1843
  }
1844
+ const isInPip = document.pictureInPictureElement === video || video.webkitPresentationMode === "picture-in-picture";
1845
+ if (!isInPip && video.readyState < HTMLMediaElement.HAVE_METADATA) {
1846
+ this.api.logger.debug("PiP: ignored, media not ready", {
1847
+ readyState: video.readyState
1848
+ });
1849
+ return;
1850
+ }
1805
1851
  try {
1806
- const isInPip = document.pictureInPictureElement === video || video.webkitPresentationMode === "picture-in-picture";
1807
1852
  if (isInPip) {
1808
1853
  if (document.pictureInPictureElement) {
1809
1854
  await document.exitPictureInPicture();
@@ -1820,7 +1865,8 @@ var PipButton = class {
1820
1865
  this.api.logger.debug("PiP: entered");
1821
1866
  }
1822
1867
  } catch (e) {
1823
- this.api.logger.warn("PiP: failed", { error: e.message });
1868
+ const message = e instanceof Error ? e.message : String(e);
1869
+ this.api.logger.warn("PiP: failed", { error: message });
1824
1870
  }
1825
1871
  }
1826
1872
  destroy() {
@@ -1845,11 +1891,11 @@ var FullscreenButton = class {
1845
1891
  update() {
1846
1892
  const fullscreen = this.api.getState("fullscreen");
1847
1893
  if (fullscreen) {
1848
- this.el.innerHTML = icons.exitFullscreen;
1849
- this.el.setAttribute("aria-label", "Exit fullscreen");
1894
+ setHTML(this.el, icons.exitFullscreen);
1895
+ setAttr(this.el, "aria-label", "Exit fullscreen");
1850
1896
  } else {
1851
- this.el.innerHTML = icons.fullscreen;
1852
- this.el.setAttribute("aria-label", "Fullscreen");
1897
+ setHTML(this.el, icons.fullscreen);
1898
+ setAttr(this.el, "aria-label", "Fullscreen");
1853
1899
  }
1854
1900
  }
1855
1901
  async toggle() {
@@ -1890,6 +1936,27 @@ var Spacer = class {
1890
1936
  // src/controls/ErrorOverlay.ts
1891
1937
  function getUserMessage(error) {
1892
1938
  if (!error) return "Something went wrong.";
1939
+ const code = error.code;
1940
+ if (code) {
1941
+ switch (code) {
1942
+ case "MEDIA_NETWORK_ERROR":
1943
+ return "Having trouble connecting. Check your internet and try again.";
1944
+ case "MEDIA_DECODE_ERROR":
1945
+ return "This video can't be played right now.";
1946
+ case "SOURCE_LOAD_FAILED":
1947
+ case "SOURCE_NOT_SUPPORTED":
1948
+ case "PROVIDER_NOT_FOUND":
1949
+ return "Unable to load video. Please try again.";
1950
+ case "PLAYBACK_FAILED":
1951
+ return "Playback stopped unexpectedly. Please try again.";
1952
+ case "MEDIA_APPEND_ERROR":
1953
+ return "Video playback was interrupted. Please try again.";
1954
+ case "MEDIA_BUFFER_FULL":
1955
+ return "Your device is low on video memory. Close other apps or tabs and try again.";
1956
+ case "PLAYLIST_INVALID":
1957
+ return "The stream is temporarily unavailable. Please try again.";
1958
+ }
1959
+ }
1893
1960
  const msg = error.message?.toLowerCase() || "";
1894
1961
  if (msg.includes("network") || msg.includes("timeout") || msg.includes("fetch") || msg.includes("connection")) {
1895
1962
  return "Having trouble connecting. Check your internet and try again.";
@@ -1917,13 +1984,6 @@ var ErrorOverlay = class {
1917
1984
  const src = source?.src || this.lastSource;
1918
1985
  if (src) {
1919
1986
  this.api.emit("error:retry", { src });
1920
- const video = this.api.container.querySelector("video");
1921
- if (video) {
1922
- video.src = src;
1923
- video.load();
1924
- video.play().catch(() => {
1925
- });
1926
- }
1927
1987
  }
1928
1988
  setTimeout(() => {
1929
1989
  this.retryBtn.disabled = false;
@@ -1984,12 +2044,36 @@ var ErrorOverlay = class {
1984
2044
  }
1985
2045
  this.visible = true;
1986
2046
  this.retryBtn.disabled = false;
2047
+ this.el.classList.remove("sp-error-overlay--reconnecting");
2048
+ this.el.classList.add("sp-error-overlay--visible");
2049
+ }
2050
+ /**
2051
+ * Show the reconnecting state.
2052
+ *
2053
+ * Displayed while the provider auto-reconnects after a fatal error, so the
2054
+ * viewer sees the player working on the problem instead of a dead-end
2055
+ * error. Try Again stays available for viewers who want to force an
2056
+ * immediate attempt.
2057
+ */
2058
+ showReconnecting() {
2059
+ const messageEl = this.el.querySelector(".sp-error-overlay__message");
2060
+ if (messageEl) {
2061
+ messageEl.textContent = "Connection lost. Reconnecting...";
2062
+ }
2063
+ const source = this.api.getState("source");
2064
+ if (source?.src) {
2065
+ this.lastSource = source.src;
2066
+ }
2067
+ this.visible = true;
2068
+ this.retryBtn.disabled = false;
2069
+ this.el.classList.add("sp-error-overlay--reconnecting");
1987
2070
  this.el.classList.add("sp-error-overlay--visible");
1988
2071
  }
1989
2072
  /** Hide the error overlay */
1990
2073
  hide() {
1991
2074
  this.visible = false;
1992
2075
  this.el.classList.remove("sp-error-overlay--visible");
2076
+ this.el.classList.remove("sp-error-overlay--reconnecting");
1993
2077
  }
1994
2078
  isVisible() {
1995
2079
  return this.visible;
@@ -2473,12 +2557,12 @@ var CaptionsButton = class {
2473
2557
  }
2474
2558
  this.el.style.display = "";
2475
2559
  if (currentTrack) {
2476
- this.el.innerHTML = icons.captions;
2477
- this.el.setAttribute("aria-label", `Captions: ${currentTrack.label}`);
2560
+ setHTML(this.el, icons.captions);
2561
+ setAttr(this.el, "aria-label", `Captions: ${currentTrack.label}`);
2478
2562
  this.el.classList.add("sp-captions--active");
2479
2563
  } else {
2480
- this.el.innerHTML = icons.captionsOff;
2481
- this.el.setAttribute("aria-label", "Captions");
2564
+ setHTML(this.el, icons.captionsOff);
2565
+ setAttr(this.el, "aria-label", "Captions");
2482
2566
  this.el.classList.remove("sp-captions--active");
2483
2567
  }
2484
2568
  }
@@ -2561,7 +2645,10 @@ function uiPlugin(config = {}) {
2561
2645
  let hideTimeout = null;
2562
2646
  let stateUnsubscribe = null;
2563
2647
  let errorUnsubscribe = null;
2648
+ let reconnectingUnsubscribe = null;
2649
+ let recoveredUnsubscribe = null;
2564
2650
  let controlsVisible = true;
2651
+ let rafHandle = null;
2565
2652
  const layout = config.controls || DEFAULT_LAYOUT;
2566
2653
  const hideDelay = config.hideDelay ?? DEFAULT_HIDE_DELAY;
2567
2654
  const createControl = (slot) => {
@@ -2613,6 +2700,13 @@ function uiPlugin(config = {}) {
2613
2700
  bufferingIndicator?.classList.toggle("sp-buffering--visible", !!showSpinner);
2614
2701
  errorOverlay?.update();
2615
2702
  };
2703
+ const scheduleUpdate = () => {
2704
+ if (rafHandle !== null) return;
2705
+ rafHandle = requestAnimationFrame(() => {
2706
+ rafHandle = null;
2707
+ updateControls();
2708
+ });
2709
+ };
2616
2710
  const showControls = () => {
2617
2711
  if (controlsVisible) {
2618
2712
  resetHideTimer();
@@ -2662,7 +2756,12 @@ function uiPlugin(config = {}) {
2662
2756
  case " ":
2663
2757
  case "k":
2664
2758
  e.preventDefault();
2665
- video.paused ? video.play() : video.pause();
2759
+ if (video.paused) {
2760
+ video.play().catch(() => {
2761
+ });
2762
+ } else {
2763
+ video.pause();
2764
+ }
2666
2765
  break;
2667
2766
  case "m":
2668
2767
  e.preventDefault();
@@ -2671,9 +2770,11 @@ function uiPlugin(config = {}) {
2671
2770
  case "f":
2672
2771
  e.preventDefault();
2673
2772
  if (document.fullscreenElement) {
2674
- document.exitFullscreen();
2773
+ document.exitFullscreen().catch(() => {
2774
+ });
2675
2775
  } else {
2676
- api.container.requestFullscreen?.();
2776
+ api.container.requestFullscreen?.().catch(() => {
2777
+ });
2677
2778
  }
2678
2779
  break;
2679
2780
  case "ArrowLeft":
@@ -2741,10 +2842,16 @@ function uiPlugin(config = {}) {
2741
2842
  container.appendChild(errorOverlay.render());
2742
2843
  errorUnsubscribe = api.on("error", (payload) => {
2743
2844
  if (payload?.fatal) {
2744
- const error = api.getState("error") || new Error(payload.message || "Playback error");
2845
+ const error = api.getState("error") || payload;
2745
2846
  errorOverlay?.show(error);
2746
2847
  }
2747
2848
  });
2849
+ reconnectingUnsubscribe = api.on("error:reconnecting", () => {
2850
+ errorOverlay?.showReconnecting();
2851
+ });
2852
+ recoveredUnsubscribe = api.on("error:recovered", () => {
2853
+ errorOverlay?.hide();
2854
+ });
2748
2855
  progressBar = new ProgressBar(api);
2749
2856
  container.appendChild(progressBar.render());
2750
2857
  if (!isPlaying) {
@@ -2768,8 +2875,8 @@ function uiPlugin(config = {}) {
2768
2875
  container.addEventListener("touchstart", handleInteraction, { passive: true });
2769
2876
  container.addEventListener("click", handleInteraction);
2770
2877
  document.addEventListener("keydown", handleKeyDown);
2771
- stateUnsubscribe = api.subscribeToState(updateControls);
2772
- document.addEventListener("fullscreenchange", updateControls);
2878
+ stateUnsubscribe = api.subscribeToState(scheduleUpdate);
2879
+ document.addEventListener("fullscreenchange", scheduleUpdate);
2773
2880
  updateControls();
2774
2881
  if (!container.hasAttribute("tabindex")) {
2775
2882
  container.setAttribute("tabindex", "0");
@@ -2786,10 +2893,18 @@ function uiPlugin(config = {}) {
2786
2893
  clearTimeout(hideTimeout);
2787
2894
  hideTimeout = null;
2788
2895
  }
2896
+ if (rafHandle !== null) {
2897
+ cancelAnimationFrame(rafHandle);
2898
+ rafHandle = null;
2899
+ }
2789
2900
  stateUnsubscribe?.();
2790
2901
  stateUnsubscribe = null;
2791
2902
  errorUnsubscribe?.();
2792
2903
  errorUnsubscribe = null;
2904
+ reconnectingUnsubscribe?.();
2905
+ reconnectingUnsubscribe = null;
2906
+ recoveredUnsubscribe?.();
2907
+ recoveredUnsubscribe = null;
2793
2908
  if (api?.container) {
2794
2909
  api.container.removeEventListener("mousemove", handleInteraction);
2795
2910
  api.container.removeEventListener("mouseenter", handleInteraction);
@@ -2798,7 +2913,7 @@ function uiPlugin(config = {}) {
2798
2913
  api.container.removeEventListener("click", handleInteraction);
2799
2914
  }
2800
2915
  document.removeEventListener("keydown", handleKeyDown);
2801
- document.removeEventListener("fullscreenchange", updateControls);
2916
+ document.removeEventListener("fullscreenchange", scheduleUpdate);
2802
2917
  controls.forEach((c) => c.destroy());
2803
2918
  controls = [];
2804
2919
  progressBar?.destroy();
package/dist/index.d.cts CHANGED
@@ -105,7 +105,7 @@ declare const icons: {
105
105
  * Modern, minimal design inspired by Mux Player and Vidstack.
106
106
  * Uses CSS custom properties for theming.
107
107
  */
108
- declare const styles = "\n/* ============================================\n Container & Base\n ============================================ */\n.sp-container {\n position: relative;\n width: 100%;\n height: 100%;\n background: #000;\n overflow: hidden;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-container video {\n width: 100%;\n height: 100%;\n display: block;\n object-fit: contain;\n}\n\n.sp-container:focus {\n outline: none;\n}\n\n/* ============================================\n Gradient Overlay\n ============================================ */\n.sp-gradient {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 160px;\n background: linear-gradient(\n to top,\n rgba(0, 0, 0, 0.8) 0%,\n rgba(0, 0, 0, 0.4) 50%,\n transparent 100%\n );\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.25s ease;\n z-index: 5;\n}\n\n.sp-gradient--visible {\n opacity: 1;\n}\n\n/* ============================================\n Controls Container\n ============================================ */\n.sp-controls {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n display: flex;\n align-items: center;\n padding: 0 12px 12px;\n gap: 4px;\n opacity: 0;\n transform: translateY(4px);\n transition: opacity 0.25s ease, transform 0.25s ease;\n z-index: 10;\n}\n\n.sp-controls--visible {\n opacity: 1;\n transform: translateY(0);\n}\n\n.sp-controls--hidden {\n opacity: 0;\n transform: translateY(4px);\n pointer-events: none;\n}\n\n/* ============================================\n Progress Bar (Above Controls)\n ============================================ */\n.sp-progress-wrapper {\n position: absolute;\n bottom: 48px;\n left: 12px;\n right: 12px;\n height: 20px;\n display: flex;\n align-items: center;\n cursor: pointer;\n z-index: 10;\n opacity: 0;\n transition: opacity 0.25s ease;\n}\n\n.sp-progress-wrapper--visible {\n opacity: 1;\n}\n\n.sp-progress {\n position: relative;\n width: 100%;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n transition: height 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress {\n height: 5px;\n }\n}\n\n.sp-progress--dragging {\n height: 5px;\n}\n\n.sp-progress__track {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n border-radius: inherit;\n overflow: hidden;\n}\n\n.sp-progress__buffered {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: rgba(255, 255, 255, 0.4);\n border-radius: inherit;\n transition: width 0.1s linear;\n}\n\n.sp-progress__filled {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: var(--sp-accent, #e50914);\n border-radius: inherit;\n}\n\n.sp-progress__handle {\n position: absolute;\n top: 50%;\n width: 14px;\n height: 14px;\n background: var(--sp-accent, #e50914);\n border-radius: 50%;\n transform: translate(-50%, -50%) scale(0);\n transition: transform 0.15s ease;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n }\n}\n\n.sp-progress--dragging .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n}\n\n/* Thumbnail Preview */\n.sp-thumbnail-preview {\n position: absolute;\n bottom: calc(100% + 8px);\n transform: translateX(-50%);\n pointer-events: none;\n display: none;\n z-index: 21;\n border-radius: 4px;\n overflow: hidden;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n border: 2px solid rgba(255, 255, 255, 0.2);\n}\n\n.sp-thumbnail-preview__img {\n background-repeat: no-repeat;\n}\n\n/* Progress Tooltip */\n.sp-progress__tooltip {\n position: absolute;\n bottom: calc(100% + 8px);\n padding: 6px 10px;\n background: rgba(20, 20, 20, 0.95);\n color: #fff;\n font-size: 12px;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n border-radius: 4px;\n white-space: nowrap;\n transform: translateX(-50%);\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.15s ease;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__tooltip {\n opacity: 1;\n }\n}\n\n/* ============================================\n Control Buttons\n ============================================ */\n.sp-control {\n background: none;\n border: none;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n padding: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: color 0.15s ease, transform 0.15s ease, background 0.15s ease;\n flex-shrink: 0;\n min-width: 44px;\n min-height: 44px;\n}\n\n@media (hover: hover) {\n .sp-control:hover {\n color: #fff;\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-control:active {\n transform: scale(0.92);\n}\n\n.sp-control:focus-visible {\n outline: 2px solid var(--sp-accent, #e50914);\n outline-offset: 2px;\n}\n\n.sp-control:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n transform: none;\n}\n\n.sp-control:disabled:hover {\n background: none;\n}\n\n.sp-control svg {\n width: 24px;\n height: 24px;\n fill: currentColor;\n display: block;\n}\n\n.sp-control--small svg {\n width: 20px;\n height: 20px;\n}\n\n/* ============================================\n Spacer\n ============================================ */\n.sp-spacer {\n flex: 1;\n min-width: 0;\n}\n\n/* ============================================\n Time Display\n ============================================ */\n.sp-time {\n font-size: 13px;\n font-variant-numeric: tabular-nums;\n color: rgba(255, 255, 255, 0.9);\n white-space: nowrap;\n padding: 0 4px;\n letter-spacing: 0.02em;\n}\n\n/* ============================================\n Volume Control\n ============================================ */\n.sp-volume {\n display: flex;\n align-items: center;\n position: relative;\n}\n\n.sp-volume__slider-wrap {\n width: 0;\n overflow: hidden;\n transition: width 0.2s ease;\n}\n\n@media (hover: hover) {\n .sp-volume:hover .sp-volume__slider-wrap {\n width: 64px;\n }\n}\n\n.sp-volume:focus-within .sp-volume__slider-wrap {\n width: 64px;\n}\n\n.sp-volume__slider {\n width: 64px;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n cursor: pointer;\n position: relative;\n margin: 0 8px 0 4px;\n}\n\n.sp-volume__level {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: #fff;\n border-radius: inherit;\n transition: width 0.1s ease;\n}\n\n/* ============================================\n Live Indicator\n ============================================ */\n.sp-live {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--sp-accent, #e50914);\n cursor: pointer;\n padding: 6px 10px;\n border-radius: 4px;\n transition: background 0.15s ease, opacity 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-live:hover {\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-live__dot {\n width: 8px;\n height: 8px;\n background: currentColor;\n border-radius: 50%;\n animation: sp-pulse 2s ease-in-out infinite;\n}\n\n.sp-live--behind {\n opacity: 0.6;\n}\n\n.sp-live--behind .sp-live__dot {\n animation: none;\n}\n\n.sp-live--behind span {\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n/* Progress bar live mode: accent color for filled bar */\n.sp-progress--live .sp-progress__filled {\n background: var(--sp-accent, #e50914);\n}\n\n@keyframes sp-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n}\n\n/* ============================================\n Quality / Settings Menu\n ============================================ */\n.sp-quality {\n position: relative;\n}\n\n.sp-quality__btn {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.sp-quality__label {\n font-size: 12px;\n font-weight: 500;\n opacity: 0.9;\n}\n\n.sp-quality-menu {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n padding: 8px 0;\n min-width: 150px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-quality-menu--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n.sp-quality-menu__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-quality-menu__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-quality-menu__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-quality-menu__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-quality-menu__item--active .sp-quality-menu__check {\n opacity: 1;\n}\n\n/* ============================================\n Settings Menu (Gear Icon)\n ============================================ */\n.sp-settings {\n position: relative;\n}\n\n.sp-settings__btn {\n display: flex;\n align-items: center;\n}\n\n.sp-settings-panel {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n min-width: 200px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n overflow: hidden;\n}\n\n.sp-settings-panel--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n/* Main menu rows */\n.sp-settings-panel--main {\n padding: 4px 0;\n}\n\n.sp-settings-panel__row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__row:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__label {\n font-weight: 500;\n}\n\n.sp-settings-panel__value {\n display: flex;\n align-items: center;\n gap: 4px;\n color: rgba(255, 255, 255, 0.6);\n font-size: 12px;\n}\n\n.sp-settings-panel__arrow {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__arrow svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n/* Sub-menu panels */\n.sp-settings-panel--sub {\n padding: 0;\n}\n\n.sp-settings-panel__header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 16px;\n font-size: 13px;\n font-weight: 600;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__header:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__back {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__back svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__header-label {\n flex: 1;\n}\n\n.sp-settings-panel__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-settings-panel__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-settings-panel__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-settings-panel__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-settings-panel__check svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__item--active .sp-settings-panel__check {\n opacity: 1;\n}\n\n/* ============================================\n Captions Button\n ============================================ */\n.sp-captions--active {\n color: var(--sp-accent, #e50914);\n}\n\n/* ============================================\n Cast Button States\n ============================================ */\n.sp-cast--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-cast--unavailable {\n opacity: 0.4;\n}\n\n/* ============================================\n Error Overlay\n ============================================ */\n.sp-error-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.85);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 25;\n opacity: 0;\n visibility: hidden;\n transition: opacity 0.25s ease, visibility 0.25s;\n}\n\n.sp-error-overlay--visible {\n opacity: 1;\n visibility: visible;\n}\n\n.sp-error-overlay__content {\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n padding: 24px;\n max-width: 360px;\n}\n\n.sp-error-overlay__icon {\n color: rgba(255, 255, 255, 0.7);\n margin-bottom: 16px;\n}\n\n.sp-error-overlay__icon svg {\n width: 48px;\n height: 48px;\n fill: currentColor;\n}\n\n.sp-error-overlay__message {\n color: rgba(255, 255, 255, 0.9);\n font-size: 15px;\n line-height: 1.5;\n margin: 0 0 24px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-error-overlay__actions {\n display: flex;\n gap: 12px;\n flex-wrap: wrap;\n justify-content: center;\n}\n\n.sp-error-overlay__retry {\n background: var(--sp-accent, #e50914);\n color: #fff;\n border: none;\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 600;\n border-radius: 6px;\n cursor: pointer;\n min-width: 120px;\n min-height: 44px;\n transition: background 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__retry:hover {\n filter: brightness(1.1);\n}\n\n.sp-error-overlay__retry:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__retry:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n.sp-error-overlay__dismiss {\n background: none;\n color: rgba(255, 255, 255, 0.7);\n border: 1px solid rgba(255, 255, 255, 0.3);\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n min-width: 100px;\n min-height: 44px;\n transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__dismiss:hover {\n color: #fff;\n border-color: rgba(255, 255, 255, 0.5);\n}\n\n.sp-error-overlay__dismiss:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__dismiss:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n/* ============================================\n Buffering Indicator\n ============================================ */\n.sp-buffering {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 15;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.2s ease;\n}\n\n.sp-buffering--visible {\n opacity: 1;\n}\n\n.sp-buffering svg {\n width: 48px;\n height: 48px;\n fill: rgba(255, 255, 255, 0.9);\n filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));\n}\n\n@keyframes sp-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n\n.sp-spin {\n animation: sp-spin 0.8s linear infinite;\n}\n\n/* ============================================\n Reduced Motion\n ============================================ */\n@media (prefers-reduced-motion: reduce) {\n .sp-gradient,\n .sp-controls,\n .sp-progress-wrapper,\n .sp-progress,\n .sp-progress__handle,\n .sp-progress__tooltip,\n .sp-control,\n .sp-volume__slider-wrap,\n .sp-quality-menu,\n .sp-settings-panel,\n .sp-settings-panel__row,\n .sp-settings-panel__item,\n .sp-settings-panel__header,\n .sp-buffering,\n .sp-error-overlay,\n .sp-error-overlay__retry,\n .sp-error-overlay__dismiss {\n transition: none;\n }\n\n .sp-live__dot,\n .sp-spin {\n animation: none;\n }\n}\n\n/* ============================================\n CSS Custom Properties (Theming)\n ============================================ */\n:root {\n --sp-accent: #e50914;\n --sp-color: #fff;\n --sp-bg: rgba(0, 0, 0, 0.8);\n --sp-control-height: 48px;\n --sp-icon-size: 24px;\n}\n";
108
+ declare const styles = "\n/* ============================================\n Container & Base\n ============================================ */\n.sp-container {\n position: relative;\n width: 100%;\n height: 100%;\n background: #000;\n overflow: hidden;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-container video {\n width: 100%;\n height: 100%;\n display: block;\n object-fit: contain;\n}\n\n.sp-container:focus {\n outline: none;\n}\n\n/* ============================================\n Gradient Overlay\n ============================================ */\n.sp-gradient {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 160px;\n background: linear-gradient(\n to top,\n rgba(0, 0, 0, 0.8) 0%,\n rgba(0, 0, 0, 0.4) 50%,\n transparent 100%\n );\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.25s ease;\n z-index: 5;\n}\n\n.sp-gradient--visible {\n opacity: 1;\n}\n\n/* ============================================\n Controls Container\n ============================================ */\n.sp-controls {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n display: flex;\n align-items: center;\n padding: 0 12px 12px;\n gap: 4px;\n opacity: 0;\n transform: translateY(4px);\n transition: opacity 0.25s ease, transform 0.25s ease;\n z-index: 10;\n}\n\n.sp-controls--visible {\n opacity: 1;\n transform: translateY(0);\n}\n\n.sp-controls--hidden {\n opacity: 0;\n transform: translateY(4px);\n pointer-events: none;\n}\n\n/* ============================================\n Progress Bar (Above Controls)\n ============================================ */\n.sp-progress-wrapper {\n position: absolute;\n bottom: 48px;\n left: 12px;\n right: 12px;\n height: 20px;\n display: flex;\n align-items: center;\n cursor: pointer;\n z-index: 10;\n opacity: 0;\n transition: opacity 0.25s ease;\n}\n\n.sp-progress-wrapper--visible {\n opacity: 1;\n}\n\n.sp-progress {\n position: relative;\n width: 100%;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n transition: height 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress {\n height: 5px;\n }\n}\n\n.sp-progress--dragging {\n height: 5px;\n}\n\n.sp-progress__track {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n border-radius: inherit;\n overflow: hidden;\n}\n\n.sp-progress__buffered {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: rgba(255, 255, 255, 0.4);\n border-radius: inherit;\n transition: width 0.1s linear;\n}\n\n.sp-progress__filled {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: var(--sp-accent, #e50914);\n border-radius: inherit;\n}\n\n.sp-progress__handle {\n position: absolute;\n top: 50%;\n width: 14px;\n height: 14px;\n background: var(--sp-accent, #e50914);\n border-radius: 50%;\n transform: translate(-50%, -50%) scale(0);\n transition: transform 0.15s ease;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n }\n}\n\n.sp-progress--dragging .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n}\n\n/* Thumbnail Preview */\n.sp-thumbnail-preview {\n position: absolute;\n bottom: calc(100% + 8px);\n transform: translateX(-50%);\n pointer-events: none;\n display: none;\n z-index: 21;\n border-radius: 4px;\n overflow: hidden;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n border: 2px solid rgba(255, 255, 255, 0.2);\n}\n\n.sp-thumbnail-preview__img {\n background-repeat: no-repeat;\n}\n\n/* Progress Tooltip */\n.sp-progress__tooltip {\n position: absolute;\n bottom: calc(100% + 8px);\n padding: 6px 10px;\n background: rgba(20, 20, 20, 0.95);\n color: #fff;\n font-size: 12px;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n border-radius: 4px;\n white-space: nowrap;\n transform: translateX(-50%);\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.15s ease;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__tooltip {\n opacity: 1;\n }\n}\n\n/* ============================================\n Control Buttons\n ============================================ */\n.sp-control {\n background: none;\n border: none;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n padding: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: color 0.15s ease, transform 0.15s ease, background 0.15s ease;\n flex-shrink: 0;\n min-width: 44px;\n min-height: 44px;\n}\n\n@media (hover: hover) {\n .sp-control:hover {\n color: #fff;\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-control:active {\n transform: scale(0.92);\n}\n\n.sp-control:focus-visible {\n outline: 2px solid var(--sp-accent, #e50914);\n outline-offset: 2px;\n}\n\n.sp-control:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n transform: none;\n}\n\n.sp-control:disabled:hover {\n background: none;\n}\n\n.sp-control svg {\n width: 24px;\n height: 24px;\n fill: currentColor;\n display: block;\n}\n\n.sp-control--small svg {\n width: 20px;\n height: 20px;\n}\n\n/* ============================================\n Spacer\n ============================================ */\n.sp-spacer {\n flex: 1;\n min-width: 0;\n}\n\n/* ============================================\n Time Display\n ============================================ */\n.sp-time {\n font-size: 13px;\n font-variant-numeric: tabular-nums;\n color: rgba(255, 255, 255, 0.9);\n white-space: nowrap;\n padding: 0 4px;\n letter-spacing: 0.02em;\n}\n\n/* ============================================\n Volume Control\n ============================================ */\n.sp-volume {\n display: flex;\n align-items: center;\n position: relative;\n}\n\n.sp-volume__slider-wrap {\n width: 0;\n overflow: hidden;\n transition: width 0.2s ease;\n}\n\n@media (hover: hover) {\n .sp-volume:hover .sp-volume__slider-wrap {\n width: 64px;\n }\n}\n\n.sp-volume:focus-within .sp-volume__slider-wrap {\n width: 64px;\n}\n\n.sp-volume__slider {\n width: 64px;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n cursor: pointer;\n position: relative;\n margin: 0 8px 0 4px;\n}\n\n.sp-volume__level {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: #fff;\n border-radius: inherit;\n transition: width 0.1s ease;\n}\n\n/* ============================================\n Live Indicator\n ============================================ */\n.sp-live {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--sp-accent, #e50914);\n cursor: pointer;\n padding: 6px 10px;\n border-radius: 4px;\n transition: background 0.15s ease, opacity 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-live:hover {\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-live__dot {\n width: 8px;\n height: 8px;\n background: currentColor;\n border-radius: 50%;\n animation: sp-pulse 2s ease-in-out infinite;\n}\n\n.sp-live--behind {\n opacity: 0.6;\n}\n\n.sp-live--behind .sp-live__dot {\n animation: none;\n}\n\n.sp-live--behind span {\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n/* Progress bar live mode: accent color for filled bar */\n.sp-progress--live .sp-progress__filled {\n background: var(--sp-accent, #e50914);\n}\n\n@keyframes sp-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n}\n\n/* ============================================\n Quality / Settings Menu\n ============================================ */\n.sp-quality {\n position: relative;\n}\n\n.sp-quality__btn {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.sp-quality__label {\n font-size: 12px;\n font-weight: 500;\n opacity: 0.9;\n}\n\n.sp-quality-menu {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n padding: 8px 0;\n min-width: 150px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-quality-menu--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n.sp-quality-menu__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-quality-menu__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-quality-menu__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-quality-menu__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-quality-menu__item--active .sp-quality-menu__check {\n opacity: 1;\n}\n\n/* ============================================\n Settings Menu (Gear Icon)\n ============================================ */\n.sp-settings {\n position: relative;\n}\n\n.sp-settings__btn {\n display: flex;\n align-items: center;\n}\n\n.sp-settings-panel {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n min-width: 200px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n overflow: hidden;\n}\n\n.sp-settings-panel--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n/* Main menu rows */\n.sp-settings-panel--main {\n padding: 4px 0;\n}\n\n.sp-settings-panel__row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__row:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__label {\n font-weight: 500;\n}\n\n.sp-settings-panel__value {\n display: flex;\n align-items: center;\n gap: 4px;\n color: rgba(255, 255, 255, 0.6);\n font-size: 12px;\n}\n\n.sp-settings-panel__arrow {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__arrow svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n/* Sub-menu panels */\n.sp-settings-panel--sub {\n padding: 0;\n}\n\n.sp-settings-panel__header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 16px;\n font-size: 13px;\n font-weight: 600;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__header:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__back {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__back svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__header-label {\n flex: 1;\n}\n\n.sp-settings-panel__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-settings-panel__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-settings-panel__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-settings-panel__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-settings-panel__check svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__item--active .sp-settings-panel__check {\n opacity: 1;\n}\n\n/* ============================================\n Captions Button\n ============================================ */\n.sp-captions--active {\n color: var(--sp-accent, #e50914);\n}\n\n/* ============================================\n Cast Button States\n ============================================ */\n.sp-cast--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-cast--unavailable {\n opacity: 0.4;\n}\n\n/* ============================================\n Error Overlay\n ============================================ */\n.sp-error-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.85);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 25;\n opacity: 0;\n visibility: hidden;\n transition: opacity 0.25s ease, visibility 0.25s;\n}\n\n.sp-error-overlay--visible {\n opacity: 1;\n visibility: visible;\n}\n\n.sp-error-overlay__content {\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n padding: 24px;\n max-width: 360px;\n}\n\n.sp-error-overlay__icon {\n color: rgba(255, 255, 255, 0.7);\n margin-bottom: 16px;\n}\n\n.sp-error-overlay__icon svg {\n width: 48px;\n height: 48px;\n fill: currentColor;\n}\n\n/* Reconnecting: pulse the icon so the overlay reads as active work,\n not a dead-end error */\n.sp-error-overlay--reconnecting .sp-error-overlay__icon {\n animation: sp-reconnect-pulse 1.2s ease-in-out infinite;\n}\n\n@keyframes sp-reconnect-pulse {\n 0%, 100% { opacity: 0.4; }\n 50% { opacity: 1; }\n}\n\n.sp-error-overlay__message {\n color: rgba(255, 255, 255, 0.9);\n font-size: 15px;\n line-height: 1.5;\n margin: 0 0 24px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-error-overlay__actions {\n display: flex;\n gap: 12px;\n flex-wrap: wrap;\n justify-content: center;\n}\n\n.sp-error-overlay__retry {\n background: var(--sp-accent, #e50914);\n color: #fff;\n border: none;\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 600;\n border-radius: 6px;\n cursor: pointer;\n min-width: 120px;\n min-height: 44px;\n transition: background 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__retry:hover {\n filter: brightness(1.1);\n}\n\n.sp-error-overlay__retry:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__retry:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n.sp-error-overlay__dismiss {\n background: none;\n color: rgba(255, 255, 255, 0.7);\n border: 1px solid rgba(255, 255, 255, 0.3);\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n min-width: 100px;\n min-height: 44px;\n transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__dismiss:hover {\n color: #fff;\n border-color: rgba(255, 255, 255, 0.5);\n}\n\n.sp-error-overlay__dismiss:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__dismiss:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n/* ============================================\n Buffering Indicator\n ============================================ */\n.sp-buffering {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 15;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.2s ease;\n}\n\n.sp-buffering--visible {\n opacity: 1;\n}\n\n.sp-buffering svg {\n width: 48px;\n height: 48px;\n fill: rgba(255, 255, 255, 0.9);\n filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));\n}\n\n@keyframes sp-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n\n.sp-spin {\n animation: sp-spin 0.8s linear infinite;\n}\n\n/* ============================================\n Reduced Motion\n ============================================ */\n@media (prefers-reduced-motion: reduce) {\n .sp-gradient,\n .sp-controls,\n .sp-progress-wrapper,\n .sp-progress,\n .sp-progress__handle,\n .sp-progress__tooltip,\n .sp-control,\n .sp-volume__slider-wrap,\n .sp-quality-menu,\n .sp-settings-panel,\n .sp-settings-panel__row,\n .sp-settings-panel__item,\n .sp-settings-panel__header,\n .sp-buffering,\n .sp-error-overlay,\n .sp-error-overlay__retry,\n .sp-error-overlay__dismiss {\n transition: none;\n }\n\n .sp-live__dot,\n .sp-spin {\n animation: none;\n }\n}\n\n/* ============================================\n CSS Custom Properties (Theming)\n ============================================ */\n:root {\n --sp-accent: #e50914;\n --sp-color: #fff;\n --sp-bg: rgba(0, 0, 0, 0.8);\n --sp-control-height: 48px;\n --sp-icon-size: 24px;\n}\n";
109
109
 
110
110
  /**
111
111
  * Formatting utility functions
package/dist/index.d.ts CHANGED
@@ -105,7 +105,7 @@ declare const icons: {
105
105
  * Modern, minimal design inspired by Mux Player and Vidstack.
106
106
  * Uses CSS custom properties for theming.
107
107
  */
108
- declare const styles = "\n/* ============================================\n Container & Base\n ============================================ */\n.sp-container {\n position: relative;\n width: 100%;\n height: 100%;\n background: #000;\n overflow: hidden;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-container video {\n width: 100%;\n height: 100%;\n display: block;\n object-fit: contain;\n}\n\n.sp-container:focus {\n outline: none;\n}\n\n/* ============================================\n Gradient Overlay\n ============================================ */\n.sp-gradient {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 160px;\n background: linear-gradient(\n to top,\n rgba(0, 0, 0, 0.8) 0%,\n rgba(0, 0, 0, 0.4) 50%,\n transparent 100%\n );\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.25s ease;\n z-index: 5;\n}\n\n.sp-gradient--visible {\n opacity: 1;\n}\n\n/* ============================================\n Controls Container\n ============================================ */\n.sp-controls {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n display: flex;\n align-items: center;\n padding: 0 12px 12px;\n gap: 4px;\n opacity: 0;\n transform: translateY(4px);\n transition: opacity 0.25s ease, transform 0.25s ease;\n z-index: 10;\n}\n\n.sp-controls--visible {\n opacity: 1;\n transform: translateY(0);\n}\n\n.sp-controls--hidden {\n opacity: 0;\n transform: translateY(4px);\n pointer-events: none;\n}\n\n/* ============================================\n Progress Bar (Above Controls)\n ============================================ */\n.sp-progress-wrapper {\n position: absolute;\n bottom: 48px;\n left: 12px;\n right: 12px;\n height: 20px;\n display: flex;\n align-items: center;\n cursor: pointer;\n z-index: 10;\n opacity: 0;\n transition: opacity 0.25s ease;\n}\n\n.sp-progress-wrapper--visible {\n opacity: 1;\n}\n\n.sp-progress {\n position: relative;\n width: 100%;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n transition: height 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress {\n height: 5px;\n }\n}\n\n.sp-progress--dragging {\n height: 5px;\n}\n\n.sp-progress__track {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n border-radius: inherit;\n overflow: hidden;\n}\n\n.sp-progress__buffered {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: rgba(255, 255, 255, 0.4);\n border-radius: inherit;\n transition: width 0.1s linear;\n}\n\n.sp-progress__filled {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: var(--sp-accent, #e50914);\n border-radius: inherit;\n}\n\n.sp-progress__handle {\n position: absolute;\n top: 50%;\n width: 14px;\n height: 14px;\n background: var(--sp-accent, #e50914);\n border-radius: 50%;\n transform: translate(-50%, -50%) scale(0);\n transition: transform 0.15s ease;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n }\n}\n\n.sp-progress--dragging .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n}\n\n/* Thumbnail Preview */\n.sp-thumbnail-preview {\n position: absolute;\n bottom: calc(100% + 8px);\n transform: translateX(-50%);\n pointer-events: none;\n display: none;\n z-index: 21;\n border-radius: 4px;\n overflow: hidden;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n border: 2px solid rgba(255, 255, 255, 0.2);\n}\n\n.sp-thumbnail-preview__img {\n background-repeat: no-repeat;\n}\n\n/* Progress Tooltip */\n.sp-progress__tooltip {\n position: absolute;\n bottom: calc(100% + 8px);\n padding: 6px 10px;\n background: rgba(20, 20, 20, 0.95);\n color: #fff;\n font-size: 12px;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n border-radius: 4px;\n white-space: nowrap;\n transform: translateX(-50%);\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.15s ease;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__tooltip {\n opacity: 1;\n }\n}\n\n/* ============================================\n Control Buttons\n ============================================ */\n.sp-control {\n background: none;\n border: none;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n padding: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: color 0.15s ease, transform 0.15s ease, background 0.15s ease;\n flex-shrink: 0;\n min-width: 44px;\n min-height: 44px;\n}\n\n@media (hover: hover) {\n .sp-control:hover {\n color: #fff;\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-control:active {\n transform: scale(0.92);\n}\n\n.sp-control:focus-visible {\n outline: 2px solid var(--sp-accent, #e50914);\n outline-offset: 2px;\n}\n\n.sp-control:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n transform: none;\n}\n\n.sp-control:disabled:hover {\n background: none;\n}\n\n.sp-control svg {\n width: 24px;\n height: 24px;\n fill: currentColor;\n display: block;\n}\n\n.sp-control--small svg {\n width: 20px;\n height: 20px;\n}\n\n/* ============================================\n Spacer\n ============================================ */\n.sp-spacer {\n flex: 1;\n min-width: 0;\n}\n\n/* ============================================\n Time Display\n ============================================ */\n.sp-time {\n font-size: 13px;\n font-variant-numeric: tabular-nums;\n color: rgba(255, 255, 255, 0.9);\n white-space: nowrap;\n padding: 0 4px;\n letter-spacing: 0.02em;\n}\n\n/* ============================================\n Volume Control\n ============================================ */\n.sp-volume {\n display: flex;\n align-items: center;\n position: relative;\n}\n\n.sp-volume__slider-wrap {\n width: 0;\n overflow: hidden;\n transition: width 0.2s ease;\n}\n\n@media (hover: hover) {\n .sp-volume:hover .sp-volume__slider-wrap {\n width: 64px;\n }\n}\n\n.sp-volume:focus-within .sp-volume__slider-wrap {\n width: 64px;\n}\n\n.sp-volume__slider {\n width: 64px;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n cursor: pointer;\n position: relative;\n margin: 0 8px 0 4px;\n}\n\n.sp-volume__level {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: #fff;\n border-radius: inherit;\n transition: width 0.1s ease;\n}\n\n/* ============================================\n Live Indicator\n ============================================ */\n.sp-live {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--sp-accent, #e50914);\n cursor: pointer;\n padding: 6px 10px;\n border-radius: 4px;\n transition: background 0.15s ease, opacity 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-live:hover {\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-live__dot {\n width: 8px;\n height: 8px;\n background: currentColor;\n border-radius: 50%;\n animation: sp-pulse 2s ease-in-out infinite;\n}\n\n.sp-live--behind {\n opacity: 0.6;\n}\n\n.sp-live--behind .sp-live__dot {\n animation: none;\n}\n\n.sp-live--behind span {\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n/* Progress bar live mode: accent color for filled bar */\n.sp-progress--live .sp-progress__filled {\n background: var(--sp-accent, #e50914);\n}\n\n@keyframes sp-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n}\n\n/* ============================================\n Quality / Settings Menu\n ============================================ */\n.sp-quality {\n position: relative;\n}\n\n.sp-quality__btn {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.sp-quality__label {\n font-size: 12px;\n font-weight: 500;\n opacity: 0.9;\n}\n\n.sp-quality-menu {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n padding: 8px 0;\n min-width: 150px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-quality-menu--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n.sp-quality-menu__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-quality-menu__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-quality-menu__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-quality-menu__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-quality-menu__item--active .sp-quality-menu__check {\n opacity: 1;\n}\n\n/* ============================================\n Settings Menu (Gear Icon)\n ============================================ */\n.sp-settings {\n position: relative;\n}\n\n.sp-settings__btn {\n display: flex;\n align-items: center;\n}\n\n.sp-settings-panel {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n min-width: 200px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n overflow: hidden;\n}\n\n.sp-settings-panel--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n/* Main menu rows */\n.sp-settings-panel--main {\n padding: 4px 0;\n}\n\n.sp-settings-panel__row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__row:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__label {\n font-weight: 500;\n}\n\n.sp-settings-panel__value {\n display: flex;\n align-items: center;\n gap: 4px;\n color: rgba(255, 255, 255, 0.6);\n font-size: 12px;\n}\n\n.sp-settings-panel__arrow {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__arrow svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n/* Sub-menu panels */\n.sp-settings-panel--sub {\n padding: 0;\n}\n\n.sp-settings-panel__header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 16px;\n font-size: 13px;\n font-weight: 600;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__header:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__back {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__back svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__header-label {\n flex: 1;\n}\n\n.sp-settings-panel__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-settings-panel__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-settings-panel__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-settings-panel__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-settings-panel__check svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__item--active .sp-settings-panel__check {\n opacity: 1;\n}\n\n/* ============================================\n Captions Button\n ============================================ */\n.sp-captions--active {\n color: var(--sp-accent, #e50914);\n}\n\n/* ============================================\n Cast Button States\n ============================================ */\n.sp-cast--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-cast--unavailable {\n opacity: 0.4;\n}\n\n/* ============================================\n Error Overlay\n ============================================ */\n.sp-error-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.85);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 25;\n opacity: 0;\n visibility: hidden;\n transition: opacity 0.25s ease, visibility 0.25s;\n}\n\n.sp-error-overlay--visible {\n opacity: 1;\n visibility: visible;\n}\n\n.sp-error-overlay__content {\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n padding: 24px;\n max-width: 360px;\n}\n\n.sp-error-overlay__icon {\n color: rgba(255, 255, 255, 0.7);\n margin-bottom: 16px;\n}\n\n.sp-error-overlay__icon svg {\n width: 48px;\n height: 48px;\n fill: currentColor;\n}\n\n.sp-error-overlay__message {\n color: rgba(255, 255, 255, 0.9);\n font-size: 15px;\n line-height: 1.5;\n margin: 0 0 24px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-error-overlay__actions {\n display: flex;\n gap: 12px;\n flex-wrap: wrap;\n justify-content: center;\n}\n\n.sp-error-overlay__retry {\n background: var(--sp-accent, #e50914);\n color: #fff;\n border: none;\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 600;\n border-radius: 6px;\n cursor: pointer;\n min-width: 120px;\n min-height: 44px;\n transition: background 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__retry:hover {\n filter: brightness(1.1);\n}\n\n.sp-error-overlay__retry:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__retry:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n.sp-error-overlay__dismiss {\n background: none;\n color: rgba(255, 255, 255, 0.7);\n border: 1px solid rgba(255, 255, 255, 0.3);\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n min-width: 100px;\n min-height: 44px;\n transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__dismiss:hover {\n color: #fff;\n border-color: rgba(255, 255, 255, 0.5);\n}\n\n.sp-error-overlay__dismiss:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__dismiss:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n/* ============================================\n Buffering Indicator\n ============================================ */\n.sp-buffering {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 15;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.2s ease;\n}\n\n.sp-buffering--visible {\n opacity: 1;\n}\n\n.sp-buffering svg {\n width: 48px;\n height: 48px;\n fill: rgba(255, 255, 255, 0.9);\n filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));\n}\n\n@keyframes sp-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n\n.sp-spin {\n animation: sp-spin 0.8s linear infinite;\n}\n\n/* ============================================\n Reduced Motion\n ============================================ */\n@media (prefers-reduced-motion: reduce) {\n .sp-gradient,\n .sp-controls,\n .sp-progress-wrapper,\n .sp-progress,\n .sp-progress__handle,\n .sp-progress__tooltip,\n .sp-control,\n .sp-volume__slider-wrap,\n .sp-quality-menu,\n .sp-settings-panel,\n .sp-settings-panel__row,\n .sp-settings-panel__item,\n .sp-settings-panel__header,\n .sp-buffering,\n .sp-error-overlay,\n .sp-error-overlay__retry,\n .sp-error-overlay__dismiss {\n transition: none;\n }\n\n .sp-live__dot,\n .sp-spin {\n animation: none;\n }\n}\n\n/* ============================================\n CSS Custom Properties (Theming)\n ============================================ */\n:root {\n --sp-accent: #e50914;\n --sp-color: #fff;\n --sp-bg: rgba(0, 0, 0, 0.8);\n --sp-control-height: 48px;\n --sp-icon-size: 24px;\n}\n";
108
+ declare const styles = "\n/* ============================================\n Container & Base\n ============================================ */\n.sp-container {\n position: relative;\n width: 100%;\n height: 100%;\n background: #000;\n overflow: hidden;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-container video {\n width: 100%;\n height: 100%;\n display: block;\n object-fit: contain;\n}\n\n.sp-container:focus {\n outline: none;\n}\n\n/* ============================================\n Gradient Overlay\n ============================================ */\n.sp-gradient {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 160px;\n background: linear-gradient(\n to top,\n rgba(0, 0, 0, 0.8) 0%,\n rgba(0, 0, 0, 0.4) 50%,\n transparent 100%\n );\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.25s ease;\n z-index: 5;\n}\n\n.sp-gradient--visible {\n opacity: 1;\n}\n\n/* ============================================\n Controls Container\n ============================================ */\n.sp-controls {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n display: flex;\n align-items: center;\n padding: 0 12px 12px;\n gap: 4px;\n opacity: 0;\n transform: translateY(4px);\n transition: opacity 0.25s ease, transform 0.25s ease;\n z-index: 10;\n}\n\n.sp-controls--visible {\n opacity: 1;\n transform: translateY(0);\n}\n\n.sp-controls--hidden {\n opacity: 0;\n transform: translateY(4px);\n pointer-events: none;\n}\n\n/* ============================================\n Progress Bar (Above Controls)\n ============================================ */\n.sp-progress-wrapper {\n position: absolute;\n bottom: 48px;\n left: 12px;\n right: 12px;\n height: 20px;\n display: flex;\n align-items: center;\n cursor: pointer;\n z-index: 10;\n opacity: 0;\n transition: opacity 0.25s ease;\n}\n\n.sp-progress-wrapper--visible {\n opacity: 1;\n}\n\n.sp-progress {\n position: relative;\n width: 100%;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n transition: height 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress {\n height: 5px;\n }\n}\n\n.sp-progress--dragging {\n height: 5px;\n}\n\n.sp-progress__track {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n border-radius: inherit;\n overflow: hidden;\n}\n\n.sp-progress__buffered {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: rgba(255, 255, 255, 0.4);\n border-radius: inherit;\n transition: width 0.1s linear;\n}\n\n.sp-progress__filled {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: var(--sp-accent, #e50914);\n border-radius: inherit;\n}\n\n.sp-progress__handle {\n position: absolute;\n top: 50%;\n width: 14px;\n height: 14px;\n background: var(--sp-accent, #e50914);\n border-radius: 50%;\n transform: translate(-50%, -50%) scale(0);\n transition: transform 0.15s ease;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n }\n}\n\n.sp-progress--dragging .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n}\n\n/* Thumbnail Preview */\n.sp-thumbnail-preview {\n position: absolute;\n bottom: calc(100% + 8px);\n transform: translateX(-50%);\n pointer-events: none;\n display: none;\n z-index: 21;\n border-radius: 4px;\n overflow: hidden;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n border: 2px solid rgba(255, 255, 255, 0.2);\n}\n\n.sp-thumbnail-preview__img {\n background-repeat: no-repeat;\n}\n\n/* Progress Tooltip */\n.sp-progress__tooltip {\n position: absolute;\n bottom: calc(100% + 8px);\n padding: 6px 10px;\n background: rgba(20, 20, 20, 0.95);\n color: #fff;\n font-size: 12px;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n border-radius: 4px;\n white-space: nowrap;\n transform: translateX(-50%);\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.15s ease;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__tooltip {\n opacity: 1;\n }\n}\n\n/* ============================================\n Control Buttons\n ============================================ */\n.sp-control {\n background: none;\n border: none;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n padding: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: color 0.15s ease, transform 0.15s ease, background 0.15s ease;\n flex-shrink: 0;\n min-width: 44px;\n min-height: 44px;\n}\n\n@media (hover: hover) {\n .sp-control:hover {\n color: #fff;\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-control:active {\n transform: scale(0.92);\n}\n\n.sp-control:focus-visible {\n outline: 2px solid var(--sp-accent, #e50914);\n outline-offset: 2px;\n}\n\n.sp-control:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n transform: none;\n}\n\n.sp-control:disabled:hover {\n background: none;\n}\n\n.sp-control svg {\n width: 24px;\n height: 24px;\n fill: currentColor;\n display: block;\n}\n\n.sp-control--small svg {\n width: 20px;\n height: 20px;\n}\n\n/* ============================================\n Spacer\n ============================================ */\n.sp-spacer {\n flex: 1;\n min-width: 0;\n}\n\n/* ============================================\n Time Display\n ============================================ */\n.sp-time {\n font-size: 13px;\n font-variant-numeric: tabular-nums;\n color: rgba(255, 255, 255, 0.9);\n white-space: nowrap;\n padding: 0 4px;\n letter-spacing: 0.02em;\n}\n\n/* ============================================\n Volume Control\n ============================================ */\n.sp-volume {\n display: flex;\n align-items: center;\n position: relative;\n}\n\n.sp-volume__slider-wrap {\n width: 0;\n overflow: hidden;\n transition: width 0.2s ease;\n}\n\n@media (hover: hover) {\n .sp-volume:hover .sp-volume__slider-wrap {\n width: 64px;\n }\n}\n\n.sp-volume:focus-within .sp-volume__slider-wrap {\n width: 64px;\n}\n\n.sp-volume__slider {\n width: 64px;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n cursor: pointer;\n position: relative;\n margin: 0 8px 0 4px;\n}\n\n.sp-volume__level {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: #fff;\n border-radius: inherit;\n transition: width 0.1s ease;\n}\n\n/* ============================================\n Live Indicator\n ============================================ */\n.sp-live {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--sp-accent, #e50914);\n cursor: pointer;\n padding: 6px 10px;\n border-radius: 4px;\n transition: background 0.15s ease, opacity 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-live:hover {\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-live__dot {\n width: 8px;\n height: 8px;\n background: currentColor;\n border-radius: 50%;\n animation: sp-pulse 2s ease-in-out infinite;\n}\n\n.sp-live--behind {\n opacity: 0.6;\n}\n\n.sp-live--behind .sp-live__dot {\n animation: none;\n}\n\n.sp-live--behind span {\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n/* Progress bar live mode: accent color for filled bar */\n.sp-progress--live .sp-progress__filled {\n background: var(--sp-accent, #e50914);\n}\n\n@keyframes sp-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n}\n\n/* ============================================\n Quality / Settings Menu\n ============================================ */\n.sp-quality {\n position: relative;\n}\n\n.sp-quality__btn {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.sp-quality__label {\n font-size: 12px;\n font-weight: 500;\n opacity: 0.9;\n}\n\n.sp-quality-menu {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n padding: 8px 0;\n min-width: 150px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-quality-menu--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n.sp-quality-menu__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-quality-menu__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-quality-menu__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-quality-menu__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-quality-menu__item--active .sp-quality-menu__check {\n opacity: 1;\n}\n\n/* ============================================\n Settings Menu (Gear Icon)\n ============================================ */\n.sp-settings {\n position: relative;\n}\n\n.sp-settings__btn {\n display: flex;\n align-items: center;\n}\n\n.sp-settings-panel {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n min-width: 200px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n overflow: hidden;\n}\n\n.sp-settings-panel--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n/* Main menu rows */\n.sp-settings-panel--main {\n padding: 4px 0;\n}\n\n.sp-settings-panel__row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__row:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__label {\n font-weight: 500;\n}\n\n.sp-settings-panel__value {\n display: flex;\n align-items: center;\n gap: 4px;\n color: rgba(255, 255, 255, 0.6);\n font-size: 12px;\n}\n\n.sp-settings-panel__arrow {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__arrow svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n/* Sub-menu panels */\n.sp-settings-panel--sub {\n padding: 0;\n}\n\n.sp-settings-panel__header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 16px;\n font-size: 13px;\n font-weight: 600;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__header:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__back {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__back svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__header-label {\n flex: 1;\n}\n\n.sp-settings-panel__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-settings-panel__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-settings-panel__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-settings-panel__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-settings-panel__check svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__item--active .sp-settings-panel__check {\n opacity: 1;\n}\n\n/* ============================================\n Captions Button\n ============================================ */\n.sp-captions--active {\n color: var(--sp-accent, #e50914);\n}\n\n/* ============================================\n Cast Button States\n ============================================ */\n.sp-cast--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-cast--unavailable {\n opacity: 0.4;\n}\n\n/* ============================================\n Error Overlay\n ============================================ */\n.sp-error-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.85);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 25;\n opacity: 0;\n visibility: hidden;\n transition: opacity 0.25s ease, visibility 0.25s;\n}\n\n.sp-error-overlay--visible {\n opacity: 1;\n visibility: visible;\n}\n\n.sp-error-overlay__content {\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n padding: 24px;\n max-width: 360px;\n}\n\n.sp-error-overlay__icon {\n color: rgba(255, 255, 255, 0.7);\n margin-bottom: 16px;\n}\n\n.sp-error-overlay__icon svg {\n width: 48px;\n height: 48px;\n fill: currentColor;\n}\n\n/* Reconnecting: pulse the icon so the overlay reads as active work,\n not a dead-end error */\n.sp-error-overlay--reconnecting .sp-error-overlay__icon {\n animation: sp-reconnect-pulse 1.2s ease-in-out infinite;\n}\n\n@keyframes sp-reconnect-pulse {\n 0%, 100% { opacity: 0.4; }\n 50% { opacity: 1; }\n}\n\n.sp-error-overlay__message {\n color: rgba(255, 255, 255, 0.9);\n font-size: 15px;\n line-height: 1.5;\n margin: 0 0 24px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-error-overlay__actions {\n display: flex;\n gap: 12px;\n flex-wrap: wrap;\n justify-content: center;\n}\n\n.sp-error-overlay__retry {\n background: var(--sp-accent, #e50914);\n color: #fff;\n border: none;\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 600;\n border-radius: 6px;\n cursor: pointer;\n min-width: 120px;\n min-height: 44px;\n transition: background 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__retry:hover {\n filter: brightness(1.1);\n}\n\n.sp-error-overlay__retry:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__retry:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n.sp-error-overlay__dismiss {\n background: none;\n color: rgba(255, 255, 255, 0.7);\n border: 1px solid rgba(255, 255, 255, 0.3);\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n min-width: 100px;\n min-height: 44px;\n transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__dismiss:hover {\n color: #fff;\n border-color: rgba(255, 255, 255, 0.5);\n}\n\n.sp-error-overlay__dismiss:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__dismiss:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n/* ============================================\n Buffering Indicator\n ============================================ */\n.sp-buffering {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 15;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.2s ease;\n}\n\n.sp-buffering--visible {\n opacity: 1;\n}\n\n.sp-buffering svg {\n width: 48px;\n height: 48px;\n fill: rgba(255, 255, 255, 0.9);\n filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));\n}\n\n@keyframes sp-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n\n.sp-spin {\n animation: sp-spin 0.8s linear infinite;\n}\n\n/* ============================================\n Reduced Motion\n ============================================ */\n@media (prefers-reduced-motion: reduce) {\n .sp-gradient,\n .sp-controls,\n .sp-progress-wrapper,\n .sp-progress,\n .sp-progress__handle,\n .sp-progress__tooltip,\n .sp-control,\n .sp-volume__slider-wrap,\n .sp-quality-menu,\n .sp-settings-panel,\n .sp-settings-panel__row,\n .sp-settings-panel__item,\n .sp-settings-panel__header,\n .sp-buffering,\n .sp-error-overlay,\n .sp-error-overlay__retry,\n .sp-error-overlay__dismiss {\n transition: none;\n }\n\n .sp-live__dot,\n .sp-spin {\n animation: none;\n }\n}\n\n/* ============================================\n CSS Custom Properties (Theming)\n ============================================ */\n:root {\n --sp-accent: #e50914;\n --sp-color: #fff;\n --sp-bg: rgba(0, 0, 0, 0.8);\n --sp-control-height: 48px;\n --sp-icon-size: 24px;\n}\n";
109
109
 
110
110
  /**
111
111
  * Formatting utility functions
package/dist/index.js CHANGED
@@ -681,6 +681,17 @@ var styles = `
681
681
  fill: currentColor;
682
682
  }
683
683
 
684
+ /* Reconnecting: pulse the icon so the overlay reads as active work,
685
+ not a dead-end error */
686
+ .sp-error-overlay--reconnecting .sp-error-overlay__icon {
687
+ animation: sp-reconnect-pulse 1.2s ease-in-out infinite;
688
+ }
689
+
690
+ @keyframes sp-reconnect-pulse {
691
+ 0%, 100% { opacity: 0.4; }
692
+ 50% { opacity: 1; }
693
+ }
694
+
684
695
  .sp-error-overlay__message {
685
696
  color: rgba(255, 255, 255, 0.9);
686
697
  font-size: 15px;
@@ -887,12 +898,24 @@ function createButton(className, label, icon) {
887
898
  "aria-label": label,
888
899
  type: "button"
889
900
  });
890
- btn.innerHTML = icon;
901
+ setHTML(btn, icon);
891
902
  return btn;
892
903
  }
893
904
  function getVideo(container) {
894
905
  return container.querySelector("video");
895
906
  }
907
+ var lastHTML = /* @__PURE__ */ new WeakMap();
908
+ function setHTML(el, html) {
909
+ if (lastHTML.get(el) === html) return false;
910
+ el.innerHTML = html;
911
+ lastHTML.set(el, html);
912
+ return true;
913
+ }
914
+ function setAttr(el, name, value) {
915
+ if (el.getAttribute(name) === value) return false;
916
+ el.setAttribute(name, value);
917
+ return true;
918
+ }
896
919
 
897
920
  // src/utils/format.ts
898
921
  function formatTime(seconds) {
@@ -947,8 +970,8 @@ var PlayButton = class {
947
970
  icon = icons.play;
948
971
  label = "Play";
949
972
  }
950
- this.el.innerHTML = icon;
951
- this.el.setAttribute("aria-label", label);
973
+ setHTML(this.el, icon);
974
+ setAttr(this.el, "aria-label", label);
952
975
  }
953
976
  toggle() {
954
977
  const video = getVideo(this.api.container);
@@ -1452,13 +1475,16 @@ var VolumeControl = class {
1452
1475
  icon = icons.volumeHigh;
1453
1476
  label = "Mute";
1454
1477
  }
1455
- this.btn.innerHTML = icon;
1456
- this.btn.setAttribute("aria-label", label);
1478
+ setHTML(this.btn, icon);
1479
+ setAttr(this.btn, "aria-label", label);
1457
1480
  const displayVolume = muted ? 0 : volume;
1458
- this.level.style.width = `${displayVolume * 100}%`;
1481
+ const width = `${displayVolume * 100}%`;
1482
+ if (this.level.style.width !== width) {
1483
+ this.level.style.width = width;
1484
+ }
1459
1485
  const volumePercent = Math.round(displayVolume * 100);
1460
- this.slider.setAttribute("aria-valuenow", String(volumePercent));
1461
- this.slider.setAttribute("aria-valuetext", `${volumePercent}%`);
1486
+ setAttr(this.slider, "aria-valuenow", String(volumePercent));
1487
+ setAttr(this.slider, "aria-valuetext", `${volumePercent}%`);
1462
1488
  }
1463
1489
  toggleMute() {
1464
1490
  const video = getVideo(this.api.container);
@@ -1695,11 +1721,11 @@ var CastButton = class {
1695
1721
  this.el.classList.toggle("sp-cast--active", !!active);
1696
1722
  this.el.classList.toggle("sp-cast--unavailable", !available && !active);
1697
1723
  if (active) {
1698
- this.el.innerHTML = icons.chromecastConnected;
1699
- this.el.setAttribute("aria-label", "Stop casting");
1724
+ setHTML(this.el, icons.chromecastConnected);
1725
+ setAttr(this.el, "aria-label", "Stop casting");
1700
1726
  } else {
1701
- this.el.innerHTML = icons.chromecast;
1702
- this.el.setAttribute("aria-label", available ? "Cast" : "No Cast devices found");
1727
+ setHTML(this.el, icons.chromecast);
1728
+ setAttr(this.el, "aria-label", available ? "Cast" : "No Cast devices found");
1703
1729
  }
1704
1730
  } else {
1705
1731
  const active = this.api.getState("airplayActive");
@@ -1707,7 +1733,7 @@ var CastButton = class {
1707
1733
  this.el.disabled = false;
1708
1734
  this.el.classList.toggle("sp-cast--active", !!active);
1709
1735
  this.el.classList.remove("sp-cast--unavailable");
1710
- this.el.setAttribute("aria-label", active ? "Stop AirPlay" : "AirPlay");
1736
+ setAttr(this.el, "aria-label", active ? "Stop AirPlay" : "AirPlay");
1711
1737
  }
1712
1738
  }
1713
1739
  handleClick() {
@@ -1745,25 +1771,38 @@ var CastButton = class {
1745
1771
  var PipButton = class {
1746
1772
  constructor(api) {
1747
1773
  this.clickHandler = () => {
1748
- this.toggle();
1774
+ void this.toggle().catch(() => {
1775
+ });
1749
1776
  };
1750
1777
  this.api = api;
1751
- const video = document.createElement("video");
1752
- this.supported = "pictureInPictureEnabled" in document || "webkitSetPresentationMode" in video;
1778
+ const probe = document.createElement("video");
1779
+ this.supported = "pictureInPictureEnabled" in document || "webkitSetPresentationMode" in probe;
1753
1780
  this.el = createButton("sp-pip", "Picture-in-Picture", icons.pip);
1754
1781
  this.el.addEventListener("click", this.clickHandler);
1755
1782
  if (!this.supported) {
1756
1783
  this.el.style.display = "none";
1784
+ } else {
1785
+ this.el.disabled = true;
1786
+ this.el.setAttribute("aria-disabled", "true");
1757
1787
  }
1758
1788
  }
1759
1789
  render() {
1760
1790
  return this.el;
1761
1791
  }
1792
+ /** Whether the media element is ready to enter PiP (metadata loaded). */
1793
+ isMediaReady() {
1794
+ const video = getVideo(this.api.container);
1795
+ return !!video && video.readyState >= HTMLMediaElement.HAVE_METADATA;
1796
+ }
1762
1797
  update() {
1763
1798
  if (!this.supported) return;
1764
- const pip = this.api.getState("pip");
1765
- this.el.setAttribute("aria-label", pip ? "Exit Picture-in-Picture" : "Picture-in-Picture");
1766
- this.el.classList.toggle("sp-pip--active", !!pip);
1799
+ const pip = !!this.api.getState("pip");
1800
+ const enabled = pip || this.isMediaReady();
1801
+ this.el.disabled = !enabled;
1802
+ setAttr(this.el, "aria-disabled", String(!enabled));
1803
+ setHTML(this.el, pip ? icons.exitPip : icons.pip);
1804
+ setAttr(this.el, "aria-label", pip ? "Exit Picture-in-Picture" : "Picture-in-Picture");
1805
+ this.el.classList.toggle("sp-pip--active", pip);
1767
1806
  }
1768
1807
  async toggle() {
1769
1808
  const video = getVideo(this.api.container);
@@ -1771,8 +1810,14 @@ var PipButton = class {
1771
1810
  this.api.logger.warn("PiP: video element not found");
1772
1811
  return;
1773
1812
  }
1813
+ const isInPip = document.pictureInPictureElement === video || video.webkitPresentationMode === "picture-in-picture";
1814
+ if (!isInPip && video.readyState < HTMLMediaElement.HAVE_METADATA) {
1815
+ this.api.logger.debug("PiP: ignored, media not ready", {
1816
+ readyState: video.readyState
1817
+ });
1818
+ return;
1819
+ }
1774
1820
  try {
1775
- const isInPip = document.pictureInPictureElement === video || video.webkitPresentationMode === "picture-in-picture";
1776
1821
  if (isInPip) {
1777
1822
  if (document.pictureInPictureElement) {
1778
1823
  await document.exitPictureInPicture();
@@ -1789,7 +1834,8 @@ var PipButton = class {
1789
1834
  this.api.logger.debug("PiP: entered");
1790
1835
  }
1791
1836
  } catch (e) {
1792
- this.api.logger.warn("PiP: failed", { error: e.message });
1837
+ const message = e instanceof Error ? e.message : String(e);
1838
+ this.api.logger.warn("PiP: failed", { error: message });
1793
1839
  }
1794
1840
  }
1795
1841
  destroy() {
@@ -1814,11 +1860,11 @@ var FullscreenButton = class {
1814
1860
  update() {
1815
1861
  const fullscreen = this.api.getState("fullscreen");
1816
1862
  if (fullscreen) {
1817
- this.el.innerHTML = icons.exitFullscreen;
1818
- this.el.setAttribute("aria-label", "Exit fullscreen");
1863
+ setHTML(this.el, icons.exitFullscreen);
1864
+ setAttr(this.el, "aria-label", "Exit fullscreen");
1819
1865
  } else {
1820
- this.el.innerHTML = icons.fullscreen;
1821
- this.el.setAttribute("aria-label", "Fullscreen");
1866
+ setHTML(this.el, icons.fullscreen);
1867
+ setAttr(this.el, "aria-label", "Fullscreen");
1822
1868
  }
1823
1869
  }
1824
1870
  async toggle() {
@@ -1859,6 +1905,27 @@ var Spacer = class {
1859
1905
  // src/controls/ErrorOverlay.ts
1860
1906
  function getUserMessage(error) {
1861
1907
  if (!error) return "Something went wrong.";
1908
+ const code = error.code;
1909
+ if (code) {
1910
+ switch (code) {
1911
+ case "MEDIA_NETWORK_ERROR":
1912
+ return "Having trouble connecting. Check your internet and try again.";
1913
+ case "MEDIA_DECODE_ERROR":
1914
+ return "This video can't be played right now.";
1915
+ case "SOURCE_LOAD_FAILED":
1916
+ case "SOURCE_NOT_SUPPORTED":
1917
+ case "PROVIDER_NOT_FOUND":
1918
+ return "Unable to load video. Please try again.";
1919
+ case "PLAYBACK_FAILED":
1920
+ return "Playback stopped unexpectedly. Please try again.";
1921
+ case "MEDIA_APPEND_ERROR":
1922
+ return "Video playback was interrupted. Please try again.";
1923
+ case "MEDIA_BUFFER_FULL":
1924
+ return "Your device is low on video memory. Close other apps or tabs and try again.";
1925
+ case "PLAYLIST_INVALID":
1926
+ return "The stream is temporarily unavailable. Please try again.";
1927
+ }
1928
+ }
1862
1929
  const msg = error.message?.toLowerCase() || "";
1863
1930
  if (msg.includes("network") || msg.includes("timeout") || msg.includes("fetch") || msg.includes("connection")) {
1864
1931
  return "Having trouble connecting. Check your internet and try again.";
@@ -1886,13 +1953,6 @@ var ErrorOverlay = class {
1886
1953
  const src = source?.src || this.lastSource;
1887
1954
  if (src) {
1888
1955
  this.api.emit("error:retry", { src });
1889
- const video = this.api.container.querySelector("video");
1890
- if (video) {
1891
- video.src = src;
1892
- video.load();
1893
- video.play().catch(() => {
1894
- });
1895
- }
1896
1956
  }
1897
1957
  setTimeout(() => {
1898
1958
  this.retryBtn.disabled = false;
@@ -1953,12 +2013,36 @@ var ErrorOverlay = class {
1953
2013
  }
1954
2014
  this.visible = true;
1955
2015
  this.retryBtn.disabled = false;
2016
+ this.el.classList.remove("sp-error-overlay--reconnecting");
2017
+ this.el.classList.add("sp-error-overlay--visible");
2018
+ }
2019
+ /**
2020
+ * Show the reconnecting state.
2021
+ *
2022
+ * Displayed while the provider auto-reconnects after a fatal error, so the
2023
+ * viewer sees the player working on the problem instead of a dead-end
2024
+ * error. Try Again stays available for viewers who want to force an
2025
+ * immediate attempt.
2026
+ */
2027
+ showReconnecting() {
2028
+ const messageEl = this.el.querySelector(".sp-error-overlay__message");
2029
+ if (messageEl) {
2030
+ messageEl.textContent = "Connection lost. Reconnecting...";
2031
+ }
2032
+ const source = this.api.getState("source");
2033
+ if (source?.src) {
2034
+ this.lastSource = source.src;
2035
+ }
2036
+ this.visible = true;
2037
+ this.retryBtn.disabled = false;
2038
+ this.el.classList.add("sp-error-overlay--reconnecting");
1956
2039
  this.el.classList.add("sp-error-overlay--visible");
1957
2040
  }
1958
2041
  /** Hide the error overlay */
1959
2042
  hide() {
1960
2043
  this.visible = false;
1961
2044
  this.el.classList.remove("sp-error-overlay--visible");
2045
+ this.el.classList.remove("sp-error-overlay--reconnecting");
1962
2046
  }
1963
2047
  isVisible() {
1964
2048
  return this.visible;
@@ -2442,12 +2526,12 @@ var CaptionsButton = class {
2442
2526
  }
2443
2527
  this.el.style.display = "";
2444
2528
  if (currentTrack) {
2445
- this.el.innerHTML = icons.captions;
2446
- this.el.setAttribute("aria-label", `Captions: ${currentTrack.label}`);
2529
+ setHTML(this.el, icons.captions);
2530
+ setAttr(this.el, "aria-label", `Captions: ${currentTrack.label}`);
2447
2531
  this.el.classList.add("sp-captions--active");
2448
2532
  } else {
2449
- this.el.innerHTML = icons.captionsOff;
2450
- this.el.setAttribute("aria-label", "Captions");
2533
+ setHTML(this.el, icons.captionsOff);
2534
+ setAttr(this.el, "aria-label", "Captions");
2451
2535
  this.el.classList.remove("sp-captions--active");
2452
2536
  }
2453
2537
  }
@@ -2530,7 +2614,10 @@ function uiPlugin(config = {}) {
2530
2614
  let hideTimeout = null;
2531
2615
  let stateUnsubscribe = null;
2532
2616
  let errorUnsubscribe = null;
2617
+ let reconnectingUnsubscribe = null;
2618
+ let recoveredUnsubscribe = null;
2533
2619
  let controlsVisible = true;
2620
+ let rafHandle = null;
2534
2621
  const layout = config.controls || DEFAULT_LAYOUT;
2535
2622
  const hideDelay = config.hideDelay ?? DEFAULT_HIDE_DELAY;
2536
2623
  const createControl = (slot) => {
@@ -2582,6 +2669,13 @@ function uiPlugin(config = {}) {
2582
2669
  bufferingIndicator?.classList.toggle("sp-buffering--visible", !!showSpinner);
2583
2670
  errorOverlay?.update();
2584
2671
  };
2672
+ const scheduleUpdate = () => {
2673
+ if (rafHandle !== null) return;
2674
+ rafHandle = requestAnimationFrame(() => {
2675
+ rafHandle = null;
2676
+ updateControls();
2677
+ });
2678
+ };
2585
2679
  const showControls = () => {
2586
2680
  if (controlsVisible) {
2587
2681
  resetHideTimer();
@@ -2631,7 +2725,12 @@ function uiPlugin(config = {}) {
2631
2725
  case " ":
2632
2726
  case "k":
2633
2727
  e.preventDefault();
2634
- video.paused ? video.play() : video.pause();
2728
+ if (video.paused) {
2729
+ video.play().catch(() => {
2730
+ });
2731
+ } else {
2732
+ video.pause();
2733
+ }
2635
2734
  break;
2636
2735
  case "m":
2637
2736
  e.preventDefault();
@@ -2640,9 +2739,11 @@ function uiPlugin(config = {}) {
2640
2739
  case "f":
2641
2740
  e.preventDefault();
2642
2741
  if (document.fullscreenElement) {
2643
- document.exitFullscreen();
2742
+ document.exitFullscreen().catch(() => {
2743
+ });
2644
2744
  } else {
2645
- api.container.requestFullscreen?.();
2745
+ api.container.requestFullscreen?.().catch(() => {
2746
+ });
2646
2747
  }
2647
2748
  break;
2648
2749
  case "ArrowLeft":
@@ -2710,10 +2811,16 @@ function uiPlugin(config = {}) {
2710
2811
  container.appendChild(errorOverlay.render());
2711
2812
  errorUnsubscribe = api.on("error", (payload) => {
2712
2813
  if (payload?.fatal) {
2713
- const error = api.getState("error") || new Error(payload.message || "Playback error");
2814
+ const error = api.getState("error") || payload;
2714
2815
  errorOverlay?.show(error);
2715
2816
  }
2716
2817
  });
2818
+ reconnectingUnsubscribe = api.on("error:reconnecting", () => {
2819
+ errorOverlay?.showReconnecting();
2820
+ });
2821
+ recoveredUnsubscribe = api.on("error:recovered", () => {
2822
+ errorOverlay?.hide();
2823
+ });
2717
2824
  progressBar = new ProgressBar(api);
2718
2825
  container.appendChild(progressBar.render());
2719
2826
  if (!isPlaying) {
@@ -2737,8 +2844,8 @@ function uiPlugin(config = {}) {
2737
2844
  container.addEventListener("touchstart", handleInteraction, { passive: true });
2738
2845
  container.addEventListener("click", handleInteraction);
2739
2846
  document.addEventListener("keydown", handleKeyDown);
2740
- stateUnsubscribe = api.subscribeToState(updateControls);
2741
- document.addEventListener("fullscreenchange", updateControls);
2847
+ stateUnsubscribe = api.subscribeToState(scheduleUpdate);
2848
+ document.addEventListener("fullscreenchange", scheduleUpdate);
2742
2849
  updateControls();
2743
2850
  if (!container.hasAttribute("tabindex")) {
2744
2851
  container.setAttribute("tabindex", "0");
@@ -2755,10 +2862,18 @@ function uiPlugin(config = {}) {
2755
2862
  clearTimeout(hideTimeout);
2756
2863
  hideTimeout = null;
2757
2864
  }
2865
+ if (rafHandle !== null) {
2866
+ cancelAnimationFrame(rafHandle);
2867
+ rafHandle = null;
2868
+ }
2758
2869
  stateUnsubscribe?.();
2759
2870
  stateUnsubscribe = null;
2760
2871
  errorUnsubscribe?.();
2761
2872
  errorUnsubscribe = null;
2873
+ reconnectingUnsubscribe?.();
2874
+ reconnectingUnsubscribe = null;
2875
+ recoveredUnsubscribe?.();
2876
+ recoveredUnsubscribe = null;
2762
2877
  if (api?.container) {
2763
2878
  api.container.removeEventListener("mousemove", handleInteraction);
2764
2879
  api.container.removeEventListener("mouseenter", handleInteraction);
@@ -2767,7 +2882,7 @@ function uiPlugin(config = {}) {
2767
2882
  api.container.removeEventListener("click", handleInteraction);
2768
2883
  }
2769
2884
  document.removeEventListener("keydown", handleKeyDown);
2770
- document.removeEventListener("fullscreenchange", updateControls);
2885
+ document.removeEventListener("fullscreenchange", scheduleUpdate);
2771
2886
  controls.forEach((c) => c.destroy());
2772
2887
  controls = [];
2773
2888
  progressBar?.destroy();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scarlett-player/ui",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "UI Controls Plugin for Scarlett Player",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -22,14 +22,14 @@
22
22
  "dist"
23
23
  ],
24
24
  "peerDependencies": {
25
- "@scarlett-player/core": "^1.0.2"
25
+ "@scarlett-player/core": "^1.0.3"
26
26
  },
27
27
  "devDependencies": {
28
28
  "tsup": "^8.0.0",
29
29
  "typescript": "^5.3.0",
30
30
  "vitest": "^1.6.0",
31
31
  "jsdom": "^24.0.0",
32
- "@scarlett-player/core": "1.0.2"
32
+ "@scarlett-player/core": "1.2.0"
33
33
  },
34
34
  "keywords": [
35
35
  "video",