diff --git a/examples/html-eject-frosted/index.html b/examples/html-eject-frosted/index.html new file mode 100644 index 00000000..58968598 --- /dev/null +++ b/examples/html-eject-frosted/index.html @@ -0,0 +1,142 @@ + + + + + + HTML Frosted Ejected + + + + + +
+ + + + +
+ +
+ + + + + + + + + + + Play + Pause + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + Enter Fullscreen + Exit Fullscreen + + + + +
+
+
+
+ + + diff --git a/examples/html-eject-frosted/package.json b/examples/html-eject-frosted/package.json new file mode 100644 index 00000000..a1e2bf54 --- /dev/null +++ b/examples/html-eject-frosted/package.json @@ -0,0 +1,19 @@ +{ + "name": "html-eject-frosted", + "type": "module", + "version": "1.0.0", + "private": true, + "description": "HTML example for Video.js 10", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@videojs/html": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.9.2", + "vite": "^7.1.6" + } +} diff --git a/examples/html-eject-frosted/src/frosted.css b/examples/html-eject-frosted/src/frosted.css new file mode 100644 index 00000000..0f60fbc0 --- /dev/null +++ b/examples/html-eject-frosted/src/frosted.css @@ -0,0 +1,360 @@ +media-container * { + box-sizing: border-box; +} + +media-container { + position: relative; + isolation: isolate; + container: root / inline-size; + overflow: clip; + font-size: 0.8125rem; + line-height: 1.5; + border-radius: inherit; + background: oklab(0 0 0); +} +media-container::before, +media-container::after { + content: ''; + position: absolute; + pointer-events: none; + border-radius: inherit; + z-index: 10; +} +media-container::before { + inset: 1px; + box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.15); +} +media-container::after { + inset: 0; + box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.1); +} + +/* Fullscreen */ +media-container:fullscreen { + border-radius: 0; +} + +media-container > ::slotted([slot='media']), +media-container > video, +media-container > audio { + display: block; + width: 100%; + height: 100%; +} + +/* Media Container UI Overlay Styling */ +media-container > .overlay { + position: absolute; + inset: 0; + display: flex; + flex-flow: column nowrap; + align-items: start; + pointer-events: none; + border-radius: inherit; + background-image: linear-gradient(to top, oklab(0 0 0 / 0.5), oklab(0 0 0 / 0.3), rgba(0, 0, 0, 0)); + backdrop-filter: saturate(1.5) brightness(0.9); + transition: opacity 0.15s ease-out; + transition-delay: 500ms; + opacity: 0; +} +media-container:hover > .overlay, +media-container:has([data-paused]) > .overlay, +media-container:has([aria-expanded='true']) > .overlay { + opacity: 1; + transition-duration: 100ms; + transition-delay: 0ms; +} + +/* Common Surface Styles - e.g. tooltips, popovers, controls */ +.surface { + background-color: oklab(1 0 0 / 0.1); + backdrop-filter: blur(64px) brightness(0.9) saturate(1.5); + box-shadow: + inset 0 0 0 1px oklab(1 0 0 / 0.15), + 0 0 0 1px oklab(0 0 0 / 0.15), + oklab(0 0 0 / 0.15) 0px 1px 3px 0px, + oklab(0 0 0 / 0.15) 0px 1px 2px -1px; +} +@media (prefers-reduced-transparency: reduce) { + .surface { + background-color: oklab(0 0 0 / 0.7); + box-shadow: + inset 0 0 0 1px oklab(0 0 0), + 0 0 0 1px oklab(1 0 0 / 0.2); + } +} +@media (prefers-contrast: more) { + .surface { + background-color: oklab(0 0 0 / 0.9); + box-shadow: + inset 0 0 0 1px oklab(0 0 0), + 0 0 0 1px oklab(1 0 0 / 0.2); + } +} + +/* Media Control Bar UI/Styles */ +.control-bar { + position: absolute; + bottom: 0.75rem; + inset-inline: 0.75rem; + padding: 0.25rem; + display: flex; + align-items: center; + gap: 0.125rem; + border-radius: calc(infinity * 1px); + will-change: scale, transform, filter, opacity; + scale: 0.9; + opacity: 0; + filter: blur(8px); + transition-property: scale, transform, filter, opacity; + transition-delay: 500ms; + transition-duration: 300ms; + transition-timing-function: ease-out; + transform-origin: bottom; + color: oklab(1 0 0); +} +media-container:hover > .control-bar, +media-container:has([data-paused]) > .control-bar, +media-container:has([aria-expanded='true']) > .control-bar { + opacity: 1; + scale: 1; + filter: blur(0px); + transition-delay: 0ms; + transition-duration: 100ms; +} + +/* Time Display Styling */ +.time-controls { + display: flex; + align-items: center; + flex: 1; + gap: 0.75rem; + padding-inline: 0.375rem; +} +media-current-time-display, +media-duration-display { + text-shadow: 0 1px 0 oklab(0 0 0 / 0.25); + font-variant-numeric: tabular-nums; +} + +/* Generic Media Button Styling */ +.button { + display: grid; + flex-shrink: 0; + padding: 0.5rem; + cursor: pointer; + background: transparent; + border: none; + border-radius: calc(infinity * 1px); + color: oklab(1 0 0 / 0.9); + user-select: none; + outline: 2px solid transparent; + outline-offset: -2px; + transition-property: background-color, color, outline-offset; + transition-duration: 150ms; + transition-timing-function: ease-out; +} +.button:hover, +.button:focus-visible, +.button[aria-expanded='true'] { + background-color: oklab(1 0 0 / 0.1); + color: oklab(1 0 0); + text-decoration: none; +} +.button:focus-visible { + outline-color: oklch(62.3% 0.214 259.815); + outline-offset: 2px; +} +.button[disabled] { + cursor: not-allowed; + opacity: 0.5; + filter: grayscale(1); +} + +.button .icon { + grid-area: 1 / 1; + width: 18px; + height: 18px; + filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.25)); +} + +/* Media Play Button UI/Styles */ +media-play-button .icon { + opacity: 0; + transition: opacity 150ms linear; +} + +media-play-button:not([data-paused]) .pause-icon, +media-play-button[data-paused] .play-icon { + opacity: 1; +} + +/* Media Fullscreen Button UI/Styles */ +media-fullscreen-button .icon { + display: none; +} +media-fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon, +media-fullscreen-button[data-fullscreen] .fullscreen-exit-icon { + display: inline; +} + +/* One way to define the "default visible" icon (CJP) */ +media-mute-button .icon { + display: none; +} +media-mute-button:not([data-volume-level]) .volume-low-icon, +media-mute-button[data-volume-level='high'] .volume-high-icon, +media-mute-button[data-volume-level='low'] .volume-low-icon, +media-mute-button[data-volume-level='medium'] .volume-low-icon, +media-mute-button[data-volume-level='off'] .volume-off-icon { + display: inline; +} + +/* TimeSlider Component Styles */ +.slider { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + position: relative; + border-radius: calc(infinity * 1px); + outline: none; +} + +/* Horizontal orientation styles */ +.slider[data-orientation='horizontal'] { + min-width: 5rem; + width: 100%; + height: 1.25rem; +} + +/* Vertical orientation styles */ +.slider[data-orientation='vertical'] { + height: 5rem; + width: 1.25rem; +} + +.slider-track { + position: relative; + isolation: isolate; + background-color: oklab(1 0 0 / 0.2); + border-radius: inherit; + overflow: hidden; + user-select: none; + outline: 2px solid transparent; + outline-offset: -2px; + transition: outline-offset 150ms ease-out; + box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05); +} + +/* Horizontal track styles */ +.slider-track[data-orientation='horizontal'] { + width: 100%; + height: 0.25rem; +} + +/* Vertical track styles */ +.slider-track[data-orientation='vertical'] { + width: 0.25rem; + height: 100%; +} + +.slider:focus-visible .slider-track { + outline-color: oklch(62.3% 0.214 259.815); + outline-offset: 6px; +} + +.slider-thumb { + width: 0.625rem; + height: 0.625rem; + background-color: oklab(1 0 0); + border-radius: calc(infinity * 1px); + user-select: none; + z-index: 10; + box-shadow: + 0 0 0 1px oklab(0 0 0 / 0.1), + 0 1px 3px 0 oklab(0 0 0 / 0.15), + 0 1px 2px -1px oklab(0 0 0 / 0.15); + opacity: 0; + transition-property: opacity, height, width; + transition-duration: 150ms; + transition-timing-function: ease-out; +} +.slider-thumb:active { + width: 0.75rem; + height: 0.75rem; +} +.slider:hover .slider-thumb, +.slider:focus-within .slider-thumb { + opacity: 1; +} +.slider-track[data-orientation='horizontal'] .slider-thumb { + cursor: ew-resize; +} +.slider-track[data-orientation='vertical'] .slider-thumb { + cursor: ns-resize; +} + +.slider-pointer { + background-color: oklab(1 0 0 / 0.2); + border-radius: inherit; +} + +.slider-progress { + background-color: oklab(1 0 0); + border-radius: inherit; +} + +media-preview-time-display { + font-variant-numeric: tabular-nums; +} + +.popup-animation { + transition-property: transform, scale, opacity, filter; + transition-duration: 200ms; + transform: scale(1); + transform-origin: bottom; + opacity: 1; + filter: blur(0px); +} +.popup-animation[data-starting-style], +.popup-animation[data-ending-style] { + transform: scale(0); + opacity: 0; + filter: blur(8px); +} +.popup-animation[data-instant] { + transition-duration: 0ms; +} + +media-popover { + margin: 0; + border: none; + box-shadow: none; + background: transparent; + padding: 0.75rem 0.25rem; + border-radius: calc(infinity * 1px); +} + +/* Tooltip Component Styles */ +media-tooltip-popup { + color: oklab(1 0 0); + padding: 0.25rem 0.625rem; + border-radius: calc(infinity * 1px); + font-size: 0.75rem; +} + +.tooltip { + display: none; + white-space: nowrap; +} + +media-tooltip-popup[data-paused] .play-tooltip, +media-tooltip-popup:not([data-paused]) .pause-tooltip { + display: block; +} + +media-tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip, +media-tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip { + display: block; +} diff --git a/examples/html-eject-frosted/src/main.ts b/examples/html-eject-frosted/src/main.ts new file mode 100644 index 00000000..c9bf17ae --- /dev/null +++ b/examples/html-eject-frosted/src/main.ts @@ -0,0 +1,14 @@ +import './frosted.css'; +import '@videojs/html/icons'; +import '@videojs/html/define/media-container'; +import '@videojs/html/define/media-provider'; +import '@videojs/html/define/media-play-button'; +import '@videojs/html/define/media-mute-button'; +import '@videojs/html/define/media-volume-slider'; +import '@videojs/html/define/media-time-slider'; +import '@videojs/html/define/media-fullscreen-button'; +import '@videojs/html/define/media-duration-display'; +import '@videojs/html/define/media-current-time-display'; +import '@videojs/html/define/media-preview-time-display'; +import '@videojs/html/define/media-popover'; +import '@videojs/html/define/media-tooltip'; diff --git a/examples/html-eject-frosted/tsconfig.json b/examples/html-eject-frosted/tsconfig.json new file mode 100644 index 00000000..50b8061b --- /dev/null +++ b/examples/html-eject-frosted/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": "." + }, + "include": ["src"] +} diff --git a/examples/html-eject-frosted/vite.config.ts b/examples/html-eject-frosted/vite.config.ts new file mode 100644 index 00000000..c9d7dbf3 --- /dev/null +++ b/examples/html-eject-frosted/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config +export default defineConfig({ + server: { + port: 5174, + }, + optimizeDeps: { + exclude: ['@vjs/html'], + }, +}); diff --git a/examples/html-eject-minimal/index.html b/examples/html-eject-minimal/index.html new file mode 100644 index 00000000..c004adea --- /dev/null +++ b/examples/html-eject-minimal/index.html @@ -0,0 +1,150 @@ + + + + + + HTML Minimal Ejected + + + + + +
+ + + + +
+ +
+ + + + + + + + + + + + Play + Pause + + + + + +
+ + + + + / + + +
+ + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + Enter Fullscreen + Exit Fullscreen + + + + +
+
+
+
+
+ + + diff --git a/examples/html-eject-minimal/package.json b/examples/html-eject-minimal/package.json new file mode 100644 index 00000000..e3aea64b --- /dev/null +++ b/examples/html-eject-minimal/package.json @@ -0,0 +1,19 @@ +{ + "name": "html-eject-minimal", + "type": "module", + "version": "1.0.0", + "private": true, + "description": "HTML example for Video.js 10", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@videojs/html": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.9.2", + "vite": "^7.1.6" + } +} diff --git a/examples/html-eject-minimal/src/main.ts b/examples/html-eject-minimal/src/main.ts new file mode 100644 index 00000000..f1790dc9 --- /dev/null +++ b/examples/html-eject-minimal/src/main.ts @@ -0,0 +1,14 @@ +import './minimal.css'; +import '@videojs/html/icons'; +import '@videojs/html/define/media-container'; +import '@videojs/html/define/media-provider'; +import '@videojs/html/define/media-play-button'; +import '@videojs/html/define/media-mute-button'; +import '@videojs/html/define/media-volume-slider'; +import '@videojs/html/define/media-time-slider'; +import '@videojs/html/define/media-fullscreen-button'; +import '@videojs/html/define/media-duration-display'; +import '@videojs/html/define/media-current-time-display'; +import '@videojs/html/define/media-preview-time-display'; +import '@videojs/html/define/media-popover'; +import '@videojs/html/define/media-tooltip'; diff --git a/examples/html-eject-minimal/src/minimal.css b/examples/html-eject-minimal/src/minimal.css new file mode 100644 index 00000000..2d774ff2 --- /dev/null +++ b/examples/html-eject-minimal/src/minimal.css @@ -0,0 +1,352 @@ +media-container * { + box-sizing: border-box; +} + +media-container { + position: relative; + isolation: isolate; + container: root / inline-size; + overflow: clip; + font-size: 0.8125rem; + line-height: 1.5; + border-radius: inherit; + background: oklab(0 0 0); +} +media-container::after { + content: ''; + position: absolute; + pointer-events: none; + border-radius: inherit; + z-index: 10; + inset: 0; + box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.2); +} +@media (prefers-color-scheme: dark) { + media-container::after { + box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.2); + } +} + +/* Fullscreen */ +media-container:fullscreen { + border-radius: 0; +} + +media-container > ::slotted([slot='media']), +media-container > video, +media-container > audio { + display: block; + width: 100%; + height: 100%; +} + +/* Media Container UI Overlay Styling */ +media-container > .overlay { + position: absolute; + inset: 0; + display: flex; + flex-flow: column nowrap; + align-items: start; + pointer-events: none; + border-radius: inherit; + background-image: linear-gradient(to top, oklab(0 0 0 / 0.7), oklab(0 0 0 / 0.5) 7.5rem, rgba(0, 0, 0, 0)); + transform: translateY(100%); + transition-property: transform, opacity; + transition-duration: 150ms; + transition-timing-function: ease-out; + transition-delay: 500ms; + opacity: 0; +} +media-container:hover > .overlay, +media-container:has([data-paused]) > .overlay, +media-container:has([aria-expanded='true']) > .overlay { + opacity: 1; + transform: translateY(0); + transition-duration: 100ms; + transition-delay: 0ms; +} + +/* Media Control Bar UI/Styles */ +.control-bar { + position: absolute; + bottom: 0; + inset-inline: 0; + padding: 2.5rem 0.75rem 0.75rem 0.75rem; + display: flex; + align-items: center; + gap: 0.875rem; + will-change: transform, filter, opacity; + transform: translateY(100%); + opacity: 0; + filter: blur(8px); + transition-property: transform, filter, opacity; + transition-delay: 500ms; + transition-duration: 300ms; + transition-timing-function: ease-out; + color: oklab(1 0 0); +} +media-container:hover > .control-bar, +media-container:has([data-paused]) > .control-bar, +media-container:has([aria-expanded='true']) > .control-bar { + opacity: 1; + transform: translateY(0); + filter: blur(0px); + transition-delay: 0ms; + transition-duration: 100ms; +} + +/* Time Display Styling */ +.time-display-group { + display: flex; + align-items: center; + gap: 0.25rem; +} +.duration-display { + display: contents; + color: oklab(1 0 0 / 0.5); +} +media-current-time-display, +media-duration-display { + text-shadow: 0 1px 0 oklab(0 0 0 / 0.2); + font-variant-numeric: tabular-nums; +} + +.button-group { + display: flex; + align-items: center; + gap: 0.375rem; +} + +/* Generic Media Button Styling */ +.button { + display: grid; + flex-shrink: 0; + padding: 0.625rem; + cursor: pointer; + background: transparent; + border: none; + border-radius: 0.375rem; + color: oklab(1 0 0); + user-select: none; + outline: 2px solid transparent; + outline-offset: -2px; + transition-property: background-color, color, outline-offset; + transition-duration: 150ms; + transition-timing-function: ease-out; +} +.button:hover, +.button:focus-visible, +.button[aria-expanded='true'] { + color: oklab(1 0 0 / 0.8); + text-decoration: none; +} +.button:focus-visible { + outline-color: oklab(1 0 0); + outline-offset: 2px; +} +.button[disabled] { + cursor: not-allowed; + opacity: 0.5; + filter: grayscale(1); +} + +.button .icon { + grid-area: 1 / 1; + width: 18px; + height: 18px; + filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.4)); +} + +/* Media Play Button UI/Styles */ +media-play-button .icon { + opacity: 0; + transition: opacity 150ms linear; +} + +media-play-button:not([data-paused]) .pause-icon, +media-play-button[data-paused] .play-icon { + opacity: 1; +} + +/* Media Fullscreen Button UI/Styles */ +media-fullscreen-button .icon { + display: none; +} +media-fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon, +media-fullscreen-button[data-fullscreen] .fullscreen-exit-icon { + display: inline; +} + +/* One way to define the "default visible" icon (CJP) */ +media-mute-button .icon { + display: none; +} +media-mute-button:not([data-volume-level]) .volume-low-icon, +media-mute-button[data-volume-level='high'] .volume-high-icon, +media-mute-button[data-volume-level='low'] .volume-low-icon, +media-mute-button[data-volume-level='medium'] .volume-low-icon, +media-mute-button[data-volume-level='off'] .volume-off-icon { + display: inline; +} + +/* TimeSlider Component Styles */ +.slider { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + position: relative; + border-radius: calc(infinity * 1px); + outline: none; +} + +/* Horizontal orientation styles */ +.slider[data-orientation='horizontal'] { + min-width: 5rem; + width: 100%; + height: 1.25rem; +} + +/* Vertical orientation styles */ +.slider[data-orientation='vertical'] { + height: 4.5rem; + width: 1.25rem; +} + +.slider-track { + position: relative; + isolation: isolate; + background-color: oklab(1 0 0 / 0.2); + border-radius: inherit; + overflow: hidden; + user-select: none; + outline: 2px solid transparent; + outline-offset: -2px; + transition: outline-offset 150ms ease-out; + box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05); +} + +/* Horizontal track styles */ +.slider-track[data-orientation='horizontal'] { + width: 100%; + height: 0.1875rem; +} + +/* Vertical track styles */ +.slider-track[data-orientation='vertical'] { + width: 0.1875rem; + height: 100%; +} + +.slider:focus-visible .slider-track { + outline-color: oklab(1 0 0); + outline-offset: 6px; +} + +.slider-thumb { + width: 0.75rem; + height: 0.75rem; + background-color: oklab(1 0 0); + border-radius: calc(infinity * 1px); + user-select: none; + z-index: 10; + box-shadow: + 0 0 0 1px oklab(0 0 0 / 0.1), + 0 1px 3px 0 oklab(0 0 0 / 0.15), + 0 1px 2px -1px oklab(0 0 0 / 0.15); + opacity: 0; + scale: 0.7; + transform-origin: center; + transition-property: opacity, scale; + transition-duration: 150ms; + transition-timing-function: ease-out; +} +.slider:hover .slider-thumb, +.slider:focus-within .slider-thumb { + opacity: 1; + scale: 1; +} +.slider-track[data-orientation='horizontal'] .slider-thumb { + cursor: ew-resize; +} +.slider-track[data-orientation='vertical'] .slider-thumb { + cursor: ns-resize; +} + +.slider-pointer { + display: none; +} + +.slider-progress { + background-color: oklab(1 0 0); + border-radius: inherit; +} + +media-preview-time-display { + font-variant-numeric: tabular-nums; +} + +.popup-animation { + transition-property: transform, scale, opacity, filter; + transition-duration: 200ms; + transform: scale(1); + transform-origin: bottom; + opacity: 1; + filter: blur(0px); +} +.popup-animation[data-starting-style], +.popup-animation[data-ending-style] { + transform: scale(0); + opacity: 0; + filter: blur(8px); +} +.popup-animation[data-instant] { + transition-duration: 0ms; +} + +media-popover { + margin: 0; + border: none; + box-shadow: none; + background: transparent; + padding: 0.75rem 0.25rem; + border-radius: calc(infinity * 1px); +} + +/* Tooltip Component Styles */ +media-tooltip-popup { + color: oklab(1 0 0); + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; + background-color: oklab(1 0 0 / 0.1); + backdrop-filter: blur(64px) brightness(0.9) saturate(1.5); + box-shadow: + 0 4px 6px -1px oklab(0 0 0 / 0.1), + 0 2px 4px -2px oklab(0 0 0 / 0.1); +} +@media (prefers-reduced-transparency: reduce) { + media-tooltip-popup { + background-color: oklab(0 0 0 / 0.7); + } +} +@media (prefers-contrast: more) { + media-tooltip-popup { + background-color: oklab(0 0 0 / 0.9); + } +} + +.tooltip { + display: none; + white-space: nowrap; +} + +media-tooltip-popup[data-paused] .play-tooltip, +media-tooltip-popup:not([data-paused]) .pause-tooltip { + display: block; +} + +media-tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip, +media-tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip { + display: block; +} diff --git a/examples/html-eject-minimal/tsconfig.json b/examples/html-eject-minimal/tsconfig.json new file mode 100644 index 00000000..50b8061b --- /dev/null +++ b/examples/html-eject-minimal/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": "." + }, + "include": ["src"] +} diff --git a/examples/html-eject-minimal/vite.config.ts b/examples/html-eject-minimal/vite.config.ts new file mode 100644 index 00000000..c9d7dbf3 --- /dev/null +++ b/examples/html-eject-minimal/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config +export default defineConfig({ + server: { + port: 5174, + }, + optimizeDeps: { + exclude: ['@vjs/html'], + }, +}); diff --git a/examples/react-eject-frosted/index.html b/examples/react-eject-frosted/index.html new file mode 100644 index 00000000..32220ada --- /dev/null +++ b/examples/react-eject-frosted/index.html @@ -0,0 +1,24 @@ + + + + + React Frosted Ejected + + + + + +
+ + + diff --git a/examples/react-eject-frosted/package.json b/examples/react-eject-frosted/package.json new file mode 100644 index 00000000..1cd669f9 --- /dev/null +++ b/examples/react-eject-frosted/package.json @@ -0,0 +1,28 @@ +{ + "name": "react-eject-frosted", + "type": "module", + "version": "1.0.0", + "private": true, + "description": "React example for Video.js 10", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@videojs/react": "workspace:*", + "clsx": "^2.1.1", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "screenfull": "^6.0.2", + "tailwindcss": "^4.1.13" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@vitejs/plugin-react": "^5.0.3", + "typescript": "^5.9.2", + "vite": "^7.1.6" + } +} diff --git a/examples/react-eject-frosted/src/FrostedSkin.tsx b/examples/react-eject-frosted/src/FrostedSkin.tsx new file mode 100644 index 00000000..9a87eb4c --- /dev/null +++ b/examples/react-eject-frosted/src/FrostedSkin.tsx @@ -0,0 +1,115 @@ +import type { PropsWithChildren } from 'react'; + +import { CurrentTimeDisplay, DurationDisplay, FullscreenButton, MediaContainer, MuteButton, PlayButton, Popover, PreviewTimeDisplay, TimeSlider, Tooltip, VolumeSlider } from '@videojs/react'; +import { + FullscreenEnterIcon, + FullscreenExitIcon, + PauseIcon, + PlayIcon, + VolumeHighIcon, + VolumeLowIcon, + VolumeOffIcon, +} from '@videojs/react/icons'; + +import './frosted.css'; + +type SkinProps = PropsWithChildren<{ + className?: string; +}>; + +export default function FrostedSkin({ children, className = '' }: SkinProps): JSX.Element { + return ( + + {children} + +
+ +
+ + + + + + + + + + + Play + Pause + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Enter Fullscreen + Exit Fullscreen + + + + +
+ + ); +} diff --git a/examples/react-eject-frosted/src/frosted.css b/examples/react-eject-frosted/src/frosted.css new file mode 100644 index 00000000..6f018520 --- /dev/null +++ b/examples/react-eject-frosted/src/frosted.css @@ -0,0 +1,357 @@ +.vjs-frosted-skin * { + box-sizing: border-box; +} + +.vjs-frosted-skin { + position: relative; + isolation: isolate; + container: root / inline-size; + overflow: clip; + font-size: 0.8125rem; + line-height: 1.5; + border-radius: inherit; + background: oklab(0 0 0); +} +.vjs-frosted-skin::before, +.vjs-frosted-skin::after { + content: ''; + position: absolute; + pointer-events: none; + border-radius: inherit; + z-index: 10; +} +.vjs-frosted-skin::before { + inset: 1px; + box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.15); +} +.vjs-frosted-skin::after { + inset: 0; + box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.1); +} + +/* Fullscreen */ +.vjs-frosted-skin:fullscreen { + border-radius: 0; +} + +.vjs-frosted-skin > ::slotted([slot='media']) { + display: block; + width: 100%; + height: 100%; +} + +/* Media Container UI Overlay Styling */ +.vjs-frosted-skin > .overlay { + position: absolute; + inset: 0; + display: flex; + flex-flow: column nowrap; + align-items: start; + pointer-events: none; + border-radius: inherit; + background-image: linear-gradient(to top, oklab(0 0 0 / 0.5), oklab(0 0 0 / 0.3), rgba(0, 0, 0, 0)); + backdrop-filter: saturate(1.5) brightness(0.9); + transition: opacity 0.15s ease-out; + transition-delay: 500ms; + opacity: 0; +} +.vjs-frosted-skin:hover > .overlay, +.vjs-frosted-skin:has([data-paused]) > .overlay, +.vjs-frosted-skin:has([aria-expanded='true']) > .overlay { + opacity: 1; + transition-duration: 100ms; + transition-delay: 0ms; +} + +/* Common Surface Styles - e.g. tooltips, popovers, controls */ +.vjs-frosted-skin .surface { + background-color: oklab(1 0 0 / 0.1); + backdrop-filter: blur(64px) brightness(0.9) saturate(1.5); + box-shadow: + inset 0 0 0 1px oklab(1 0 0 / 0.15), + 0 0 0 1px oklab(0 0 0 / 0.15), + oklab(0 0 0 / 0.15) 0px 1px 3px 0px, + oklab(0 0 0 / 0.15) 0px 1px 2px -1px; +} +@media (prefers-reduced-transparency: reduce) { + .vjs-frosted-skin .surface { + background-color: oklab(0 0 0 / 0.7); + box-shadow: + inset 0 0 0 1px oklab(0 0 0), + 0 0 0 1px oklab(1 0 0 / 0.2); + } +} +@media (prefers-contrast: more) { + .vjs-frosted-skin .surface { + background-color: oklab(0 0 0 / 0.9); + box-shadow: + inset 0 0 0 1px oklab(0 0 0), + 0 0 0 1px oklab(1 0 0 / 0.2); + } +} + +/* Media Control Bar UI/Styles */ +.vjs-frosted-skin .control-bar { + position: absolute; + bottom: 0.75rem; + inset-inline: 0.75rem; + padding: 0.25rem; + display: flex; + align-items: center; + gap: 0.125rem; + border-radius: calc(infinity * 1px); + will-change: scale, transform, filter, opacity; + scale: 0.9; + opacity: 0; + filter: blur(8px); + transition-property: scale, transform, filter, opacity; + transition-delay: 500ms; + transition-duration: 300ms; + transition-timing-function: ease-out; + transform-origin: bottom; + color: oklab(1 0 0); +} +.vjs-frosted-skin:hover > .control-bar, +.vjs-frosted-skin:has([data-paused]) > .control-bar, +.vjs-frosted-skin:has([aria-expanded='true']) > .control-bar { + opacity: 1; + scale: 1; + filter: blur(0px); + transition-delay: 0ms; + transition-duration: 100ms; +} + +/* Time Display Styling */ +.vjs-frosted-skin .time-controls { + display: flex; + align-items: center; + flex: 1; + gap: 0.75rem; + padding-inline: 0.375rem; +} +.vjs-frosted-skin .time-display { + text-shadow: 0 1px 0 oklab(0 0 0 / 0.25); + font-variant-numeric: tabular-nums; +} + +/* Generic Media Button Styling */ +.vjs-frosted-skin .button { + display: grid; + flex-shrink: 0; + padding: 0.5rem; + cursor: pointer; + background: transparent; + border: none; + border-radius: calc(infinity * 1px); + color: oklab(1 0 0 / 0.9); + user-select: none; + outline: 2px solid transparent; + outline-offset: -2px; + transition-property: background-color, color, outline-offset; + transition-duration: 150ms; + transition-timing-function: ease-out; +} +.vjs-frosted-skin .button:hover, +.vjs-frosted-skin .button:focus-visible, +.vjs-frosted-skin .button[aria-expanded='true'] { + background-color: oklab(1 0 0 / 0.1); + color: oklab(1 0 0); + text-decoration: none; +} +.vjs-frosted-skin .button:focus-visible { + outline-color: oklch(62.3% 0.214 259.815); + outline-offset: 2px; +} +.vjs-frosted-skin .button[disabled] { + cursor: not-allowed; + opacity: 0.5; + filter: grayscale(1); +} + +.vjs-frosted-skin .button .icon { + grid-area: 1 / 1; + width: 18px; + height: 18px; + filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.25)); +} + +/* Media Play Button UI/Styles */ +.vjs-frosted-skin .play-button .icon { + opacity: 0; + transition: opacity 150ms linear; +} + +.vjs-frosted-skin .play-button:not([data-paused]) .pause-icon, +.vjs-frosted-skin .play-button[data-paused] .play-icon { + opacity: 1; +} + +/* Media Fullscreen Button UI/Styles */ +.vjs-frosted-skin .fullscreen-button .icon { + display: none; +} +.vjs-frosted-skin .fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon, +.vjs-frosted-skin .fullscreen-button[data-fullscreen] .fullscreen-exit-icon { + display: inline; +} + +/* One way to define the "default visible" icon (CJP) */ +.vjs-frosted-skin .mute-button .icon { + display: none; +} +.vjs-frosted-skin .mute-button:not([data-volume-level]) .volume-low-icon, +.vjs-frosted-skin .mute-button[data-volume-level='high'] .volume-high-icon, +.vjs-frosted-skin .mute-button[data-volume-level='low'] .volume-low-icon, +.vjs-frosted-skin .mute-button[data-volume-level='medium'] .volume-low-icon, +.vjs-frosted-skin .mute-button[data-volume-level='off'] .volume-off-icon { + display: inline; +} + +/* TimeSlider Component Styles */ +.vjs-frosted-skin .slider { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + position: relative; + border-radius: calc(infinity * 1px); + outline: none; +} + +/* Horizontal orientation styles */ +.vjs-frosted-skin .slider[data-orientation='horizontal'] { + min-width: 5rem; + width: 100%; + height: 1.25rem; +} + +/* Vertical orientation styles */ +.vjs-frosted-skin .slider[data-orientation='vertical'] { + height: 5rem; + width: 1.25rem; +} + +.vjs-frosted-skin .slider-track { + position: relative; + isolation: isolate; + background-color: oklab(1 0 0 / 0.2); + border-radius: inherit; + overflow: hidden; + user-select: none; + outline: 2px solid transparent; + outline-offset: -2px; + transition: outline-offset 150ms ease-out; + box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05); +} + +/* Horizontal track styles */ +.vjs-frosted-skin .slider-track[data-orientation='horizontal'] { + width: 100%; + height: 0.25rem; +} + +/* Vertical track styles */ +.vjs-frosted-skin .slider-track[data-orientation='vertical'] { + width: 0.25rem; + height: 100%; +} + +.vjs-frosted-skin .slider:focus-visible .slider-track { + outline-color: oklch(62.3% 0.214 259.815); + outline-offset: 6px; +} + +.vjs-frosted-skin .slider-thumb { + width: 0.625rem; + height: 0.625rem; + background-color: oklab(1 0 0); + border-radius: calc(infinity * 1px); + user-select: none; + z-index: 10; + box-shadow: + 0 0 0 1px oklab(0 0 0 / 0.1), + 0 1px 3px 0 oklab(0 0 0 / 0.15), + 0 1px 2px -1px oklab(0 0 0 / 0.15); + opacity: 0; + transition-property: opacity, height, width; + transition-duration: 150ms; + transition-timing-function: ease-out; +} +.vjs-frosted-skin .slider-thumb:active { + width: 0.75rem; + height: 0.75rem; +} +.vjs-frosted-skin .slider:hover .slider-thumb, +.vjs-frosted-skin .slider:focus-within .slider-thumb { + opacity: 1; +} +.vjs-frosted-skin .slider-track[data-orientation='horizontal'] .slider-thumb { + cursor: ew-resize; +} +.vjs-frosted-skin .slider-track[data-orientation='vertical'] .slider-thumb { + cursor: ns-resize; +} + +.vjs-frosted-skin .slider-pointer { + background-color: oklab(1 0 0 / 0.2); + border-radius: inherit; +} + +.vjs-frosted-skin .slider-progress { + background-color: oklab(1 0 0); + border-radius: inherit; +} + +.vjs-frosted-skin .media-preview-time-display { + font-variant-numeric: tabular-nums; +} + +.vjs-frosted-skin .popup-animation { + transition-property: transform, scale, opacity, filter; + transition-duration: 200ms; + transform: scale(1); + transform-origin: bottom; + opacity: 1; + filter: blur(0px); +} +.vjs-frosted-skin .popup-animation[data-starting-style], +.vjs-frosted-skin .popup-animation[data-ending-style] { + transform: scale(0); + opacity: 0; + filter: blur(8px); +} +.vjs-frosted-skin .popup-animation[data-instant] { + transition-duration: 0ms; +} + +.vjs-frosted-skin .popover-popup { + margin: 0; + border: none; + box-shadow: none; + background: transparent; + padding: 0.75rem 0.25rem; + border-radius: calc(infinity * 1px); +} + +/* Tooltip Component Styles */ +.vjs-frosted-skin .tooltip-popup { + color: oklab(1 0 0); + padding: 0.25rem 0.625rem; + border-radius: calc(infinity * 1px); + font-size: 0.75rem; +} + +.vjs-frosted-skin .tooltip { + display: none; + white-space: nowrap; +} + +.vjs-frosted-skin .tooltip-popup[data-paused] .play-tooltip, +.vjs-frosted-skin .tooltip-popup:not([data-paused]) .pause-tooltip { + display: block; +} + +.vjs-frosted-skin .tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip, +.vjs-frosted-skin .tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip { + display: block; +} diff --git a/examples/react-eject-frosted/src/main.tsx b/examples/react-eject-frosted/src/main.tsx new file mode 100644 index 00000000..61f69887 --- /dev/null +++ b/examples/react-eject-frosted/src/main.tsx @@ -0,0 +1,20 @@ +import { MediaProvider, Video } from '@videojs/react'; +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; + +import FrostedSkin from './FrostedSkin'; + +createRoot(document.getElementById('root')!).render( + + + + + + , +); diff --git a/examples/react-eject-frosted/tsconfig.json b/examples/react-eject-frosted/tsconfig.json new file mode 100644 index 00000000..40aa8ebc --- /dev/null +++ b/examples/react-eject-frosted/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "baseUrl": "." + }, + "include": ["src"] +} diff --git a/examples/react-eject-frosted/vite.config.ts b/examples/react-eject-frosted/vite.config.ts new file mode 100644 index 00000000..6869bb0a --- /dev/null +++ b/examples/react-eject-frosted/vite.config.ts @@ -0,0 +1,15 @@ +import tailwindcss from '@tailwindcss/vite'; + +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config +export default defineConfig({ + plugins: [react(), tailwindcss()], + server: { + port: 5173, + }, + optimizeDeps: { + exclude: ['@videojs/react'], + }, +}); diff --git a/examples/react-eject-minimal/index.html b/examples/react-eject-minimal/index.html new file mode 100644 index 00000000..497b4a8a --- /dev/null +++ b/examples/react-eject-minimal/index.html @@ -0,0 +1,24 @@ + + + + + React Minimal Ejected + + + + + +
+ + + diff --git a/examples/react-eject-minimal/package.json b/examples/react-eject-minimal/package.json new file mode 100644 index 00000000..2fe5ef4c --- /dev/null +++ b/examples/react-eject-minimal/package.json @@ -0,0 +1,28 @@ +{ + "name": "react-eject-minimal", + "type": "module", + "version": "1.0.0", + "private": true, + "description": "React example for Video.js 10", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@videojs/react": "workspace:*", + "clsx": "^2.1.1", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "screenfull": "^6.0.2", + "tailwindcss": "^4.1.13" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@vitejs/plugin-react": "^5.0.3", + "typescript": "^5.9.2", + "vite": "^7.1.6" + } +} diff --git a/examples/react-eject-minimal/src/MinimalSkin.tsx b/examples/react-eject-minimal/src/MinimalSkin.tsx new file mode 100644 index 00000000..329e027c --- /dev/null +++ b/examples/react-eject-minimal/src/MinimalSkin.tsx @@ -0,0 +1,120 @@ +import type { PropsWithChildren } from 'react'; + +import { CurrentTimeDisplay, DurationDisplay, FullscreenButton, MediaContainer, MuteButton, PlayButton, Popover, PreviewTimeDisplay, TimeSlider, Tooltip, VolumeSlider } from '@videojs/react'; +import { + FullscreenEnterAltIcon, + FullscreenExitAltIcon, + PauseIcon, + PlayIcon, + VolumeHighIcon, + VolumeLowIcon, + VolumeOffIcon, +} from '@videojs/react/icons'; + +import './minimal.css'; + +type SkinProps = PropsWithChildren<{ + className?: string; +}>; + +export default function MinimalSkin({ children, className = '' }: SkinProps): JSX.Element { + return ( + + {children} + +
+ +
+ + + + + + + + + + + Play + Pause + + + + + +
+ + + + / + + +
+ + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Enter Fullscreen + Exit Fullscreen + + + + +
+
+ + ); +} diff --git a/examples/react-eject-minimal/src/main.tsx b/examples/react-eject-minimal/src/main.tsx new file mode 100644 index 00000000..a827a7de --- /dev/null +++ b/examples/react-eject-minimal/src/main.tsx @@ -0,0 +1,20 @@ +import { MediaProvider, Video } from '@videojs/react'; +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; + +import MinimalSkin from './MinimalSkin'; + +createRoot(document.getElementById('root')!).render( + + + + + + , +); diff --git a/examples/react-eject-minimal/src/minimal.css b/examples/react-eject-minimal/src/minimal.css new file mode 100644 index 00000000..a31687bd --- /dev/null +++ b/examples/react-eject-minimal/src/minimal.css @@ -0,0 +1,349 @@ +.vjs-minimal-skin * { + box-sizing: border-box; +} + +.vjs-minimal-skin { + position: relative; + isolation: isolate; + container: root / inline-size; + overflow: clip; + font-size: 0.8125rem; + line-height: 1.5; + border-radius: inherit; + background: oklab(0 0 0); +} +.vjs-minimal-skin::after { + content: ''; + position: absolute; + pointer-events: none; + border-radius: inherit; + z-index: 10; + inset: 0; + box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.2); +} +@media (prefers-color-scheme: dark) { + .vjs-minimal-skin::after { + box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.2); + } +} + +/* Fullscreen */ +.vjs-minimal-skin:fullscreen { + border-radius: 0; +} + +.vjs-minimal-skin > ::slotted([slot='media']) { + display: block; + width: 100%; + height: 100%; +} + +/* Media Container UI Overlay Styling */ +.vjs-minimal-skin > .overlay { + position: absolute; + inset: 0; + display: flex; + flex-flow: column nowrap; + align-items: start; + pointer-events: none; + border-radius: inherit; + background-image: linear-gradient(to top, oklab(0 0 0 / 0.7), oklab(0 0 0 / 0.5) 7.5rem, rgba(0, 0, 0, 0)); + transform: translateY(100%); + transition-property: transform, opacity; + transition-duration: 150ms; + transition-timing-function: ease-out; + transition-delay: 500ms; + opacity: 0; +} +.vjs-minimal-skin:hover > .overlay, +.vjs-minimal-skin:has([data-paused]) > .overlay, +.vjs-minimal-skin:has([aria-expanded='true']) > .overlay { + opacity: 1; + transform: translateY(0); + transition-duration: 100ms; + transition-delay: 0ms; +} + +/* Media Control Bar UI/Styles */ +.vjs-minimal-skin .control-bar { + position: absolute; + bottom: 0; + inset-inline: 0; + padding: 2.5rem 0.75rem 0.75rem 0.75rem; + display: flex; + align-items: center; + gap: 0.875rem; + will-change: transform, filter, opacity; + transform: translateY(100%); + opacity: 0; + filter: blur(8px); + transition-property: transform, filter, opacity; + transition-delay: 500ms; + transition-duration: 300ms; + transition-timing-function: ease-out; + color: oklab(1 0 0); +} +.vjs-minimal-skin:hover > .control-bar, +.vjs-minimal-skin:has([data-paused]) > .control-bar, +.vjs-minimal-skin:has([aria-expanded='true']) > .control-bar { + opacity: 1; + transform: translateY(0); + filter: blur(0px); + transition-delay: 0ms; + transition-duration: 100ms; +} + +/* Time Display Styling */ +.vjs-minimal-skin .time-display-group { + display: flex; + align-items: center; + gap: 0.25rem; +} +.vjs-minimal-skin .duration-display { + display: contents; + color: oklab(1 0 0 / 0.5); +} +.vjs-minimal-skin .time-display { + text-shadow: 0 1px 0 oklab(0 0 0 / 0.2); + font-variant-numeric: tabular-nums; +} + +.vjs-minimal-skin .button-group { + display: flex; + align-items: center; + gap: 0.375rem; +} + +/* Generic Media Button Styling */ +.vjs-minimal-skin .button { + display: grid; + flex-shrink: 0; + padding: 0.625rem; + cursor: pointer; + background: transparent; + border: none; + border-radius: 0.375rem; + color: oklab(1 0 0); + user-select: none; + outline: 2px solid transparent; + outline-offset: -2px; + transition-property: background-color, color, outline-offset; + transition-duration: 150ms; + transition-timing-function: ease-out; +} +.vjs-minimal-skin .button:hover, +.vjs-minimal-skin .button:focus-visible, +.vjs-minimal-skin .button[aria-expanded='true'] { + color: oklab(1 0 0 / 0.8); + text-decoration: none; +} +.vjs-minimal-skin .button:focus-visible { + outline-color: oklab(1 0 0); + outline-offset: 2px; +} +.vjs-minimal-skin .button[disabled] { + cursor: not-allowed; + opacity: 0.5; + filter: grayscale(1); +} + +.vjs-minimal-skin .button .icon { + grid-area: 1 / 1; + width: 18px; + height: 18px; + filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.4)); +} + +/* Media Play Button UI/Styles */ +.vjs-minimal-skin .play-button .icon { + opacity: 0; + transition: opacity 150ms linear; +} + +.vjs-minimal-skin .play-button:not([data-paused]) .pause-icon, +.vjs-minimal-skin .play-button[data-paused] .play-icon { + opacity: 1; +} + +/* Media Fullscreen Button UI/Styles */ +.vjs-minimal-skin .fullscreen-button .icon { + display: none; +} +.vjs-minimal-skin .fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon, +.vjs-minimal-skin .fullscreen-button[data-fullscreen] .fullscreen-exit-icon { + display: inline; +} + +/* One way to define the "default visible" icon (CJP) */ +.vjs-minimal-skin .mute-button .icon { + display: none; +} +.vjs-minimal-skin .mute-button:not([data-volume-level]) .volume-low-icon, +.vjs-minimal-skin .mute-button[data-volume-level='high'] .volume-high-icon, +.vjs-minimal-skin .mute-button[data-volume-level='low'] .volume-low-icon, +.vjs-minimal-skin .mute-button[data-volume-level='medium'] .volume-low-icon, +.vjs-minimal-skin .mute-button[data-volume-level='off'] .volume-off-icon { + display: inline; +} + +/* TimeSlider Component Styles */ +.vjs-minimal-skin .slider { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + position: relative; + border-radius: calc(infinity * 1px); + outline: none; +} + +/* Horizontal orientation styles */ +.vjs-minimal-skin .slider[data-orientation='horizontal'] { + min-width: 5rem; + width: 100%; + height: 1.25rem; +} + +/* Vertical orientation styles */ +.vjs-minimal-skin .slider[data-orientation='vertical'] { + height: 4.5rem; + width: 1.25rem; +} + +.vjs-minimal-skin .slider-track { + position: relative; + isolation: isolate; + background-color: oklab(1 0 0 / 0.2); + border-radius: inherit; + overflow: hidden; + user-select: none; + outline: 2px solid transparent; + outline-offset: -2px; + transition: outline-offset 150ms ease-out; + box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05); +} + +/* Horizontal track styles */ +.vjs-minimal-skin .slider-track[data-orientation='horizontal'] { + width: 100%; + height: 0.1875rem; +} + +/* Vertical track styles */ +.vjs-minimal-skin .slider-track[data-orientation='vertical'] { + width: 0.1875rem; + height: 100%; +} + +.vjs-minimal-skin .slider:focus-visible .slider-track { + outline-color: oklab(1 0 0); + outline-offset: 6px; +} + +.vjs-minimal-skin .slider-thumb { + width: 0.75rem; + height: 0.75rem; + background-color: oklab(1 0 0); + border-radius: calc(infinity * 1px); + user-select: none; + z-index: 10; + box-shadow: + 0 0 0 1px oklab(0 0 0 / 0.1), + 0 1px 3px 0 oklab(0 0 0 / 0.15), + 0 1px 2px -1px oklab(0 0 0 / 0.15); + opacity: 0; + scale: 0.7; + transform-origin: center; + transition-property: opacity, scale; + transition-duration: 150ms; + transition-timing-function: ease-out; +} +.vjs-minimal-skin .slider:hover .slider-thumb, +.vjs-minimal-skin .slider:focus-within .slider-thumb { + opacity: 1; + scale: 1; +} +.vjs-minimal-skin .slider-track[data-orientation='horizontal'] .slider-thumb { + cursor: ew-resize; +} +.vjs-minimal-skin .slider-track[data-orientation='vertical'] .slider-thumb { + cursor: ns-resize; +} + +.vjs-minimal-skin .slider-pointer { + display: none; +} + +.vjs-minimal-skin .slider-progress { + background-color: oklab(1 0 0); + border-radius: inherit; +} + +.vjs-minimal-skin .media-preview-time-display { + font-variant-numeric: tabular-nums; +} + +.vjs-minimal-skin .popup-animation { + transition-property: transform, scale, opacity, filter; + transition-duration: 200ms; + transform: scale(1); + transform-origin: bottom; + opacity: 1; + filter: blur(0px); +} +.vjs-minimal-skin .popup-animation[data-starting-style], +.vjs-minimal-skin .popup-animation[data-ending-style] { + transform: scale(0); + opacity: 0; + filter: blur(8px); +} +.vjs-minimal-skin .popup-animation[data-instant] { + transition-duration: 0ms; +} + +.vjs-minimal-skin .popover-popup { + margin: 0; + border: none; + box-shadow: none; + background: transparent; + padding: 0.75rem 0.25rem; + border-radius: calc(infinity * 1px); +} + +/* Tooltip Component Styles */ +.vjs-minimal-skin .tooltip-popup { + color: oklab(1 0 0); + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; + background-color: oklab(1 0 0 / 0.1); + backdrop-filter: blur(64px) brightness(0.9) saturate(1.5); + box-shadow: + 0 4px 6px -1px oklab(0 0 0 / 0.1), + 0 2px 4px -2px oklab(0 0 0 / 0.1); +} +@media (prefers-reduced-transparency: reduce) { + .vjs-minimal-skin .tooltip-popup { + background-color: oklab(0 0 0 / 0.7); + } +} +@media (prefers-contrast: more) { + .vjs-minimal-skin .tooltip-popup { + background-color: oklab(0 0 0 / 0.9); + } +} + +.vjs-minimal-skin .tooltip { + display: none; + white-space: nowrap; +} + +.vjs-minimal-skin .tooltip-popup[data-paused] .play-tooltip, +.vjs-minimal-skin .tooltip-popup:not([data-paused]) .pause-tooltip { + display: block; +} + +.vjs-minimal-skin .tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip, +.vjs-minimal-skin .tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip { + display: block; +} diff --git a/examples/react-eject-minimal/tsconfig.json b/examples/react-eject-minimal/tsconfig.json new file mode 100644 index 00000000..40aa8ebc --- /dev/null +++ b/examples/react-eject-minimal/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "baseUrl": "." + }, + "include": ["src"] +} diff --git a/examples/react-eject-minimal/vite.config.ts b/examples/react-eject-minimal/vite.config.ts new file mode 100644 index 00000000..6869bb0a --- /dev/null +++ b/examples/react-eject-minimal/vite.config.ts @@ -0,0 +1,15 @@ +import tailwindcss from '@tailwindcss/vite'; + +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config +export default defineConfig({ + plugins: [react(), tailwindcss()], + server: { + port: 5173, + }, + optimizeDeps: { + exclude: ['@videojs/react'], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24e82aea..f0088dbb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -94,6 +94,32 @@ importers: specifier: ^7.1.6 version: 7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1) + examples/html-eject-frosted: + dependencies: + '@videojs/html': + specifier: workspace:* + version: link:../../packages/html + devDependencies: + typescript: + specifier: ^5.9.2 + version: 5.9.3 + vite: + specifier: ^7.1.6 + version: 7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1) + + examples/html-eject-minimal: + dependencies: + '@videojs/html': + specifier: workspace:* + version: link:../../packages/html + devDependencies: + typescript: + specifier: ^5.9.2 + version: 5.9.3 + vite: + specifier: ^7.1.6 + version: 7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1) + examples/react-demo: dependencies: '@videojs/react': @@ -134,6 +160,86 @@ importers: specifier: ^7.1.6 version: 7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1) + examples/react-eject-frosted: + dependencies: + '@videojs/react': + specifier: workspace:* + version: link:../../packages/react + clsx: + specifier: ^2.1.1 + version: 2.1.1 + react: + specifier: ^18.0.0 + version: 18.3.1 + react-dom: + specifier: ^18.0.0 + version: 18.3.1(react@18.3.1) + screenfull: + specifier: ^6.0.2 + version: 6.0.2 + tailwindcss: + specifier: ^4.1.13 + version: 4.1.14 + devDependencies: + '@tailwindcss/vite': + specifier: ^4.1.0 + version: 4.1.14(vite@7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)) + '@types/react': + specifier: ^18.0.0 + version: 18.3.24 + '@types/react-dom': + specifier: ^18.0.0 + version: 18.3.7(@types/react@18.3.24) + '@vitejs/plugin-react': + specifier: ^5.0.3 + version: 5.0.3(vite@7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)) + typescript: + specifier: ^5.9.2 + version: 5.9.3 + vite: + specifier: ^7.1.6 + version: 7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1) + + examples/react-eject-minimal: + dependencies: + '@videojs/react': + specifier: workspace:* + version: link:../../packages/react + clsx: + specifier: ^2.1.1 + version: 2.1.1 + react: + specifier: ^18.0.0 + version: 18.3.1 + react-dom: + specifier: ^18.0.0 + version: 18.3.1(react@18.3.1) + screenfull: + specifier: ^6.0.2 + version: 6.0.2 + tailwindcss: + specifier: ^4.1.13 + version: 4.1.14 + devDependencies: + '@tailwindcss/vite': + specifier: ^4.1.0 + version: 4.1.14(vite@7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)) + '@types/react': + specifier: ^18.0.0 + version: 18.3.24 + '@types/react-dom': + specifier: ^18.0.0 + version: 18.3.7(@types/react@18.3.24) + '@vitejs/plugin-react': + specifier: ^5.0.3 + version: 5.0.3(vite@7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)) + typescript: + specifier: ^5.9.2 + version: 5.9.3 + vite: + specifier: ^7.1.6 + version: 7.1.6(@types/node@22.18.6)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1) + packages/core: dependencies: '@videojs/utils': diff --git a/site/src/components/HomePageDemo/Base.tsx b/site/src/components/HomePageDemo/Base.tsx index 3448d52b..868faa4d 100644 --- a/site/src/components/HomePageDemo/Base.tsx +++ b/site/src/components/HomePageDemo/Base.tsx @@ -1,71 +1,59 @@ -import type { Media, Skin } from '@/stores/homePageDemos'; +import type { Skin } from '@/stores/homePageDemos'; import { useStore } from '@nanostores/react'; import { TabsPanel, TabsRoot } from '@/components/Tabs'; -import { framework, media, skin } from '@/stores/homePageDemos'; +import { framework, skin } from '@/stores/homePageDemos'; import ClientCode from '../Code/ClientCode'; -function generateHTMLCode(skin: Skin, media: Media): string { +function generateHTMLCode(skin: Skin): string { const skinTag = `${skin}-skin`; - const mediaTag = media; - const videoExtension = media === 'hls-video' ? 'm3u8' : 'mp4'; return ` <${skinTag}> - <${mediaTag} src="https://example.com/video.${videoExtension}"> + `; } -function generateReactCode(skin: Skin, media: Media): string { +function generateReactCode(skin: Skin): string { const skinComponent = skin === 'frosted' ? 'FrostedSkin' : 'MinimalSkin'; - const mediaComponent = media === 'video' ? 'Video' : 'HLSVideo'; const skinImport = skin === 'frosted' ? 'frosted' : 'minimal'; - const videoExtension = media === 'hls-video' ? 'm3u8' : 'mp4'; - return `import { VideoProvider, ${mediaComponent} } from '@videojs/react'; + return `import { VideoProvider, Video } from '@videojs/react'; import { ${skinComponent} } from '@videojs/react/skins/${skinImport}'; export const VideoPlayer = () => { return ( <${skinComponent}> - <${mediaComponent} src="https://example.com/video.${videoExtension}" /> + ); };`; } -function generateCSS(_skin: Skin, _media: Media): string { - return 'Coming soon'; -} - -function generateJS(_skin: Skin, _media: Media): string { - return 'Coming soon'; +function generateJS(_skin: Skin): string { + return `import '@videojs/html/skins/frosted';`; } export default function BaseDemo({ className }: { className?: string }) { const $framework = useStore(framework); const $skin = useStore(skin); - const $media = useStore(media); if ($framework === 'html') { return ( - - - - + - + ); @@ -80,7 +68,7 @@ export default function BaseDemo({ className }: { className?: string }) { maxWidth={false} > - + ); diff --git a/site/src/components/HomePageDemo/Eject.tsx b/site/src/components/HomePageDemo/Eject.tsx index 54ec3fc1..ed2a81df 100644 --- a/site/src/components/HomePageDemo/Eject.tsx +++ b/site/src/components/HomePageDemo/Eject.tsx @@ -1,33 +1,39 @@ -import type { Media, Skin } from '@/stores/homePageDemos'; +import type { Skin } from '@/stores/homePageDemos'; import { useStore } from '@nanostores/react'; import { TabsPanel, TabsRoot } from '@/components/Tabs'; -import { framework, media, skin } from '@/stores/homePageDemos'; +import { framework, skin } from '@/stores/homePageDemos'; +import { + generateHTMLCSS, + generateHTMLJS, + generateHTMLMarkup, + generateReactComponent, + generateReactCSS as genReactCSS, +} from '@/utils/ejectCodeGenerator'; import ClientCode from '../Code/ClientCode'; -function generateHTMLCode(_skin: Skin, _media: Media): string { - return 'Coming soon'; +function generateReactCode(skin: Skin): string { + return generateReactComponent(skin); } -function generateReactCode(_skin: Skin, _media: Media): string { - return 'Coming soon'; +function generateReactCSS(skin: Skin): string { + return genReactCSS(skin); } -function generateCSSModuleCode(_skin: Skin, _media: Media): string { - return 'Coming soon'; +function generateHTMLCode(skin: Skin): string { + return generateHTMLMarkup(skin); } -function generateCSS(_skin: Skin, _media: Media): string { - return 'Coming soon'; +function generateCSS(skin: Skin): string { + return generateHTMLCSS(skin); } -function generateJS(_skin: Skin, _media: Media): string { - return 'Coming soon'; +function generateJS(skin: Skin): string { + return generateHTMLJS(skin); } export default function EjectDemo({ className }: { className?: string }) { const $framework = useStore(framework); const $skin = useStore(skin); - const $media = useStore(media); if ($framework === 'html') { return ( @@ -39,13 +45,13 @@ export default function EjectDemo({ className }: { className?: string }) { maxWidth={false} > - + - + - + ); @@ -55,15 +61,15 @@ export default function EjectDemo({ className }: { className?: string }) { - + - + ); diff --git a/site/src/content/docs/how-to/customize-skins.mdx b/site/src/content/docs/how-to/customize-skins.mdx index 6cb3b6ce..1e19dc90 100644 --- a/site/src/content/docs/how-to/customize-skins.mdx +++ b/site/src/content/docs/how-to/customize-skins.mdx @@ -3,4 +3,96 @@ title: Customize Skins description: Learn how to customize Video.js v10 skins by copying and modifying them --- -I'm going to put the eject examples here in another PR \ No newline at end of file +import FrameworkCase from '@/components/docs/FrameworkCase.astro'; +import ServerCode from '@/components/Code/ServerCode.astro'; +import { TabsRoot, TabsPanel } from '@/components/Tabs.tsx'; +import { + generateHTMLCSS, + generateHTMLJS, + generateHTMLMarkup, + generateReactComponent, + generateReactCSS, +} from '@/utils/ejectCodeGenerator'; + + +Video.js v10 comes with pre-built skins like Frosted and Minimal, but you can fully customize them by "ejecting" the code and making it your own. While eventually we'll have a CLI that'll eject skins in your preferred framework and style, for now we invite you to try it out with these copy-paste-ready implementations. + +> ⚠️ **Notice:** The HTML examples below aren't quite working, yet. Stay tuned for updates. + +## Frosted Skin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +## Minimal Skin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/site/src/stores/homePageDemos.ts b/site/src/stores/homePageDemos.ts index 2aca562b..c33432f2 100644 --- a/site/src/stores/homePageDemos.ts +++ b/site/src/stores/homePageDemos.ts @@ -2,8 +2,6 @@ import { atom } from 'nanostores'; export type Framework = 'html' | 'react'; export type Skin = 'frosted' | 'minimal'; -export type Media = 'video' | 'hls-video'; export const framework = atom('react'); export const skin = atom('frosted'); -export const media = atom('video'); diff --git a/site/src/utils/ejectCodeGenerator.ts b/site/src/utils/ejectCodeGenerator.ts new file mode 100644 index 00000000..cc5ab429 --- /dev/null +++ b/site/src/utils/ejectCodeGenerator.ts @@ -0,0 +1,1919 @@ +/** + * Shared code generation module for ejected skin examples. + * This is the single source of truth for code displayed on the home page demo and in docs. + */ + +type Skin = 'frosted' | 'minimal'; + +/** + * Generate React component code for the specified skin. + * Always uses .m3u8 video source. + */ +export function generateReactComponent(skin: Skin): string { + if (skin === 'frosted') { + return `import type { PropsWithChildren } from 'react'; + +import { CurrentTimeDisplay, DurationDisplay, FullscreenButton, MediaContainer, MuteButton, PlayButton, Popover, PreviewTimeDisplay, TimeSlider, Tooltip, VolumeSlider } from '@videojs/react'; +import { + FullscreenEnterIcon, + FullscreenExitIcon, + PauseIcon, + PlayIcon, + VolumeHighIcon, + VolumeLowIcon, + VolumeOffIcon, +} from '@videojs/react/icons'; + +import './frosted.css'; + +type SkinProps = PropsWithChildren<{ + className?: string; +}>; + +export default function FrostedSkin({ children, className = '' }: SkinProps): JSX.Element { + return ( + + {children} + +
+ +
+ + + + + + + + + + + Play + Pause + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Enter Fullscreen + Exit Fullscreen + + + + +
+ + ); +}`; + } else { + return `import type { PropsWithChildren } from 'react'; + +import { CurrentTimeDisplay, DurationDisplay, FullscreenButton, MediaContainer, MuteButton, PlayButton, Popover, PreviewTimeDisplay, TimeSlider, Tooltip, VolumeSlider } from '@videojs/react'; +import { + FullscreenEnterAltIcon, + FullscreenExitAltIcon, + PauseIcon, + PlayIcon, + VolumeHighIcon, + VolumeLowIcon, + VolumeOffIcon, +} from '@videojs/react/icons'; + +import './minimal.css'; + +type SkinProps = PropsWithChildren<{ + className?: string; +}>; + +export default function MinimalSkin({ children, className = '' }: SkinProps): JSX.Element { + return ( + + {children} + +
+ +
+ + + + + + + + + + + Play + Pause + + + + + +
+ + + + / + + +
+ + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Enter Fullscreen + Exit Fullscreen + + + + +
+
+ + ); +}`; + } +} + +/** + * Generate React CSS code for the specified skin. + */ +export function generateReactCSS(skin: Skin): string { + if (skin === 'frosted') { + return `.vjs-frosted-skin * { + box-sizing: border-box; +} + +.vjs-frosted-skin { + position: relative; + isolation: isolate; + container: root / inline-size; + overflow: clip; + font-size: 0.8125rem; + line-height: 1.5; + border-radius: inherit; + background: oklab(0 0 0); +} +.vjs-frosted-skin::before, +.vjs-frosted-skin::after { + content: ''; + position: absolute; + pointer-events: none; + border-radius: inherit; + z-index: 10; +} +.vjs-frosted-skin::before { + inset: 1px; + box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.15); +} +.vjs-frosted-skin::after { + inset: 0; + box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.1); +} + +/* Fullscreen */ +.vjs-frosted-skin:fullscreen { + border-radius: 0; +} + +.vjs-frosted-skin > ::slotted([slot='media']) { + display: block; + width: 100%; + height: 100%; +} + +/* Media Container UI Overlay Styling */ +.vjs-frosted-skin > .overlay { + position: absolute; + inset: 0; + display: flex; + flex-flow: column nowrap; + align-items: start; + pointer-events: none; + border-radius: inherit; + background-image: linear-gradient(to top, oklab(0 0 0 / 0.5), oklab(0 0 0 / 0.3), rgba(0, 0, 0, 0)); + backdrop-filter: saturate(1.5) brightness(0.9); + transition: opacity 0.15s ease-out; + transition-delay: 500ms; + opacity: 0; +} +.vjs-frosted-skin:hover > .overlay, +.vjs-frosted-skin:has([data-paused]) > .overlay, +.vjs-frosted-skin:has([aria-expanded='true']) > .overlay { + opacity: 1; + transition-duration: 100ms; + transition-delay: 0ms; +} + +/* Common Surface Styles - e.g. tooltips, popovers, controls */ +.vjs-frosted-skin .surface { + background-color: oklab(1 0 0 / 0.1); + backdrop-filter: blur(64px) brightness(0.9) saturate(1.5); + box-shadow: + inset 0 0 0 1px oklab(1 0 0 / 0.15), + 0 0 0 1px oklab(0 0 0 / 0.15), + oklab(0 0 0 / 0.15) 0px 1px 3px 0px, + oklab(0 0 0 / 0.15) 0px 1px 2px -1px; +} +@media (prefers-reduced-transparency: reduce) { + .vjs-frosted-skin .surface { + background-color: oklab(0 0 0 / 0.7); + box-shadow: + inset 0 0 0 1px oklab(0 0 0), + 0 0 0 1px oklab(1 0 0 / 0.2); + } +} +@media (prefers-contrast: more) { + .vjs-frosted-skin .surface { + background-color: oklab(0 0 0 / 0.9); + box-shadow: + inset 0 0 0 1px oklab(0 0 0), + 0 0 0 1px oklab(1 0 0 / 0.2); + } +} + +/* Media Control Bar UI/Styles */ +.vjs-frosted-skin .control-bar { + position: absolute; + bottom: 0.75rem; + inset-inline: 0.75rem; + padding: 0.25rem; + display: flex; + align-items: center; + gap: 0.125rem; + border-radius: calc(infinity * 1px); + will-change: scale, transform, filter, opacity; + scale: 0.9; + opacity: 0; + filter: blur(8px); + transition-property: scale, transform, filter, opacity; + transition-delay: 500ms; + transition-duration: 300ms; + transition-timing-function: ease-out; + transform-origin: bottom; + color: oklab(1 0 0); +} +.vjs-frosted-skin:hover > .control-bar, +.vjs-frosted-skin:has([data-paused]) > .control-bar, +.vjs-frosted-skin:has([aria-expanded='true']) > .control-bar { + opacity: 1; + scale: 1; + filter: blur(0px); + transition-delay: 0ms; + transition-duration: 100ms; +} + +/* Time Display Styling */ +.vjs-frosted-skin .time-controls { + display: flex; + align-items: center; + flex: 1; + gap: 0.75rem; + padding-inline: 0.375rem; +} +.vjs-frosted-skin .time-display { + text-shadow: 0 1px 0 oklab(0 0 0 / 0.25); + font-variant-numeric: tabular-nums; +} + +/* Generic Media Button Styling */ +.vjs-frosted-skin .button { + display: grid; + flex-shrink: 0; + padding: 0.5rem; + cursor: pointer; + background: transparent; + border: none; + border-radius: calc(infinity * 1px); + color: oklab(1 0 0 / 0.9); + user-select: none; + outline: 2px solid transparent; + outline-offset: -2px; + transition-property: background-color, color, outline-offset; + transition-duration: 150ms; + transition-timing-function: ease-out; +} +.vjs-frosted-skin .button:hover, +.vjs-frosted-skin .button:focus-visible, +.vjs-frosted-skin .button[aria-expanded='true'] { + background-color: oklab(1 0 0 / 0.1); + color: oklab(1 0 0); + text-decoration: none; +} +.vjs-frosted-skin .button:focus-visible { + outline-color: oklch(62.3% 0.214 259.815); + outline-offset: 2px; +} +.vjs-frosted-skin .button[disabled] { + cursor: not-allowed; + opacity: 0.5; + filter: grayscale(1); +} + +.vjs-frosted-skin .button .icon { + grid-area: 1 / 1; + width: 18px; + height: 18px; + filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.25)); +} + +/* Media Play Button UI/Styles */ +.vjs-frosted-skin .play-button .icon { + opacity: 0; + transition: opacity 150ms linear; +} + +.vjs-frosted-skin .play-button:not([data-paused]) .pause-icon, +.vjs-frosted-skin .play-button[data-paused] .play-icon { + opacity: 1; +} + +/* Media Fullscreen Button UI/Styles */ +.vjs-frosted-skin .fullscreen-button .icon { + display: none; +} +.vjs-frosted-skin .fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon, +.vjs-frosted-skin .fullscreen-button[data-fullscreen] .fullscreen-exit-icon { + display: inline; +} + +/* One way to define the "default visible" icon (CJP) */ +.vjs-frosted-skin .mute-button .icon { + display: none; +} +.vjs-frosted-skin .mute-button:not([data-volume-level]) .volume-low-icon, +.vjs-frosted-skin .mute-button[data-volume-level='high'] .volume-high-icon, +.vjs-frosted-skin .mute-button[data-volume-level='low'] .volume-low-icon, +.vjs-frosted-skin .mute-button[data-volume-level='medium'] .volume-low-icon, +.vjs-frosted-skin .mute-button[data-volume-level='off'] .volume-off-icon { + display: inline; +} + +/* TimeSlider Component Styles */ +.vjs-frosted-skin .slider { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + position: relative; + border-radius: calc(infinity * 1px); + outline: none; +} + +/* Horizontal orientation styles */ +.vjs-frosted-skin .slider[data-orientation='horizontal'] { + min-width: 5rem; + width: 100%; + height: 1.25rem; +} + +/* Vertical orientation styles */ +.vjs-frosted-skin .slider[data-orientation='vertical'] { + height: 5rem; + width: 1.25rem; +} + +.vjs-frosted-skin .slider-track { + position: relative; + isolation: isolate; + background-color: oklab(1 0 0 / 0.2); + border-radius: inherit; + overflow: hidden; + user-select: none; + outline: 2px solid transparent; + outline-offset: -2px; + transition: outline-offset 150ms ease-out; + box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05); +} + +/* Horizontal track styles */ +.vjs-frosted-skin .slider-track[data-orientation='horizontal'] { + width: 100%; + height: 0.25rem; +} + +/* Vertical track styles */ +.vjs-frosted-skin .slider-track[data-orientation='vertical'] { + width: 0.25rem; + height: 100%; +} + +.vjs-frosted-skin .slider:focus-visible .slider-track { + outline-color: oklch(62.3% 0.214 259.815); + outline-offset: 6px; +} + +.vjs-frosted-skin .slider-thumb { + width: 0.625rem; + height: 0.625rem; + background-color: oklab(1 0 0); + border-radius: calc(infinity * 1px); + user-select: none; + z-index: 10; + box-shadow: + 0 0 0 1px oklab(0 0 0 / 0.1), + 0 1px 3px 0 oklab(0 0 0 / 0.15), + 0 1px 2px -1px oklab(0 0 0 / 0.15); + opacity: 0; + transition-property: opacity, height, width; + transition-duration: 150ms; + transition-timing-function: ease-out; +} +.vjs-frosted-skin .slider-thumb:active { + width: 0.75rem; + height: 0.75rem; +} +.vjs-frosted-skin .slider:hover .slider-thumb, +.vjs-frosted-skin .slider:focus-within .slider-thumb { + opacity: 1; +} +.vjs-frosted-skin .slider-track[data-orientation='horizontal'] .slider-thumb { + cursor: ew-resize; +} +.vjs-frosted-skin .slider-track[data-orientation='vertical'] .slider-thumb { + cursor: ns-resize; +} + +.vjs-frosted-skin .slider-pointer { + background-color: oklab(1 0 0 / 0.2); + border-radius: inherit; +} + +.vjs-frosted-skin .slider-progress { + background-color: oklab(1 0 0); + border-radius: inherit; +} + +.vjs-frosted-skin .media-preview-time-display { + font-variant-numeric: tabular-nums; +} + +.vjs-frosted-skin .popup-animation { + transition-property: transform, scale, opacity, filter; + transition-duration: 200ms; + transform: scale(1); + transform-origin: bottom; + opacity: 1; + filter: blur(0px); +} +.vjs-frosted-skin .popup-animation[data-starting-style], +.vjs-frosted-skin .popup-animation[data-ending-style] { + transform: scale(0); + opacity: 0; + filter: blur(8px); +} +.vjs-frosted-skin .popup-animation[data-instant] { + transition-duration: 0ms; +} + +.vjs-frosted-skin .popover-popup { + margin: 0; + border: none; + box-shadow: none; + background: transparent; + padding: 0.75rem 0.25rem; + border-radius: calc(infinity * 1px); +} + +/* Tooltip Component Styles */ +.vjs-frosted-skin .tooltip-popup { + color: oklab(1 0 0); + padding: 0.25rem 0.625rem; + border-radius: calc(infinity * 1px); + font-size: 0.75rem; +} + +.vjs-frosted-skin .tooltip { + display: none; + white-space: nowrap; +} + +.vjs-frosted-skin .tooltip-popup[data-paused] .play-tooltip, +.vjs-frosted-skin .tooltip-popup:not([data-paused]) .pause-tooltip { + display: block; +} + +.vjs-frosted-skin .tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip, +.vjs-frosted-skin .tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip { + display: block; +}`; + } else { + return `.vjs-minimal-skin * { + box-sizing: border-box; +} + +.vjs-minimal-skin { + position: relative; + isolation: isolate; + container: root / inline-size; + overflow: clip; + font-size: 0.8125rem; + line-height: 1.5; + border-radius: inherit; + background: oklab(0 0 0); +} +.vjs-minimal-skin::after { + content: ''; + position: absolute; + pointer-events: none; + border-radius: inherit; + z-index: 10; + inset: 0; + box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.2); +} +@media (prefers-color-scheme: dark) { + .vjs-minimal-skin::after { + box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.2); + } +} + +/* Fullscreen */ +.vjs-minimal-skin:fullscreen { + border-radius: 0; +} + +.vjs-minimal-skin > ::slotted([slot='media']) { + display: block; + width: 100%; + height: 100%; +} + +/* Media Container UI Overlay Styling */ +.vjs-minimal-skin > .overlay { + position: absolute; + inset: 0; + display: flex; + flex-flow: column nowrap; + align-items: start; + pointer-events: none; + border-radius: inherit; + background-image: linear-gradient(to top, oklab(0 0 0 / 0.7), oklab(0 0 0 / 0.5) 7.5rem, rgba(0, 0, 0, 0)); + transform: translateY(100%); + transition-property: transform, opacity; + transition-duration: 150ms; + transition-timing-function: ease-out; + transition-delay: 500ms; + opacity: 0; +} +.vjs-minimal-skin:hover > .overlay, +.vjs-minimal-skin:has([data-paused]) > .overlay, +.vjs-minimal-skin:has([aria-expanded='true']) > .overlay { + opacity: 1; + transform: translateY(0); + transition-duration: 100ms; + transition-delay: 0ms; +} + +/* Media Control Bar UI/Styles */ +.vjs-minimal-skin .control-bar { + position: absolute; + bottom: 0; + inset-inline: 0; + padding: 2.5rem 0.75rem 0.75rem 0.75rem; + display: flex; + align-items: center; + gap: 0.875rem; + will-change: transform, filter, opacity; + transform: translateY(100%); + opacity: 0; + filter: blur(8px); + transition-property: transform, filter, opacity; + transition-delay: 500ms; + transition-duration: 300ms; + transition-timing-function: ease-out; + color: oklab(1 0 0); +} +.vjs-minimal-skin:hover > .control-bar, +.vjs-minimal-skin:has([data-paused]) > .control-bar, +.vjs-minimal-skin:has([aria-expanded='true']) > .control-bar { + opacity: 1; + transform: translateY(0); + filter: blur(0px); + transition-delay: 0ms; + transition-duration: 100ms; +} + +/* Time Display Styling */ +.vjs-minimal-skin .time-display-group { + display: flex; + align-items: center; + gap: 0.25rem; +} +.vjs-minimal-skin .duration-display { + display: contents; + color: oklab(1 0 0 / 0.5); +} +.vjs-minimal-skin .time-display { + text-shadow: 0 1px 0 oklab(0 0 0 / 0.2); + font-variant-numeric: tabular-nums; +} + +.vjs-minimal-skin .button-group { + display: flex; + align-items: center; + gap: 0.375rem; +} + +/* Generic Media Button Styling */ +.vjs-minimal-skin .button { + display: grid; + flex-shrink: 0; + padding: 0.625rem; + cursor: pointer; + background: transparent; + border: none; + border-radius: 0.375rem; + color: oklab(1 0 0); + user-select: none; + outline: 2px solid transparent; + outline-offset: -2px; + transition-property: background-color, color, outline-offset; + transition-duration: 150ms; + transition-timing-function: ease-out; +} +.vjs-minimal-skin .button:hover, +.vjs-minimal-skin .button:focus-visible, +.vjs-minimal-skin .button[aria-expanded='true'] { + color: oklab(1 0 0 / 0.8); + text-decoration: none; +} +.vjs-minimal-skin .button:focus-visible { + outline-color: oklab(1 0 0); + outline-offset: 2px; +} +.vjs-minimal-skin .button[disabled] { + cursor: not-allowed; + opacity: 0.5; + filter: grayscale(1); +} + +.vjs-minimal-skin .button .icon { + grid-area: 1 / 1; + width: 18px; + height: 18px; + filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.4)); +} + +/* Media Play Button UI/Styles */ +.vjs-minimal-skin .play-button .icon { + opacity: 0; + transition: opacity 150ms linear; +} + +.vjs-minimal-skin .play-button:not([data-paused]) .pause-icon, +.vjs-minimal-skin .play-button[data-paused] .play-icon { + opacity: 1; +} + +/* Media Fullscreen Button UI/Styles */ +.vjs-minimal-skin .fullscreen-button .icon { + display: none; +} +.vjs-minimal-skin .fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon, +.vjs-minimal-skin .fullscreen-button[data-fullscreen] .fullscreen-exit-icon { + display: inline; +} + +/* One way to define the "default visible" icon (CJP) */ +.vjs-minimal-skin .mute-button .icon { + display: none; +} +.vjs-minimal-skin .mute-button:not([data-volume-level]) .volume-low-icon, +.vjs-minimal-skin .mute-button[data-volume-level='high'] .volume-high-icon, +.vjs-minimal-skin .mute-button[data-volume-level='low'] .volume-low-icon, +.vjs-minimal-skin .mute-button[data-volume-level='medium'] .volume-low-icon, +.vjs-minimal-skin .mute-button[data-volume-level='off'] .volume-off-icon { + display: inline; +} + +/* TimeSlider Component Styles */ +.vjs-minimal-skin .slider { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + position: relative; + border-radius: calc(infinity * 1px); + outline: none; +} + +/* Horizontal orientation styles */ +.vjs-minimal-skin .slider[data-orientation='horizontal'] { + min-width: 5rem; + width: 100%; + height: 1.25rem; +} + +/* Vertical orientation styles */ +.vjs-minimal-skin .slider[data-orientation='vertical'] { + height: 4.5rem; + width: 1.25rem; +} + +.vjs-minimal-skin .slider-track { + position: relative; + isolation: isolate; + background-color: oklab(1 0 0 / 0.2); + border-radius: inherit; + overflow: hidden; + user-select: none; + outline: 2px solid transparent; + outline-offset: -2px; + transition: outline-offset 150ms ease-out; + box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05); +} + +/* Horizontal track styles */ +.vjs-minimal-skin .slider-track[data-orientation='horizontal'] { + width: 100%; + height: 0.1875rem; +} + +/* Vertical track styles */ +.vjs-minimal-skin .slider-track[data-orientation='vertical'] { + width: 0.1875rem; + height: 100%; +} + +.vjs-minimal-skin .slider:focus-visible .slider-track { + outline-color: oklab(1 0 0); + outline-offset: 6px; +} + +.vjs-minimal-skin .slider-thumb { + width: 0.75rem; + height: 0.75rem; + background-color: oklab(1 0 0); + border-radius: calc(infinity * 1px); + user-select: none; + z-index: 10; + box-shadow: + 0 0 0 1px oklab(0 0 0 / 0.1), + 0 1px 3px 0 oklab(0 0 0 / 0.15), + 0 1px 2px -1px oklab(0 0 0 / 0.15); + opacity: 0; + scale: 0.7; + transform-origin: center; + transition-property: opacity, scale; + transition-duration: 150ms; + transition-timing-function: ease-out; +} +.vjs-minimal-skin .slider:hover .slider-thumb, +.vjs-minimal-skin .slider:focus-within .slider-thumb { + opacity: 1; + scale: 1; +} +.vjs-minimal-skin .slider-track[data-orientation='horizontal'] .slider-thumb { + cursor: ew-resize; +} +.vjs-minimal-skin .slider-track[data-orientation='vertical'] .slider-thumb { + cursor: ns-resize; +} + +.vjs-minimal-skin .slider-pointer { + display: none; +} + +.vjs-minimal-skin .slider-progress { + background-color: oklab(1 0 0); + border-radius: inherit; +} + +.vjs-minimal-skin .media-preview-time-display { + font-variant-numeric: tabular-nums; +} + +.vjs-minimal-skin .popup-animation { + transition-property: transform, scale, opacity, filter; + transition-duration: 200ms; + transform: scale(1); + transform-origin: bottom; + opacity: 1; + filter: blur(0px); +} +.vjs-minimal-skin .popup-animation[data-starting-style], +.vjs-minimal-skin .popup-animation[data-ending-style] { + transform: scale(0); + opacity: 0; + filter: blur(8px); +} +.vjs-minimal-skin .popup-animation[data-instant] { + transition-duration: 0ms; +} + +.vjs-minimal-skin .popover-popup { + margin: 0; + border: none; + box-shadow: none; + background: transparent; + padding: 0.75rem 0.25rem; + border-radius: calc(infinity * 1px); +} + +/* Tooltip Component Styles */ +.vjs-minimal-skin .tooltip-popup { + color: oklab(1 0 0); + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; + background-color: oklab(1 0 0 / 0.1); + backdrop-filter: blur(64px) brightness(0.9) saturate(1.5); + box-shadow: + 0 4px 6px -1px oklab(0 0 0 / 0.1), + 0 2px 4px -2px oklab(0 0 0 / 0.1); +} +@media (prefers-reduced-transparency: reduce) { + .vjs-minimal-skin .tooltip-popup { + background-color: oklab(0 0 0 / 0.7); + } +} +@media (prefers-contrast: more) { + .vjs-minimal-skin .tooltip-popup { + background-color: oklab(0 0 0 / 0.9); + } +} + +.vjs-minimal-skin .tooltip { + display: none; + white-space: nowrap; +} + +.vjs-minimal-skin .tooltip-popup[data-paused] .play-tooltip, +.vjs-minimal-skin .tooltip-popup:not([data-paused]) .pause-tooltip { + display: block; +} + +.vjs-minimal-skin .tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip, +.vjs-minimal-skin .tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip { + display: block; +}`; + } +} + +/** + * Generate HTML markup for the specified skin. + * Always uses .mp4 video source. + */ +export function generateHTMLMarkup(skin: Skin): string { + if (skin === 'frosted') { + return ` + + + +
+ +
+ + + + + + + + + + + Play + Pause + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + Enter Fullscreen + Exit Fullscreen + + + + +
+
+
`; + } else { + return ` + + + +
+ +
+ + + + + + + + + + + Play + Pause + + + + + +
+ + + + + / + + +
+ + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + Enter Fullscreen + Exit Fullscreen + + + + +
+
+
+
`; + } +} + +/** + * Generate HTML CSS code for the specified skin. + */ +export function generateHTMLCSS(skin: Skin): string { + // HTML CSS is nearly identical to React CSS, with just element selectors instead of class selectors + if (skin === 'frosted') { + return `media-container * { + box-sizing: border-box; +} + +media-container { + position: relative; + isolation: isolate; + container: root / inline-size; + overflow: clip; + font-size: 0.8125rem; + line-height: 1.5; + border-radius: inherit; + background: oklab(0 0 0); +} +media-container::before, +media-container::after { + content: ''; + position: absolute; + pointer-events: none; + border-radius: inherit; + z-index: 10; +} +media-container::before { + inset: 1px; + box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.15); +} +media-container::after { + inset: 0; + box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.1); +} + +/* Fullscreen */ +media-container:fullscreen { + border-radius: 0; +} + +media-container > ::slotted([slot='media']), +media-container > video, +media-container > audio { + display: block; + width: 100%; + height: 100%; +} + +/* Media Container UI Overlay Styling */ +media-container > .overlay { + position: absolute; + inset: 0; + display: flex; + flex-flow: column nowrap; + align-items: start; + pointer-events: none; + border-radius: inherit; + background-image: linear-gradient(to top, oklab(0 0 0 / 0.5), oklab(0 0 0 / 0.3), rgba(0, 0, 0, 0)); + backdrop-filter: saturate(1.5) brightness(0.9); + transition: opacity 0.15s ease-out; + transition-delay: 500ms; + opacity: 0; +} +media-container:hover > .overlay, +media-container:has([data-paused]) > .overlay, +media-container:has([aria-expanded='true']) > .overlay { + opacity: 1; + transition-duration: 100ms; + transition-delay: 0ms; +} + +/* Common Surface Styles - e.g. tooltips, popovers, controls */ +.surface { + background-color: oklab(1 0 0 / 0.1); + backdrop-filter: blur(64px) brightness(0.9) saturate(1.5); + box-shadow: + inset 0 0 0 1px oklab(1 0 0 / 0.15), + 0 0 0 1px oklab(0 0 0 / 0.15), + oklab(0 0 0 / 0.15) 0px 1px 3px 0px, + oklab(0 0 0 / 0.15) 0px 1px 2px -1px; +} +@media (prefers-reduced-transparency: reduce) { + .surface { + background-color: oklab(0 0 0 / 0.7); + box-shadow: + inset 0 0 0 1px oklab(0 0 0), + 0 0 0 1px oklab(1 0 0 / 0.2); + } +} +@media (prefers-contrast: more) { + .surface { + background-color: oklab(0 0 0 / 0.9); + box-shadow: + inset 0 0 0 1px oklab(0 0 0), + 0 0 0 1px oklab(1 0 0 / 0.2); + } +} + +/* Media Control Bar UI/Styles */ +.control-bar { + position: absolute; + bottom: 0.75rem; + inset-inline: 0.75rem; + padding: 0.25rem; + display: flex; + align-items: center; + gap: 0.125rem; + border-radius: calc(infinity * 1px); + will-change: scale, transform, filter, opacity; + scale: 0.9; + opacity: 0; + filter: blur(8px); + transition-property: scale, transform, filter, opacity; + transition-delay: 500ms; + transition-duration: 300ms; + transition-timing-function: ease-out; + transform-origin: bottom; + color: oklab(1 0 0); +} +media-container:hover > .control-bar, +media-container:has([data-paused]) > .control-bar, +media-container:has([aria-expanded='true']) > .control-bar { + opacity: 1; + scale: 1; + filter: blur(0px); + transition-delay: 0ms; + transition-duration: 100ms; +} + +/* Time Display Styling */ +.time-controls { + display: flex; + align-items: center; + flex: 1; + gap: 0.75rem; + padding-inline: 0.375rem; +} +media-current-time-display, +media-duration-display { + text-shadow: 0 1px 0 oklab(0 0 0 / 0.25); + font-variant-numeric: tabular-nums; +} + +/* Generic Media Button Styling */ +.button { + display: grid; + flex-shrink: 0; + padding: 0.5rem; + cursor: pointer; + background: transparent; + border: none; + border-radius: calc(infinity * 1px); + color: oklab(1 0 0 / 0.9); + user-select: none; + outline: 2px solid transparent; + outline-offset: -2px; + transition-property: background-color, color, outline-offset; + transition-duration: 150ms; + transition-timing-function: ease-out; +} +.button:hover, +.button:focus-visible, +.button[aria-expanded='true'] { + background-color: oklab(1 0 0 / 0.1); + color: oklab(1 0 0); + text-decoration: none; +} +.button:focus-visible { + outline-color: oklch(62.3% 0.214 259.815); + outline-offset: 2px; +} +.button[disabled] { + cursor: not-allowed; + opacity: 0.5; + filter: grayscale(1); +} + +.button .icon { + grid-area: 1 / 1; + width: 18px; + height: 18px; + filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.25)); +} + +/* Media Play Button UI/Styles */ +media-play-button .icon { + opacity: 0; + transition: opacity 150ms linear; +} + +media-play-button:not([data-paused]) .pause-icon, +media-play-button[data-paused] .play-icon { + opacity: 1; +} + +/* Media Fullscreen Button UI/Styles */ +media-fullscreen-button .icon { + display: none; +} +media-fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon, +media-fullscreen-button[data-fullscreen] .fullscreen-exit-icon { + display: inline; +} + +/* One way to define the "default visible" icon (CJP) */ +media-mute-button .icon { + display: none; +} +media-mute-button:not([data-volume-level]) .volume-low-icon, +media-mute-button[data-volume-level='high'] .volume-high-icon, +media-mute-button[data-volume-level='low'] .volume-low-icon, +media-mute-button[data-volume-level='medium'] .volume-low-icon, +media-mute-button[data-volume-level='off'] .volume-off-icon { + display: inline; +} + +/* TimeSlider Component Styles */ +.slider { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + position: relative; + border-radius: calc(infinity * 1px); + outline: none; +} + +/* Horizontal orientation styles */ +.slider[data-orientation='horizontal'] { + min-width: 5rem; + width: 100%; + height: 1.25rem; +} + +/* Vertical orientation styles */ +.slider[data-orientation='vertical'] { + height: 5rem; + width: 1.25rem; +} + +.slider-track { + position: relative; + isolation: isolate; + background-color: oklab(1 0 0 / 0.2); + border-radius: inherit; + overflow: hidden; + user-select: none; + outline: 2px solid transparent; + outline-offset: -2px; + transition: outline-offset 150ms ease-out; + box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05); +} + +/* Horizontal track styles */ +.slider-track[data-orientation='horizontal'] { + width: 100%; + height: 0.25rem; +} + +/* Vertical track styles */ +.slider-track[data-orientation='vertical'] { + width: 0.25rem; + height: 100%; +} + +.slider:focus-visible .slider-track { + outline-color: oklch(62.3% 0.214 259.815); + outline-offset: 6px; +} + +.slider-thumb { + width: 0.625rem; + height: 0.625rem; + background-color: oklab(1 0 0); + border-radius: calc(infinity * 1px); + user-select: none; + z-index: 10; + box-shadow: + 0 0 0 1px oklab(0 0 0 / 0.1), + 0 1px 3px 0 oklab(0 0 0 / 0.15), + 0 1px 2px -1px oklab(0 0 0 / 0.15); + opacity: 0; + transition-property: opacity, height, width; + transition-duration: 150ms; + transition-timing-function: ease-out; +} +.slider-thumb:active { + width: 0.75rem; + height: 0.75rem; +} +.slider:hover .slider-thumb, +.slider:focus-within .slider-thumb { + opacity: 1; +} +.slider-track[data-orientation='horizontal'] .slider-thumb { + cursor: ew-resize; +} +.slider-track[data-orientation='vertical'] .slider-thumb { + cursor: ns-resize; +} + +.slider-pointer { + background-color: oklab(1 0 0 / 0.2); + border-radius: inherit; +} + +.slider-progress { + background-color: oklab(1 0 0); + border-radius: inherit; +} + +media-preview-time-display { + font-variant-numeric: tabular-nums; +} + +.popup-animation { + transition-property: transform, scale, opacity, filter; + transition-duration: 200ms; + transform: scale(1); + transform-origin: bottom; + opacity: 1; + filter: blur(0px); +} +.popup-animation[data-starting-style], +.popup-animation[data-ending-style] { + transform: scale(0); + opacity: 0; + filter: blur(8px); +} +.popup-animation[data-instant] { + transition-duration: 0ms; +} + +media-popover { + margin: 0; + border: none; + box-shadow: none; + background: transparent; + padding: 0.75rem 0.25rem; + border-radius: calc(infinity * 1px); +} + +/* Tooltip Component Styles */ +media-tooltip-popup { + color: oklab(1 0 0); + padding: 0.25rem 0.625rem; + border-radius: calc(infinity * 1px); + font-size: 0.75rem; +} + +.tooltip { + display: none; + white-space: nowrap; +} + +media-tooltip-popup[data-paused] .play-tooltip, +media-tooltip-popup:not([data-paused]) .pause-tooltip { + display: block; +} + +media-tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip, +media-tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip { + display: block; +}`; + } else { + return `media-container * { + box-sizing: border-box; +} + +media-container { + position: relative; + isolation: isolate; + container: root / inline-size; + overflow: clip; + font-size: 0.8125rem; + line-height: 1.5; + border-radius: inherit; + background: oklab(0 0 0); +} +media-container::after { + content: ''; + position: absolute; + pointer-events: none; + border-radius: inherit; + z-index: 10; + inset: 0; + box-shadow: inset 0 0 0 1px oklab(0 0 0 / 0.2); +} +@media (prefers-color-scheme: dark) { + media-container::after { + box-shadow: inset 0 0 0 1px oklab(1 0 0 / 0.2); + } +} + +/* Fullscreen */ +media-container:fullscreen { + border-radius: 0; +} + +media-container > ::slotted([slot='media']), +media-container > video, +media-container > audio { + display: block; + width: 100%; + height: 100%; +} + +/* Media Container UI Overlay Styling */ +media-container > .overlay { + position: absolute; + inset: 0; + display: flex; + flex-flow: column nowrap; + align-items: start; + pointer-events: none; + border-radius: inherit; + background-image: linear-gradient(to top, oklab(0 0 0 / 0.7), oklab(0 0 0 / 0.5) 7.5rem, rgba(0, 0, 0, 0)); + transform: translateY(100%); + transition-property: transform, opacity; + transition-duration: 150ms; + transition-timing-function: ease-out; + transition-delay: 500ms; + opacity: 0; +} +media-container:hover > .overlay, +media-container:has([data-paused]) > .overlay, +media-container:has([aria-expanded='true']) > .overlay { + opacity: 1; + transform: translateY(0); + transition-duration: 100ms; + transition-delay: 0ms; +} + +/* Media Control Bar UI/Styles */ +.control-bar { + position: absolute; + bottom: 0; + inset-inline: 0; + padding: 2.5rem 0.75rem 0.75rem 0.75rem; + display: flex; + align-items: center; + gap: 0.875rem; + will-change: transform, filter, opacity; + transform: translateY(100%); + opacity: 0; + filter: blur(8px); + transition-property: transform, filter, opacity; + transition-delay: 500ms; + transition-duration: 300ms; + transition-timing-function: ease-out; + color: oklab(1 0 0); +} +media-container:hover > .control-bar, +media-container:has([data-paused]) > .control-bar, +media-container:has([aria-expanded='true']) > .control-bar { + opacity: 1; + transform: translateY(0); + filter: blur(0px); + transition-delay: 0ms; + transition-duration: 100ms; +} + +/* Time Display Styling */ +.time-display-group { + display: flex; + align-items: center; + gap: 0.25rem; +} +.duration-display { + display: contents; + color: oklab(1 0 0 / 0.5); +} +media-current-time-display, +media-duration-display { + text-shadow: 0 1px 0 oklab(0 0 0 / 0.2); + font-variant-numeric: tabular-nums; +} + +.button-group { + display: flex; + align-items: center; + gap: 0.375rem; +} + +/* Generic Media Button Styling */ +.button { + display: grid; + flex-shrink: 0; + padding: 0.625rem; + cursor: pointer; + background: transparent; + border: none; + border-radius: 0.375rem; + color: oklab(1 0 0); + user-select: none; + outline: 2px solid transparent; + outline-offset: -2px; + transition-property: background-color, color, outline-offset; + transition-duration: 150ms; + transition-timing-function: ease-out; +} +.button:hover, +.button:focus-visible, +.button[aria-expanded='true'] { + color: oklab(1 0 0 / 0.8); + text-decoration: none; +} +.button:focus-visible { + outline-color: oklab(1 0 0); + outline-offset: 2px; +} +.button[disabled] { + cursor: not-allowed; + opacity: 0.5; + filter: grayscale(1); +} + +.button .icon { + grid-area: 1 / 1; + width: 18px; + height: 18px; + filter: drop-shadow(0 1px 0 oklab(0 0 0 / 0.4)); +} + +/* Media Play Button UI/Styles */ +media-play-button .icon { + opacity: 0; + transition: opacity 150ms linear; +} + +media-play-button:not([data-paused]) .pause-icon, +media-play-button[data-paused] .play-icon { + opacity: 1; +} + +/* Media Fullscreen Button UI/Styles */ +media-fullscreen-button .icon { + display: none; +} +media-fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon, +media-fullscreen-button[data-fullscreen] .fullscreen-exit-icon { + display: inline; +} + +/* One way to define the "default visible" icon (CJP) */ +media-mute-button .icon { + display: none; +} +media-mute-button:not([data-volume-level]) .volume-low-icon, +media-mute-button[data-volume-level='high'] .volume-high-icon, +media-mute-button[data-volume-level='low'] .volume-low-icon, +media-mute-button[data-volume-level='medium'] .volume-low-icon, +media-mute-button[data-volume-level='off'] .volume-off-icon { + display: inline; +} + +/* TimeSlider Component Styles */ +.slider { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + position: relative; + border-radius: calc(infinity * 1px); + outline: none; +} + +/* Horizontal orientation styles */ +.slider[data-orientation='horizontal'] { + min-width: 5rem; + width: 100%; + height: 1.25rem; +} + +/* Vertical orientation styles */ +.slider[data-orientation='vertical'] { + height: 4.5rem; + width: 1.25rem; +} + +.slider-track { + position: relative; + isolation: isolate; + background-color: oklab(1 0 0 / 0.2); + border-radius: inherit; + overflow: hidden; + user-select: none; + outline: 2px solid transparent; + outline-offset: -2px; + transition: outline-offset 150ms ease-out; + box-shadow: 0 0 0 1px oklab(0 0 0 / 0.05); +} + +/* Horizontal track styles */ +.slider-track[data-orientation='horizontal'] { + width: 100%; + height: 0.1875rem; +} + +/* Vertical track styles */ +.slider-track[data-orientation='vertical'] { + width: 0.1875rem; + height: 100%; +} + +.slider:focus-visible .slider-track { + outline-color: oklab(1 0 0); + outline-offset: 6px; +} + +.slider-thumb { + width: 0.75rem; + height: 0.75rem; + background-color: oklab(1 0 0); + border-radius: calc(infinity * 1px); + user-select: none; + z-index: 10; + box-shadow: + 0 0 0 1px oklab(0 0 0 / 0.1), + 0 1px 3px 0 oklab(0 0 0 / 0.15), + 0 1px 2px -1px oklab(0 0 0 / 0.15); + opacity: 0; + scale: 0.7; + transform-origin: center; + transition-property: opacity, scale; + transition-duration: 150ms; + transition-timing-function: ease-out; +} +.slider:hover .slider-thumb, +.slider:focus-within .slider-thumb { + opacity: 1; + scale: 1; +} +.slider-track[data-orientation='horizontal'] .slider-thumb { + cursor: ew-resize; +} +.slider-track[data-orientation='vertical'] .slider-thumb { + cursor: ns-resize; +} + +.slider-pointer { + display: none; +} + +.slider-progress { + background-color: oklab(1 0 0); + border-radius: inherit; +} + +media-preview-time-display { + font-variant-numeric: tabular-nums; +} + +.popup-animation { + transition-property: transform, scale, opacity, filter; + transition-duration: 200ms; + transform: scale(1); + transform-origin: bottom; + opacity: 1; + filter: blur(0px); +} +.popup-animation[data-starting-style], +.popup-animation[data-ending-style] { + transform: scale(0); + opacity: 0; + filter: blur(8px); +} +.popup-animation[data-instant] { + transition-duration: 0ms; +} + +media-popover { + margin: 0; + border: none; + box-shadow: none; + background: transparent; + padding: 0.75rem 0.25rem; + border-radius: calc(infinity * 1px); +} + +/* Tooltip Component Styles */ +media-tooltip-popup { + color: oklab(1 0 0); + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; + background-color: oklab(1 0 0 / 0.1); + backdrop-filter: blur(64px) brightness(0.9) saturate(1.5); + box-shadow: + 0 4px 6px -1px oklab(0 0 0 / 0.1), + 0 2px 4px -2px oklab(0 0 0 / 0.1); +} +@media (prefers-reduced-transparency: reduce) { + media-tooltip-popup { + background-color: oklab(0 0 0 / 0.7); + } +} +@media (prefers-contrast: more) { + media-tooltip-popup { + background-color: oklab(0 0 0 / 0.9); + } +} + +.tooltip { + display: none; + white-space: nowrap; +} + +media-tooltip-popup[data-paused] .play-tooltip, +media-tooltip-popup:not([data-paused]) .pause-tooltip { + display: block; +} + +media-tooltip-popup[data-fullscreen] .fullscreen-exit-tooltip, +media-tooltip-popup:not([data-fullscreen]) .fullscreen-enter-tooltip { + display: block; +}`; + } +} + +/** + * Generate HTML JavaScript imports for the specified skin. + */ +export function generateHTMLJS(skin: Skin): string { + return `import './${skin}.css'; +import '@videojs/html/icons'; +import '@videojs/html/define/media-container'; +import '@videojs/html/define/media-provider'; +import '@videojs/html/define/media-play-button'; +import '@videojs/html/define/media-mute-button'; +import '@videojs/html/define/media-volume-slider'; +import '@videojs/html/define/media-time-slider'; +import '@videojs/html/define/media-fullscreen-button'; +import '@videojs/html/define/media-duration-display'; +import '@videojs/html/define/media-current-time-display'; +import '@videojs/html/define/media-preview-time-display'; +import '@videojs/html/define/media-popover'; +import '@videojs/html/define/media-tooltip';`; +}