diff --git a/packages/html/src/define/audio/minimal-skin.tailwind.ts b/packages/html/src/define/audio/minimal-skin.tailwind.ts index d4d76b06..dec1ea14 100644 --- a/packages/html/src/define/audio/minimal-skin.tailwind.ts +++ b/packages/html/src/define/audio/minimal-skin.tailwind.ts @@ -14,6 +14,7 @@ import { seek, slider, time, + tooltipState, } from '@videojs/skins/minimal/tailwind/audio.tailwind'; import { cn } from '@videojs/utils/style'; import { SkinMixin } from '../skin-mixin'; @@ -27,6 +28,7 @@ import '../ui/popover'; import '../ui/seek-button'; import '../ui/time'; import '../ui/time-slider'; +import '../ui/tooltip'; import '../ui/volume-slider'; const SEEK_TIME = 10; @@ -38,25 +40,38 @@ function getTemplateHTML() {
- - ${renderIcon('restart', { class: cn(icon, iconState.play.restart) })} - ${renderIcon('play', { class: cn(icon, iconState.play.play) })} - ${renderIcon('pause', { class: cn(icon, iconState.play.pause) })} - + + + ${renderIcon('restart', { class: cn(icon, iconState.play.restart) })} + ${renderIcon('play', { class: cn(icon, iconState.play.play) })} + ${renderIcon('pause', { class: cn(icon, iconState.play.pause) })} + + + Replay + Play + Pause + + - + ${renderIcon('seek', { class: cn(icon, iconFlipped) })} ${SEEK_TIME} + + Seek backward ${SEEK_TIME} seconds + - + ${renderIcon('seek', { class: icon })} ${SEEK_TIME} + + Seek forward ${SEEK_TIME} seconds + @@ -76,8 +91,11 @@ function getTemplateHTML() { - + + + Toggle playback rate + ${renderIcon('volume-off', { class: cn(icon, iconState.mute.volumeOff) })} diff --git a/packages/html/src/define/audio/minimal-skin.ts b/packages/html/src/define/audio/minimal-skin.ts index 9ab891d0..0e13fc63 100644 --- a/packages/html/src/define/audio/minimal-skin.ts +++ b/packages/html/src/define/audio/minimal-skin.ts @@ -12,6 +12,7 @@ import '../ui/popover'; import '../ui/seek-button'; import '../ui/time'; import '../ui/time-slider'; +import '../ui/tooltip'; import '../ui/volume-slider'; const SEEK_TIME = 10; @@ -23,25 +24,36 @@ function getTemplateHTML() {
- + ${renderIcon('restart', { class: 'media-icon media-icon--restart' })} ${renderIcon('play', { class: 'media-icon media-icon--play' })} ${renderIcon('pause', { class: 'media-icon media-icon--pause' })} + + Replay + Play + Pause + - + ${renderIcon('seek', { class: 'media-icon media-icon--seek media-icon--flipped' })} ${SEEK_TIME} + + Seek backward ${SEEK_TIME} seconds + - + ${renderIcon('seek', { class: 'media-icon media-icon--seek' })} ${SEEK_TIME} + + Seek forward ${SEEK_TIME} seconds + @@ -61,8 +73,11 @@ function getTemplateHTML() { - + + + Toggle playback rate + ${renderIcon('volume-off', { class: 'media-icon media-icon--volume-off' })} @@ -70,7 +85,7 @@ function getTemplateHTML() { ${renderIcon('volume-high', { class: 'media-icon media-icon--volume-high' })} - + diff --git a/packages/html/src/define/audio/skin.tailwind.ts b/packages/html/src/define/audio/skin.tailwind.ts index ab517861..16244124 100644 --- a/packages/html/src/define/audio/skin.tailwind.ts +++ b/packages/html/src/define/audio/skin.tailwind.ts @@ -13,6 +13,7 @@ import { seek, slider, time, + tooltipState, } from '@videojs/skins/default/tailwind/audio.tailwind'; import { cn } from '@videojs/utils/style'; import { SkinMixin } from '../skin-mixin'; @@ -26,6 +27,7 @@ import '../ui/popover'; import '../ui/seek-button'; import '../ui/time'; import '../ui/time-slider'; +import '../ui/tooltip'; import '../ui/volume-slider'; const SEEK_TIME = 10; @@ -36,25 +38,38 @@ function getTemplateHTML() {
- - ${renderIcon('restart', { class: cn(icon, iconState.play.restart) })} - ${renderIcon('play', { class: cn(icon, iconState.play.play) })} - ${renderIcon('pause', { class: cn(icon, iconState.play.pause) })} - + + + ${renderIcon('restart', { class: cn(icon, iconState.play.restart) })} + ${renderIcon('play', { class: cn(icon, iconState.play.play) })} + ${renderIcon('pause', { class: cn(icon, iconState.play.pause) })} + + + Replay + Play + Pause + + - + ${renderIcon('seek', { class: cn(icon, iconFlipped) })} ${SEEK_TIME} + + Seek backward ${SEEK_TIME} seconds + - + ${renderIcon('seek', { class: icon })} ${SEEK_TIME} + + Seek forward ${SEEK_TIME} seconds + @@ -68,8 +83,10 @@ function getTemplateHTML() { - - + + + Toggle playback rate + ${renderIcon('volume-off', { class: cn(icon, iconState.mute.volumeOff) })} @@ -77,7 +94,7 @@ function getTemplateHTML() { ${renderIcon('volume-high', { class: cn(icon, iconState.mute.volumeHigh) })} - + diff --git a/packages/html/src/define/audio/skin.ts b/packages/html/src/define/audio/skin.ts index 7e86fab3..19310086 100644 --- a/packages/html/src/define/audio/skin.ts +++ b/packages/html/src/define/audio/skin.ts @@ -12,6 +12,7 @@ import '../ui/popover'; import '../ui/seek-button'; import '../ui/time'; import '../ui/time-slider'; +import '../ui/tooltip'; import '../ui/volume-slider'; const SEEK_TIME = 10; @@ -22,25 +23,36 @@ function getTemplateHTML() {
- + ${renderIcon('restart', { class: 'media-icon media-icon--restart' })} ${renderIcon('play', { class: 'media-icon media-icon--play' })} ${renderIcon('pause', { class: 'media-icon media-icon--pause' })} + + Replay + Play + Pause + - + ${renderIcon('seek', { class: 'media-icon media-icon--seek media-icon--flipped' })} ${SEEK_TIME} + + Seek backward ${SEEK_TIME} seconds + - + ${renderIcon('seek', { class: 'media-icon media-icon--seek' })} ${SEEK_TIME} + + Seek forward ${SEEK_TIME} seconds + @@ -54,8 +66,10 @@ function getTemplateHTML() { - - + + + Toggle playback rate + ${renderIcon('volume-off', { class: 'media-icon media-icon--volume-off' })} @@ -63,7 +77,7 @@ function getTemplateHTML() { ${renderIcon('volume-high', { class: 'media-icon media-icon--volume-high' })} - + diff --git a/packages/html/src/define/video/minimal-skin.tailwind.ts b/packages/html/src/define/video/minimal-skin.tailwind.ts index 0725df76..84e95f31 100644 --- a/packages/html/src/define/video/minimal-skin.tailwind.ts +++ b/packages/html/src/define/video/minimal-skin.tailwind.ts @@ -16,6 +16,7 @@ import { seek, slider, time, + tooltipState, } from '@videojs/skins/minimal/tailwind/video.tailwind'; import { cn } from '@videojs/utils/style'; import { SkinMixin } from '../skin-mixin'; @@ -34,6 +35,7 @@ import '../ui/popover'; import '../ui/seek-button'; import '../ui/time'; import '../ui/time-slider'; +import '../ui/tooltip'; import '../ui/volume-slider'; import { playbackRate } from '@videojs/skins/default/tailwind/video.tailwind'; @@ -62,25 +64,38 @@ function getTemplateHTML() { - - ${renderIcon('restart', { class: cn(icon, iconState.play.restart) })} - ${renderIcon('play', { class: cn(icon, iconState.play.play) })} - ${renderIcon('pause', { class: cn(icon, iconState.play.pause) })} - + + + ${renderIcon('restart', { class: cn(icon, iconState.play.restart) })} + ${renderIcon('play', { class: cn(icon, iconState.play.play) })} + ${renderIcon('pause', { class: cn(icon, iconState.play.pause) })} + + + Replay + Play + Pause + + - + ${renderIcon('seek', { class: cn(icon, iconFlipped) })} ${SEEK_TIME} + + Seek backward ${SEEK_TIME} seconds + - + ${renderIcon('seek', { class: icon })} ${SEEK_TIME} + + Seek forward ${SEEK_TIME} seconds + @@ -100,8 +115,11 @@ function getTemplateHTML() { - + + + Toggle playback rate + ${renderIcon('volume-off', { class: cn(icon, iconState.mute.volumeOff) })} @@ -118,19 +136,37 @@ function getTemplateHTML() { - - ${renderIcon('captions-off', { class: cn(icon, iconState.captions.off) })} - ${renderIcon('captions-on', { class: cn(icon, iconState.captions.on) })} - + + + ${renderIcon('captions-off', { class: cn(icon, iconState.captions.off) })} + ${renderIcon('captions-on', { class: cn(icon, iconState.captions.on) })} + + + Enable captions + Disable captions + + - - ${renderIcon('pip', { class: icon })} - + + + ${renderIcon('pip', { class: icon })} + + + Enter picture-in-picture + Exit picture-in-picture + + - - ${renderIcon('fullscreen-enter', { class: cn(icon, iconState.fullscreen.enter) })} - ${renderIcon('fullscreen-exit', { class: cn(icon, iconState.fullscreen.exit) })} - + + + ${renderIcon('fullscreen-enter', { class: cn(icon, iconState.fullscreen.enter) })} + ${renderIcon('fullscreen-exit', { class: cn(icon, iconState.fullscreen.exit) })} + + + Enter fullscreen + Exit fullscreen + + diff --git a/packages/html/src/define/video/minimal-skin.ts b/packages/html/src/define/video/minimal-skin.ts index 54f0f6c4..c7bc1bee 100644 --- a/packages/html/src/define/video/minimal-skin.ts +++ b/packages/html/src/define/video/minimal-skin.ts @@ -16,6 +16,7 @@ import '../ui/popover'; import '../ui/seek-button'; import '../ui/time'; import '../ui/time-slider'; +import '../ui/tooltip'; import '../ui/volume-slider'; const SEEK_TIME = 10; @@ -43,25 +44,36 @@ function getTemplateHTML() { - + ${renderIcon('restart', { class: 'media-icon media-icon--restart' })} ${renderIcon('play', { class: 'media-icon media-icon--play' })} ${renderIcon('pause', { class: 'media-icon media-icon--pause' })} + + Replay + Play + Pause + - + ${renderIcon('seek', { class: 'media-icon media-icon--flipped' })} ${SEEK_TIME} + + Seek backward ${SEEK_TIME} seconds + - + ${renderIcon('seek', { class: 'media-icon' })} ${SEEK_TIME} + + Seek forward ${SEEK_TIME} seconds + @@ -81,8 +93,10 @@ function getTemplateHTML() { - - + + + Toggle playback rate + ${renderIcon('volume-off', { class: 'media-icon media-icon--volume-off' })} @@ -90,7 +104,7 @@ function getTemplateHTML() { ${renderIcon('volume-high', { class: 'media-icon media-icon--volume-high' })} - + @@ -99,19 +113,30 @@ function getTemplateHTML() { - + + + Toggle captions + - + ${renderIcon('pip', { class: 'media-icon' })} + + Enter picture-in-picture + Exit picture-in-picture + - + ${renderIcon('fullscreen-enter', { class: 'media-icon media-icon--fullscreen-enter' })} ${renderIcon('fullscreen-exit', { class: 'media-icon media-icon--fullscreen-exit' })} + + Enter fullscreen + Exit fullscreen + diff --git a/packages/html/src/define/video/skin.tailwind.ts b/packages/html/src/define/video/skin.tailwind.ts index 99315627..4837bd93 100644 --- a/packages/html/src/define/video/skin.tailwind.ts +++ b/packages/html/src/define/video/skin.tailwind.ts @@ -16,6 +16,7 @@ import { seek, slider, time, + tooltipState, } from '@videojs/skins/default/tailwind/video.tailwind'; import { cn } from '@videojs/utils/style'; import { SkinMixin } from '../skin-mixin'; @@ -34,6 +35,7 @@ import '../ui/popover'; import '../ui/seek-button'; import '../ui/time'; import '../ui/time-slider'; +import '../ui/tooltip'; import '../ui/volume-slider'; const SEEK_TIME = 10; @@ -62,25 +64,38 @@ function getTemplateHTML() {
--> - - ${renderIcon('restart', { class: cn(icon, iconState.play.restart) })} - ${renderIcon('play', { class: cn(icon, iconState.play.play) })} - ${renderIcon('pause', { class: cn(icon, iconState.play.pause) })} - + + + ${renderIcon('restart', { class: cn(icon, iconState.play.restart) })} + ${renderIcon('play', { class: cn(icon, iconState.play.play) })} + ${renderIcon('pause', { class: cn(icon, iconState.play.pause) })} + + + Replay + Play + Pause + + - + ${renderIcon('seek', { class: cn(icon, iconFlipped) })} ${SEEK_TIME} + + Seek backward ${SEEK_TIME} seconds + - + ${renderIcon('seek', { class: icon })} ${SEEK_TIME} + + Seek forward ${SEEK_TIME} seconds + @@ -94,8 +109,10 @@ function getTemplateHTML() { - - + + + Toggle playback rate + ${renderIcon('volume-off', { class: cn(icon, iconState.mute.volumeOff) })} @@ -103,7 +120,7 @@ function getTemplateHTML() { ${renderIcon('volume-high', { class: cn(icon, iconState.mute.volumeHigh) })} - + @@ -112,19 +129,37 @@ function getTemplateHTML() { - - ${renderIcon('captions-off', { class: cn(icon, iconState.captions.off) })} - ${renderIcon('captions-on', { class: cn(icon, iconState.captions.on) })} - + + + ${renderIcon('captions-off', { class: cn(icon, iconState.captions.off) })} + ${renderIcon('captions-on', { class: cn(icon, iconState.captions.on) })} + + + Enable captions + Disable captions + + - - ${renderIcon('pip', { class: icon })} - + + + ${renderIcon('pip', { class: icon })} + + + Enter picture-in-picture + Exit picture-in-picture + + - - ${renderIcon('fullscreen-enter', { class: cn(icon, iconState.fullscreen.enter) })} - ${renderIcon('fullscreen-exit', { class: cn(icon, iconState.fullscreen.exit) })} - + + + ${renderIcon('fullscreen-enter', { class: cn(icon, iconState.fullscreen.enter) })} + ${renderIcon('fullscreen-exit', { class: cn(icon, iconState.fullscreen.exit) })} + + + Enter fullscreen + Exit fullscreen + +
diff --git a/packages/html/src/define/video/skin.ts b/packages/html/src/define/video/skin.ts index 8220a0c8..30612022 100644 --- a/packages/html/src/define/video/skin.ts +++ b/packages/html/src/define/video/skin.ts @@ -17,6 +17,7 @@ import '../ui/popover'; import '../ui/seek-button'; import '../ui/time'; import '../ui/time-slider'; +import '../ui/tooltip'; import '../ui/volume-slider'; const SEEK_TIME = 10; @@ -45,25 +46,36 @@ function getTemplateHTML() {
--> - + ${renderIcon('restart', { class: 'media-icon media-icon--restart' })} ${renderIcon('play', { class: 'media-icon media-icon--play' })} ${renderIcon('pause', { class: 'media-icon media-icon--pause' })} + + Replay + Play + Pause + - + ${renderIcon('seek', { class: 'media-icon media-icon--flipped' })} ${SEEK_TIME} + + Seek backward ${SEEK_TIME} seconds + - + ${renderIcon('seek', { class: 'media-icon' })} ${SEEK_TIME} + + Seek forward ${SEEK_TIME} seconds + @@ -77,8 +89,10 @@ function getTemplateHTML() { - - + + + Toggle playback rate + ${renderIcon('volume-off', { class: 'media-icon media-icon--volume-off' })} @@ -86,7 +100,7 @@ function getTemplateHTML() { ${renderIcon('volume-high', { class: 'media-icon media-icon--volume-high' })} - + @@ -95,19 +109,31 @@ function getTemplateHTML() { - + ${renderIcon('captions-off', { class: 'media-icon media-icon--captions-off' })} ${renderIcon('captions-on', { class: 'media-icon media-icon--captions-on' })} + + Enable captions + Disable captions + - + ${renderIcon('pip', { class: 'media-icon' })} + + Enter picture-in-picture + Exit picture-in-picture + - + ${renderIcon('fullscreen-enter', { class: 'media-icon media-icon--fullscreen-enter' })} ${renderIcon('fullscreen-exit', { class: 'media-icon media-icon--fullscreen-exit' })} + + Enter fullscreen + Exit fullscreen +
diff --git a/packages/html/src/ui/tooltip/tooltip-element.ts b/packages/html/src/ui/tooltip/tooltip-element.ts index e097bbb1..211cc609 100644 --- a/packages/html/src/ui/tooltip/tooltip-element.ts +++ b/packages/html/src/ui/tooltip/tooltip-element.ts @@ -13,7 +13,7 @@ import { import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element'; import { ContextConsumer } from '@videojs/element/context'; import { SnapshotController } from '@videojs/store/html'; -import { applyStyles, supportsAnchorPositioning } from '@videojs/utils/dom'; +import { applyStyles, supportsAnchorPositioning, tryHidePopover, tryShowPopover } from '@videojs/utils/dom'; import { MediaElement } from '../media-element'; import { tooltipGroupContext } from './context'; @@ -136,6 +136,14 @@ export class TooltipElement extends MediaElement { applyElementProps(this, this.#core.getPopupAttrs(state)); applyStateDataAttrs(this, state, TooltipDataAttrs); + // Show/hide via Popover API AFTER data attributes are applied so + // `data-starting-style` is present before the first visible frame. + if (state.open) { + tryShowPopover(this); + } else { + tryHidePopover(this); + } + // Apply trigger ARIA and anchor-name to the discovered trigger. if (this.#currentTrigger) { applyElementProps(this.#currentTrigger, this.#core.getTriggerAttrs(state, this.id)); diff --git a/packages/react/src/presets/audio/minimal-skin.tailwind.tsx b/packages/react/src/presets/audio/minimal-skin.tailwind.tsx index 65b35e55..c6552d63 100644 --- a/packages/react/src/presets/audio/minimal-skin.tailwind.tsx +++ b/packages/react/src/presets/audio/minimal-skin.tailwind.tsx @@ -24,7 +24,7 @@ import { } from '@videojs/skins/minimal/tailwind/audio.tailwind'; import { cn } from '@videojs/utils/style'; import { type ComponentProps, forwardRef, type ReactNode } from 'react'; -import { Container } from '@/player/context'; +import { Container, usePlayer } from '@/player/context'; import { MuteButton } from '@/ui/mute-button'; import { PlayButton } from '@/ui/play-button'; import { PlaybackRateButton } from '@/ui/playback-rate-button'; @@ -32,6 +32,7 @@ import { Popover } from '@/ui/popover'; import { SeekButton } from '@/ui/seek-button'; import { Time } from '@/ui/time'; import { TimeSlider } from '@/ui/time-slider'; +import { Tooltip } from '@/ui/tooltip'; import { VolumeSlider } from '@/ui/volume-slider'; import type { MinimalAudioSkinProps } from './minimal-skin'; @@ -90,6 +91,13 @@ const SliderThumb = forwardRef & { persist ); }); +function PlayLabel(): ReactNode { + const paused = usePlayer((s) => Boolean(s.paused)); + const ended = usePlayer((s) => Boolean(s.ended)); + if (ended) return <>Replay; + return paused ? <>Play : <>Pause; +} + /* ------------------------------------------ Skin ------------------------------------------- */ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNode { @@ -101,39 +109,62 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod
- ( - - )} - /> + + ( + + )} + /> + } + /> + + + + - ( - - )} - /> + + ( + + )} + /> + } + /> + Seek backward {SEEK_TIME} seconds + - ( - - )} - /> + + ( + + )} + /> + } + /> + Seek forward {SEEK_TIME} seconds + @@ -153,9 +184,16 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod - - )} - /> + + ( + + )} + /> + } + /> + + + + - ( - - )} - /> + + ( + + )} + /> + } + /> + + Seek backward {SEEK_TIME} seconds + + - ( - - )} - /> + + ( + + )} + /> + } + /> + Seek forward {SEEK_TIME} seconds + @@ -89,9 +122,16 @@ export function MinimalAudioSkin(props: MinimalAudioSkinProps): ReactNode { - - )} - /> + + ( + + )} + /> + } + /> + + + + - ( - - )} - /> + + ( + + )} + /> + } + /> + Seek backward {SEEK_TIME} seconds + - ( - - )} - /> + + ( + + )} + /> + } + /> + Seek forward {SEEK_TIME} seconds + @@ -145,7 +176,16 @@ export function AudioSkinTailwind(props: AudioSkinProps): ReactNode { - - )} - /> + + ( + + )} + /> + } + /> + + + + - ( - - )} - /> + + ( + + )} + /> + } + /> + + Seek backward {SEEK_TIME} seconds + + - ( - - )} - /> + + ( + + )} + /> + } + /> + + Seek forward {SEEK_TIME} seconds + + @@ -82,9 +117,16 @@ export function AudioSkin(props: AudioSkinProps): ReactNode { - - )} - /> + + ( + + )} + /> + } + /> + + + + - ( - - )} - /> + + ( + + )} + /> + } + /> + Seek backward {SEEK_TIME} seconds + - ( - - )} - /> + + ( + + )} + /> + } + /> + Seek forward {SEEK_TIME} seconds + @@ -190,9 +236,16 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod - - )} - /> + + ( + + )} + /> + } + /> + + + + - ( - - )} - /> + + ( + + )} + /> + } + /> + + + + - ( - - )} - /> + + ( + + )} + /> + } + /> + + + + diff --git a/packages/react/src/presets/video/minimal-skin.tsx b/packages/react/src/presets/video/minimal-skin.tsx index 57abd46a..7d01964d 100644 --- a/packages/react/src/presets/video/minimal-skin.tsx +++ b/packages/react/src/presets/video/minimal-skin.tsx @@ -15,7 +15,7 @@ import { } from '@videojs/icons/react/minimal'; import { cn } from '@videojs/utils/style'; import { type ComponentProps, forwardRef, type ReactNode } from 'react'; -import { Container } from '@/player/context'; +import { Container, usePlayer } from '@/player/context'; import { BufferingIndicator } from '@/ui/buffering-indicator'; import { CaptionsButton } from '@/ui/captions-button'; import { Controls } from '@/ui/controls'; @@ -28,6 +28,7 @@ import { Popover } from '@/ui/popover'; import { SeekButton } from '@/ui/seek-button'; import { Time } from '@/ui/time'; import { TimeSlider } from '@/ui/time-slider'; +import { Tooltip } from '@/ui/tooltip'; import { VolumeSlider } from '@/ui/volume-slider'; import type { BaseSkinProps } from '../types'; import { ErrorDialog } from './error-dialog'; @@ -50,6 +51,28 @@ const errorClasses = { close: 'media-button', }; +function PlayLabel(): ReactNode { + const paused = usePlayer((s) => Boolean(s.paused)); + const ended = usePlayer((s) => Boolean(s.ended)); + if (ended) return <>Replay; + return paused ? <>Play : <>Pause; +} + +function CaptionsLabel(): ReactNode { + const active = usePlayer((s) => Boolean(s.subtitlesShowing)); + return active ? <>Disable captions : <>Enable captions; +} + +function PiPLabel(): ReactNode { + const pip = usePlayer((s) => Boolean(s.pip)); + return pip ? <>Exit picture-in-picture : <>Enter picture-in-picture; +} + +function FullscreenLabel(): ReactNode { + const fullscreen = usePlayer((s) => Boolean(s.fullscreen)); + return fullscreen ? <>Exit fullscreen : <>Enter fullscreen; +} + export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode { const { children, className, ...rest } = props; @@ -69,39 +92,64 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode { - ( - - )} - /> + + ( + + )} + /> + } + /> + + + + - ( - - )} - /> + + ( + + )} + /> + } + /> + + Seek backward {SEEK_TIME} seconds + + - ( - - )} - /> + + ( + + )} + /> + } + /> + Seek forward {SEEK_TIME} seconds + @@ -121,9 +169,16 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode { - - )} - /> + + ( + + )} + /> + } + /> + + + + - ( - - )} - /> + + ( + + )} + /> + } + /> + + + + - ( - - )} - /> + + ( + + )} + /> + } + /> + + + + diff --git a/packages/react/src/presets/video/skin.tailwind.tsx b/packages/react/src/presets/video/skin.tailwind.tsx index b57b8d84..00851950 100644 --- a/packages/react/src/presets/video/skin.tailwind.tsx +++ b/packages/react/src/presets/video/skin.tailwind.tsx @@ -32,7 +32,7 @@ import { } from '@videojs/skins/default/tailwind/video.tailwind'; import { cn } from '@videojs/utils/style'; import { type ComponentProps, forwardRef, type ReactNode } from 'react'; -import { Container } from '@/player/context'; +import { Container, usePlayer } from '@/player/context'; import { BufferingIndicator } from '@/ui/buffering-indicator'; import { CaptionsButton } from '@/ui/captions-button'; import { Controls } from '@/ui/controls'; @@ -45,6 +45,7 @@ import { Popover } from '@/ui/popover'; import { SeekButton } from '@/ui/seek-button'; import { Time } from '@/ui/time'; import { TimeSlider } from '@/ui/time-slider'; +import { Tooltip } from '@/ui/tooltip'; import { VolumeSlider } from '@/ui/volume-slider'; import { ErrorDialog } from './error-dialog'; import type { VideoSkinProps } from './skin'; @@ -114,6 +115,28 @@ const errorClasses = { close: cn(button.base, button.default), }; +function PlayLabel(): ReactNode { + const paused = usePlayer((s) => Boolean(s.paused)); + const ended = usePlayer((s) => Boolean(s.ended)); + if (ended) return <>Replay; + return paused ? <>Play : <>Pause; +} + +function CaptionsLabel(): ReactNode { + const active = usePlayer((s) => Boolean(s.subtitlesShowing)); + return active ? <>Disable captions : <>Enable captions; +} + +function PiPLabel(): ReactNode { + const pip = usePlayer((s) => Boolean(s.pip)); + return pip ? <>Exit picture-in-picture : <>Enter picture-in-picture; +} + +function FullscreenLabel(): ReactNode { + const fullscreen = usePlayer((s) => Boolean(s.fullscreen)); + return fullscreen ? <>Exit fullscreen : <>Enter fullscreen; +} + /* ------------------------------------------ Skin ------------------------------------------- */ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode { @@ -139,39 +162,62 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode { data-controls="" // Used as a hook for Tailwind has-[] styles className={controls} > - ( - - )} - /> + + ( + + )} + /> + } + /> + + + + - ( - - )} - /> + + ( + + )} + /> + } + /> + Seek backward {SEEK_TIME} seconds + - ( - - )} - /> + + ( + + )} + /> + } + /> + Seek forward {SEEK_TIME} seconds + @@ -185,7 +231,16 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode { - - )} - /> + + ( + + )} + /> + } + /> + + + + - ( - - )} - /> + + ( + + )} + /> + } + /> + + + + - ( - - )} - /> + + ( + + )} + /> + } + /> + + + + {/*
diff --git a/packages/react/src/presets/video/skin.tsx b/packages/react/src/presets/video/skin.tsx index 207372a7..f0b2331f 100644 --- a/packages/react/src/presets/video/skin.tsx +++ b/packages/react/src/presets/video/skin.tsx @@ -15,7 +15,7 @@ import { } from '@videojs/icons/react'; import { cn } from '@videojs/utils/style'; import { type ComponentProps, forwardRef, type ReactNode } from 'react'; -import { Container } from '@/player/context'; +import { Container, usePlayer } from '@/player/context'; import { BufferingIndicator } from '@/ui/buffering-indicator'; import { CaptionsButton } from '@/ui/captions-button'; import { Controls } from '@/ui/controls'; @@ -28,6 +28,7 @@ import { Popover } from '@/ui/popover'; import { SeekButton } from '@/ui/seek-button'; import { Time } from '@/ui/time'; import { TimeSlider } from '@/ui/time-slider'; +import { Tooltip } from '@/ui/tooltip'; import { VolumeSlider } from '@/ui/volume-slider'; import type { BaseSkinProps } from '../types'; import { ErrorDialog } from './error-dialog'; @@ -50,6 +51,28 @@ const errorClasses = { close: 'media-button', }; +function PlayLabel(): ReactNode { + const paused = usePlayer((s) => Boolean(s.paused)); + const ended = usePlayer((s) => Boolean(s.ended)); + if (ended) return <>Replay; + return paused ? <>Play : <>Pause; +} + +function CaptionsLabel(): ReactNode { + const active = usePlayer((s) => Boolean(s.subtitlesShowing)); + return active ? <>Disable captions : <>Enable captions; +} + +function PiPLabel(): ReactNode { + const pip = usePlayer((s) => Boolean(s.pip)); + return pip ? <>Exit picture-in-picture : <>Enter picture-in-picture; +} + +function FullscreenLabel(): ReactNode { + const fullscreen = usePlayer((s) => Boolean(s.fullscreen)); + return fullscreen ? <>Exit fullscreen : <>Enter fullscreen; +} + export function VideoSkin(props: VideoSkinProps): ReactNode { const { children, className, ...rest } = props; @@ -70,39 +93,66 @@ export function VideoSkin(props: VideoSkinProps): ReactNode { - ( - - )} - /> + + ( + + )} + /> + } + /> + + + + - ( - - )} - /> + + ( + + )} + /> + } + /> + + Seek backward {SEEK_TIME} seconds + + - ( - - )} - /> + + ( + + )} + /> + } + /> + + Seek forward {SEEK_TIME} seconds + + @@ -116,9 +166,16 @@ export function VideoSkin(props: VideoSkinProps): ReactNode { - - )} - /> + + ( + + )} + /> + } + /> + + + + - ( - - )} - /> + + ( + + )} + /> + } + /> + + + + - ( - - )} - /> + + ( + + )} + /> + } + /> + + + + {/*
diff --git a/packages/skins/src/default/css/audio.css b/packages/skins/src/default/css/audio.css index 1b0fb2a8..a8c2ef06 100644 --- a/packages/skins/src/default/css/audio.css +++ b/packages/skins/src/default/css/audio.css @@ -1,4 +1,5 @@ @import "../../shared/audio-icon-state.css"; +@import "../../shared/tooltip-state.css"; @import "./components/reset.css"; @import "./components/root.css"; @import "./components/surface.css"; @@ -10,7 +11,6 @@ @import "./components/icons.css"; @import "./components/slider.css"; @import "./components/popup.css"; -@import "./components/tooltip.css"; /* ========================================================================== Root diff --git a/packages/skins/src/default/css/components/popup.css b/packages/skins/src/default/css/components/popup.css index 39caaa45..7409d1c9 100644 --- a/packages/skins/src/default/css/components/popup.css +++ b/packages/skins/src/default/css/components/popup.css @@ -1,8 +1,12 @@ /* ========================================================================== - Popups & Animations + Popups & Tooltips ========================================================================== */ -.media-default-skin .media-popup-animation { +.media-default-skin .media-popup { + margin: 0; + border: 0; + color: inherit; + overflow: visible; transition-property: transform, scale, opacity, filter; transition-duration: 200ms; @@ -31,12 +35,18 @@ } } -.media-default-skin .media-popup { - margin: 0; - border: 0; - color: inherit; +.media-default-skin .media-popup--popover { --media-popover-side-offset: 0.5rem; } + +.media-default-skin .media-popup--tooltip { + padding: 0.25rem 0.625rem; + border-radius: calc(infinity * 1px); + font-size: 0.75rem; + white-space: nowrap; + --media-tooltip-side-offset: 0.5rem; +} + .media-default-skin .media-popup--volume { padding: 0.625rem 0.25rem; border-radius: calc(infinity * 1px); diff --git a/packages/skins/src/default/css/components/tooltip.css b/packages/skins/src/default/css/components/tooltip.css deleted file mode 100644 index ee091f4b..00000000 --- a/packages/skins/src/default/css/components/tooltip.css +++ /dev/null @@ -1,13 +0,0 @@ -/* ========================================================================== - Tooltips - ========================================================================== */ - -.media-default-skin .media-tooltip { - white-space: nowrap; -} -.media-default-skin .media-tooltip-popup { - padding: 0.25rem 0.625rem; - border-radius: calc(infinity * 1px); - color: oklch(1 0 0); - font-size: 0.75rem; -} diff --git a/packages/skins/src/default/css/video.css b/packages/skins/src/default/css/video.css index 061e95c0..b86fd9f9 100644 --- a/packages/skins/src/default/css/video.css +++ b/packages/skins/src/default/css/video.css @@ -1,4 +1,5 @@ @import "../../shared/video-icon-state.css"; +@import "../../shared/tooltip-state.css"; @import "./components/reset.css"; @import "./components/root.css"; @import "./components/surface.css"; @@ -12,7 +13,6 @@ @import "./components/icons.css"; @import "./components/slider.css"; @import "./components/popup.css"; -@import "./components/tooltip.css"; @import "./components/captions.css"; /* ========================================================================== diff --git a/packages/skins/src/default/tailwind/audio.tailwind.ts b/packages/skins/src/default/tailwind/audio.tailwind.ts index f62e53b6..a0ec165a 100644 --- a/packages/skins/src/default/tailwind/audio.tailwind.ts +++ b/packages/skins/src/default/tailwind/audio.tailwind.ts @@ -57,6 +57,19 @@ export const iconState = { }, }; +/* ========================================================================== + Tooltip State (audio: play only) + ========================================================================== */ + +export const tooltipState = { + play: { + wrapper: 'contents group/play-tip', + replay: 'hidden group-has-data-ended/play-tip:block', + play: 'hidden group-[:has([data-paused]):not(:has([data-ended]))]/play-tip:block', + pause: 'hidden group-[:not(:has([data-paused])):not(:has([data-ended]))]/play-tip:block', + }, +}; + /* ========================================================================== Popup (with audio surface) ========================================================================== */ diff --git a/packages/skins/src/default/tailwind/components/popup.ts b/packages/skins/src/default/tailwind/components/popup.ts index 7192b50c..72e030fe 100644 --- a/packages/skins/src/default/tailwind/components/popup.ts +++ b/packages/skins/src/default/tailwind/components/popup.ts @@ -3,9 +3,7 @@ import { cn } from '@videojs/utils/style'; export const popup = { base: cn( // Reset default popover styles - 'm-0 border-0 text-inherit', - // Default offset - '[--media-popover-side-offset:0.5rem]', + 'm-0 border-0 text-inherit overflow-visible', // Animation 'transition-[transform,scale,opacity,filter] duration-200', 'data-starting-style:opacity-0 data-starting-style:scale-0 data-starting-style:blur-sm', @@ -14,5 +12,7 @@ export const popup = { // Ensure we animate from the correct origin based on the side the popover is on 'data-[side=top]:origin-bottom data-[side=bottom]:origin-top data-[side=left]:origin-right data-[side=right]:origin-left' ), + popover: '[--media-popover-side-offset:0.5rem]', + tooltip: cn('py-1 px-2.5 rounded-full text-[0.75rem] whitespace-nowrap', '[--media-tooltip-side-offset:0.5rem]'), volume: 'py-2.5 px-1 rounded-full', }; diff --git a/packages/skins/src/default/tailwind/components/tooltip-state.ts b/packages/skins/src/default/tailwind/components/tooltip-state.ts new file mode 100644 index 00000000..463d2513 --- /dev/null +++ b/packages/skins/src/default/tailwind/components/tooltip-state.ts @@ -0,0 +1,23 @@ +export const tooltipState = { + play: { + wrapper: 'contents group/play-tip', + replay: 'hidden group-has-data-ended/play-tip:block', + play: 'hidden group-[:has([data-paused]):not(:has([data-ended]))]/play-tip:block', + pause: 'hidden group-[:not(:has([data-paused])):not(:has([data-ended]))]/play-tip:block', + }, + fullscreen: { + wrapper: 'contents group/fs-tip', + enter: 'hidden group-[:not(:has([data-fullscreen]))]/fs-tip:block', + exit: 'hidden group-has-data-fullscreen/fs-tip:block', + }, + captions: { + wrapper: 'contents group/cc-tip', + enable: 'hidden group-[:not(:has([data-active]))]/cc-tip:block', + disable: 'hidden group-has-data-active/cc-tip:block', + }, + pip: { + wrapper: 'contents group/pip-tip', + enter: 'hidden group-[:not(:has([data-pip]))]/pip-tip:block', + exit: 'hidden group-has-data-pip/pip-tip:block', + }, +}; diff --git a/packages/skins/src/default/tailwind/video.tailwind.ts b/packages/skins/src/default/tailwind/video.tailwind.ts index 1a267094..f72f8a7e 100644 --- a/packages/skins/src/default/tailwind/video.tailwind.ts +++ b/packages/skins/src/default/tailwind/video.tailwind.ts @@ -142,3 +142,4 @@ export { overlay } from './components/overlay'; export { playbackRate } from './components/playback-rate'; export { seek } from './components/seek'; export { time } from './components/time'; +export { tooltipState } from './components/tooltip-state'; diff --git a/packages/skins/src/minimal/css/audio.css b/packages/skins/src/minimal/css/audio.css index a323fc81..bf41d820 100644 --- a/packages/skins/src/minimal/css/audio.css +++ b/packages/skins/src/minimal/css/audio.css @@ -1,4 +1,5 @@ @import "../../shared/audio-icon-state.css"; +@import "../../shared/tooltip-state.css"; @import "./components/reset.css"; @import "./components/root.css"; @import "./components/buffering.css"; @@ -9,7 +10,6 @@ @import "./components/icons.css"; @import "./components/slider.css"; @import "./components/popup.css"; -@import "./components/tooltip.css"; /* ========================================================================== Controls diff --git a/packages/skins/src/minimal/css/components/popup.css b/packages/skins/src/minimal/css/components/popup.css index 1109616a..dfd21e8d 100644 --- a/packages/skins/src/minimal/css/components/popup.css +++ b/packages/skins/src/minimal/css/components/popup.css @@ -2,7 +2,11 @@ Popups & Animations ========================================================================== */ -.media-minimal-skin .media-popup-animation { +.media-minimal-skin .media-popup { + margin: 0; + border: 0; + color: inherit; + overflow: visible; transition-property: transform, scale, opacity, filter; transition-duration: 200ms; @@ -31,8 +35,23 @@ } } -.media-minimal-skin .media-popup { - margin: 0; - border: 0; - color: inherit; +.media-minimal-skin .media-popup--tooltip { + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + background-color: oklch(1 0 0 / 0.1); + backdrop-filter: blur(64px) brightness(0.9) saturate(1.5); + box-shadow: + 0 4px 6px -1px oklch(0 0 0 / 0.1), + 0 2px 4px -2px oklch(0 0 0 / 0.1); + font-size: 0.75rem; + white-space: nowrap; + --media-tooltip-side-offset: 0.5rem; + + @media (prefers-reduced-transparency: reduce) { + background-color: oklch(0 0 0 / 0.7); + } + + @media (prefers-contrast: more) { + background-color: oklch(0 0 0 / 0.9); + } } diff --git a/packages/skins/src/minimal/css/components/tooltip.css b/packages/skins/src/minimal/css/components/tooltip.css deleted file mode 100644 index 959efc72..00000000 --- a/packages/skins/src/minimal/css/components/tooltip.css +++ /dev/null @@ -1,27 +0,0 @@ -/* ========================================================================== - Tooltips - ========================================================================== */ - -.media-minimal-skin .media-tooltip { - white-space: nowrap; -} - -.media-minimal-skin .media-tooltip-popup { - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; - background-color: oklch(1 0 0 / 0.1); - backdrop-filter: blur(64px) brightness(0.9) saturate(1.5); - box-shadow: - 0 4px 6px -1px oklch(0 0 0 / 0.1), - 0 2px 4px -2px oklch(0 0 0 / 0.1); - color: oklch(1 0 0); - font-size: 0.75rem; - - @media (prefers-reduced-transparency: reduce) { - background-color: oklch(0 0 0 / 0.7); - } - - @media (prefers-contrast: more) { - background-color: oklch(0 0 0 / 0.9); - } -} diff --git a/packages/skins/src/minimal/css/video.css b/packages/skins/src/minimal/css/video.css index f5cd2539..7599a723 100644 --- a/packages/skins/src/minimal/css/video.css +++ b/packages/skins/src/minimal/css/video.css @@ -1,4 +1,5 @@ @import "../../shared/video-icon-state.css"; +@import "../../shared/tooltip-state.css"; @import "./components/reset.css"; @import "./components/root.css"; @import "./components/media.css"; @@ -11,7 +12,6 @@ @import "./components/icons.css"; @import "./components/slider.css"; @import "./components/popup.css"; -@import "./components/tooltip.css"; @import "./components/captions.css"; /* ========================================================================== diff --git a/packages/skins/src/minimal/tailwind/audio.tailwind.ts b/packages/skins/src/minimal/tailwind/audio.tailwind.ts index 7b7e9285..185457fc 100644 --- a/packages/skins/src/minimal/tailwind/audio.tailwind.ts +++ b/packages/skins/src/minimal/tailwind/audio.tailwind.ts @@ -35,6 +35,19 @@ export const controls = cn( 'ring-1 ring-(color:--media-controls-border-color)' ); +/* ========================================================================== + Tooltip State (audio: play only) + ========================================================================== */ + +export const tooltipState = { + play: { + wrapper: 'contents group/play-tip', + replay: 'hidden group-has-data-ended/play-tip:block', + play: 'hidden group-[:has([data-paused]):not(:has([data-ended]))]/play-tip:block', + pause: 'hidden group-[:not(:has([data-paused])):not(:has([data-ended]))]/play-tip:block', + }, +}; + /* ========================================================================== Popup ========================================================================== */ diff --git a/packages/skins/src/minimal/tailwind/components/popup.ts b/packages/skins/src/minimal/tailwind/components/popup.ts index 67bb7f00..cb455de7 100644 --- a/packages/skins/src/minimal/tailwind/components/popup.ts +++ b/packages/skins/src/minimal/tailwind/components/popup.ts @@ -3,7 +3,7 @@ import { cn } from '@videojs/utils/style'; export const popup = { base: cn( // Reset default popover styles - 'm-0 border-0 text-inherit', + 'm-0 border-0 text-inherit overflow-visible', // Animation 'transition-[transform,scale,opacity,filter] duration-200', 'data-starting-style:opacity-0 data-starting-style:scale-0 data-starting-style:blur-sm', @@ -12,4 +12,9 @@ export const popup = { // Ensure we animate from the correct origin based on the side the popover is on 'data-[side=top]:origin-bottom data-[side=bottom]:origin-top data-[side=left]:origin-right data-[side=right]:origin-left' ), + tooltip: cn( + 'px-2 py-1 rounded-sm shadow-md shadow-black/10 bg-white/10 backdrop-blur-3xl backdrop-saturate-150 backdrop-brightness-90 text-[0.75rem] whitespace-nowrap', + '[--media-tooltip-side-offset:0.5rem]' + ), + volume: 'py-2.5 px-1 rounded-full', }; diff --git a/packages/skins/src/minimal/tailwind/components/tooltip-state.ts b/packages/skins/src/minimal/tailwind/components/tooltip-state.ts new file mode 100644 index 00000000..463d2513 --- /dev/null +++ b/packages/skins/src/minimal/tailwind/components/tooltip-state.ts @@ -0,0 +1,23 @@ +export const tooltipState = { + play: { + wrapper: 'contents group/play-tip', + replay: 'hidden group-has-data-ended/play-tip:block', + play: 'hidden group-[:has([data-paused]):not(:has([data-ended]))]/play-tip:block', + pause: 'hidden group-[:not(:has([data-paused])):not(:has([data-ended]))]/play-tip:block', + }, + fullscreen: { + wrapper: 'contents group/fs-tip', + enter: 'hidden group-[:not(:has([data-fullscreen]))]/fs-tip:block', + exit: 'hidden group-has-data-fullscreen/fs-tip:block', + }, + captions: { + wrapper: 'contents group/cc-tip', + enable: 'hidden group-[:not(:has([data-active]))]/cc-tip:block', + disable: 'hidden group-has-data-active/cc-tip:block', + }, + pip: { + wrapper: 'contents group/pip-tip', + enter: 'hidden group-[:not(:has([data-pip]))]/pip-tip:block', + exit: 'hidden group-has-data-pip/pip-tip:block', + }, +}; diff --git a/packages/skins/src/minimal/tailwind/video.tailwind.ts b/packages/skins/src/minimal/tailwind/video.tailwind.ts index 9e7dca02..25ac610b 100644 --- a/packages/skins/src/minimal/tailwind/video.tailwind.ts +++ b/packages/skins/src/minimal/tailwind/video.tailwind.ts @@ -109,3 +109,4 @@ export { iconState } from './components/icon-state'; export { overlay } from './components/overlay'; export { seek } from './components/seek'; export { time } from './components/time'; +export { tooltipState } from './components/tooltip-state'; diff --git a/packages/skins/src/shared/tooltip-state.css b/packages/skins/src/shared/tooltip-state.css new file mode 100644 index 00000000..1152d415 --- /dev/null +++ b/packages/skins/src/shared/tooltip-state.css @@ -0,0 +1,43 @@ +/* ========================================================================== + Tooltip Label State Visibility + + Data-attribute-driven visibility rules for multi-state tooltip labels. + Uses adjacent sibling selectors to match button state → tooltip content. + ========================================================================== */ + +/* --- All multi-state labels hidden by default --- */ + +.media-tooltip-label { + display: none; +} + +/* --- Active label per state --- */ + +/* Play: ended → replay */ +.media-button--play[data-ended] + media-tooltip .media-tooltip-label--replay, +/* Play: paused (not ended) → play */ + .media-button--play:not([data-ended])[data-paused] + media-tooltip + .media-tooltip-label--play, +/* Play: playing (not paused, not ended) → pause */ + .media-button--play:not([data-paused]):not([data-ended]) + media-tooltip + .media-tooltip-label--pause, +/* Fullscreen: not fullscreen → enter */ + .media-button--fullscreen:not([data-fullscreen]) + media-tooltip + .media-tooltip-label--enter-fullscreen, +/* Fullscreen: fullscreen → exit */ + .media-button--fullscreen[data-fullscreen] + media-tooltip + .media-tooltip-label--exit-fullscreen, +/* Captions: not active → enable */ + .media-button--captions:not([data-active]) + media-tooltip + .media-tooltip-label--enable-captions, +/* Captions: active → disable */ + .media-button--captions[data-active] + media-tooltip + .media-tooltip-label--disable-captions, +/* PiP: not in pip → enter */ + media-pip-button:not([data-pip]) + media-tooltip + .media-tooltip-label--enter-pip, +/* PiP: in pip → exit */ + media-pip-button[data-pip] + media-tooltip + .media-tooltip-label--exit-pip { + display: block; +}