diff --git a/packages/icons/scripts/build.ts b/packages/icons/scripts/build.ts index f016c445..f46fca80 100644 --- a/packages/icons/scripts/build.ts +++ b/packages/icons/scripts/build.ts @@ -25,19 +25,16 @@ const SVGO_CONFIG: Config = { params: { overrides: { removeViewBox: false, + convertColors: { + currentColor: /^black$/, + }, }, }, }, { name: 'removeAttrs', params: { - attrs: ['^fill$', '^stroke$', '^clip-rule$', '^fill-rule$'], - }, - }, - { - name: 'addAttributesToSVGElement', - params: { - attributes: [{ fill: 'currentColor' }], + attrs: ['^clip-rule$', '^fill-rule$'], }, }, ], @@ -64,7 +61,10 @@ function getSvgFiles(setName: string): string[] { } function optimizeSvg(svgContent: string): string { - return optimize(svgContent, SVGO_CONFIG).data; + const optimized = optimize(svgContent, SVGO_CONFIG).data; + return optimized + .replaceAll('fill="black"', 'fill="currentColor"') + .replaceAll('stroke="black"', 'stroke="currentColor"'); } async function buildReactComponent(svgContent: string, componentName: string): Promise<{ js: string; tsx: string }> { diff --git a/packages/icons/src/assets/default/captions-off.svg b/packages/icons/src/assets/default/captions-off.svg new file mode 100644 index 00000000..a64feb4d --- /dev/null +++ b/packages/icons/src/assets/default/captions-off.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/icons/src/assets/default/captions-on.svg b/packages/icons/src/assets/default/captions-on.svg new file mode 100644 index 00000000..c0d904f6 --- /dev/null +++ b/packages/icons/src/assets/default/captions-on.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/icons/src/assets/minimal/captions-off.svg b/packages/icons/src/assets/minimal/captions-off.svg new file mode 100644 index 00000000..f0a6fdec --- /dev/null +++ b/packages/icons/src/assets/minimal/captions-off.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/icons/src/assets/minimal/captions-on.svg b/packages/icons/src/assets/minimal/captions-on.svg new file mode 100644 index 00000000..3aded322 --- /dev/null +++ b/packages/icons/src/assets/minimal/captions-on.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/react/src/presets/video/minimal-skin.css b/packages/react/src/presets/video/minimal-skin.css index de1abc5c..83680a21 100644 --- a/packages/react/src/presets/video/minimal-skin.css +++ b/packages/react/src/presets/video/minimal-skin.css @@ -103,7 +103,7 @@ inset: 0; border-radius: inherit; background-image: linear-gradient(to top, oklch(0 0 0 / 0.7), oklch(0 0 0 / 0.5) 7.5rem, oklch(0 0 0 / 0)); - backdrop-filter: blur(0) saturate(1.5) brightness(0.9); + backdrop-filter: blur(0) saturate(1.2) brightness(0.9); opacity: 0; transition-property: opacity, backdrop-filter; transition-duration: 500ms; @@ -123,7 +123,7 @@ transition-delay: 0ms; } .media-minimal-skin .media-error[data-visible] ~ .media-overlay { - backdrop-filter: blur(8px) saturate(1.5) brightness(0.9); + backdrop-filter: blur(8px) saturate(1.2) brightness(0.9); } /* ========================================================================== diff --git a/packages/react/src/presets/video/minimal-skin.tailwind.tsx b/packages/react/src/presets/video/minimal-skin.tailwind.tsx index 16d7974e..9b4e87df 100644 --- a/packages/react/src/presets/video/minimal-skin.tailwind.tsx +++ b/packages/react/src/presets/video/minimal-skin.tailwind.tsx @@ -1,5 +1,7 @@ import type { FullscreenButtonState, MuteButtonState, PlayButtonState } from '@videojs/core'; import { + CaptionsOffIcon, + CaptionsOnIcon, FullscreenEnterIcon, FullscreenExitIcon, PauseIcon, @@ -16,6 +18,7 @@ import { cn } from '@videojs/utils/style'; import { type ComponentProps, forwardRef, type ReactNode } from 'react'; import { Container } from '@/player/context'; import { BufferingIndicator } from '@/ui/buffering-indicator'; +import { CaptionsButton, type CaptionsButtonState } from '@/ui/captions-button'; import { Controls } from '@/ui/controls'; import { ErrorDialog } from '@/ui/error-dialog'; import { FullscreenButton } from '@/ui/fullscreen-button'; @@ -98,6 +101,16 @@ function MuteButtonIcon({ state, className, ...rest }: { state: MuteButtonState ); } +function CaptionsButtonIcon({ state, className, ...rest }: { state: CaptionsButtonState } & ComponentProps<'svg'>) { + const { active } = state; + return ( + <> + + + + ); +} + function FullscreenButtonIcon({ state, className, ...rest }: { state: FullscreenButtonState } & ComponentProps<'svg'>) { const { fullscreen } = state; return ( @@ -278,6 +291,14 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod )} /> + ( + + )} + /> + ( + )} + /> + ( + )} + /> + ( + )} + /> + ( + * )} + * /> + * ``` + */ +export const CaptionsButton = forwardRef(function CaptionsButton( + componentProps: CaptionsButtonProps, + forwardedRef: ForwardedRef +) { + const { render, className, style, label, disabled, ...elementProps } = componentProps; + + // FIXME: Replace with actual captions state + const [isActive, setIsActive] = useState(false); + + const { getButtonProps, buttonRef } = useButton({ + displayName: 'CaptionsButton', + onActivate: () => setIsActive((active) => !active), // FIXME: Replace with actual toggle logic + isDisabled: () => disabled ?? false, + }); + + return renderElement( + 'button', + { render, className, style }, + { + state: { active: isActive }, // FIXME: Replace with actual toggle logic + stateAttrMap: CaptionButtonDataAttrs, + ref: [forwardedRef, buttonRef], + props: [elementProps, getButtonProps(), { 'aria-pressed': isActive }], + } + ); +}); + +export namespace CaptionsButton { + export type Props = CaptionsButtonProps; + export type State = CaptionsButtonState; +} diff --git a/packages/react/src/ui/captions-button/index.ts b/packages/react/src/ui/captions-button/index.ts new file mode 100644 index 00000000..2328b961 --- /dev/null +++ b/packages/react/src/ui/captions-button/index.ts @@ -0,0 +1 @@ +export { CaptionsButton, type CaptionsButtonProps, type CaptionsButtonState } from './captions-button';