feat(react): implement video skins with responsive layout (#568)

This commit is contained in:
Sam Potts
2026-02-19 15:45:41 +11:00
committed by GitHub
parent 7d3be367f5
commit 846d38e79b
5 changed files with 82 additions and 35 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
export * from './feature';
export * from './media/proxy';
export type { AudioApiProxy, MediaApiProxy, MediaApiProxyTarget, VideoApiProxy } from './media/proxy';
export * from './media/types';
export * from './store/features';
export * from './store/selectors';
@@ -96,6 +96,7 @@
/* Media Control Bar UI/Styles */
.media-minimal-skin .media-controls {
position: absolute;
container: media-controls / inline-size;
bottom: 0;
inset-inline: 0;
padding: 2rem 0.375rem 0.375rem 0.375rem;
@@ -133,20 +134,40 @@
}
}
/* Time Display */
.media-minimal-skin .media-time-group {
/* Time Controls */
.media-minimal-skin .media-time-controls {
display: flex;
flex-direction: row-reverse;
align-items: center;
flex: 1;
gap: 0.75rem;
}
.media-minimal-skin .media-time {
display: flex;
align-items: center;
gap: 0.25rem;
}
.media-minimal-skin .media-duration-display {
display: contents;
color: oklab(1 0 0 / 0.5);
.media-minimal-skin .media-time__value--current,
.media-minimal-skin .media-time__separator {
display: none;
}
.media-minimal-skin .media-time-display {
.media-minimal-skin .media-time__value {
text-shadow: 0 1px 0 oklab(0 0 0 / 0.2);
font-variant-numeric: tabular-nums;
}
@container media-controls (width > 480px) {
.media-minimal-skin .media-time-controls {
flex-direction: row;
}
.media-minimal-skin .media-time__value--duration,
.media-minimal-skin .media-time__separator {
color: oklab(1 0 0 / 0.5);
}
.media-minimal-skin .media-time__value--current,
.media-minimal-skin .media-time__separator {
display: inline;
}
}
/* Button Groups (layout only) */
.media-minimal-skin .media-button-group {
@@ -192,6 +213,11 @@
opacity: 0.5;
filter: grayscale(1);
}
@container media-controls (width < 480px) {
.media-minimal-skin .media-button--seek {
display: none;
}
}
/* Icons */
.media-minimal-skin .media-icon__container {
@@ -29,8 +29,8 @@ const SEEK_TIME = 10;
export type MinimalVideoSkinProps = BaseSkinProps;
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button(props, ref) {
return <button type="button" className="media-button" ref={ref} {...props} />;
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
return <button ref={ref} type="button" className={cn('media-button', className)} {...props} />;
});
function PlayButtonIcon({ state, className, ...rest }: { state: PlayButtonState } & ComponentProps<'svg'>) {
@@ -95,7 +95,7 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
<SeekButton
seconds={-SEEK_TIME}
render={(props) => (
<Button {...props}>
<Button {...props} className="media-button--seek">
<span className="media-icon__container">
<SeekIcon className="media-icon media-icon--seek media-icon--flipped" />
<span className="media-icon__label">{SEEK_TIME}</span>
@@ -107,7 +107,7 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
<SeekButton
seconds={SEEK_TIME}
render={(props) => (
<Button {...props}>
<Button {...props} className="media-button--seek">
<span className="media-icon__container">
<SeekIcon className="media-icon media-icon--seek" />
<span className="media-icon__label">{SEEK_TIME}</span>
@@ -117,18 +117,18 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
/>
</div>
<Time.Group className="media-time-group">
<Time.Value type="current" className="media-time-display" />
<span className="media-duration-display">
<Time.Separator />
<Time.Value type="duration" className="media-time-display" />
</span>
</Time.Group>
<span className="media-time-controls">
<Time.Group className="media-time">
<Time.Value type="current" className="media-time__value media-time__value--current" />
<Time.Separator className="media-time__separator" />
<Time.Value type="duration" className="media-time__value media-time__value--duration" />
</Time.Group>
{/* Temporary spacer */}
<div style={{ flex: '1', borderRadius: '100vh', height: '4px', background: 'oklab(1 0 0 / 0.2)' }} />
{/* Temporary spacer */}
<span className="media-slider" style={{ height: '4px', background: 'oklab(1 0 0 / 0.2)' }} />
</span>
<div className="media-button-group">
<span className="media-button-group">
<MuteButton
render={(props, state) => (
<Button {...props}>
@@ -152,7 +152,7 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
</Button>
)}
/>
</div>
</span>
</Controls.Root>
<div className="media-overlay" />
+26 -5
View File
@@ -129,12 +129,13 @@
/* Media Control Bar UI/Styles */
.media-default-skin .media-controls {
position: absolute;
container: media-controls / inline-size;
bottom: 0.75rem;
inset-inline: 0.75rem;
padding: 0.25rem;
padding: 0.175rem;
display: flex;
align-items: center;
gap: 0.125rem;
gap: 0.075rem;
color: oklab(1 0 0);
border-radius: calc(infinity * 1px);
will-change: scale, transform, filter, opacity;
@@ -160,19 +161,34 @@
scale: 1;
}
}
@container media-root (width > 640px) {
.media-default-skin .media-controls {
padding: 0.25rem;
gap: 0.125rem;
}
}
/* Time Display */
.media-default-skin .media-time-group {
.media-default-skin .media-time {
container: media-time / inline-size;
display: flex;
align-items: center;
flex: 1;
gap: 0.75rem;
padding-inline: 0.375rem;
padding-inline: 0.5rem;
}
.media-default-skin .media-time-display {
.media-default-skin .media-time__value {
text-shadow: 0 1px 0 oklab(0 0 0 / 0.25);
font-variant-numeric: tabular-nums;
}
.media-default-skin .media-time .media-time__value:first-child {
display: none;
}
@container media-time (width > 240px) {
.media-default-skin .media-time .media-time__value:first-child {
display: block;
}
}
/* Buttons */
.media-default-skin .media-button {
@@ -208,6 +224,11 @@
opacity: 0.5;
filter: grayscale(1);
}
@container media-controls (width < 480px) {
.media-default-skin .media-button--seek {
display: none;
}
}
/* Icons */
.media-default-skin .media-icon__container {
+8 -8
View File
@@ -29,8 +29,8 @@ const SEEK_TIME = 10;
export type VideoSkinProps = BaseSkinProps;
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button(props, ref) {
return <button ref={ref} type="button" className="media-button" {...props} />;
const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
return <button ref={ref} type="button" className={cn('media-button', className)} {...props} />;
});
function PlayButtonIcon({ state, className, ...rest }: { state: PlayButtonState } & ComponentProps<'svg'>) {
@@ -96,7 +96,7 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
<SeekButton
seconds={-SEEK_TIME}
render={(props) => (
<Button {...props}>
<Button {...props} className="media-button--seek">
<span className="media-icon__container">
<SeekIcon className="media-icon media-icon--seek media-icon--flipped" />
<span className="media-icon__label">{SEEK_TIME}</span>
@@ -108,7 +108,7 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
<SeekButton
seconds={SEEK_TIME}
render={(props) => (
<Button {...props}>
<Button {...props} className="media-button--seek">
<span className="media-icon__container">
<SeekIcon className="media-icon media-icon--seek" />
<span className="media-icon__label">{SEEK_TIME}</span>
@@ -117,11 +117,11 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
)}
/>
<Time.Group className="media-time-group">
<Time.Value type="current" className="media-time-display" />
<Time.Group className="media-time">
<Time.Value type="current" className="media-time__value" />
{/* Temporary spacer */}
<div style={{ flex: '1', borderRadius: '100vh', height: '4px', background: 'oklab(1 0 0 / 0.2)' }} />
<Time.Value type="duration" className="media-time-display" />
<div className="media-slider" style={{ height: '4px', background: 'oklab(1 0 0 / 0.2)' }} />
<Time.Value type="duration" className="media-time__value" />
</Time.Group>
<MuteButton