From 86b2f4d3b426ce8747af234f2d0f4c4a3ec402ec Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 2 Mar 2026 11:40:58 +1100 Subject: [PATCH] feat(react): orientation-aware buffer styling and slider improvements (#671) --- .../react/src/presets/video/minimal-skin.css | 11 +++++++- .../presets/video/minimal-skin.tailwind.tsx | 27 +++++++++++++++---- .../react/src/presets/video/minimal-skin.tsx | 1 - packages/react/src/presets/video/skin.css | 5 +++- .../react/src/presets/video/skin.tailwind.tsx | 27 +++++++++++++++---- packages/react/src/presets/video/skin.tsx | 1 - packages/react/src/ui/hooks/use-slider.ts | 16 +++++------ packages/react/src/utils/use-force-render.ts | 2 ++ 8 files changed, 68 insertions(+), 22 deletions(-) diff --git a/packages/react/src/presets/video/minimal-skin.css b/packages/react/src/presets/video/minimal-skin.css index f9cfb5e4..65e7b637 100644 --- a/packages/react/src/presets/video/minimal-skin.css +++ b/packages/react/src/presets/video/minimal-skin.css @@ -528,7 +528,16 @@ .media-minimal-skin .media-slider__buffer { background-color: oklch(1 0 0 / 0.2); width: var(--media-slider-buffer, 0%); - transition: width 0.25s ease-out; + transition-duration: 0.25s; + transition-timing-function: ease-out; +} +.media-default-skin .media-slider__buffer[data-orientation="horizontal"] { + width: var(--media-slider-buffer); + transition-property: width; +} +.media-default-skin .media-slider__buffer[data-orientation="vertical"] { + height: var(--media-slider-buffer); + transition-property: height; } /* Fill */ diff --git a/packages/react/src/presets/video/minimal-skin.tailwind.tsx b/packages/react/src/presets/video/minimal-skin.tailwind.tsx index 0208d829..cace967f 100644 --- a/packages/react/src/presets/video/minimal-skin.tailwind.tsx +++ b/packages/react/src/presets/video/minimal-skin.tailwind.tsx @@ -163,16 +163,33 @@ const SliderTrack = forwardRef>(function S ); }); -const SliderFill = forwardRef>(function SliderFill({ className, ...props }, ref) { +const SliderFill = forwardRef & { type?: 'fill' | 'buffer' }>(function SliderFill( + { type = 'fill', className, ...props }, + ref +) { return (
}> }> } /> - + } /> } /> diff --git a/packages/react/src/presets/video/minimal-skin.tsx b/packages/react/src/presets/video/minimal-skin.tsx index 6de868fd..f298a0d9 100644 --- a/packages/react/src/presets/video/minimal-skin.tsx +++ b/packages/react/src/presets/video/minimal-skin.tsx @@ -189,7 +189,6 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode { )} - {...props} /> } /> diff --git a/packages/react/src/presets/video/skin.css b/packages/react/src/presets/video/skin.css index 7ed7f10e..2ebfdbfa 100644 --- a/packages/react/src/presets/video/skin.css +++ b/packages/react/src/presets/video/skin.css @@ -543,13 +543,16 @@ /* Buffer */ .media-default-skin .media-slider__buffer { background-color: oklch(1 0 0 / 0.2); - transition: width 0.25s ease-out; + transition-duration: 0.25s; + transition-timing-function: ease-out; } .media-default-skin .media-slider__buffer[data-orientation="horizontal"] { width: var(--media-slider-buffer); + transition-property: width; } .media-default-skin .media-slider__buffer[data-orientation="vertical"] { height: var(--media-slider-buffer); + transition-property: height; } /* Fill */ diff --git a/packages/react/src/presets/video/skin.tailwind.tsx b/packages/react/src/presets/video/skin.tailwind.tsx index 8800308f..91a48d27 100644 --- a/packages/react/src/presets/video/skin.tailwind.tsx +++ b/packages/react/src/presets/video/skin.tailwind.tsx @@ -177,16 +177,33 @@ const SliderTrack = forwardRef>(function S ); }); -const SliderFill = forwardRef>(function SliderFill({ className, ...props }, ref) { +const SliderFill = forwardRef & { type?: 'fill' | 'buffer' }>(function SliderFill( + { type = 'fill', className, ...props }, + ref +) { return (
}> }> } /> - + } /> } /> diff --git a/packages/react/src/presets/video/skin.tsx b/packages/react/src/presets/video/skin.tsx index a993e747..2027ff91 100644 --- a/packages/react/src/presets/video/skin.tsx +++ b/packages/react/src/presets/video/skin.tsx @@ -184,7 +184,6 @@ export function VideoSkin(props: VideoSkinProps): ReactNode { )} - {...props} /> } /> diff --git a/packages/react/src/ui/hooks/use-slider.ts b/packages/react/src/ui/hooks/use-slider.ts index 866c6e90..81835fdd 100644 --- a/packages/react/src/ui/hooks/use-slider.ts +++ b/packages/react/src/ui/hooks/use-slider.ts @@ -84,20 +84,20 @@ export function useSlider( // Cleanup on unmount. useEffect(() => () => slider.destroy(), [slider]); - // Force a synchronous re-render after mount so edge thumb alignment - // can read DOM measurements from the now-populated element refs. - useLayoutEffect(() => { - if (rootElementRef.current && thumbElementRef.current) { - forceRender(); - } - }, []); - // Subscribe to interaction state. const interaction = useSnapshot(slider.interaction); // Compute derived state from interaction + caller-provided projection. const state = options.computeState(interaction); + // Force a synchronous re-render after mount so edge thumb alignment + // can read DOM measurements from the now-populated element refs. + useLayoutEffect(() => { + if (state.thumbAlignment === 'edge' && rootElementRef.current && thumbElementRef.current) { + forceRender(); + } + }, [state.thumbAlignment]); + // Adjust CSS var percents for edge thumb alignment when DOM elements are available. const rootEl = rootElementRef.current; const thumbEl = thumbElementRef.current; diff --git a/packages/react/src/utils/use-force-render.ts b/packages/react/src/utils/use-force-render.ts index 181151bc..6e6a16be 100644 --- a/packages/react/src/utils/use-force-render.ts +++ b/packages/react/src/utils/use-force-render.ts @@ -1,3 +1,5 @@ +'use client'; + import { useReducer } from 'react'; export function useForceRender() {