@scarlett-player/ui 1.0.1 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +114 -32
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +114 -32
- package/package.json +3 -3
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
|
|
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
|
|
982
|
-
this.el
|
|
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
|
|
1487
|
-
this.btn
|
|
1509
|
+
setHTML(this.btn, icon);
|
|
1510
|
+
setAttr(this.btn, "aria-label", label);
|
|
1488
1511
|
const displayVolume = muted ? 0 : volume;
|
|
1489
|
-
|
|
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
|
|
1492
|
-
this.slider
|
|
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
|
|
1730
|
-
this.el
|
|
1755
|
+
setHTML(this.el, icons.chromecastConnected);
|
|
1756
|
+
setAttr(this.el, "aria-label", "Stop casting");
|
|
1731
1757
|
} else {
|
|
1732
|
-
this.el
|
|
1733
|
-
this.el
|
|
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
|
|
1767
|
+
setAttr(this.el, "aria-label", active ? "Stop AirPlay" : "AirPlay");
|
|
1742
1768
|
}
|
|
1743
1769
|
}
|
|
1744
1770
|
handleClick() {
|
|
@@ -1845,11 +1871,11 @@ var FullscreenButton = class {
|
|
|
1845
1871
|
update() {
|
|
1846
1872
|
const fullscreen = this.api.getState("fullscreen");
|
|
1847
1873
|
if (fullscreen) {
|
|
1848
|
-
this.el
|
|
1849
|
-
this.el
|
|
1874
|
+
setHTML(this.el, icons.exitFullscreen);
|
|
1875
|
+
setAttr(this.el, "aria-label", "Exit fullscreen");
|
|
1850
1876
|
} else {
|
|
1851
|
-
this.el
|
|
1852
|
-
this.el
|
|
1877
|
+
setHTML(this.el, icons.fullscreen);
|
|
1878
|
+
setAttr(this.el, "aria-label", "Fullscreen");
|
|
1853
1879
|
}
|
|
1854
1880
|
}
|
|
1855
1881
|
async toggle() {
|
|
@@ -1890,6 +1916,21 @@ var Spacer = class {
|
|
|
1890
1916
|
// src/controls/ErrorOverlay.ts
|
|
1891
1917
|
function getUserMessage(error) {
|
|
1892
1918
|
if (!error) return "Something went wrong.";
|
|
1919
|
+
const code = error.code;
|
|
1920
|
+
if (code) {
|
|
1921
|
+
switch (code) {
|
|
1922
|
+
case "MEDIA_NETWORK_ERROR":
|
|
1923
|
+
return "Having trouble connecting. Check your internet and try again.";
|
|
1924
|
+
case "MEDIA_DECODE_ERROR":
|
|
1925
|
+
return "This video can't be played right now.";
|
|
1926
|
+
case "SOURCE_LOAD_FAILED":
|
|
1927
|
+
case "SOURCE_NOT_SUPPORTED":
|
|
1928
|
+
case "PROVIDER_NOT_FOUND":
|
|
1929
|
+
return "Unable to load video. Please try again.";
|
|
1930
|
+
case "PLAYBACK_FAILED":
|
|
1931
|
+
return "Playback stopped unexpectedly. Please try again.";
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1893
1934
|
const msg = error.message?.toLowerCase() || "";
|
|
1894
1935
|
if (msg.includes("network") || msg.includes("timeout") || msg.includes("fetch") || msg.includes("connection")) {
|
|
1895
1936
|
return "Having trouble connecting. Check your internet and try again.";
|
|
@@ -1917,13 +1958,6 @@ var ErrorOverlay = class {
|
|
|
1917
1958
|
const src = source?.src || this.lastSource;
|
|
1918
1959
|
if (src) {
|
|
1919
1960
|
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
1961
|
}
|
|
1928
1962
|
setTimeout(() => {
|
|
1929
1963
|
this.retryBtn.disabled = false;
|
|
@@ -1984,12 +2018,36 @@ var ErrorOverlay = class {
|
|
|
1984
2018
|
}
|
|
1985
2019
|
this.visible = true;
|
|
1986
2020
|
this.retryBtn.disabled = false;
|
|
2021
|
+
this.el.classList.remove("sp-error-overlay--reconnecting");
|
|
2022
|
+
this.el.classList.add("sp-error-overlay--visible");
|
|
2023
|
+
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Show the reconnecting state.
|
|
2026
|
+
*
|
|
2027
|
+
* Displayed while the provider auto-reconnects after a fatal error, so the
|
|
2028
|
+
* viewer sees the player working on the problem instead of a dead-end
|
|
2029
|
+
* error. Try Again stays available for viewers who want to force an
|
|
2030
|
+
* immediate attempt.
|
|
2031
|
+
*/
|
|
2032
|
+
showReconnecting() {
|
|
2033
|
+
const messageEl = this.el.querySelector(".sp-error-overlay__message");
|
|
2034
|
+
if (messageEl) {
|
|
2035
|
+
messageEl.textContent = "Connection lost. Reconnecting...";
|
|
2036
|
+
}
|
|
2037
|
+
const source = this.api.getState("source");
|
|
2038
|
+
if (source?.src) {
|
|
2039
|
+
this.lastSource = source.src;
|
|
2040
|
+
}
|
|
2041
|
+
this.visible = true;
|
|
2042
|
+
this.retryBtn.disabled = false;
|
|
2043
|
+
this.el.classList.add("sp-error-overlay--reconnecting");
|
|
1987
2044
|
this.el.classList.add("sp-error-overlay--visible");
|
|
1988
2045
|
}
|
|
1989
2046
|
/** Hide the error overlay */
|
|
1990
2047
|
hide() {
|
|
1991
2048
|
this.visible = false;
|
|
1992
2049
|
this.el.classList.remove("sp-error-overlay--visible");
|
|
2050
|
+
this.el.classList.remove("sp-error-overlay--reconnecting");
|
|
1993
2051
|
}
|
|
1994
2052
|
isVisible() {
|
|
1995
2053
|
return this.visible;
|
|
@@ -2473,12 +2531,12 @@ var CaptionsButton = class {
|
|
|
2473
2531
|
}
|
|
2474
2532
|
this.el.style.display = "";
|
|
2475
2533
|
if (currentTrack) {
|
|
2476
|
-
this.el
|
|
2477
|
-
this.el
|
|
2534
|
+
setHTML(this.el, icons.captions);
|
|
2535
|
+
setAttr(this.el, "aria-label", `Captions: ${currentTrack.label}`);
|
|
2478
2536
|
this.el.classList.add("sp-captions--active");
|
|
2479
2537
|
} else {
|
|
2480
|
-
this.el
|
|
2481
|
-
this.el
|
|
2538
|
+
setHTML(this.el, icons.captionsOff);
|
|
2539
|
+
setAttr(this.el, "aria-label", "Captions");
|
|
2482
2540
|
this.el.classList.remove("sp-captions--active");
|
|
2483
2541
|
}
|
|
2484
2542
|
}
|
|
@@ -2561,7 +2619,10 @@ function uiPlugin(config = {}) {
|
|
|
2561
2619
|
let hideTimeout = null;
|
|
2562
2620
|
let stateUnsubscribe = null;
|
|
2563
2621
|
let errorUnsubscribe = null;
|
|
2622
|
+
let reconnectingUnsubscribe = null;
|
|
2623
|
+
let recoveredUnsubscribe = null;
|
|
2564
2624
|
let controlsVisible = true;
|
|
2625
|
+
let rafHandle = null;
|
|
2565
2626
|
const layout = config.controls || DEFAULT_LAYOUT;
|
|
2566
2627
|
const hideDelay = config.hideDelay ?? DEFAULT_HIDE_DELAY;
|
|
2567
2628
|
const createControl = (slot) => {
|
|
@@ -2613,6 +2674,13 @@ function uiPlugin(config = {}) {
|
|
|
2613
2674
|
bufferingIndicator?.classList.toggle("sp-buffering--visible", !!showSpinner);
|
|
2614
2675
|
errorOverlay?.update();
|
|
2615
2676
|
};
|
|
2677
|
+
const scheduleUpdate = () => {
|
|
2678
|
+
if (rafHandle !== null) return;
|
|
2679
|
+
rafHandle = requestAnimationFrame(() => {
|
|
2680
|
+
rafHandle = null;
|
|
2681
|
+
updateControls();
|
|
2682
|
+
});
|
|
2683
|
+
};
|
|
2616
2684
|
const showControls = () => {
|
|
2617
2685
|
if (controlsVisible) {
|
|
2618
2686
|
resetHideTimer();
|
|
@@ -2741,10 +2809,16 @@ function uiPlugin(config = {}) {
|
|
|
2741
2809
|
container.appendChild(errorOverlay.render());
|
|
2742
2810
|
errorUnsubscribe = api.on("error", (payload) => {
|
|
2743
2811
|
if (payload?.fatal) {
|
|
2744
|
-
const error = api.getState("error") ||
|
|
2812
|
+
const error = api.getState("error") || payload;
|
|
2745
2813
|
errorOverlay?.show(error);
|
|
2746
2814
|
}
|
|
2747
2815
|
});
|
|
2816
|
+
reconnectingUnsubscribe = api.on("error:reconnecting", () => {
|
|
2817
|
+
errorOverlay?.showReconnecting();
|
|
2818
|
+
});
|
|
2819
|
+
recoveredUnsubscribe = api.on("error:recovered", () => {
|
|
2820
|
+
errorOverlay?.hide();
|
|
2821
|
+
});
|
|
2748
2822
|
progressBar = new ProgressBar(api);
|
|
2749
2823
|
container.appendChild(progressBar.render());
|
|
2750
2824
|
if (!isPlaying) {
|
|
@@ -2768,8 +2842,8 @@ function uiPlugin(config = {}) {
|
|
|
2768
2842
|
container.addEventListener("touchstart", handleInteraction, { passive: true });
|
|
2769
2843
|
container.addEventListener("click", handleInteraction);
|
|
2770
2844
|
document.addEventListener("keydown", handleKeyDown);
|
|
2771
|
-
stateUnsubscribe = api.subscribeToState(
|
|
2772
|
-
document.addEventListener("fullscreenchange",
|
|
2845
|
+
stateUnsubscribe = api.subscribeToState(scheduleUpdate);
|
|
2846
|
+
document.addEventListener("fullscreenchange", scheduleUpdate);
|
|
2773
2847
|
updateControls();
|
|
2774
2848
|
if (!container.hasAttribute("tabindex")) {
|
|
2775
2849
|
container.setAttribute("tabindex", "0");
|
|
@@ -2786,10 +2860,18 @@ function uiPlugin(config = {}) {
|
|
|
2786
2860
|
clearTimeout(hideTimeout);
|
|
2787
2861
|
hideTimeout = null;
|
|
2788
2862
|
}
|
|
2863
|
+
if (rafHandle !== null) {
|
|
2864
|
+
cancelAnimationFrame(rafHandle);
|
|
2865
|
+
rafHandle = null;
|
|
2866
|
+
}
|
|
2789
2867
|
stateUnsubscribe?.();
|
|
2790
2868
|
stateUnsubscribe = null;
|
|
2791
2869
|
errorUnsubscribe?.();
|
|
2792
2870
|
errorUnsubscribe = null;
|
|
2871
|
+
reconnectingUnsubscribe?.();
|
|
2872
|
+
reconnectingUnsubscribe = null;
|
|
2873
|
+
recoveredUnsubscribe?.();
|
|
2874
|
+
recoveredUnsubscribe = null;
|
|
2793
2875
|
if (api?.container) {
|
|
2794
2876
|
api.container.removeEventListener("mousemove", handleInteraction);
|
|
2795
2877
|
api.container.removeEventListener("mouseenter", handleInteraction);
|
|
@@ -2798,7 +2880,7 @@ function uiPlugin(config = {}) {
|
|
|
2798
2880
|
api.container.removeEventListener("click", handleInteraction);
|
|
2799
2881
|
}
|
|
2800
2882
|
document.removeEventListener("keydown", handleKeyDown);
|
|
2801
|
-
document.removeEventListener("fullscreenchange",
|
|
2883
|
+
document.removeEventListener("fullscreenchange", scheduleUpdate);
|
|
2802
2884
|
controls.forEach((c) => c.destroy());
|
|
2803
2885
|
controls = [];
|
|
2804
2886
|
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
|
|
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
|
|
951
|
-
this.el
|
|
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
|
|
1456
|
-
this.btn
|
|
1478
|
+
setHTML(this.btn, icon);
|
|
1479
|
+
setAttr(this.btn, "aria-label", label);
|
|
1457
1480
|
const displayVolume = muted ? 0 : volume;
|
|
1458
|
-
|
|
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
|
|
1461
|
-
this.slider
|
|
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
|
|
1699
|
-
this.el
|
|
1724
|
+
setHTML(this.el, icons.chromecastConnected);
|
|
1725
|
+
setAttr(this.el, "aria-label", "Stop casting");
|
|
1700
1726
|
} else {
|
|
1701
|
-
this.el
|
|
1702
|
-
this.el
|
|
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
|
|
1736
|
+
setAttr(this.el, "aria-label", active ? "Stop AirPlay" : "AirPlay");
|
|
1711
1737
|
}
|
|
1712
1738
|
}
|
|
1713
1739
|
handleClick() {
|
|
@@ -1814,11 +1840,11 @@ var FullscreenButton = class {
|
|
|
1814
1840
|
update() {
|
|
1815
1841
|
const fullscreen = this.api.getState("fullscreen");
|
|
1816
1842
|
if (fullscreen) {
|
|
1817
|
-
this.el
|
|
1818
|
-
this.el
|
|
1843
|
+
setHTML(this.el, icons.exitFullscreen);
|
|
1844
|
+
setAttr(this.el, "aria-label", "Exit fullscreen");
|
|
1819
1845
|
} else {
|
|
1820
|
-
this.el
|
|
1821
|
-
this.el
|
|
1846
|
+
setHTML(this.el, icons.fullscreen);
|
|
1847
|
+
setAttr(this.el, "aria-label", "Fullscreen");
|
|
1822
1848
|
}
|
|
1823
1849
|
}
|
|
1824
1850
|
async toggle() {
|
|
@@ -1859,6 +1885,21 @@ var Spacer = class {
|
|
|
1859
1885
|
// src/controls/ErrorOverlay.ts
|
|
1860
1886
|
function getUserMessage(error) {
|
|
1861
1887
|
if (!error) return "Something went wrong.";
|
|
1888
|
+
const code = error.code;
|
|
1889
|
+
if (code) {
|
|
1890
|
+
switch (code) {
|
|
1891
|
+
case "MEDIA_NETWORK_ERROR":
|
|
1892
|
+
return "Having trouble connecting. Check your internet and try again.";
|
|
1893
|
+
case "MEDIA_DECODE_ERROR":
|
|
1894
|
+
return "This video can't be played right now.";
|
|
1895
|
+
case "SOURCE_LOAD_FAILED":
|
|
1896
|
+
case "SOURCE_NOT_SUPPORTED":
|
|
1897
|
+
case "PROVIDER_NOT_FOUND":
|
|
1898
|
+
return "Unable to load video. Please try again.";
|
|
1899
|
+
case "PLAYBACK_FAILED":
|
|
1900
|
+
return "Playback stopped unexpectedly. Please try again.";
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1862
1903
|
const msg = error.message?.toLowerCase() || "";
|
|
1863
1904
|
if (msg.includes("network") || msg.includes("timeout") || msg.includes("fetch") || msg.includes("connection")) {
|
|
1864
1905
|
return "Having trouble connecting. Check your internet and try again.";
|
|
@@ -1886,13 +1927,6 @@ var ErrorOverlay = class {
|
|
|
1886
1927
|
const src = source?.src || this.lastSource;
|
|
1887
1928
|
if (src) {
|
|
1888
1929
|
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
1930
|
}
|
|
1897
1931
|
setTimeout(() => {
|
|
1898
1932
|
this.retryBtn.disabled = false;
|
|
@@ -1953,12 +1987,36 @@ var ErrorOverlay = class {
|
|
|
1953
1987
|
}
|
|
1954
1988
|
this.visible = true;
|
|
1955
1989
|
this.retryBtn.disabled = false;
|
|
1990
|
+
this.el.classList.remove("sp-error-overlay--reconnecting");
|
|
1991
|
+
this.el.classList.add("sp-error-overlay--visible");
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Show the reconnecting state.
|
|
1995
|
+
*
|
|
1996
|
+
* Displayed while the provider auto-reconnects after a fatal error, so the
|
|
1997
|
+
* viewer sees the player working on the problem instead of a dead-end
|
|
1998
|
+
* error. Try Again stays available for viewers who want to force an
|
|
1999
|
+
* immediate attempt.
|
|
2000
|
+
*/
|
|
2001
|
+
showReconnecting() {
|
|
2002
|
+
const messageEl = this.el.querySelector(".sp-error-overlay__message");
|
|
2003
|
+
if (messageEl) {
|
|
2004
|
+
messageEl.textContent = "Connection lost. Reconnecting...";
|
|
2005
|
+
}
|
|
2006
|
+
const source = this.api.getState("source");
|
|
2007
|
+
if (source?.src) {
|
|
2008
|
+
this.lastSource = source.src;
|
|
2009
|
+
}
|
|
2010
|
+
this.visible = true;
|
|
2011
|
+
this.retryBtn.disabled = false;
|
|
2012
|
+
this.el.classList.add("sp-error-overlay--reconnecting");
|
|
1956
2013
|
this.el.classList.add("sp-error-overlay--visible");
|
|
1957
2014
|
}
|
|
1958
2015
|
/** Hide the error overlay */
|
|
1959
2016
|
hide() {
|
|
1960
2017
|
this.visible = false;
|
|
1961
2018
|
this.el.classList.remove("sp-error-overlay--visible");
|
|
2019
|
+
this.el.classList.remove("sp-error-overlay--reconnecting");
|
|
1962
2020
|
}
|
|
1963
2021
|
isVisible() {
|
|
1964
2022
|
return this.visible;
|
|
@@ -2442,12 +2500,12 @@ var CaptionsButton = class {
|
|
|
2442
2500
|
}
|
|
2443
2501
|
this.el.style.display = "";
|
|
2444
2502
|
if (currentTrack) {
|
|
2445
|
-
this.el
|
|
2446
|
-
this.el
|
|
2503
|
+
setHTML(this.el, icons.captions);
|
|
2504
|
+
setAttr(this.el, "aria-label", `Captions: ${currentTrack.label}`);
|
|
2447
2505
|
this.el.classList.add("sp-captions--active");
|
|
2448
2506
|
} else {
|
|
2449
|
-
this.el
|
|
2450
|
-
this.el
|
|
2507
|
+
setHTML(this.el, icons.captionsOff);
|
|
2508
|
+
setAttr(this.el, "aria-label", "Captions");
|
|
2451
2509
|
this.el.classList.remove("sp-captions--active");
|
|
2452
2510
|
}
|
|
2453
2511
|
}
|
|
@@ -2530,7 +2588,10 @@ function uiPlugin(config = {}) {
|
|
|
2530
2588
|
let hideTimeout = null;
|
|
2531
2589
|
let stateUnsubscribe = null;
|
|
2532
2590
|
let errorUnsubscribe = null;
|
|
2591
|
+
let reconnectingUnsubscribe = null;
|
|
2592
|
+
let recoveredUnsubscribe = null;
|
|
2533
2593
|
let controlsVisible = true;
|
|
2594
|
+
let rafHandle = null;
|
|
2534
2595
|
const layout = config.controls || DEFAULT_LAYOUT;
|
|
2535
2596
|
const hideDelay = config.hideDelay ?? DEFAULT_HIDE_DELAY;
|
|
2536
2597
|
const createControl = (slot) => {
|
|
@@ -2582,6 +2643,13 @@ function uiPlugin(config = {}) {
|
|
|
2582
2643
|
bufferingIndicator?.classList.toggle("sp-buffering--visible", !!showSpinner);
|
|
2583
2644
|
errorOverlay?.update();
|
|
2584
2645
|
};
|
|
2646
|
+
const scheduleUpdate = () => {
|
|
2647
|
+
if (rafHandle !== null) return;
|
|
2648
|
+
rafHandle = requestAnimationFrame(() => {
|
|
2649
|
+
rafHandle = null;
|
|
2650
|
+
updateControls();
|
|
2651
|
+
});
|
|
2652
|
+
};
|
|
2585
2653
|
const showControls = () => {
|
|
2586
2654
|
if (controlsVisible) {
|
|
2587
2655
|
resetHideTimer();
|
|
@@ -2710,10 +2778,16 @@ function uiPlugin(config = {}) {
|
|
|
2710
2778
|
container.appendChild(errorOverlay.render());
|
|
2711
2779
|
errorUnsubscribe = api.on("error", (payload) => {
|
|
2712
2780
|
if (payload?.fatal) {
|
|
2713
|
-
const error = api.getState("error") ||
|
|
2781
|
+
const error = api.getState("error") || payload;
|
|
2714
2782
|
errorOverlay?.show(error);
|
|
2715
2783
|
}
|
|
2716
2784
|
});
|
|
2785
|
+
reconnectingUnsubscribe = api.on("error:reconnecting", () => {
|
|
2786
|
+
errorOverlay?.showReconnecting();
|
|
2787
|
+
});
|
|
2788
|
+
recoveredUnsubscribe = api.on("error:recovered", () => {
|
|
2789
|
+
errorOverlay?.hide();
|
|
2790
|
+
});
|
|
2717
2791
|
progressBar = new ProgressBar(api);
|
|
2718
2792
|
container.appendChild(progressBar.render());
|
|
2719
2793
|
if (!isPlaying) {
|
|
@@ -2737,8 +2811,8 @@ function uiPlugin(config = {}) {
|
|
|
2737
2811
|
container.addEventListener("touchstart", handleInteraction, { passive: true });
|
|
2738
2812
|
container.addEventListener("click", handleInteraction);
|
|
2739
2813
|
document.addEventListener("keydown", handleKeyDown);
|
|
2740
|
-
stateUnsubscribe = api.subscribeToState(
|
|
2741
|
-
document.addEventListener("fullscreenchange",
|
|
2814
|
+
stateUnsubscribe = api.subscribeToState(scheduleUpdate);
|
|
2815
|
+
document.addEventListener("fullscreenchange", scheduleUpdate);
|
|
2742
2816
|
updateControls();
|
|
2743
2817
|
if (!container.hasAttribute("tabindex")) {
|
|
2744
2818
|
container.setAttribute("tabindex", "0");
|
|
@@ -2755,10 +2829,18 @@ function uiPlugin(config = {}) {
|
|
|
2755
2829
|
clearTimeout(hideTimeout);
|
|
2756
2830
|
hideTimeout = null;
|
|
2757
2831
|
}
|
|
2832
|
+
if (rafHandle !== null) {
|
|
2833
|
+
cancelAnimationFrame(rafHandle);
|
|
2834
|
+
rafHandle = null;
|
|
2835
|
+
}
|
|
2758
2836
|
stateUnsubscribe?.();
|
|
2759
2837
|
stateUnsubscribe = null;
|
|
2760
2838
|
errorUnsubscribe?.();
|
|
2761
2839
|
errorUnsubscribe = null;
|
|
2840
|
+
reconnectingUnsubscribe?.();
|
|
2841
|
+
reconnectingUnsubscribe = null;
|
|
2842
|
+
recoveredUnsubscribe?.();
|
|
2843
|
+
recoveredUnsubscribe = null;
|
|
2762
2844
|
if (api?.container) {
|
|
2763
2845
|
api.container.removeEventListener("mousemove", handleInteraction);
|
|
2764
2846
|
api.container.removeEventListener("mouseenter", handleInteraction);
|
|
@@ -2767,7 +2849,7 @@ function uiPlugin(config = {}) {
|
|
|
2767
2849
|
api.container.removeEventListener("click", handleInteraction);
|
|
2768
2850
|
}
|
|
2769
2851
|
document.removeEventListener("keydown", handleKeyDown);
|
|
2770
|
-
document.removeEventListener("fullscreenchange",
|
|
2852
|
+
document.removeEventListener("fullscreenchange", scheduleUpdate);
|
|
2771
2853
|
controls.forEach((c) => c.destroy());
|
|
2772
2854
|
controls = [];
|
|
2773
2855
|
progressBar?.destroy();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scarlett-player/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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.
|
|
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.
|
|
32
|
+
"@scarlett-player/core": "1.1.1"
|
|
33
33
|
},
|
|
34
34
|
"keywords": [
|
|
35
35
|
"video",
|