feat(packages): add UI support for gestures and hotkeys (#1388)

Co-authored-by: Rahim <rahim.alwer@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sam Potts
2026-05-05 10:34:12 +10:00
committed by GitHub
co-authored by Rahim Claude Opus 4.6
parent 6c81f2d190
commit 0620814a67
187 changed files with 5665 additions and 405 deletions
@@ -79,7 +79,12 @@
}
& .media-icon {
filter: drop-shadow(0 1px 0 var(--media-controls-current-shadow-color, oklch(0 0 0 / 0.25)));
grid-area: 1 / 1;
transition-behavior: allow-discrete;
transition-property: display, opacity;
transition-duration: 150ms;
transition-timing-function: ease-out;
filter: drop-shadow(0 1px 0 var(--media-current-shadow-color));
}
}
@@ -3,16 +3,11 @@
========================================================================== */
.media-default-skin .media-controls {
--media-controls-current-shadow-color: oklch(from currentColor 0 0 0 / clamp(0, calc((l - 0.5) * 0.5), 0.15));
--media-controls-current-shadow-color-subtle: oklch(
from var(--media-controls-current-shadow-color) l c h /
calc(alpha * 0.4)
);
display: flex;
column-gap: 0.075rem;
align-items: center;
padding: 0.375rem;
container: media-controls / inline-size;
text-shadow: 0 1px 0 var(--media-controls-current-shadow-color);
text-shadow: 0 1px 0 var(--media-current-shadow-color);
border-radius: 1.5rem;
}
@@ -6,15 +6,9 @@
position: relative;
}
.media-default-skin .media-icon {
display: block;
flex-shrink: 0;
grid-area: 1 / 1;
width: 18px;
height: 18px;
transition-behavior: allow-discrete;
transition-property: display, opacity;
transition-duration: 150ms;
transition-timing-function: ease-out;
width: var(--media-icon-size);
height: var(--media-icon-size);
}
.media-default-skin .media-icon--flipped {
scale: -1 1;
@@ -0,0 +1,228 @@
/* ==========================================================================
Input Feedback
========================================================================== */
.media-default-skin .media-input-feedback {
position: absolute;
inset-inline: 0;
top: 0;
bottom: 3.5rem; /* Shift up a little in smaller containers */
display: grid;
grid-template-columns: 1fr 1fr 1fr;
align-items: center;
justify-items: center;
color: var(--media-color-primary, oklch(1 0 0));
pointer-events: none;
@container media-root (width > 24rem) {
bottom: 0;
}
}
/* --- Feedback islands ------------------------------------------------------- */
.media-default-skin .media-input-feedback-island {
--media-surface-background-color: oklch(0 0 0 / 0.25);
position: absolute;
top: 0.75rem;
font-weight: 500;
color: inherit;
pointer-events: none;
border-radius: calc(Infinity * 1px);
transform-origin: top center;
transition-timing-function: ease-out;
transition-duration: 100ms;
.media-input-feedback-island__content {
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0.25rem 0.625rem;
/* Increase contrast of the content */
* {
mix-blend-mode: difference;
}
}
.media-icon {
display: none;
flex-shrink: 0;
}
.media-input-feedback-island__value {
margin-left: auto;
}
@media (pointer: coarse) {
transition-property: scale, translate, opacity;
will-change: scale, translate, opacity;
}
@media (pointer: fine) and (prefers-reduced-motion: no-preference) {
transition-property: scale, translate, filter, opacity;
will-change: scale, translate, filter, opacity;
}
@media (prefers-reduced-transparency: reduce) or (prefers-contrast: more) {
--media-surface-background-color: oklch(0 0 0);
}
/* Default hidden state */
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transition-timing-function: ease-in;
transition-duration: 250ms;
@media (pointer: fine) and (prefers-reduced-motion: no-preference) {
filter: blur(8px);
scale: 0.9;
}
@media (prefers-reduced-motion: no-preference) {
&[data-ending-style] {
translate: 0 -25%;
}
}
}
}
.media-default-skin .media-input-feedback-island--volume {
width: min(80%, 12rem);
.media-input-feedback-island__content {
--media-progress-fill: var(--media-volume-fill);
background-image: linear-gradient(
to right,
currentColor 0%,
currentColor var(--media-progress-fill),
transparent var(--media-progress-fill),
transparent 100%
);
border-radius: inherit;
transition: --media-progress-fill 200ms linear;
}
}
.media-default-skin .media-input-feedback-island--volume[data-level="high"] .media-icon--volume-high,
.media-default-skin .media-input-feedback-island--volume[data-level="low"] .media-icon--volume-low,
.media-default-skin .media-input-feedback-island--volume[data-level="off"] .media-icon--volume-off {
display: block;
}
.media-default-skin .media-input-feedback-island--status[data-status="captions-on"] .media-icon--captions-on,
.media-default-skin .media-input-feedback-island--status[data-status="captions-off"] .media-icon--captions-off,
.media-default-skin .media-input-feedback-island--status[data-status="fullscreen"] .media-icon--fullscreen-enter,
.media-default-skin .media-input-feedback-island--status[data-status="exit-fullscreen"] .media-icon--fullscreen-exit,
.media-default-skin .media-input-feedback-island--status[data-status="pip"] .media-icon--pip-enter,
.media-default-skin .media-input-feedback-island--status[data-status="exit-pip"] .media-icon--pip-exit {
display: block;
}
/* --- Boundary shake ------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
.media-default-skin .media-input-feedback-island--volume[data-min],
.media-default-skin .media-input-feedback-island--volume[data-max] {
animation: media-shake 300ms ease-in-out;
}
}
/* --- Bubble ---------------------------------------------------------------- */
.media-default-skin .media-input-feedback-bubble {
display: flex;
flex-direction: column;
grid-row: 1;
grid-column: 2; /* default to center for status bubbles and undirected seeks */
align-items: center;
justify-content: center;
padding: 1rem;
transition: opacity 250ms ease-out;
@container media-root (width > 24rem) {
padding: 2rem;
}
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transition-timing-function: ease-in;
transition-duration: 200ms;
}
}
/* Direction placement — seek bubbles move to the side implied by their direction. */
.media-default-skin .media-input-feedback-bubble[data-direction="backward"] {
grid-column: 1;
justify-self: left;
}
.media-default-skin .media-input-feedback-bubble:not([data-direction]) {
grid-column: 2;
transition-timing-function:
ease-out, linear(0, 0.12 1.5%, 1.35 9.7%, 2.2 13.9%, 3 19.9%, 2.7 21.8%, 0.62 37.5%, 0.96 50.9%, 1);
transition-duration: 600ms;
transition-property: opacity, scale;
@media (prefers-reduced-motion: reduce) {
transition: opacity 100ms ease-out;
}
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
scale: 0.8;
transition-timing-function: ease-in;
transition-duration: 200ms;
}
}
.media-default-skin .media-input-feedback-bubble[data-direction="forward"] {
grid-column: 3;
justify-self: right;
}
/* --- Bubble icons ---------------------------------------------------------- */
.media-default-skin .media-input-feedback-bubble .media-icon {
display: none;
width: 36px;
height: 36px;
}
/* seek: seek icon, flipped for backward */
.media-default-skin .media-input-feedback-bubble[data-direction] .media-icon--seek {
display: block;
}
.media-default-skin .media-input-feedback-bubble[data-direction="backward"] .media-icon--seek {
transform: scaleX(-1);
}
@media (prefers-reduced-motion: no-preference) {
.media-default-skin
.media-input-feedback-bubble[data-direction="forward"]:not([data-starting-style])
.media-icon--seek {
animation: media-slide-in-forward 300ms ease-in-out;
}
.media-default-skin
.media-input-feedback-bubble[data-direction="backward"]:not([data-starting-style])
.media-icon--seek {
animation: media-slide-in-backward 300ms ease-in-out;
}
.media-default-skin .media-input-feedback-island--status[data-status]:not([data-starting-style]) .media-icon,
.media-default-skin .media-input-feedback-bubble[data-status]:not([data-starting-style]) .media-icon {
animation: media-pop-in 250ms ease-out;
}
}
.media-default-skin .media-input-feedback-bubble[data-status="pause"] .media-icon--pause,
.media-default-skin .media-input-feedback-bubble[data-status="play"] .media-icon--play {
display: block;
}
@@ -16,6 +16,10 @@
.media-default-skin button {
font: inherit;
}
.media-default-skin [hidden][hidden] {
/* Keep authored templates hidden even when component classes set display. */
display: none;
}
@media (prefers-reduced-motion: no-preference) {
.media-default-skin {
interpolate-size: allow-keywords;
@@ -3,6 +3,9 @@
========================================================================== */
.media-default-skin {
--media-current-shadow-color: oklch(from currentColor 0 0 0 / clamp(0, calc((l - 0.5) * 0.5), 0.15));
--media-current-shadow-color-subtle: oklch(from var(--media-current-shadow-color) l c h / calc(alpha * 0.4));
--media-icon-size: 18px;
position: relative;
display: block;
width: 100%;
@@ -19,20 +22,22 @@
line-height: 1.5;
letter-spacing: normal;
outline: 2px solid transparent;
outline-offset: 2px;
outline-offset: -4px;
border-radius: var(--media-border-radius, 2rem);
isolation: isolate;
transition-timing-function: ease-out;
transition-duration: 100ms;
transition-property: outline-offset, outline-color;
&:focus-visible {
outline-color: currentColor;
outline-offset: 2px;
}
& > * {
font-size: 0.75rem; /* 12px at 100% font size */
}
@container media-root (width > 48rem) {
& > * {
@container media-root (width > 48rem) {
font-size: 0.875rem; /* 14px at 100% font size */
}
}
@@ -55,7 +55,7 @@
background-color: currentColor;
border-radius: calc(infinity * 1px);
box-shadow:
0 0 0 1px var(--media-controls-current-shadow-color-subtle, oklch(0 0 0 / 0.1)),
0 0 0 1px var(--media-current-shadow-color-subtle, oklch(0 0 0 / 0.1)),
0 1px 3px 0 oklch(0 0 0 / 0.15),
0 1px 2px -1px oklch(0 0 0 / 0.15);
opacity: 0;
+5 -2
View File
@@ -15,14 +15,17 @@
@import "./components/slider.css";
@import "./components/popup.css";
@import "./components/captions.css";
@import "./components/input-feedback.css";
@import "../../shared/css/video/icon-state.css";
@import "../../shared/global/video/keyframes.css";
@import "../../shared/global/video/properties.css";
/* ==========================================================================
Root
========================================================================== */
.media-default-skin--video {
--media-spring-transition: linear(
--media-spring-timing-function: linear(
0,
0.034 1.5%,
0.763 9.7%,
@@ -44,7 +47,7 @@
--media-controls-transition-timing-function: ease-out;
--media-error-dialog-transition-duration: 350ms;
--media-error-dialog-transition-delay: 100ms;
--media-error-dialog-transition-timing-function: var(--media-spring-transition);
--media-error-dialog-transition-timing-function: var(--media-spring-timing-function);
--media-popup-transition-duration: 100ms;
--media-popup-transition-timing-function: ease-out;
--media-tooltip-side-offset: 0.75rem;
@@ -46,7 +46,7 @@ export const root = cn(
Controls
========================================================================== */
export const controls = cn(baseControls, surface, 'text-(--media-text-color)', 'peer-data-open/error:[&_*]:invisible');
export const controls = cn(baseControls, surface, 'text-(--media-text-color)', 'peer-data-open/error:**:invisible');
/* ==========================================================================
Sliders
@@ -7,9 +7,6 @@ export const controls = cn(
'@container/media-controls',
'p-[0.375rem] flex items-center gap-x-[0.075rem]',
'rounded-3xl',
// Shadow color variables (derived from currentColor lightness)
'[--media-controls-current-shadow-color:oklch(from_currentColor_0_0_0/clamp(0,calc((l-0.5)*0.5),0.15))]',
'[--media-controls-current-shadow-color-subtle:oklch(from_var(--media-controls-current-shadow-color)_l_c_h/calc(alpha*0.4))]',
// Text shadow
'text-shadow-2xs text-shadow-(color:--media-controls-current-shadow-color)'
'text-shadow-2xs text-shadow-(color:--media-current-shadow-color)'
);
@@ -1,8 +1,8 @@
import { cn } from '@videojs/utils/style';
export const icon = cn(
'block [grid-area:1/1] size-4.5 shrink-0',
'drop-shadow-[0_1px_0_var(--media-controls-current-shadow-color,oklch(0_0_0/0.25))]',
'block [grid-area:1/1] size-(--media-icon-size) shrink-0',
'drop-shadow-[0_1px_0_var(--media-current-shadow-color)]',
'transition-discrete transition-[display,opacity] duration-150 ease-out'
);
@@ -0,0 +1,151 @@
import { cn } from '@videojs/utils/style';
/**
* NOTE: tailwind.css is required to support the `@property --media-progress-fill` registration and animation keyframes. You should import from either:
- "@videojs/html/tailwind.css" for HTML skins
- "@videojs/react/tailwind.css" for React skins
*/
export const inputFeedback = {
root: cn(
// Layout
'absolute inset-x-0 top-0 bottom-14 pointer-events-none',
'grid grid-cols-3 items-center justify-items-center',
// Shift to full extent in larger containers
'@2xl/media-root:bottom-0',
// Color
'[color:var(--media-color-primary,oklch(1_0_0))]'
),
island: {
base: cn(
'group/input-indicator',
// Surface override (darker than default)
'[--media-surface-background-color:oklch(0_0_0/0.25)]',
// Layout
'absolute top-3 rounded-full origin-top pointer-events-none',
'text-inherit font-medium',
// Transition
'duration-100 ease-out',
'data-starting-style:opacity-0',
'data-ending-style:opacity-0',
'data-starting-style:duration-250',
'data-starting-style:ease-in',
'data-ending-style:duration-250',
'data-ending-style:ease-in',
'pointer-coarse:will-change-[scale,translate,opacity]',
'pointer-coarse:transition-[scale,translate,opacity]',
'pointer-fine:motion-safe:will-change-[scale,translate,filter,opacity]',
'pointer-fine:motion-safe:transition-[scale,translate,filter,opacity]',
'pointer-fine:motion-safe:data-starting-style:blur-sm',
'pointer-fine:motion-safe:data-starting-style:scale-90',
'pointer-fine:motion-safe:data-ending-style:blur-sm',
'pointer-fine:motion-safe:data-ending-style:scale-90',
'motion-safe:data-ending-style:-translate-y-1/4',
// Reduced transparency / high contrast: solid surface background
'[@media(prefers-reduced-transparency:reduce)]:[--media-surface-background-color:oklch(0_0_0)]',
'contrast-more:[--media-surface-background-color:oklch(0_0_0)]'
),
content: cn(
'flex justify-between items-center gap-2 px-2.5 py-1 w-full',
// Increase contrast of content via blend mode
'**:mix-blend-difference'
),
// Volume island sizing + progress-fill gradient on the content child
volume: cn(
'w-[min(80%,12rem)]',
'*:[--media-progress-fill:var(--media-volume-fill)]',
'*:rounded-[inherit]',
'*:[background-image:linear-gradient(to_right,currentColor_0%,currentColor_var(--media-progress-fill),transparent_var(--media-progress-fill),transparent_100%)]',
'*:[transition:--media-progress-fill_200ms_linear]'
),
// Shown state — applied on the active item itself
shownVolume: cn(
'data-open:duration-100',
// Boundary shake (keyframes must be registered and media-shake added to @theme — see note at top)
'data-min:animate-media-shake',
'data-max:animate-media-shake',
'motion-reduce:data-min:animate-none',
'motion-reduce:data-max:animate-none'
),
shownStatus: cn('data-open:duration-100'),
// Icon inside island — hidden by default; specific icons opt in via shown* below.
icon: cn('hidden shrink-0'),
// Volume level → which icon shows
shownVolumeHigh: 'group-data-[level=high]/input-indicator:block',
shownVolumeLow: 'group-data-[level=low]/input-indicator:block',
shownVolumeOff: 'group-data-[level=off]/input-indicator:block',
// Captions state → which icon shows
shownCaptionsOn: 'group-data-[status=captions-on]/input-indicator:block',
shownCaptionsOff: 'group-data-[status=captions-off]/input-indicator:block',
shownFullscreenEnter: cn(
'group-data-[status=fullscreen]/input-indicator:block',
'motion-safe:group-not-data-starting-style/input-indicator:group-data-[status=fullscreen]/input-indicator:animate-media-pop-in'
),
shownFullscreenExit: cn(
'group-data-[status=exit-fullscreen]/input-indicator:block',
'motion-safe:group-not-data-starting-style/input-indicator:group-data-[status=exit-fullscreen]/input-indicator:animate-media-pop-in'
),
shownPipEnter: cn(
'group-data-[status=pip]/input-indicator:block',
'motion-safe:group-not-data-starting-style/input-indicator:group-data-[status=pip]/input-indicator:animate-media-pop-in'
),
shownPipExit: cn(
'group-data-[status=exit-pip]/input-indicator:block',
'motion-safe:group-not-data-starting-style/input-indicator:group-data-[status=exit-pip]/input-indicator:animate-media-pop-in'
),
value: 'ml-auto',
},
bubble: {
base: cn(
'group/input-indicator',
// Default placement — center column for status bubbles and undirected seeks
'col-start-2 row-start-1',
'flex flex-col items-center justify-center p-4',
'transition-opacity duration-250 ease-out',
'data-starting-style:opacity-0',
'data-ending-style:opacity-0',
'data-starting-style:duration-200',
'data-starting-style:ease-in',
'data-ending-style:duration-200',
'data-ending-style:ease-in',
'@2xl/media-root:p-8',
'not-data-direction:[transition-property:opacity,scale]',
'not-data-direction:duration-600',
'not-data-direction:[transition-timing-function:ease-out,linear(0,0.12_1.5%,1.35_9.7%,2.2_13.9%,3_19.9%,2.7_21.8%,0.62_37.5%,0.96_50.9%,1)]',
'motion-reduce:not-data-direction:transition-opacity',
'motion-reduce:not-data-direction:duration-100',
'motion-reduce:not-data-direction:ease-out',
'not-data-direction:data-starting-style:scale-80',
'not-data-direction:data-ending-style:scale-80',
'not-data-direction:data-starting-style:duration-200',
'not-data-direction:data-starting-style:ease-in',
'not-data-direction:data-ending-style:duration-200',
'not-data-direction:data-ending-style:ease-in',
// Direction placement
'data-[direction=backward]:col-start-1 data-[direction=backward]:justify-self-start',
'data-[direction=forward]:col-start-3 data-[direction=forward]:justify-self-end'
),
// Icons in the bubble
icon: 'hidden w-9 h-9',
// seek icon: shown for seekStep + seekToPercent; flipped for backward; slides in on active
shownSeek: cn(
'group-data-direction/input-indicator:block',
'group-data-[direction=backward]/input-indicator:-scale-x-100',
// Slide animation (keyframes registered in companion CSS)
'group-not-data-starting-style/input-indicator:group-data-[direction=forward]/input-indicator:animate-media-slide-in-forward',
'group-not-data-starting-style/input-indicator:group-data-[direction=backward]/input-indicator:animate-media-slide-in-backward',
'motion-reduce:group-data-direction/input-indicator:animate-none'
),
// togglePaused: pause icon when paused, play icon when playing
shownPause: cn(
'group-data-[status=pause]/input-indicator:block',
'motion-safe:group-not-data-starting-style/input-indicator:group-data-[status=pause]/input-indicator:animate-media-pop-in'
),
shownPlay: cn(
'group-data-[status=play]/input-indicator:block',
'motion-safe:group-not-data-starting-style/input-indicator:group-data-[status=play]/input-indicator:animate-media-pop-in'
),
time: 'tabular-nums',
},
};
@@ -0,0 +1,9 @@
import { cn } from '@videojs/utils/style';
export const reset = cn(
'**:box-border',
// Keep authored templates hidden even when component classes set display.
'[&_[hidden][hidden]]:hidden',
'[&_button]:font-[inherit]',
'motion-safe:[interpolate-size:allow-keywords]'
);
@@ -1,14 +1,21 @@
import { cn } from '@videojs/utils/style';
import { reset } from './reset';
export const root = cn(
reset,
// Layout & containment
'block relative isolate h-full w-full @container/media-root',
// Appearance
'rounded-(--media-border-radius,2rem)',
'font-[Inter_Variable,Inter,ui-sans-serif,system-ui,sans-serif] leading-normal subpixel-antialiased',
'[&>*]:text-xs @3xl/media-root:[&>*]:text-sm',
// Resets
'**:box-border',
'[&_button]:font-[inherit]',
'motion-safe:[interpolate-size:allow-keywords]'
'*:text-xs @3xl/media-root:*:text-sm',
// Focus ring
'outline-2 outline-transparent -outline-offset-4',
'transition-[outline-offset,outline-color] duration-100 ease-out',
'focus-visible:outline-current focus-visible:outline-offset-2',
// Shadow color variables (derived from currentColor lightness)
'[--media-current-shadow-color:oklch(from_currentColor_0_0_0/clamp(0,calc((l-0.5)*0.5),0.15))]',
'[--media-current-shadow-color-subtle:oklch(from_var(--media-current-shadow-color)_l_c_h/calc(alpha*0.4))]',
// Icon sizing
'[--media-icon-size:18px]'
);
@@ -40,7 +40,7 @@ export const slider = {
base: cn(
'z-10 absolute -translate-x-1/2 -translate-y-1/2',
'bg-current rounded-full',
'shadow-[0_0_0_1px_var(--media-controls-current-shadow-color-subtle,oklch(0_0_0/0.1)),0_1px_3px_0_oklch(0_0_0/0.15),0_1px_2px_-1px_oklch(0_0_0/0.15)]',
'shadow-[0_0_0_1px_var(--media-current-shadow-color-subtle,oklch(0_0_0/0.1)),0_1px_3px_0_oklch(0_0_0/0.15),0_1px_2px_-1px_oklch(0_0_0/0.15)]',
'transition-[opacity,height,width,outline-offset] duration-150 ease-out select-none',
'outline-4 outline-transparent -outline-offset-4',
'hover:outline-current/25 hover:outline-offset-0',
@@ -3,6 +3,7 @@ import { bufferingIndicator as baseBufferingIndicator } from './components/buffe
import { buttonGroup as baseButtonGroup } from './components/button-group';
import { controls as baseControls } from './components/controls';
import { error as baseError } from './components/error';
import { inputFeedback as baseInputFeedback } from './components/input-feedback';
import { popup as basePopup } from './components/popup';
import { preview as basePreview } from './components/preview';
import { root as baseRoot } from './components/root';
@@ -28,13 +29,13 @@ export const root = (isShadowDOM: boolean) =>
'[&_video]:block [&_video]:w-full [&_video]:h-full [&_video]:rounded-[inherit] [&_video]:[object-fit:var(--media-object-fit,contain)] [&_video]:[object-position:var(--media-object-position,center)]':
!isShadowDOM,
},
'[--media-spring-transition:linear(0,0.034_1.5%,0.763_9.7%,1.066_13.9%,1.198_19.9%,1.184_21.8%,0.963_37.5%,0.997_50.9%,1)]',
'[--media-spring-timing-function:linear(0,0.034_1.5%,0.763_9.7%,1.066_13.9%,1.198_19.9%,1.184_21.8%,0.963_37.5%,0.997_50.9%,1)]',
'[--media-video-border-radius:var(--media-border-radius,2rem)]',
'[--media-controls-transition-duration:100ms]',
'[--media-controls-transition-timing-function:ease-out]',
'[--media-error-dialog-transition-duration:350ms]',
'[--media-error-dialog-transition-delay:100ms]',
'[--media-error-dialog-transition-timing-function:var(--media-spring-transition)]',
'[--media-error-dialog-transition-timing-function:var(--media-spring-timing-function)]',
'[--media-popup-transition-duration:100ms]',
'[--media-popup-transition-timing-function:ease-out]',
'[--media-surface-background-color:oklch(1_0_0/0.1)]',
@@ -54,15 +55,15 @@ export const root = (isShadowDOM: boolean) =>
'contrast-more:[--media-surface-inner-border-color:oklch(1_0_0/0.25)]',
'[@media(prefers-reduced-transparency:reduce)]:[--media-surface-outer-border-color:transparent]',
'contrast-more:[--media-surface-outer-border-color:transparent]',
'[@media(pointer:fine)]:has-[[data-controls]:not([data-visible])]:[--media-controls-transition-duration:300ms]',
'[@media(pointer:coarse)]:has-[[data-controls]:not([data-visible])]:[--media-controls-transition-duration:150ms]',
'pointer-fine:has-[[data-controls]:not([data-visible])]:[--media-controls-transition-duration:300ms]',
'pointer-coarse:has-[[data-controls]:not([data-visible])]:[--media-controls-transition-duration:150ms]',
'motion-reduce:has-[[data-controls]:not([data-visible])]:[--media-controls-transition-duration:50ms]',
// Caption track CSS variables (consumed by the native caption bridge in light DOM)
'[--media-caption-track-y:-0.5rem]',
'[--media-caption-track-delay:25ms]',
'[--media-caption-track-duration:var(--media-controls-transition-duration)]',
'has-[[data-controls][data-visible]]:[--media-caption-track-y:-5.5rem]',
'@2xl/media-root:has-[[data-controls][data-visible]]:[&>*]:[--media-caption-track-y:-3.5rem]',
'@2xl/media-root:has-[[data-controls][data-visible]]:*:[--media-caption-track-y:-3.5rem]',
// Native caption track container
!isShadowDOM
? [
@@ -97,14 +98,14 @@ export const controls = cn(
'peer-data-open/error:hidden',
'ease-(--media-controls-transition-timing-function) origin-bottom',
'duration-(--media-controls-transition-duration)',
'[@media(pointer:fine)]:will-change-[scale,filter,opacity]',
'[@media(pointer:fine)]:transition-[scale,filter,opacity]',
'[@media(pointer:coarse)]:will-change-[scale,opacity]',
'[@media(pointer:coarse)]:transition-[scale,opacity]',
'pointer-fine:will-change-[scale,filter,opacity]',
'pointer-fine:transition-[scale,filter,opacity]',
'pointer-coarse:will-change-[scale,opacity]',
'pointer-coarse:transition-[scale,opacity]',
// Hidden state
'not-data-visible:pointer-events-none not-data-visible:opacity-0',
'motion-safe:not-data-visible:scale-90',
'[@media(pointer:fine)]:motion-safe:not-data-visible:blur-sm',
'pointer-fine:motion-safe:not-data-visible:blur-sm',
// Single-row layout (large)
'@2xl/media-root:bottom-3 @2xl/media-root:inset-x-3 @2xl/media-root:flex-nowrap @2xl/media-root:gap-x-0.5 @2xl/media-root:p-1'
);
@@ -141,8 +142,8 @@ export const preview = {
'opacity-0 scale-80 blur-sm origin-bottom',
'transition-[scale,opacity,filter] duration-150',
'group-data-pointing/slider:opacity-100 group-data-pointing/slider:scale-100 group-data-pointing/slider:blur-none',
'[&:has([role=img][data-hidden])]:opacity-0 [&:has([role=img][data-hidden])]:scale-80 [&:has([role=img][data-hidden])]:blur-sm',
'[&:has([role=img][data-loading])]:max-h-24',
'has-[[role=img][data-hidden]]:opacity-0 has-[[role=img][data-hidden]]:scale-80 has-[[role=img][data-hidden]]:blur-sm',
'has-[[role=img][data-loading]]:max-h-24',
surface,
basePreview.root
),
@@ -188,6 +189,18 @@ export const error = {
title: cn(baseError.title, 'text-base'),
};
/* ==========================================================================
Input Feedback (islands use video surface)
========================================================================== */
export const inputFeedback = {
...baseInputFeedback,
island: {
...baseInputFeedback.island,
base: cn(baseInputFeedback.island.base, surface),
},
};
/* ==========================================================================
Shared components (no overrides)
========================================================================== */