diff --git a/package.json b/package.json index bcf1b1f0..0e44421d 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "lint": "npm run lint:ts && npm run lint:scss", "lint:ts": "biome lint --write src", "lint:scss": "stylelint 'src/**/*.{css,scss}' --allow-empty-input --fix", - "format": "biome format --write src", + "format": "biome check --write", "dev": "storybook dev -p 3020", "test": "jest --cache", "test:watch": "npm run test -- --watch", diff --git a/src/components/AudioWaveFormDisplay/AudioWaveFormDisplay.helpers.ts b/src/components/AudioWaveFormDisplay/AudioWaveFormDisplay.helpers.ts new file mode 100644 index 00000000..5a410eae --- /dev/null +++ b/src/components/AudioWaveFormDisplay/AudioWaveFormDisplay.helpers.ts @@ -0,0 +1,88 @@ +export enum AudioWaveFormDisplaySize { + Small = 'small', + Large = 'large', +} + +export interface WaveFormBar { + /** Horizontal position of the bar (viewBox units). Shared by both endpoints since the bar is a vertical line. */ + x: number; + /** Y-coordinate of the bar's top endpoint (viewBox units). */ + yTop: number; + /** Y-coordinate of the bar's bottom endpoint (viewBox units). */ + yBottom: number; +} + +// Bar geometry traced from the original design asset. +const WAVE_FORM_VIEW_BOX_WIDTH = 92; +const WAVE_FORM_VIEW_BOX_HEIGHT = 44; +const WAVE_FORM_CENTER_Y = 21.6; +const WAVE_FORM_FIRST_BAR_X = 0.9; +const WAVE_FORM_BAR_SPACING = 3; +export const WAVE_FORM_STROKE_WIDTH = 1.8; + +// Padding around the bars, baked into the viewBox rather than CSS padding so it can't collapse +// to zero on a short/narrow container. +const WAVE_FORM_PADDING_X_RATIO = 0.15; +const WAVE_FORM_PADDING_Y_RATIO = 0.3; + +// Exported so PeakDisplay.tsx can account for this padding in its own clip-path math - otherwise +// the played/unplayed reveal "plays through" blank space before any bar is visible. +export const WAVE_FORM_PADDING_X_PERCENT = WAVE_FORM_PADDING_X_RATIO * 100; + +// Half the height of each bar (viewBox units), left to right, traced from the original asset. +const WAVE_FORM_BAR_HALF_HEIGHTS: readonly number[] = [ + 0.3, 3.3, 3.3, 6.9, 3.3, 6.9, 13.5, 20.7, 10.5, 6.9, 17.1, 13.5, 10.5, 3.3, 6.9, 3.3, 3.3, 6.9, + 10.5, 13.5, 6.9, 3.3, 3.3, 6.9, 3.3, 3.3, 6.9, 3.3, 3.3, 1.5, +]; + +const WAVE_FORM_BAR_COUNT = WAVE_FORM_BAR_HALF_HEIGHTS.length; + +// Right margin the reference asset leaves after its last bar, reused to size the large viewBox. +const WAVE_FORM_RIGHT_MARGIN = + WAVE_FORM_VIEW_BOX_WIDTH - + (WAVE_FORM_FIRST_BAR_X + (WAVE_FORM_BAR_COUNT - 1) * WAVE_FORM_BAR_SPACING); + +function buildWaveFormBars( + barCount: number, + halfHeightAt: (index: number) => number +): WaveFormBar[] { + return Array.from({ length: barCount }, (_, index) => { + const x = WAVE_FORM_FIRST_BAR_X + index * WAVE_FORM_BAR_SPACING; + const halfHeight = halfHeightAt(index); + return { x, yTop: WAVE_FORM_CENTER_Y - halfHeight, yBottom: WAVE_FORM_CENTER_Y + halfHeight }; + }); +} + +function getWaveFormViewBoxWidth(barCount: number): number { + return WAVE_FORM_FIRST_BAR_X + (barCount - 1) * WAVE_FORM_BAR_SPACING + WAVE_FORM_RIGHT_MARGIN; +} + +const SMALL_WAVE_FORM_BARS: readonly WaveFormBar[] = buildWaveFormBars( + WAVE_FORM_BAR_COUNT, + (index) => WAVE_FORM_BAR_HALF_HEIGHTS[index] +); + +// Large: the small waveform immediately followed by its own mirror, on one continuous grid. +const LARGE_WAVE_FORM_BARS: readonly WaveFormBar[] = buildWaveFormBars( + WAVE_FORM_BAR_COUNT * 2, + (index) => + WAVE_FORM_BAR_HALF_HEIGHTS[ + index < WAVE_FORM_BAR_COUNT ? index : WAVE_FORM_BAR_COUNT * 2 - 1 - index + ] +); + +export function getWaveFormBars(size: AudioWaveFormDisplaySize): readonly WaveFormBar[] { + return size === AudioWaveFormDisplaySize.Large ? LARGE_WAVE_FORM_BARS : SMALL_WAVE_FORM_BARS; +} + +// Expands the bars' bounding box to the full display box, per the padding ratios above. +export function getWaveFormViewBox(size: AudioWaveFormDisplaySize): string { + const contentWidth = getWaveFormViewBoxWidth(getWaveFormBars(size).length); + const contentHeight = WAVE_FORM_VIEW_BOX_HEIGHT; + + const width = contentWidth / (1 - 2 * WAVE_FORM_PADDING_X_RATIO); + const height = contentHeight / (1 - 2 * WAVE_FORM_PADDING_Y_RATIO); + const minX = -(width - contentWidth) / 2; + const minY = -(height - contentHeight) / 2; + return `${minX} ${minY} ${width} ${height}`; +} diff --git a/src/components/AudioWaveFormDisplay/AudioWaveFormDisplay.scss b/src/components/AudioWaveFormDisplay/AudioWaveFormDisplay.scss new file mode 100644 index 00000000..c81a4aa8 --- /dev/null +++ b/src/components/AudioWaveFormDisplay/AudioWaveFormDisplay.scss @@ -0,0 +1,22 @@ +.c-audio-wave-form-display { + display: block; + width: 100%; + height: 100%; + background-color: var(--c-audio-wave-form-display-bg, transparent); + + &__scaler { + display: block; + width: 100%; + height: 100%; + } + + &__svg { + display: block; + width: 100%; + height: 100%; + } + + &__bar { + stroke: var(--c-audio-wave-form-display-bar-color, var(--c-audio-wave-form-display-wave-color, #fff)); + } +} diff --git a/src/components/AudioWaveFormDisplay/AudioWaveFormDisplay.stories.tsx b/src/components/AudioWaveFormDisplay/AudioWaveFormDisplay.stories.tsx new file mode 100644 index 00000000..18ee8c3b --- /dev/null +++ b/src/components/AudioWaveFormDisplay/AudioWaveFormDisplay.stories.tsx @@ -0,0 +1,50 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { AudioWaveFormDisplay } from './AudioWaveFormDisplay'; +import { AudioWaveFormDisplaySize } from './AudioWaveFormDisplay.helpers'; + +const meta: Meta = { + title: 'Components/AudioWaveFormDisplay', + component: AudioWaveFormDisplay, +}; +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => ( +
+
+ +
+
+ +
+
+ ), + args: {}, +}; + +export const CustomColors: Story = { + render: () => ( +
+ +
+ ), + args: {}, +}; diff --git a/src/components/AudioWaveFormDisplay/AudioWaveFormDisplay.tsx b/src/components/AudioWaveFormDisplay/AudioWaveFormDisplay.tsx new file mode 100644 index 00000000..b39a1c8c --- /dev/null +++ b/src/components/AudioWaveFormDisplay/AudioWaveFormDisplay.tsx @@ -0,0 +1,67 @@ +import clsx from 'clsx'; +import { type CSSProperties, type FC, memo } from 'react'; +import { + AudioWaveFormDisplaySize, + getWaveFormBars, + getWaveFormViewBox, + WAVE_FORM_STROKE_WIDTH, +} from './AudioWaveFormDisplay.helpers'; +import type { AudioWaveFormDisplayProps } from './AudioWaveFormDisplay.types'; + +import './AudioWaveFormDisplay.scss'; + +// Memoized so PeakDisplay's static "inactive" waveform layer (unchanging colors/size) skips +// reconciling its ~30-60 elements on every playback timeupdate tick, which only changes +// the sibling "active" layer's clip-path, not either layer's own props. +export const AudioWaveFormDisplay: FC = memo( + function AudioWaveFormDisplay({ + className, + rootClassName: root = 'c-audio-wave-form-display', + ariaLabel, + waveColor, + backgroundColor, + size = AudioWaveFormDisplaySize.Small, + }) { + const bars = getWaveFormBars(size); + const viewBox = getWaveFormViewBox(size); + + return ( +
+ {/* Plain box for consumers to hook a hover-zoom transform onto: transitioning `transform` + on an itself doesn't animate smoothly in every browser, unlike an ordinary element. */} +
+ +
+
+ ); + } +); diff --git a/src/components/AudioWaveFormDisplay/AudioWaveFormDisplay.types.tsx b/src/components/AudioWaveFormDisplay/AudioWaveFormDisplay.types.tsx new file mode 100644 index 00000000..2d771a91 --- /dev/null +++ b/src/components/AudioWaveFormDisplay/AudioWaveFormDisplay.types.tsx @@ -0,0 +1,9 @@ +import type { DefaultComponentProps } from '../../types'; +import type { AudioWaveFormDisplaySize } from './AudioWaveFormDisplay.helpers'; + +export type AudioWaveFormDisplayProps = DefaultComponentProps & { + waveColor?: string; + backgroundColor?: string; + size?: AudioWaveFormDisplaySize; + ariaLabel?: string; +}; diff --git a/src/components/AudioWaveFormDisplay/index.ts b/src/components/AudioWaveFormDisplay/index.ts new file mode 100644 index 00000000..3717d284 --- /dev/null +++ b/src/components/AudioWaveFormDisplay/index.ts @@ -0,0 +1,6 @@ +export { AudioWaveFormDisplay } from './AudioWaveFormDisplay'; +export { + AudioWaveFormDisplaySize, + WAVE_FORM_PADDING_X_PERCENT, +} from './AudioWaveFormDisplay.helpers'; +export * from './AudioWaveFormDisplay.types'; diff --git a/src/components/Dropdown/Dropdown.test.tsx b/src/components/Dropdown/Dropdown.test.tsx index 0c876ea6..c7e7c108 100644 --- a/src/components/Dropdown/Dropdown.test.tsx +++ b/src/components/Dropdown/Dropdown.test.tsx @@ -170,4 +170,30 @@ describe('', () => { expect(dropdownFullWidthRoot).not.toHaveClass('c-dropdown__trigger'); expect(dropdownFitContentRoot).toHaveClass('c-dropdown__trigger'); }); + + it('Should render correctly with `shiftPadding` unset (default, unshifted positioning)', async () => { + // Regression: `shift` middleware used to be added unconditionally for every Dropdown + // consumer, silently changing positioning behaviour for consumers that never opted in. + const label = 'Show options'; + const children =
content item
; + const { container } = renderDropdown({ children, label, isOpen: true, id: 'test-id-5' }); + + const dropdownContent = await waitFor(() => container.querySelector('.c-dropdown')); + expect(dropdownContent).toBeInTheDocument(); + }); + + it('Should render correctly with `shiftPadding` set', async () => { + const label = 'Show options'; + const children =
content item
; + const { container } = renderDropdown({ + children, + label, + isOpen: true, + shiftPadding: 8, + id: 'test-id-6', + }); + + const dropdownContent = await waitFor(() => container.querySelector('.c-dropdown')); + expect(dropdownContent).toBeInTheDocument(); + }); }); diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index c85eaeac..250d4cb5 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -1,6 +1,8 @@ import { autoUpdate, offset as offsetHelper, + shift, + size, useClick, useDismiss, useFloating, @@ -49,6 +51,8 @@ const Dropdown: FC = ({ children, ...props }) => { variants, isDisabled, offset = 10, + shiftPadding, + maxHeightPadding, } = props; const { refs, floatingStyles, context } = useFloating({ placement, @@ -57,7 +61,29 @@ const Dropdown: FC = ({ children, ...props }) => { open ? onOpen() : onClose(); }, whileElementsMounted: autoUpdate, - middleware: [offsetHelper(offset)], + middleware: [ + offsetHelper(offset), + // `shift` nudges the flyout back within its clipping ancestor near an edge, instead of + // letting it get clipped. Opt-in via `shiftPadding` so other consumers are unaffected. + ...(shiftPadding !== undefined ? [shift({ padding: shiftPadding })] : []), + // Caps the flyout to whatever space is actually available in its clipping ancestor (e.g. + // a small video player) and viewport, scrolling its own content instead of overflowing - + // mirrors Flowplayer's native menu (`.fp-menu ol { max-height: 80%; overflow-y: auto }`). + // Opt-in via `maxHeightPadding` so other consumers are unaffected. + ...(maxHeightPadding !== undefined + ? [ + size({ + padding: maxHeightPadding, + apply({ availableHeight, elements }) { + Object.assign(elements.floating.style, { + maxHeight: `${availableHeight}px`, + overflowY: 'auto', + }); + }, + }), + ] + : []), + ], }); const click = useClick(context); diff --git a/src/components/Dropdown/Dropdown.types.ts b/src/components/Dropdown/Dropdown.types.ts index 7518299a..52f6a9f4 100644 --- a/src/components/Dropdown/Dropdown.types.ts +++ b/src/components/Dropdown/Dropdown.types.ts @@ -23,4 +23,18 @@ export interface DropdownProps extends DefaultComponentProps { triggerWidth?: 'fit-content' | 'full-width'; isDisabled?: boolean; offset?: number; + /** + * Opts into floating-ui's `shift` middleware, nudging the flyout back within its clipping + * ancestor (e.g. a container with `overflow: hidden`) instead of letting it get silently + * clipped. Off by default so existing consumers keep their current positioning unchanged; pass + * a padding value (in px) to enable it. + */ + shiftPadding?: number; + /** + * Opts into floating-ui's `size` middleware, capping the flyout's height to whatever space is + * actually available in its clipping ancestor and the viewport (scrolling its own content + * instead of overflowing past that ancestor's edge), with this many px kept clear on every + * side. Off by default so existing consumers keep their current (unconstrained) height. + */ + maxHeightPadding?: number; } diff --git a/src/components/FlowPlayer/Controls/ControlBar.scss b/src/components/FlowPlayer/Controls/ControlBar.scss new file mode 100644 index 00000000..396d90bb --- /dev/null +++ b/src/components/FlowPlayer/Controls/ControlBar.scss @@ -0,0 +1,403 @@ +@use "sass:math"; +@use "../flowplayer-shared" as *; + +$g-spacer-unit: 0.8rem; +$flowplayer-controls-radius: 99.9rem; // always fully round - clamps automatically to half the shorter side + +// Measurements below are taken from the Figma export, not assumed. + +// Play/pause and fullscreen: each a 48x36 "stadium" pill (rx=18). 36px is also the height of +// every other segment in the bar. +$flowplayer-icon-button-width: $g-spacer-unit * 6; // 4.8rem +$flowplayer-icon-button-height: $g-spacer-unit * 4.5; // 3.6rem +$flowplayer-controls-edge-offset: $g-spacer-unit; + +// Progress segment: [current time] -- gap -- [track] -- gap -- [duration], same value for the +// gap and the segment's own inline padding. +$flowplayer-progress-inline-gap: 1.68rem; +$flowplayer-progress-track-height: math.div($g-spacer-unit, 2); // 0.4rem +$flowplayer-progress-handle-size: $g-spacer-unit; +$flowplayer-track-background: #222; // unplayed track +$flowplayer-buffered-background: #505050; // buffered range + +// Volume/subtitles/speed share one pill: 4px inset on every edge, then each icon gets a 40x28 +// slot (28 = 36 - 2*4) with zero gap between adjacent slots - they sit flush against each other. +$flowplayer-slot-inset: math.div($g-spacer-unit, 2); // 0.4rem +$flowplayer-slot-width: $g-spacer-unit * 5; // 4rem +$flowplayer-slot-height: $g-spacer-unit * 3.5; // 2.8rem + +// This codebase's root font-size is 10px (62.5% reset), not the usual 16px - 0.8rem/1.6rem are the correct 8px/16px here. +$flowplayer-controls-transition: opacity 0.2s ease; +$flowplayer-focus-outline-width: 0.2rem; +$flowplayer-focus-outline-offset: 0.2rem; + +// Subtitles flyout card: 240px wide, square corners, drop shadow, "Off" row + track list. +$flowplayer-subtitles-flyout-width: 24rem; +$flowplayer-flyout-shadow: 0 0 $g-spacer-unit rgb(0 0 0 / 24%); + +// Option row: checkmark box (reserves its space even when empty) -- gap -- label (+ optional +// trailing icon), with an optional HTML sublabel below, indented to sit under the label/icon +// rather than the checkmark. One divider line between every option, not just at fixed points. +$flowplayer-flyout-option-padding-block: $g-spacer-unit * 2; +$flowplayer-flyout-option-padding-inline-start: $g-spacer-unit; +$flowplayer-flyout-option-padding-inline-end: $g-spacer-unit * 2; +$flowplayer-flyout-option-row-gap: $g-spacer-unit; +$flowplayer-flyout-option-sublabel-gap: math.div($g-spacer-unit, 2); +$flowplayer-flyout-option-icon-size: 2rem; +$flowplayer-flyout-background: #fff; +$flowplayer-flyout-option-divider-color: #dbdbdb; // silver +$flowplayer-flyout-option-text-color: #000; +$flowplayer-flyout-option-sublabel-color: #666; // "leisteen" + +@mixin flowplayer-focus-ring { + &:focus-visible { + outline: $flowplayer-focus-outline-width solid var(--flowplayer-controls-accent); + outline-offset: $flowplayer-focus-outline-offset; + } +} + +@mixin flowplayer-absolute-fill-bar($background) { + position: absolute; + top: 0; + left: 0; + height: 100%; + border-radius: $flowplayer-controls-radius; + background-color: $background; +} + +// Nested under this real ancestor so every rule here outranks Flowplayer's own +// `.flowplayer * { background-color: transparent; ... }` reset, which loads after this file. +.c-video-player-inner { + // Transparent flex row - each segment below carries its own pill background (4 independent + // groups: play/pause, progress, volume/subtitles/speed, fullscreen), not one continuous bar. + .c-flowplayer-control-bar { + position: absolute; + left: $flowplayer-controls-edge-offset; + right: $flowplayer-controls-edge-offset; + bottom: $flowplayer-controls-edge-offset; + z-index: 5; + display: flex; + align-items: center; + gap: $g-spacer-unit; // gap between the 4 top-level segments + color: var(--flowplayer-controls-fg); + transition: $flowplayer-controls-transition; + opacity: 1; + + // Flowplayer's own `.flowplayer * { color: #fff; }` reset matches every element here too - + // without re-affirming inherit, a `color` set here or via a flyout's inline color gets lost one level down. + * { + color: inherit; + } + + &--hidden { + opacity: 0; + pointer-events: none; + } + + &__segment { + display: flex; + align-items: center; + height: $flowplayer-icon-button-height; + border-radius: $flowplayer-controls-radius; + background-color: var(--flowplayer-controls-bg); + + &--icon { + flex: 0 0 auto; + + // No padding: the button itself is the full 48x36 pill, matching the reference exactly. + padding: 0; + } + + &--progress { + flex: 1 1 auto; + min-width: 0; + + // Padding/gap live on `.c-flowplayer-progress` below, not here. + } + + &--secondary { + flex: 0 0 auto; + gap: 0; // slots sit flush against each other, no inter-icon gap + padding: $flowplayer-slot-inset; + } + } + } + + // Opt-in (`customControlsConfig.showTitleOverlay`), fullscreen-only to match native mode's own + // behaviour. Fades the title/logo overlay (built by FlowPlayer.internal.tsx) off the same + // auto-hide signal as the control bar, so both fade together. + &.c-video-player-inner--show-title-overlay.is-fullscreen { + .c-title-overlay, + .c-logo-overlay { + display: block; + opacity: 1; + transition: $flowplayer-controls-transition; + } + + &.c-video-player-inner--controls-hidden { + .c-title-overlay, + .c-logo-overlay { + opacity: 0; + pointer-events: none; + } + } + } + + .c-flowplayer-control-button { + flex: 0 0 auto; + width: $flowplayer-icon-button-width; + height: $flowplayer-icon-button-height; + display: inline-flex; + align-items: center; + justify-content: center; + + // Resets a host app's own global + + ); + })} + + +
+ ); +}; diff --git a/src/components/FlowPlayer/Controls/Controls.consts.ts b/src/components/FlowPlayer/Controls/Controls.consts.ts new file mode 100644 index 00000000..6e22f4bb --- /dev/null +++ b/src/components/FlowPlayer/Controls/Controls.consts.ts @@ -0,0 +1,58 @@ +import { Locale } from '../../../types'; +import { + type FlowPlayerControlsColors, + FlowPlayerControlsLabelKey, + type FlowPlayerControlsLabels, +} from '../FlowPlayer.types'; + +export const DEFAULT_AUTO_HIDE_DELAY_MS = 3000; +export const DEFAULT_SHOW_PEAK = true; +export const DEFAULT_PEAK_MODE = 'data' as const; + +/** Whether the generic built-in waveform (`PeakDisplay`) should render - shared by ControlBar.tsx and FlowPlayer.internal.tsx so they can't drift. */ +export function isGenericPeakMode( + showPeak: boolean | undefined, + peakMode: 'data' | 'generic' | undefined +): boolean { + return (showPeak ?? DEFAULT_SHOW_PEAK) && (peakMode ?? DEFAULT_PEAK_MODE) === 'generic'; +} + +// Sensible defaults only - not "the design". A real theme is applied via the `colors` config. +export const defaultControlsColors: Required = { + backgroundColor: '#000000', + foregroundColor: '#FFFFFF', + progressColor: '#00CCA9', + accentColor: '#009991', + cuepointColor: '#009991', +}; + +// Base label sets per locale - the consumer's `labels` config overrides individual keys on top +// of whichever set `locale` resolves to. Defaults to nl, matching the rest of FlowPlayer.consts.ts. +export const FLOW_PLAYER_CONTROLS_LABELS: Record = { + [Locale.nl]: { + [FlowPlayerControlsLabelKey.Play]: 'Afspelen', + [FlowPlayerControlsLabelKey.Pause]: 'Pauzeren', + [FlowPlayerControlsLabelKey.Mute]: 'Dempen', + [FlowPlayerControlsLabelKey.Unmute]: 'Dempen opheffen', + [FlowPlayerControlsLabelKey.Volume]: 'Volume', + [FlowPlayerControlsLabelKey.FullscreenEnter]: 'Volledig scherm', + [FlowPlayerControlsLabelKey.FullscreenExit]: 'Volledig scherm sluiten', + [FlowPlayerControlsLabelKey.Subtitles]: 'Ondertitels', + [FlowPlayerControlsLabelKey.SubtitlesOff]: 'Uit', + [FlowPlayerControlsLabelKey.Speed]: 'Snelheid', + [FlowPlayerControlsLabelKey.ProgressBar]: 'Voortgang', + }, + [Locale.en]: { + [FlowPlayerControlsLabelKey.Play]: 'Play', + [FlowPlayerControlsLabelKey.Pause]: 'Pause', + [FlowPlayerControlsLabelKey.Mute]: 'Mute', + [FlowPlayerControlsLabelKey.Unmute]: 'Unmute', + [FlowPlayerControlsLabelKey.Volume]: 'Volume', + [FlowPlayerControlsLabelKey.FullscreenEnter]: 'Enter fullscreen', + [FlowPlayerControlsLabelKey.FullscreenExit]: 'Exit fullscreen', + [FlowPlayerControlsLabelKey.Subtitles]: 'Subtitles', + [FlowPlayerControlsLabelKey.SubtitlesOff]: 'Off', + [FlowPlayerControlsLabelKey.Speed]: 'Speed', + [FlowPlayerControlsLabelKey.ProgressBar]: 'Progress', + }, +}; diff --git a/src/components/FlowPlayer/Controls/Controls.icons.tsx b/src/components/FlowPlayer/Controls/Controls.icons.tsx new file mode 100644 index 00000000..394a9fa2 --- /dev/null +++ b/src/components/FlowPlayer/Controls/Controls.icons.tsx @@ -0,0 +1,113 @@ +import type { FC, SVGProps } from 'react'; + +const baseProps: SVGProps = { + xmlns: 'http://www.w3.org/2000/svg', + focusable: false, +}; + +// Traced from meemoo's own icon set, each at its own natural size (viewBox cropped tight to the +// glyph) - no forced uniform grid, flex centering in `.c-flowplayer-control-button` handles it. + +export const PlayIcon: FC = () => ( + +); + +export const PauseIcon: FC = () => ( + +); + +export const VolumeIcon: FC = () => ( + +); + +export const MuteIcon: FC = () => ( + +); + +export const FullscreenEnterIcon: FC = () => ( + +); + +export const FullscreenExitIcon: FC = () => ( + +); + +// Same regular/highlighted split as volume/mute above. + +export const SubtitlesIcon: FC = () => ( + +); + +export const SubtitlesHighlightedIcon: FC = () => ( + +); + +export const CheckIcon: FC = () => ( + +); diff --git a/src/components/FlowPlayer/Controls/FullscreenButton.tsx b/src/components/FlowPlayer/Controls/FullscreenButton.tsx new file mode 100644 index 00000000..e9c7b26b --- /dev/null +++ b/src/components/FlowPlayer/Controls/FullscreenButton.tsx @@ -0,0 +1,20 @@ +import type { FC } from 'react'; +import { Button } from '../../Button'; +import type { FlowPlayerControlsLabels } from '../FlowPlayer.types'; +import { FullscreenEnterIcon, FullscreenExitIcon } from './Controls.icons'; + +export interface FullscreenButtonProps { + isFullscreen: boolean; + onToggle: () => void; + labels: FlowPlayerControlsLabels; +} + +export const FullscreenButton: FC = ({ isFullscreen, onToggle, labels }) => ( +