diff --git a/commitlint.config.js b/commitlint.config.js index 0ba9dca5..c67ed3d6 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -23,6 +23,7 @@ export default { 'react', 'root', 'site', + 'skins', 'test', 'utils', ]], diff --git a/examples/html-demo/index.html b/examples/html-demo/index.html index d2d41990..d70e9b88 100644 --- a/examples/html-demo/index.html +++ b/examples/html-demo/index.html @@ -11,6 +11,10 @@ font-family: InterVariable, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + margin: 0; + } + * { + box-sizing: border-box; } diff --git a/examples/html-demo/src/main.ts b/examples/html-demo/src/main.ts index 1d2dd9d9..0fc88f0f 100644 --- a/examples/html-demo/src/main.ts +++ b/examples/html-demo/src/main.ts @@ -1,15 +1,22 @@ import { defineVjsPlayer } from '@videojs/html'; import '@videojs/html/skins/frosted'; +import '@videojs/html/skins/minimal'; defineVjsPlayer(); document.body.innerHTML = /* html */ ` - -
+
+ - + -
- + + + + + + + +
`; diff --git a/examples/react-demo/src/App.tsx b/examples/react-demo/src/App.tsx index 565b1b71..61bb56de 100644 --- a/examples/react-demo/src/App.tsx +++ b/examples/react-demo/src/App.tsx @@ -185,7 +185,7 @@ export default function App(): JSX.Element { {/* @ts-expect-error -- types are incorrect */} - diff --git a/examples/react-demo/src/globals.css b/examples/react-demo/src/globals.css index b2dac7b1..88210933 100644 --- a/examples/react-demo/src/globals.css +++ b/examples/react-demo/src/globals.css @@ -6,13 +6,5 @@ 'Noto Color Emoji'; } -.body { - @apply antialiased font-[510] font-sans text-[0.8125rem] @7xl/root:text-[0.9375rem] leading-normal tracking-[-0.0125em]; -} - -:root { - font-feature-settings: 'cv01', 'ss01', 'ss03'; -} - /* Make reduced-transparency variant */ @custom-variant reduced-transparency @media (prefers-reduced-transparency: reduce); diff --git a/packages/html/src/components/media-time-slider.ts b/packages/html/src/components/media-time-slider.ts index 2ccdce29..3cafef21 100644 --- a/packages/html/src/components/media-time-slider.ts +++ b/packages/html/src/components/media-time-slider.ts @@ -186,11 +186,11 @@ export class TimeSliderThumbBase extends HTMLElement { if (props['data-orientation'] === 'horizontal') { this.style.left = 'var(--slider-fill, 0%)'; this.style.top = '50%'; - this.style.transform = 'translate(-50%, -50%)'; + this.style.translate = '-50% -50%'; } else { this.style.bottom = 'var(--slider-fill, 0%)'; this.style.left = '50%'; - this.style.transform = 'translate(-50%, 50%)'; + this.style.translate = '-50% 50%'; } } } diff --git a/packages/html/src/components/media-volume-slider.ts b/packages/html/src/components/media-volume-slider.ts index 85ed6251..00d56f9a 100644 --- a/packages/html/src/components/media-volume-slider.ts +++ b/packages/html/src/components/media-volume-slider.ts @@ -173,11 +173,11 @@ export class VolumeSliderThumbBase extends HTMLElement { if (props['data-orientation'] === 'horizontal') { this.style.left = 'var(--slider-fill, 0%)'; this.style.top = '50%'; - this.style.transform = 'translate(-50%, -50%)'; + this.style.translate = '-50% -50%'; } else { this.style.bottom = 'var(--slider-fill, 0%)'; this.style.left = '50%'; - this.style.transform = 'translate(-50%, 50%)'; + this.style.translate = '-50% 50%'; } } } diff --git a/packages/html/src/icons/index.ts b/packages/html/src/icons/index.ts index 971a31b4..d57489a8 100644 --- a/packages/html/src/icons/index.ts +++ b/packages/html/src/icons/index.ts @@ -1,4 +1,6 @@ +export * as MediaFullscreenEnterAltIcon from './media-fullscreen-enter-alt-icon'; export * as MediaFullscreenEnterIcon from './media-fullscreen-enter-icon'; +export * as MediaFullscreenExitAltIcon from './media-fullscreen-exit-alt-icon'; export * as MediaFullscreenExitIcon from './media-fullscreen-exit-icon'; export * as MediaPauseIcon from './media-pause-icon'; export * as MediaPlayIcon from './media-play-icon'; diff --git a/packages/html/src/icons/media-fullscreen-enter-alt-icon.ts b/packages/html/src/icons/media-fullscreen-enter-alt-icon.ts new file mode 100644 index 00000000..13e76ec5 --- /dev/null +++ b/packages/html/src/icons/media-fullscreen-enter-alt-icon.ts @@ -0,0 +1,21 @@ +import { SVG_ICONS } from '@videojs/icons'; + +import { MediaChromeIcon } from './media-chrome-icon'; + +export function getTemplateHTML() { + return /* html */ ` + ${MediaChromeIcon.getTemplateHTML()} + + ${SVG_ICONS.fullscreenEnterAlt} + `; +} + +export class MediaFullscreenEnterAltIcon extends MediaChromeIcon { + static getTemplateHTML: () => string = getTemplateHTML; +} + +customElements.define('media-fullscreen-enter-alt-icon', MediaFullscreenEnterAltIcon); diff --git a/packages/html/src/icons/media-fullscreen-exit-alt-icon.ts b/packages/html/src/icons/media-fullscreen-exit-alt-icon.ts new file mode 100644 index 00000000..24564926 --- /dev/null +++ b/packages/html/src/icons/media-fullscreen-exit-alt-icon.ts @@ -0,0 +1,21 @@ +import { SVG_ICONS } from '@videojs/icons'; + +import { MediaChromeIcon } from './media-chrome-icon'; + +export function getTemplateHTML() { + return /* html */ ` + ${MediaChromeIcon.getTemplateHTML()} + + ${SVG_ICONS.fullscreenExitAlt} + `; +} + +export class MediaFullscreenExitAltIcon extends MediaChromeIcon { + static getTemplateHTML: () => string = getTemplateHTML; +} + +customElements.define('media-fullscreen-exit-alt-icon', MediaFullscreenExitAltIcon); diff --git a/packages/html/src/skins/frosted/index.ts b/packages/html/src/skins/frosted/index.ts index 16aecc55..ae85dc5a 100644 --- a/packages/html/src/skins/frosted/index.ts +++ b/packages/html/src/skins/frosted/index.ts @@ -22,11 +22,12 @@ export function getTemplateHTML() { +
- + @@ -91,7 +92,7 @@ export function getTemplateHTML() { - + diff --git a/packages/html/src/skins/frosted/styles.css b/packages/html/src/skins/frosted/styles.css index bdf818af..29772b12 100644 --- a/packages/html/src/skins/frosted/styles.css +++ b/packages/html/src/skins/frosted/styles.css @@ -33,10 +33,6 @@ media-container::after { media-container:fullscreen { border-radius: 0; } -media-container:fullscreen video { - height: 100%; - width: 100%; -} media-container > ::slotted([slot='media']) { display: block; @@ -52,6 +48,7 @@ media-container > .overlay { 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; @@ -62,12 +59,13 @@ 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: rgba(255, 255, 255, 0.1); + 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), @@ -223,15 +221,15 @@ media-mute-button[data-volume-level='off'] .volume-off-icon { /* Horizontal orientation styles */ .slider-root[data-orientation='horizontal'] { - min-width: 80px; + min-width: 5rem; width: 100%; - height: 20px; + height: 1.25rem; } /* Vertical orientation styles */ .slider-root[data-orientation='vertical'] { - height: 80px; - width: 20px; + height: 5rem; + width: 1.25rem; } .slider-track { @@ -278,7 +276,7 @@ media-mute-button[data-volume-level='off'] .volume-off-icon { opacity: 0; transition-property: opacity, height, width; transition-duration: 150ms; - transition-timing-function: ease-in-out; + transition-timing-function: ease-out; } .slider-thumb:active { width: 0.75rem; diff --git a/packages/html/src/skins/minimal/index.ts b/packages/html/src/skins/minimal/index.ts index e06afd25..a2fe1975 100644 --- a/packages/html/src/skins/minimal/index.ts +++ b/packages/html/src/skins/minimal/index.ts @@ -1,4 +1,5 @@ import { MediaSkin } from '@/media/media-skin'; +import styles from './styles.css'; import '@/media/media-container'; import '@/media/media-provider'; import '@/components/media-play-button'; @@ -11,11 +12,109 @@ import '@/components/media-current-time-display'; import '@/components/media-preview-time-display'; import '@/components/media-popover'; import '@/components/media-tooltip'; + import '@/icons'; export function getTemplateHTML() { return /* html */` ${MediaSkin.getTemplateHTML()} + + + + + +
+ +
+ + + + + + + + + + + + Play + Pause + + + + + +
+ + + + + / + + +
+ + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Enter Fullscreen + Exit Fullscreen + + + + +
+
+
`; } diff --git a/packages/html/src/skins/minimal/styles.css b/packages/html/src/skins/minimal/styles.css index e69de29b..80d5b3c6 100644 --- a/packages/html/src/skins/minimal/styles.css +++ b/packages/html/src/skins/minimal/styles.css @@ -0,0 +1,346 @@ +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']) { + 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-root { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + position: relative; + border-radius: calc(infinity * 1px); + outline: none; +} + +/* Horizontal orientation styles */ +.slider-root[data-orientation='horizontal'] { + min-width: 5rem; + width: 100%; + height: 1.25rem; +} + +/* Vertical orientation styles */ +.slider-root[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-root: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-root:hover .slider-thumb, +.slider-root: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; +} + +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-popup { + 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/packages/react/src/skins/frosted/FrostedSkin.tsx b/packages/react/src/skins/frosted/FrostedSkin.tsx index 8d641594..8a61c3d7 100644 --- a/packages/react/src/skins/frosted/FrostedSkin.tsx +++ b/packages/react/src/skins/frosted/FrostedSkin.tsx @@ -1,5 +1,16 @@ import type { PropsWithChildren } from 'react'; +import { CurrentTimeDisplay } from '@/components/CurrentTimeDisplay'; +import { DurationDisplay } from '@/components/DurationDisplay'; +import { FullscreenButton } from '@/components/FullscreenButton'; +import { MediaContainer } from '@/components/MediaContainer'; +import MuteButton from '@/components/MuteButton'; +import PlayButton from '@/components/PlayButton'; +import { Popover } from '@/components/Popover'; +import { PreviewTimeDisplay } from '@/components/PreviewTimeDisplay'; +import { TimeSlider } from '@/components/TimeSlider'; +import { Tooltip } from '@/components/Tooltip'; +import { VolumeSlider } from '@/components/VolumeSlider'; import { FullscreenEnterIcon, FullscreenExitIcon, @@ -9,18 +20,6 @@ import { VolumeLowIcon, VolumeOffIcon, } from '@/icons'; - -import { CurrentTimeDisplay } from '../../components/CurrentTimeDisplay'; -import { DurationDisplay } from '../../components/DurationDisplay'; -import { FullscreenButton } from '../../components/FullscreenButton'; -import { MediaContainer } from '../../components/MediaContainer'; -import MuteButton from '../../components/MuteButton'; -import PlayButton from '../../components/PlayButton'; -import { Popover } from '../../components/Popover'; -import { PreviewTimeDisplay } from '../../components/PreviewTimeDisplay'; -import { TimeSlider } from '../../components/TimeSlider'; -import { Tooltip } from '../../components/Tooltip'; -import { VolumeSlider } from '../../components/VolumeSlider'; import styles from './styles'; type SkinProps = PropsWithChildren<{ @@ -32,8 +31,7 @@ export default function FrostedSkin({ children, className = '' }: SkinProps): JS {children} - {/* Background gradient to help with controls contrast. */} -