fix: design tweaks to toasted skin, lint rule tweaks (#44)

This commit is contained in:
Sam Potts
2025-10-09 10:21:18 +11:00
committed by GitHub
parent 5cb93a14a7
commit 3a0767c340
34 changed files with 434 additions and 463 deletions
@@ -36,7 +36,7 @@ export function useFullscreenButtonProps(props: PropsWithChildren, state: Return
const baseProps: Record<string, any> = {
/** @TODO Need another state provider in core for i18n (CJP) */
/** aria attributes/props */
'role': 'button',
role: 'button',
'aria-label': state.fullscreen ? 'exit fullscreen' : 'enter fullscreen',
/** tooltip */
'data-tooltip': state.fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen',
@@ -64,8 +64,7 @@ export function renderFullscreenButton(props: FullscreenButtonProps, state: Full
if (props.disabled) return;
if (state.fullscreen) {
state.requestExitFullscreen();
}
else {
} else {
state.requestEnterFullscreen();
}
}}
@@ -38,7 +38,7 @@ export function useMuteButtonProps(props: PropsWithChildren, state: ReturnType<t
'data-volume-level': state.volumeLevel,
/** @TODO Need another state provider in core for i18n (CJP) */
/** aria attributes/props */
'role': 'button',
role: 'button',
'aria-label': state.muted ? 'unmute' : 'mute',
/** tooltip */
'data-tooltip': state.muted ? 'Unmute' : 'Mute',
@@ -66,8 +66,7 @@ export function renderMuteButton(props: MuteButtonProps, state: MuteButtonState)
if (props.disabled) return;
if (state.volumeLevel === 'off') {
state.requestUnmute();
}
else {
} else {
state.requestMute();
}
}}
@@ -34,7 +34,7 @@ export function usePlayButtonProps(props: Record<string, unknown>, state: Return
const baseProps: Record<string, any> = {
/** @TODO Need another state provider in core for i18n (CJP) */
/** aria attributes/props */
'role': 'button',
role: 'button',
'aria-label': state.paused ? 'play' : 'pause',
/** tooltip */
'data-tooltip': state.paused ? 'Play' : 'Pause',
@@ -62,8 +62,7 @@ export function renderPlayButton(props: PlayButtonProps, state: PlayButtonState)
if (props.disabled) return;
if (state.paused) {
state.requestPlay();
}
else {
} else {
state.requestPause();
}
}}
@@ -53,12 +53,12 @@ export function useTimeRangeRootProps(props: TimeRange.Props, state: TimeRange.S
const { children, className, id, style, orientation = 'horizontal' } = props;
return {
'ref': useCallback((el: HTMLDivElement) => {
ref: useCallback((el: HTMLDivElement) => {
if (!el) return;
state.core?.attach(el);
}, []),
id,
'role': 'slider',
role: 'slider',
'aria-label': 'Seek',
'aria-valuemin': 0,
'aria-valuemax': 100,
@@ -69,10 +69,10 @@ export function useTimeRangeRootProps(props: TimeRange.Props, state: TimeRange.S
'data-current-time': state.currentTime,
'data-duration': state.duration,
className,
'style': {
style: {
...style,
'--slider-fill': `${_fillWidth.toFixed(3)}%`,
'--slider-pointer': `${_pointerWidth.toFixed(3)}%`,
'--slider-pointer': `${(_pointerWidth * 100).toFixed(3)}%`,
} as React.CSSProperties,
children,
};
@@ -98,12 +98,12 @@ const TimeRangeRoot: ConnectedComponent<TimeRange.Props, typeof renderTimeRangeR
export function useTimeRangeTrackProps(props: React.ComponentProps<'div'>, context: TimeRange.State): TimeRangeRenderProps {
return {
'ref': useCallback((el: HTMLDivElement) => {
ref: useCallback((el: HTMLDivElement) => {
context.core?.setState({ _trackElement: el });
}, []),
'data-orientation': context.orientation,
...props,
'style': {
style: {
...props.style,
[context.orientation === 'horizontal' ? 'width' : 'height']: '100%',
},
@@ -130,7 +130,7 @@ export function useTimeRangeThumbProps(props: React.ComponentProps<'div'>, conte
return {
'data-orientation': context.orientation,
...props,
'style': {
style: {
...props.style,
[context.orientation === 'horizontal' ? 'insetInlineStart' : 'insetBlockEnd']: 'var(--slider-fill)',
[context.orientation === 'horizontal' ? 'top' : 'left']: '50%',
@@ -160,7 +160,7 @@ export function useTimeRangePointerProps(props: React.ComponentProps<'div'>, con
return {
'data-orientation': context.orientation,
...props,
'style': {
style: {
...props.style,
[context.orientation === 'horizontal' ? 'width' : 'height']: 'var(--slider-pointer, 0%)',
[context.orientation === 'horizontal' ? 'height' : 'width']: '100%',
@@ -188,7 +188,7 @@ export function useTimeRangeProgressProps(props: React.ComponentProps<'div'>, co
return {
'data-orientation': context.orientation,
...props,
'style': {
style: {
...props.style,
[context.orientation === 'horizontal' ? 'width' : 'height']: 'var(--slider-fill, 0%)',
[context.orientation === 'horizontal' ? 'height' : 'width']: '100%',
@@ -150,7 +150,7 @@ function TooltipTrigger({ children }: TooltipTriggerProps): JSX.Element {
const { refs, open } = context;
return React.cloneElement(React.Children.only(children) as JSX.Element, {
'ref': refs.setReference,
ref: refs.setReference,
...getReferenceProps(),
'data-popup-open': open ? '' : undefined,
});
@@ -57,12 +57,12 @@ export function useVolumeRangeRootProps(props: VolumeRange.Props, state: VolumeR
const { children, className, id, style, orientation = 'horizontal' } = props;
return {
'ref': useCallback((el: HTMLDivElement) => {
ref: useCallback((el: HTMLDivElement) => {
if (!el) return;
state.core?.attach(el);
}, []),
id,
'role': 'slider',
role: 'slider',
'aria-label': 'Volume',
'aria-valuemin': 0,
'aria-valuemax': 100,
@@ -73,7 +73,7 @@ export function useVolumeRangeRootProps(props: VolumeRange.Props, state: VolumeR
'data-muted': state.muted,
'data-volume-level': state.volumeLevel,
className,
'style': {
style: {
...style,
'--slider-fill': `${_fillWidth.toFixed(3)}%`,
'--slider-pointer': `${_pointerWidth.toFixed(3)}%`,
@@ -102,12 +102,12 @@ const VolumeRangeRoot: ConnectedComponent<VolumeRange.Props, typeof renderVolume
export function useVolumeRangeTrackProps(props: React.ComponentProps<'div'>, context: VolumeRange.State): VolumeRangeRenderProps {
return {
'ref': useCallback((el: HTMLDivElement) => {
ref: useCallback((el: HTMLDivElement) => {
context.core?.setState({ _trackElement: el });
}, []),
'data-orientation': context.orientation,
...props,
'style': {
style: {
...props.style,
[context.orientation === 'horizontal' ? 'width' : 'height']: '100%',
},
@@ -133,7 +133,7 @@ export function useVolumeRangeThumbProps(props: React.ComponentProps<'div'>, con
return {
'data-orientation': context.orientation,
...props,
'style': {
style: {
...props.style,
[context.orientation === 'horizontal' ? 'insetInlineStart' : 'insetBlockEnd']: 'var(--slider-fill)',
[context.orientation === 'horizontal' ? 'top' : 'left']: '50%',
@@ -162,7 +162,7 @@ export function useVolumeRangeProgressProps(props: React.ComponentProps<'div'>,
return {
'data-orientation': context.orientation,
...props,
'style': {
style: {
...props.style,
[context.orientation === 'horizontal' ? 'width' : 'height']: 'var(--slider-fill, 0%)',
[context.orientation === 'horizontal' ? 'height' : 'width']: '100%',
@@ -75,7 +75,7 @@ export default function MediaSkinDefault({ children, className = '' }: SkinProps
<Popover.Root openOnHover delay={200} closeDelay={100}>
<Popover.Trigger>
<MuteButton className={`${styles.Button} ${styles.IconButton} ${styles.VolumeButton}`}>
<MuteButton className={`${styles.Button} ${styles.IconButton} ${styles.MuteButton}`}>
<VolumeHighIcon className={styles.VolumeHighIcon} />
<VolumeLowIcon className={styles.VolumeLowIcon} />
<VolumeOffIcon className={styles.VolumeOffIcon} />
@@ -88,7 +88,7 @@ const styles: MediaDefaultSkinStyles = {
),
PlayTooltip: cn('play-tooltip'),
PauseTooltip: cn('pause-tooltip'),
VolumeButton: cn(
MuteButton: cn(
'[&_svg]:opacity-0',
'[&[data-volume-level="high"]_.volume-high-icon]:opacity-100',
'[&[data-volume-level="medium"]_.volume-low-icon]:opacity-100',
@@ -11,7 +11,7 @@ export interface MediaDefaultSkinStyles {
readonly PlayTooltip: string;
readonly PauseTooltip: string;
readonly TooltipPopup: string;
readonly VolumeButton: string;
readonly MuteButton: string;
readonly VolumeHighIcon: string;
readonly VolumeLowIcon: string;
readonly VolumeOffIcon: string;
@@ -17,7 +17,6 @@ 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<{
@@ -29,63 +28,43 @@ export default function MediaSkinDefault({ children, className = '' }: SkinProps
<MediaContainer className={`${styles.MediaContainer} ${className}`}>
{children}
<div className={styles.Controls} data-testid="media-controls">
{/* <header className='py-2 px-2.5 text-shadow-sm text-shadow-black/10'>
<h1 className="text-base font-medium">Example Video</h1>
<p className="text-stone-400">This is just a description for the example video.</p>
</header> */}
<div className={styles.Overlay} aria-hidden="true" />
<div className={styles.ControlsRow}>
<TimeRange.Root className={styles.SliderRoot}>
<TimeRange.Track className={styles.SliderTrack}>
<TimeRange.Progress className={styles.SliderProgress} />
<TimeRange.Pointer className={styles.SliderPointer} />
</TimeRange.Track>
<TimeRange.Thumb className={`${styles.SliderThumb} ${styles.TimeSliderThumb}`} />
</TimeRange.Root>
<div className={styles.Controls}>
<PlayButton className={`${styles.Button} ${styles.IconButton} ${styles.PlayButton}`}>
<PlayIcon className={styles.PlayIcon}></PlayIcon>
<PauseIcon className={styles.PauseIcon}></PauseIcon>
</PlayButton>
<div className="flex items-center gap-1">
<CurrentTimeDisplay
// Use showRemaining to show count down/remaining time
// showRemaining
className={styles.TimeDisplay}
/>
<span className="opacity-50">/</span>
<DurationDisplay className={`${styles.TimeDisplay} opacity-50`} />
</div>
<div className={styles.ControlsRow}>
<div className="flex items-center gap-3">
<PlayButton className={`${styles.Button} ${styles.IconButton} ${styles.PlayButton}`}>
<PlayIcon className={styles.PlayIcon}></PlayIcon>
<PauseIcon className={styles.PauseIcon}></PauseIcon>
</PlayButton>
<TimeRange.Root className={`${styles.SliderRoot} ${styles.TimeSliderRoot}`}>
<TimeRange.Track className={styles.SliderTrack}>
<TimeRange.Progress className={styles.SliderProgress} />
<TimeRange.Pointer className={styles.SliderPointer} />
</TimeRange.Track>
<TimeRange.Thumb className={`${styles.SliderThumb} ${styles.TimeSliderThumb}`} />
</TimeRange.Root>
<div className="flex items-center gap-1">
<CurrentTimeDisplay
// Use showRemaining to show count down/remaining time
// showRemaining
className={styles.TimeDisplay}
/>
<span className="opacity-50">/</span>
<DurationDisplay className={`${styles.TimeDisplay} opacity-50`} />
</div>
</div>
<div className={styles.ButtonGroup}>
<MuteButton className={`${styles.Button} ${styles.IconButton} ${styles.MuteButton}`}>
<VolumeHighIcon className={styles.VolumeHighIcon} />
<VolumeLowIcon className={styles.VolumeLowIcon} />
<VolumeOffIcon className={styles.VolumeOffIcon} />
</MuteButton>
<div className="flex items-center gap-0.5">
<div className={styles.VolumeControls}>
<MuteButton className={`${styles.Button} ${styles.IconButton} ${styles.VolumeButton}`}>
<VolumeHighIcon className={styles.VolumeHighIcon} />
<VolumeLowIcon className={styles.VolumeLowIcon} />
<VolumeOffIcon className={styles.VolumeOffIcon} />
</MuteButton>
<div className={styles.VolumeSlider}>
<VolumeRange.Root className={styles.SliderRoot}>
<VolumeRange.Track className={styles.SliderTrack}>
<VolumeRange.Progress className={styles.SliderProgress} />
</VolumeRange.Track>
<VolumeRange.Thumb className={styles.SliderThumb} />
</VolumeRange.Root>
</div>
</div>
<FullscreenButton className={`${styles.Button} ${styles.IconButton} ${styles.FullScreenButton}`}>
<FullscreenEnterIcon className={styles.FullScreenEnterIcon} />
<FullscreenExitIcon className={styles.FullScreenExitIcon} />
</FullscreenButton>
</div>
<FullscreenButton className={`${styles.Button} ${styles.IconButton} ${styles.FullScreenButton}`}>
<FullscreenEnterIcon className={styles.FullScreenEnterIcon} />
<FullscreenExitIcon className={styles.FullScreenExitIcon} />
</FullscreenButton>
</div>
</div>
</MediaContainer>
@@ -5,7 +5,7 @@ const styles: MediaToastedSkinStyles = {
MediaContainer: cn(
'relative @container/root group/root overflow-clip bg-black',
// Base typography
'text-sm',
'text-[0.8125rem]', // 13px
// '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.
@@ -13,36 +13,43 @@ const styles: MediaToastedSkinStyles = {
// Ensure the nested video inherits the radius.
'[&_video]:rounded-[inherit] [&_video]:w-full [&_video]:h-auto',
),
Overlay: cn(
'absolute inset-0 rounded-[inherit] bg-black/30',
'bg-gradient-to-t from-black/30 to-transparent to-[120px]',
'opacity-0 delay-500 duration-300',
// FIXME: Temporary hide/show logic
'has-[+.controls_[data-paused]]:opacity-100 has-[+.controls_[data-paused]]:delay-0 has-[+.controls_[data-paused]]:duration-75',
'group-hover/root:opacity-100 group-hover/root:delay-0 group-hover/root:duration-75',
),
Controls: cn(
'@container/controls absolute inset-x-0 bottom-0 top-1/3 flex flex-col justify-end z-20 px-2.5 pb-2.5 text-white text-shadow',
'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-0 bottom-0 flex items-center gap-3.5 z-20 px-6 pb-6 pt-10 text-white text-shadow',
'shadow-sm shadow-black/15',
// Background
'bg-gradient-to-t from-stone-950/70 via-stone-950/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',
'translate-y-full opacity-0 delay-500 duration-300',
'has-[[data-paused]]:translate-y-0 has-[[data-paused]]:opacity-100 has-[[data-paused]]:delay-0 has-[[data-paused]]:duration-75',
'group-hover/root:translate-y-0 group-hover/root:opacity-100 group-hover/root:delay-0 group-hover/root:duration-75',
),
ControlsRow: cn('flex items-center justify-between'),
Button: cn(
'group/button cursor-pointer relative shrink-0 transition select-none p-2 rounded-md',
// Background/foreground
'bg-transparent text-white/90',
'bg-transparent text-white',
// Hover and focus states
'hover:no-underline hover:bg-stone-100/10 hover:backdrop-blur-md hover:text-white focus-visible:no-underline focus-visible:bg-stone-100/10 focus-visible:text-white',
'hover:text-white/70 focus-visible:text-white/70',
// Focus state
'-outline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-500',
'-outline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white',
// 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-stone-100/10 aria-expanded:text-white',
'aria-expanded:text-white/70',
// Pressed state
'active:scale-95',
),
ButtonGroup: cn('flex items-center gap-1.5'),
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',
@@ -53,13 +60,7 @@ const styles: MediaToastedSkinStyles = {
),
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(
MuteButton: cn(
'[&_svg]:hidden',
'[&[data-volume-level="high"]_.volume-high-icon]:inline',
'[&[data-volume-level="medium"]_.volume-low-icon]:inline',
@@ -86,10 +87,8 @@ const styles: MediaToastedSkinStyles = {
'group-hover/button:[&_.arrow-1]:translate-0',
'group-hover/button:[&_.arrow-2]:translate-0',
),
TimeSliderThumb: cn(
'opacity-0',
'group-hover/slider:opacity-100 group-focus-within/slider:opacity-100',
),
TimeSliderRoot: cn('mx-2'),
TimeSliderThumb: cn('opacity-0'),
TimeDisplay: cn('tabular-nums text-shadow-2xs shadow-black/50'),
SliderRoot: cn(
'flex items-center justify-center flex-1 group/slider relative',
@@ -97,13 +96,12 @@ const styles: MediaToastedSkinStyles = {
'[&[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',
'relative select-none rounded-full bg-white/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]'),
SliderProgress: cn('bg-white rounded-[inherit]'),
SliderPointer: cn('hidden'),
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',
@@ -1,21 +1,21 @@
export interface MediaToastedSkinStyles {
readonly MediaContainer: string;
readonly Overlay: string;
readonly Controls: string;
readonly ControlsRow: string;
readonly Button: string;
readonly ButtonGroup: string;
readonly IconButton: string;
readonly PlayButton: string;
readonly PlayIcon: string;
readonly PauseIcon: string;
readonly VolumeControls: string;
readonly VolumeSlider: string;
readonly VolumeButton: string;
readonly MuteButton: string;
readonly VolumeHighIcon: string;
readonly VolumeLowIcon: string;
readonly VolumeOffIcon: string;
readonly FullScreenButton: string;
readonly FullScreenEnterIcon: string;
readonly FullScreenExitIcon: string;
readonly TimeSliderRoot: string;
readonly TimeSliderThumb: string;
readonly TimeDisplay: string;
readonly SliderRoot: string;
@@ -9,8 +9,7 @@ type PossibleRef<T> = React.Ref<T> | undefined;
function setRef<T>(ref: PossibleRef<T>, value: T): (() => void) | void | undefined {
if (typeof ref === 'function') {
return ref(value);
}
else if (ref !== null && ref !== undefined) {
} else if (ref !== null && ref !== undefined) {
(ref as React.MutableRefObject<T>).current = value;
}
}
@@ -40,8 +39,7 @@ function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
const cleanup = cleanups[i];
if (typeof cleanup == 'function') {
cleanup();
}
else {
} else {
setRef(refs[i], null);
}
}