diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..e2228113 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +22.19.0 diff --git a/cspell.json b/cspell.json index 105988ea..a83aeef3 100644 --- a/cspell.json +++ b/cspell.json @@ -14,8 +14,10 @@ ], "words": [ "antfu", + "noto", "nums", - "rahim" + "rahim", + "segoe" ], "ignoreWords": [], "import": [] diff --git a/examples/react-demo/index.html b/examples/react-demo/index.html index 7a6a9840..e6d535d5 100644 --- a/examples/react-demo/index.html +++ b/examples/react-demo/index.html @@ -6,7 +6,7 @@ - +
diff --git a/examples/react-demo/src/App.tsx b/examples/react-demo/src/App.tsx new file mode 100644 index 00000000..12b7542d --- /dev/null +++ b/examples/react-demo/src/App.tsx @@ -0,0 +1,149 @@ +import { MediaProvider, MediaSkinDefault, MediaSkinToasted, Video } from '@vjs-10/react'; + +import './globals.css'; +import { useCallback, useMemo, useState, type ChangeEventHandler } from 'react'; + +const skins = [{ + key: 'default', + name: 'Frosted', + component: MediaSkinDefault, +}, { + key: 'toasted', + name: 'Toasted', + component: MediaSkinToasted, +}] as const; + +type SkinKey = (typeof skins)[number]['key']; + +const mediaSources = [{ + key: '1', + name: 'Mux 1', + value: 'https://stream.mux.com/a4nOgmxGWg6gULfcBbAa00gXyfcwPnAFldF8RdsNyk8M.m3u8' +}, { + key: '2', + name: 'Mux 2', + value: 'https://stream.mux.com/fXNzVtmtWuyz00xnSrJg4OJH6PyNo6D02UzmgeKGkP5YQ.m3u8' +}, { + key: '3', + name: 'Mux 3', + value: 'https://stream.mux.com/A3VXy02VoUinw01pwyomEO3bHnG4P32xzV7u1j1FSzjNg/high.mp4' +}, { + key: '4', + name: 'Mux 4', + value: 'https://stream.mux.com/lyrKpPcGfqyzeI00jZAfW6MvP6GNPrkML.m3u8' +}] as const; + +type MediaSourceKey = (typeof mediaSources)[number]['key']; + +function getParam(key: string, defaultValue: T): T { + const params = new URLSearchParams(window.location.search); + return params.get(key) as T || defaultValue; +} +function setParam(key: string, value: string) { + const params = new URLSearchParams(window.location.search); + params.set(key, value); + const search = params.toString(); + const url = window.location.pathname + (search ? `?${search}` : ''); + window.history.replaceState(null, '', url); +} + +const DEFAULT_SKIN: SkinKey = 'toasted'; +const DEFAULT_MEDIA_SOURCE: MediaSourceKey = '1'; + +export default function App(): JSX.Element { + const [skinKey, setSkinKey] = useState(getParam('skin', DEFAULT_SKIN)); + const [mediaSourceKey, setMediaSourceKey] = useState(getParam('source', DEFAULT_MEDIA_SOURCE)); + + const mediaSource = useMemo(() => { + let match = mediaSources.find(m => m.key === mediaSourceKey); + if (!match) { + match = mediaSources.find(m => m.key === DEFAULT_MEDIA_SOURCE)!; + setMediaSourceKey(match.key); + } + return match.value; + }, [mediaSourceKey]); + + const Skin = useMemo(() => { + let match = skins.find(s => s.key === skinKey); + if (!match) { + match = skins.find(s => s.key === DEFAULT_SKIN)!; + setSkinKey(match.key); + } + return match.component; + }, [skinKey]); + + const onChangeSkin: ChangeEventHandler = useCallback((event) => { + const value = event.target.value as SkinKey; + setSkinKey(value); + setParam('skin', value); + }, []); + const onChangeMediaSource: ChangeEventHandler = useCallback((event) => { + const value = event.target.value as MediaSourceKey; + setMediaSourceKey(value); + setParam('source', value); + }, []); + + // Force a re-render on changes. + const key = `${skinKey}-${mediaSourceKey}`; + + const skinClassName = useMemo(() => { + switch (skinKey) { + case 'default': + return 'rounded-4xl shadow shadow-lg shadow-black/15'; + case 'toasted': + return 'rounded-lg shadow shadow-lg shadow-black/15'; + default: + return ''; + } + }, [skinKey]); + + return ( + <> +
+ + +
+
+

Playground

+ Test out the various skins for Video.js. +
+ + +
+
+ +
+
+ + + {/* @ts-ignore -- types are incorrect */} + + +
+
+ + ); +} + diff --git a/examples/react-demo/src/globals.css b/examples/react-demo/src/globals.css index 555d3400..ca5200df 100644 --- a/examples/react-demo/src/globals.css +++ b/examples/react-demo/src/globals.css @@ -4,18 +4,16 @@ @source "../../../packages/react"; @theme { - --font-sans: - InterVariable, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', - 'Noto Color Emoji'; + --font-sans: InterVariable, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', '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 hocus (hover + focus-visible) variants */ -@custom-variant hocus (&:is(:hover, :focus-visible)); -@custom-variant group-hocus (&:is(:hover, :focus-visible) &); -@custom-variant peer-hocus (&:is(:hover, :focus-visible) ~ &); /* Make reduced-transparency variant */ @custom-variant reduced-transparency @media (prefers-reduced-transparency: reduce); diff --git a/examples/react-demo/src/main.tsx b/examples/react-demo/src/main.tsx index 606c7ec7..30c39a5e 100644 --- a/examples/react-demo/src/main.tsx +++ b/examples/react-demo/src/main.tsx @@ -1,38 +1,10 @@ -import ReactDOM from 'react-dom/client'; - -import { MediaProvider, MediaSkinDefault, Video } from '@vjs-10/react'; - +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; import './globals.css'; -function DemoPlayer() { - return ( -
-
- -
- - {/* @ts-ignore -- types are incorrect */} - {/* -
-
- - -
- - {/* @ts-ignore -- types are incorrect */} - -
-
-
-
- ); -} - -ReactDOM.createRoot(document.getElementById('root')!).render(); +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/package.json b/package.json index 10bb8fe5..d1acafd5 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "eslint": "^9.36.0", "eslint-plugin-format": "^1.0.2", "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.21", "lint-staged": "^16.2.0", diff --git a/packages/react/react/src/skins/default/MediaSkinDefault.tsx b/packages/react/react/src/skins/default/MediaSkinDefault.tsx index 4e48c0ce..1a3ca851 100644 --- a/packages/react/react/src/skins/default/MediaSkinDefault.tsx +++ b/packages/react/react/src/skins/default/MediaSkinDefault.tsx @@ -46,12 +46,12 @@ export default function MediaSkinDefault({ children, className = '' }: SkinProps className={styles.TimeDisplay} /> - - - - + + + + - + @@ -66,12 +66,12 @@ export default function MediaSkinDefault({ children, className = '' }: SkinProps - - - - + + + + - + diff --git a/packages/react/react/src/skins/default/styles.ts b/packages/react/react/src/skins/default/styles.ts index 0450a52e..a94fb73a 100644 --- a/packages/react/react/src/skins/default/styles.ts +++ b/packages/react/react/src/skins/default/styles.ts @@ -1,43 +1,12 @@ -// A (very crude) utility to merge class names -// Usually I'd use something like `clsx` or `classnames` but this is ok for our simple use case. -// It just makes the billions of Tailwind classes a little easier to read. -const cn = (...classes: (string | undefined)[]): string => classes.filter(Boolean).join(' '); +import type { MediaDefaultSkinStyles } from "./types"; -export interface MediaDefaultSkinStyles { - readonly MediaContainer: string; - readonly Overlay: string; - readonly Controls: string; - readonly Button: string; - readonly IconButton: string; - readonly PlayButton: string; - readonly PlayIcon: string; - readonly PauseIcon: string; - readonly VolumeButton: string; - readonly VolumeHighIcon: string; - readonly VolumeLowIcon: string; - readonly VolumeOffIcon: string; - readonly FullScreenButton: string; - readonly FullScreenEnterIcon: string; - readonly FullScreenExitIcon: string; - readonly TimeControls: string; - readonly TimeDisplay: string; - readonly TimeRangeRoot: string; - readonly TimeRangeTrack: string; - readonly TimeRangeProgress: string; - readonly TimeRangePointer: string; - readonly TimeRangeThumb: string; - readonly VolumePopup: string; - readonly VolumeRangeRoot: string; - readonly VolumeRangeTrack: string; - readonly VolumeRangeProgress: string; - readonly VolumeRangeThumb: string; -} +import { cn } from "../../utils/cn"; const styles: MediaDefaultSkinStyles = { MediaContainer: cn( 'relative @container/root group/root overflow-clip', // Base typography - 'antialiased font-[510] font-sans text-[0.8125rem] @7xl/root:text-[0.9375rem] leading-normal tracking-[-0.0125em]', + 'text-sm', // Prevent rounded corners in fullscreen. '[&:fullscreen]:rounded-none [&:fullscreen]:[&_video]:h-full [&:fullscreen]:[&_video]:w-full', // Fancy borders. @@ -49,25 +18,26 @@ const styles: MediaDefaultSkinStyles = { Overlay: cn( 'opacity-0 delay-500 rounded-[inherit] absolute inset-0 pointer-events-none z-10 bg-gradient-to-t from-black/50 via-black/20 to-transparent transition-opacity backdrop-saturate-150 backdrop-brightness-90', // Hide when playing (for now). - // This is crude temporary logic, we’ll improve it later I guess with a [data-show-controls] attribute or something? + // FIXME: This is crude temporary logic, we’ll improve it later I guess with a [data-show-controls] attribute or something? 'has-[+.controls_[data-paused]]:opacity-100 has-[+.controls_[data-paused]]:delay-0', 'group-hover/root:opacity-100 group-hover/root:delay-0' ), Controls: cn( - 'controls', // Temporary className hook for above logic. Can be removed once have a proper selector as above. + 'controls', // FIXME: Temporary className hook for above logic in the overlay. Can be removed once have a proper way to handle controls visibility. '@container/controls absolute inset-x-3 bottom-3 rounded-full z-20 flex items-center p-1 ring ring-white/10 ring-inset gap-0.5 text-white text-shadow', 'shadow-sm shadow-black/15', // Background 'bg-white/10 backdrop-blur-3xl backdrop-saturate-150 backdrop-brightness-90', // Animation 'transition will-change-transform origin-bottom ease-out', - // Temporary hide/show logic + // FIXME: Temporary hide/show logic 'scale-90 opacity-0 delay-500', 'has-[[data-paused]]:scale-100 has-[[data-paused]]:opacity-100 has-[[data-paused]]:delay-0', 'group-hover/root:scale-100 group-hover/root:opacity-100 group-hover/root:delay-0', // Border to enhance contrast on lighter videos 'after:absolute after:inset-0 after:ring after:rounded-[inherit] after:ring-black/15 after:pointer-events-none after:z-10', // Reduced transparency for users with preference + // XXX: This requires a Tailwind custom variant (see 1 below) 'reduced-transparency:bg-black/70 reduced-transparency:ring-black reduced-transparency:after:ring-white/20', // High contrast mode 'contrast-more:bg-black/90 contrast-more:ring-black contrast-more:after:ring-white/20' @@ -77,7 +47,7 @@ const styles: MediaDefaultSkinStyles = { // Background/foreground 'bg-transparent text-white/90', // Hover and focus states - 'hocus:no-underline hocus:bg-white/10 hocus:text-white', + 'hover:no-underline hover:bg-white/10 hover:text-white focus-visible:no-underline focus-visible:bg-white/10 focus-visible:text-white', // Focus state '-outline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500', // Disabled state @@ -128,39 +98,42 @@ const styles: MediaDefaultSkinStyles = { ), TimeControls: cn('flex-1 flex items-center gap-3 px-1.5'), TimeDisplay: cn('tabular-nums text-shadow-2xs shadow-black/50'), - TimeRangeRoot: cn('flex [&[data-orientation="horizontal"]]:h-5 [&[data-orientation="vertical"]]:w-5 [&[data-orientation="vertical"]]:h-20 items-center justify-center flex-1 group/slider relative'), - TimeRangeTrack: cn('[&[data-orientation="horizontal"]]:h-1 [&[data-orientation="vertical"]]:w-1 w-full relative select-none rounded-full bg-white/20 ring-1 ring-black/5'), - TimeRangeProgress: cn('bg-white rounded-[inherit]'), + SliderRoot: cn( + 'flex items-center justify-center flex-1 group/slider relative', + '[&[data-orientation="horizontal"]]:h-5 [&[data-orientation="horizontal"]]:min-w-20', + '[&[data-orientation="vertical"]]:w-5 [&[data-orientation="vertical"]]:h-20', + ), + SliderTrack: cn( + 'w-full relative select-none rounded-full bg-white/20 ring-1 ring-black/5', + '[&[data-orientation="horizontal"]]:h-1', + '[&[data-orientation="vertical"]]:w-1' + ), + SliderProgress: cn('bg-white rounded-[inherit]'), // TODO: Work out what we want to do here. - TimeRangePointer: cn('rounded-[inherit]'), - TimeRangeThumb: cn( + SliderPointer: cn('rounded-[inherit]'), + SliderThumb: cn( 'bg-white z-10 select-none ring ring-black/10 rounded-full shadow-sm shadow-black/15', 'opacity-0 transition-[opacity,height,width] ease-in-out', '-outline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500', 'group-hover/slider:opacity-100 group-focus-within/slider:opacity-100', - 'size-2.5 active:size-3 group-active/slider:size-3' + 'size-2.5 active:size-3 group-active/slider:size-3 hover:cursor-ew-resize' ), - VolumePopup: cn( + PopoverPopup: cn( 'relative z-30 px-2 py-4 rounded-2xl', 'bg-white/10 backdrop-blur-3xl backdrop-saturate-150 backdrop-brightness-90', 'ring ring-white/10 ring-inset shadow-sm shadow-black/15', // Border to enhance contrast on lighter videos 'after:absolute after:inset-0 after:ring after:rounded-[inherit] after:ring-black/15 after:pointer-events-none after:z-10', // Reduced transparency for users with preference + // XXX: This requires a Tailwind custom variant (see 1 below) 'reduced-transparency:bg-black/70 reduced-transparency:ring-black reduced-transparency:after:ring-white/20', // High contrast mode 'contrast-more:bg-black/90 contrast-more:ring-black contrast-more:after:ring-white/20' ), - VolumeRangeRoot: cn('flex [&[data-orientation="horizontal"]]:w-20 [&[data-orientation="horizontal"]]:h-5 [&[data-orientation="vertical"]]:w-5 [&[data-orientation="vertical"]]:h-20 items-center justify-center group/slider relative'), - VolumeRangeTrack: cn('[&[data-orientation="horizontal"]]:h-1 [&[data-orientation="vertical"]]:w-1 w-full relative select-none rounded-full bg-white/20 ring-1 ring-black/5'), - VolumeRangeProgress: cn('bg-white rounded-[inherit]'), - VolumeRangeThumb: cn( - 'bg-white z-10 select-none ring ring-black/10 rounded-full shadow-sm shadow-black/15', - 'opacity-0 transition-[opacity,height,width] ease-in-out', - '-outline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500', - 'group-hover/slider:opacity-100 group-focus-within/slider:opacity-100', - 'size-2.5 active:size-3 group-active/slider:size-3' - ), }; +/* +[1] @custom-variant reduced-transparency @media (prefers-reduced-transparency: reduce); +*/ + export default styles; diff --git a/packages/react/react/src/skins/default/types.ts b/packages/react/react/src/skins/default/types.ts new file mode 100644 index 00000000..a58af1cf --- /dev/null +++ b/packages/react/react/src/skins/default/types.ts @@ -0,0 +1,25 @@ +export interface MediaDefaultSkinStyles { + readonly MediaContainer: string; + readonly Overlay: string; + readonly Controls: string; + readonly Button: string; + readonly IconButton: string; + readonly PlayButton: string; + readonly PlayIcon: string; + readonly PauseIcon: string; + readonly VolumeButton: string; + readonly VolumeHighIcon: string; + readonly VolumeLowIcon: string; + readonly VolumeOffIcon: string; + readonly FullScreenButton: string; + readonly FullScreenEnterIcon: string; + readonly FullScreenExitIcon: string; + readonly TimeControls: string; + readonly TimeDisplay: string; + readonly SliderRoot: string; + readonly SliderTrack: string; + readonly SliderProgress: string; + readonly SliderPointer: string; + readonly SliderThumb: string; + readonly PopoverPopup: string; +} \ No newline at end of file diff --git a/packages/react/react/src/skins/index.ts b/packages/react/react/src/skins/index.ts index 479a9ab2..bd5af525 100644 --- a/packages/react/react/src/skins/index.ts +++ b/packages/react/react/src/skins/index.ts @@ -1 +1,2 @@ -export * from './default' \ No newline at end of file +export * from './default' +export * from './toasted' \ No newline at end of file diff --git a/packages/react/react/src/skins/toasted/MediaSkinToasted.tsx b/packages/react/react/src/skins/toasted/MediaSkinToasted.tsx new file mode 100644 index 00000000..1ee90287 --- /dev/null +++ b/packages/react/react/src/skins/toasted/MediaSkinToasted.tsx @@ -0,0 +1,94 @@ +import type { PropsWithChildren } from 'react'; + +import { + FullscreenEnterIcon, + FullscreenExitIcon, + PauseIcon, + PlayIcon, + VolumeHighIcon, + VolumeLowIcon, + VolumeOffIcon, +} from '@vjs-10/react-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 { TimeRange } from '../../components/TimeRange'; +import { VolumeRange } from '../../components/VolumeRange'; +import styles from './styles'; + +type SkinProps = PropsWithChildren<{ + className?: string; +}>; + +export default function MediaSkinDefault({ children, className = '' }: SkinProps): JSX.Element { + return ( + + {children} + +
+ {/*
+

View From a Blue Moon

+

A story about Jon Jon Florence, a surfer from Hawaii.

+
*/} + +
+ + + + + + + +
+ +
+
+ + + + + +
+ + / + +
+
+ + +
+
+ + + + + + +
+ + + + + + +
+
+ + + + + +
+
+
+
+ ); +}; diff --git a/packages/react/react/src/skins/toasted/index.ts b/packages/react/react/src/skins/toasted/index.ts new file mode 100644 index 00000000..692577a7 --- /dev/null +++ b/packages/react/react/src/skins/toasted/index.ts @@ -0,0 +1 @@ +export { default as MediaSkinToasted } from './MediaSkinToasted'; \ No newline at end of file diff --git a/packages/react/react/src/skins/toasted/styles.ts b/packages/react/react/src/skins/toasted/styles.ts new file mode 100644 index 00000000..6e3a9a69 --- /dev/null +++ b/packages/react/react/src/skins/toasted/styles.ts @@ -0,0 +1,115 @@ +import type { MediaToastedSkinStyles } from "./types"; +import { cn } from "../../utils/cn"; + +const styles: MediaToastedSkinStyles = { + MediaContainer: cn( + 'relative @container/root group/root overflow-clip bg-black', + // Base typography + 'text-sm', + // 'ring-1 ring-inset ring-black/10 dark:ring-white/10', + 'after:absolute after:inset-0 after:ring-black/10 after:ring-1 dark:after:ring-white/10 after:ring-inset after:z-10 after:pointer-events-none after:rounded-[inherit]', + // Prevent rounded corners in fullscreen. + '[&:fullscreen]:rounded-none [&:fullscreen]:[&_video]:h-full [&:fullscreen]:[&_video]:w-full', + // Ensure the nested video inherits the radius. + '[&_video]:rounded-[inherit] [&_video]:w-full [&_video]:h-auto', + ), + Controls: cn( + '@container/controls absolute inset-x-0 bottom-0 top-1/3 flex flex-col justify-end z-20 px-1 pb-1.5 text-white text-shadow', + 'shadow-sm shadow-black/15', + // Background + 'bg-gradient-to-t from-stone-900/70 via-stone-900/60 via-35% to-transparent', + // Animation + 'transition ease-in-out', + // FIXME: Temporary hide/show logic + 'translate-y-full opacity-0 delay-500 pointer-events-none', + 'has-[[data-paused]]:translate-y-0 has-[[data-paused]]:opacity-100 has-[[data-paused]]:delay-0 has-[[data-paused]]:pointer-events-auto', + 'group-hover/root:translate-y-0 group-hover/root:opacity-100 group-hover/root:delay-0 group-hover/root:pointer-events-auto', + ), + ControlsRow: cn('flex items-center justify-between px-1.5 pb-0.5'), + Button: cn( + 'group/button cursor-pointer relative shrink-0 transition select-none p-2 rounded-md', + // Background/foreground + 'bg-transparent text-white/90', + // Hover and focus states + 'hover:no-underline hover:bg-white/15 hover:backdrop-blur-md hover:text-white focus-visible:no-underline focus-visible:bg-white/10 focus-visible:text-white', + // Focus state + '-outline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-500', + // Disabled state + 'aria-disabled:grayscale aria-disabled:opacity-50 aria-disabled:cursor-not-allowed', + // Loading state + 'aria-busy:pointer-events-none aria-busy:cursor-not-allowed', + // Expanded state + 'aria-expanded:bg-white/10 aria-expanded:text-white', + // Pressed state + 'active:scale-95', + ), + IconButton: cn( + 'grid [&_svg]:[grid-area:1/1]', + '[&_svg]:shrink-0 [&_svg]:transition [&_svg]:duration-300 [&_svg]:ease-out [&_svg]:drop-shadow-[0_1px_0_var(--tw-shadow-color)] [&_svg]:shadow-black/20', + ), + PlayButton: cn( + '[&_.pause-icon]:opacity-100 [&[data-paused]_.pause-icon]:opacity-0', + '[&_.play-icon]:opacity-0 [&[data-paused]_.play-icon]:opacity-100', + ), + PlayIcon: cn('play-icon'), + PauseIcon: cn('pause-icon'), + VolumeControls: cn('flex items-center flex-row-reverse group/volume'), + VolumeSlider: cn( + 'w-0 px-3 overflow-hidden pointer-events-none transition-[opacity,width] opacity-0 ease-out delay-500', + 'group-hover/volume:w-28 group-hover/volume:pointer-events-auto group-hover/volume:opacity-100 group-hover/volume:delay-0', + 'group-focus-within/volume:w-28 group-focus-within/volume:pointer-events-auto group-focus-within/volume:opacity-100 group-focus-within/volume:delay-0', + ), + VolumeButton: cn( + '[&_svg]:hidden', + '[&[data-volume-level="high"]_.volume-high-icon]:inline', + '[&[data-volume-level="medium"]_.volume-low-icon]:inline', + '[&[data-volume-level="low"]_.volume-low-icon]:inline', + '[&[data-volume-level="off"]_.volume-off-icon]:inline', + ), + VolumeHighIcon: cn('volume-high-icon'), + VolumeLowIcon: cn('volume-low-icon'), + VolumeOffIcon: cn('volume-off-icon'), + FullScreenButton: cn( + '[&_.fullscreen-enter-icon]:opacity-100 [&[data-fullscreen]_.fullscreen-enter-icon]:opacity-0', + '[&_.fullscreen-exit-icon]:opacity-0 [&[data-fullscreen]_.fullscreen-exit-icon]:opacity-100', + '[&_path]:transition-transform ease-out', + ), + FullScreenEnterIcon: cn( + 'fullscreen-enter-icon', + 'group-hover/button:[&_.arrow-1]:-translate-x-px group-hover/button:[&_.arrow-1]:-translate-y-px', + 'group-hover/button:[&_.arrow-2]:translate-x-px group-hover/button:[&_.arrow-2]:translate-y-px', + ), + FullScreenExitIcon: cn( + 'fullscreen-exit-icon', + '[&_.arrow-1]:-translate-x-px [&_.arrow-1]:-translate-y-px', + '[&_.arrow-2]:translate-x-px [&_.arrow-2]:translate-y-px', + 'group-hover/button:[&_.arrow-1]:translate-0', + 'group-hover/button:[&_.arrow-2]:translate-0', + ), + TimeSlider: cn('px-1.5'), + TimeSliderThumb: cn( + 'opacity-0', + 'group-hover/slider:opacity-100 group-focus-within/slider:opacity-100', + ), + TimeDisplay: cn('tabular-nums text-shadow-2xs shadow-black/50'), + SliderRoot: cn( + 'flex items-center justify-center flex-1 group/slider relative', + '[&[data-orientation="horizontal"]]:h-5 [&[data-orientation="horizontal"]]:min-w-20', + '[&[data-orientation="vertical"]]:w-5 [&[data-orientation="vertical"]]:h-20', + ), + SliderTrack: cn( + 'relative select-none rounded-full bg-white/25 backdrop-blur-sm backdrop-brightness-90 backdrop-saturate-150 shadow-sm shadow-black/10', + '[&[data-orientation="horizontal"]]:w-full [&[data-orientation="horizontal"]]:h-1', + '[&[data-orientation="vertical"]]:w-1', + ), + SliderProgress: cn('bg-amber-500 rounded-[inherit]'), + // TODO: Work out what we want to do here. + SliderPointer: cn('rounded-[inherit]'), + SliderThumb: cn( + 'bg-white z-10 select-none ring ring-black/10 rounded-full shadow-sm shadow-black/15 transition-[opacity,height,width] ease-in-out', + '-outline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-500', + 'size-3 active:size-3.5 group-active/slider:size-3.5 hover:cursor-ew-resize', + ), +}; + +export default styles; diff --git a/packages/react/react/src/skins/toasted/types.ts b/packages/react/react/src/skins/toasted/types.ts new file mode 100644 index 00000000..f8276f7d --- /dev/null +++ b/packages/react/react/src/skins/toasted/types.ts @@ -0,0 +1,27 @@ +export interface MediaToastedSkinStyles { + readonly MediaContainer: string; + readonly Controls: string; + readonly ControlsRow: string; + readonly Button: string; + readonly IconButton: string; + readonly PlayButton: string; + readonly PlayIcon: string; + readonly PauseIcon: string; + readonly VolumeControls: string; + readonly VolumeSlider: string; + readonly VolumeButton: string; + readonly VolumeHighIcon: string; + readonly VolumeLowIcon: string; + readonly VolumeOffIcon: string; + readonly FullScreenButton: string; + readonly FullScreenEnterIcon: string; + readonly FullScreenExitIcon: string; + readonly TimeSlider: string; + readonly TimeSliderThumb: string; + readonly TimeDisplay: string; + readonly SliderRoot: string; + readonly SliderTrack: string; + readonly SliderProgress: string; + readonly SliderPointer: string; + readonly SliderThumb: string; +} \ No newline at end of file diff --git a/packages/react/react/src/utils/cn.ts b/packages/react/react/src/utils/cn.ts new file mode 100644 index 00000000..af3edfde --- /dev/null +++ b/packages/react/react/src/utils/cn.ts @@ -0,0 +1,6 @@ +// A (very crude) utility to merge class names +// Usually I'd use something like `clsx` or `classnames` but this is ok for our simple use case. +// It just makes the billions of Tailwind classes a little easier to read. +export function cn(...classes: (string | undefined)[]): string { + return classes.filter(Boolean).join(' '); +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 061dc93b..ff4dd784 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,9 +35,6 @@ importers: eslint-plugin-jsx-a11y: specifier: ^6.10.2 version: 6.10.2(eslint@9.36.0(jiti@2.5.1)) - eslint-plugin-react: - specifier: ^7.37.5 - version: 7.37.5(eslint@9.36.0(jiti@2.5.1)) eslint-plugin-react-hooks: specifier: ^5.2.0 version: 5.2.0(eslint@9.36.0(jiti@2.5.1)) @@ -1547,10 +1544,6 @@ packages: resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} engines: {node: '>= 0.4'} - array.prototype.findlast@1.2.5: - resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} - engines: {node: '>= 0.4'} - array.prototype.flat@1.3.3: resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} engines: {node: '>= 0.4'} @@ -1559,10 +1552,6 @@ packages: resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} engines: {node: '>= 0.4'} - array.prototype.tosorted@1.1.4: - resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} - engines: {node: '>= 0.4'} - arraybuffer.prototype.slice@1.0.4: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} @@ -1840,10 +1829,6 @@ packages: resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} engines: {node: '>=0.3.1'} - doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -1913,10 +1898,6 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-iterator-helpers@1.2.1: - resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} - engines: {node: '>= 0.4'} - es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -2142,12 +2123,6 @@ packages: typescript: optional: true - eslint-plugin-react@7.37.5: - resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} - engines: {node: '>=4'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - eslint-plugin-regexp@2.10.0: resolution: {integrity: sha512-ovzQT8ESVn5oOe5a7gIDPD5v9bCSjIFJu57sVPDqgPRXicQzOnYfFN21WoQBQF18vrhT5o7UMKFwJQVVjyJ0ng==} engines: {node: ^18 || >=20} @@ -2495,10 +2470,6 @@ packages: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} - is-data-view@1.0.2: resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} engines: {node: '>= 0.4'} @@ -2591,10 +2562,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - iterator.prototype@1.1.5: - resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} - engines: {node: '>= 0.4'} - jiti@2.5.1: resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} hasBin: true @@ -2988,10 +2955,6 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - object-deep-merge@1.0.5: resolution: {integrity: sha512-3DioFgOzetbxbeUq8pB2NunXo8V0n4EvqsWM/cJoI6IA9zghd7cl/2pBOuWRf4dlvA+fcg5ugFMZaN2/RuoaGg==} @@ -3007,10 +2970,6 @@ packages: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} - object.entries@1.1.9: - resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} - engines: {node: '>= 0.4'} - object.fromentries@2.0.8: resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} engines: {node: '>= 0.4'} @@ -3071,9 +3030,6 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -3140,9 +3096,6 @@ packages: engines: {node: '>=14'} hasBin: true - prop-types@15.8.1: - resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3158,9 +3111,6 @@ packages: peerDependencies: react: ^18.3.1 - react-is@16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -3207,10 +3157,6 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve@2.0.0-next.5: - resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} - hasBin: true - restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -3372,13 +3318,6 @@ packages: resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} engines: {node: '>= 0.4'} - string.prototype.matchall@4.0.12: - resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} - engines: {node: '>= 0.4'} - - string.prototype.repeat@1.0.0: - resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} - string.prototype.trim@1.2.10: resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} engines: {node: '>= 0.4'} @@ -3407,10 +3346,6 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - svg-parser@2.0.4: resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} @@ -4837,15 +4772,6 @@ snapshots: is-string: 1.1.1 math-intrinsics: 1.1.0 - array.prototype.findlast@1.2.5: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 1.1.0 - array.prototype.flat@1.3.3: dependencies: call-bind: 1.0.8 @@ -4860,14 +4786,6 @@ snapshots: es-abstract: 1.24.0 es-shim-unscopables: 1.1.0 - array.prototype.tosorted@1.1.4: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - es-shim-unscopables: 1.1.0 - arraybuffer.prototype.slice@1.0.4: dependencies: array-buffer-byte-length: 1.0.2 @@ -5120,10 +5038,6 @@ snapshots: diff@8.0.2: {} - doctrine@2.1.0: - dependencies: - esutils: 2.0.3 - dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -5237,25 +5151,6 @@ snapshots: es-errors@1.3.0: {} - es-iterator-helpers@1.2.1: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - es-set-tostringtag: 2.1.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - internal-slot: 1.1.0 - iterator.prototype: 1.1.5 - safe-array-concat: 1.1.3 - es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -5602,28 +5497,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react@7.37.5(eslint@9.36.0(jiti@2.5.1)): - dependencies: - array-includes: 3.1.9 - array.prototype.findlast: 1.2.5 - array.prototype.flatmap: 1.3.3 - array.prototype.tosorted: 1.1.4 - doctrine: 2.1.0 - es-iterator-helpers: 1.2.1 - eslint: 9.36.0(jiti@2.5.1) - estraverse: 5.3.0 - hasown: 2.0.2 - jsx-ast-utils: 3.3.5 - minimatch: 3.1.2 - object.entries: 1.1.9 - object.fromentries: 2.0.8 - object.values: 1.2.1 - prop-types: 15.8.1 - resolve: 2.0.0-next.5 - semver: 6.3.1 - string.prototype.matchall: 4.0.12 - string.prototype.repeat: 1.0.0 - eslint-plugin-regexp@2.10.0(eslint@9.36.0(jiti@2.5.1)): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.5.1)) @@ -6010,10 +5883,6 @@ snapshots: is-callable@1.2.7: {} - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 - is-data-view@1.0.2: dependencies: call-bound: 1.0.4 @@ -6110,15 +5979,6 @@ snapshots: isexe@2.0.0: {} - iterator.prototype@1.1.5: - dependencies: - define-data-property: 1.1.4 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - has-symbols: 1.1.0 - set-function-name: 2.0.2 - jiti@2.5.1: {} js-tokens@4.0.0: {} @@ -6660,8 +6520,6 @@ snapshots: dependencies: boolbase: 1.0.0 - object-assign@4.1.1: {} - object-deep-merge@1.0.5: dependencies: type-fest: 4.2.0 @@ -6679,13 +6537,6 @@ snapshots: has-symbols: 1.1.0 object-keys: 1.1.1 - object.entries@1.1.9: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - object.fromentries@2.0.8: dependencies: call-bind: 1.0.8 @@ -6756,8 +6607,6 @@ snapshots: path-key@3.1.1: {} - path-parse@1.0.7: {} - path-type@4.0.0: {} pathe@2.0.3: {} @@ -6811,12 +6660,6 @@ snapshots: prettier@3.6.2: {} - prop-types@15.8.1: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react-is: 16.13.1 - punycode@2.3.1: {} quansync@0.2.11: {} @@ -6829,8 +6672,6 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 - react-is@16.13.1: {} - react-refresh@0.17.0: {} react@18.3.1: @@ -6880,12 +6721,6 @@ snapshots: resolve-pkg-maps@1.0.0: {} - resolve@2.0.0-next.5: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -7108,27 +6943,6 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.0 - string.prototype.matchall@4.0.12: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-symbols: 1.1.0 - internal-slot: 1.1.0 - regexp.prototype.flags: 1.5.4 - set-function-name: 2.0.2 - side-channel: 1.1.0 - - string.prototype.repeat@1.0.0: - dependencies: - define-properties: 1.2.1 - es-abstract: 1.24.0 - string.prototype.trim@1.2.10: dependencies: call-bind: 1.0.8 @@ -7164,8 +6978,6 @@ snapshots: dependencies: has-flag: 4.0.0 - supports-preserve-symlinks-flag@1.0.0: {} - svg-parser@2.0.4: {} svgo@3.3.2: