@design-edito/tools 0.4.11 → 0.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/agnostic/arrays/index.d.ts +1 -1
  2. package/agnostic/arrays/index.js +1 -1
  3. package/agnostic/colors/index.d.ts +2 -2
  4. package/agnostic/colors/index.js +2 -2
  5. package/agnostic/css/index.d.ts +1 -1
  6. package/agnostic/css/index.js +1 -1
  7. package/agnostic/html/deep-select/index.d.ts +31 -0
  8. package/agnostic/html/deep-select/index.js +52 -0
  9. package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +14 -14
  10. package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +14 -14
  11. package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +2 -2
  12. package/agnostic/html/hyper-json/smart-tags/isolated/index.js +2 -2
  13. package/agnostic/html/index.d.ts +3 -1
  14. package/agnostic/html/index.js +3 -1
  15. package/agnostic/html/watch-selection/index.d.ts +41 -0
  16. package/agnostic/html/watch-selection/index.js +50 -0
  17. package/agnostic/index.d.ts +3 -3
  18. package/agnostic/index.js +3 -3
  19. package/agnostic/misc/index.d.ts +3 -3
  20. package/agnostic/misc/index.js +3 -3
  21. package/agnostic/misc/logs/index.d.ts +1 -1
  22. package/agnostic/misc/logs/index.js +1 -1
  23. package/agnostic/numbers/index.d.ts +2 -2
  24. package/agnostic/numbers/index.js +2 -2
  25. package/agnostic/objects/index.d.ts +3 -3
  26. package/agnostic/objects/index.js +3 -3
  27. package/agnostic/random/index.d.ts +1 -1
  28. package/agnostic/random/index.js +1 -1
  29. package/agnostic/sanitization/index.d.ts +1 -1
  30. package/agnostic/sanitization/index.js +1 -1
  31. package/agnostic/strings/index.d.ts +3 -2
  32. package/agnostic/strings/index.js +3 -2
  33. package/agnostic/strings/split-trim/index.d.ts +27 -0
  34. package/agnostic/strings/split-trim/index.js +36 -0
  35. package/components/Video/index.controlled.d.ts +153 -0
  36. package/components/Video/index.controlled.js +255 -0
  37. package/components/Video/index.d.ts +10 -114
  38. package/components/Video/index.js +140 -265
  39. package/components/Video/utils.d.ts +11 -10
  40. package/components/Video/utils.js +30 -37
  41. package/components/public-classnames.d.ts +1 -0
  42. package/components/public-classnames.js +1 -0
  43. package/index.d.ts +1 -1
  44. package/index.js +1 -1
  45. package/node/@google-cloud/storage/file/index.d.ts +2 -2
  46. package/node/@google-cloud/storage/file/index.js +2 -2
  47. package/node/ftps/file/index.d.ts +2 -2
  48. package/node/ftps/file/index.js +2 -2
  49. package/node/images/transform/operations/index.d.ts +1 -1
  50. package/node/images/transform/operations/index.js +1 -1
  51. package/node/index.d.ts +1 -1
  52. package/node/index.js +1 -1
  53. package/package.json +22 -1
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Splits a string into segments, trims whitespace from each part, and optionally removes empty segments.
3
+ *
4
+ * Each segment is trimmed with `String.prototype.trim`. When `removeEmpty` is `true`, segments that
5
+ * become empty after trimming are filtered out.
6
+ *
7
+ * When `splitter` is an array, each separator is applied in order: every current segment is split
8
+ * with the next separator before moving on to the following one.
9
+ *
10
+ * @param toSplit - The string to split.
11
+ * @param splitter - A single separator, or an ordered list of separators passed to `String.prototype.split`.
12
+ * @param removeEmpty - When `true`, drops segments that are empty after trimming. Defaults to `false`.
13
+ * @returns An array of trimmed string segments.
14
+ *
15
+ * @example
16
+ * splitTrim(' foo , bar , baz ', ',')
17
+ * // => ['foo', 'bar', 'baz']
18
+ *
19
+ * @example
20
+ * splitTrim('foo,,bar', ',', true)
21
+ * // => ['foo', 'bar']
22
+ *
23
+ * @example
24
+ * splitTrim('foo bar,baz', [' ', ','])
25
+ * // => ['foo', 'bar', 'baz']
26
+ */
27
+ export declare const splitTrim: (toSplit: string, splitter: string | RegExp | Array<string | RegExp>, removeEmpty?: boolean) => string[];
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Splits a string into segments, trims whitespace from each part, and optionally removes empty segments.
3
+ *
4
+ * Each segment is trimmed with `String.prototype.trim`. When `removeEmpty` is `true`, segments that
5
+ * become empty after trimming are filtered out.
6
+ *
7
+ * When `splitter` is an array, each separator is applied in order: every current segment is split
8
+ * with the next separator before moving on to the following one.
9
+ *
10
+ * @param toSplit - The string to split.
11
+ * @param splitter - A single separator, or an ordered list of separators passed to `String.prototype.split`.
12
+ * @param removeEmpty - When `true`, drops segments that are empty after trimming. Defaults to `false`.
13
+ * @returns An array of trimmed string segments.
14
+ *
15
+ * @example
16
+ * splitTrim(' foo , bar , baz ', ',')
17
+ * // => ['foo', 'bar', 'baz']
18
+ *
19
+ * @example
20
+ * splitTrim('foo,,bar', ',', true)
21
+ * // => ['foo', 'bar']
22
+ *
23
+ * @example
24
+ * splitTrim('foo bar,baz', [' ', ','])
25
+ * // => ['foo', 'bar', 'baz']
26
+ */
27
+ export const splitTrim = (toSplit, splitter, removeEmpty = false) => {
28
+ const splitters = Array.isArray(splitter) ? splitter : [splitter];
29
+ let returned = [toSplit];
30
+ for (const s of splitters) {
31
+ returned = returned.flatMap(e => e.split(s));
32
+ }
33
+ return returned
34
+ .map(e => e.trim())
35
+ .filter(e => !removeEmpty || e !== '');
36
+ };
@@ -0,0 +1,153 @@
1
+ import { type FunctionComponent, type PropsWithChildren, type VideoHTMLAttributes, type ReactEventHandler } from 'react';
2
+ import { type WithClassName } from '../utils/types.js';
3
+ import { type Props as SubsProps } from '../Subtitles/index.js';
4
+ /**
5
+ * Describes a single video source.
6
+ *
7
+ * @property src - URL of the video file.
8
+ * @property type - MIME type of the source (e.g. `'video/mp4'`).
9
+ */
10
+ type SourceData = {
11
+ src?: string;
12
+ type?: string;
13
+ };
14
+ /**
15
+ * Describes a single text track (subtitles, captions, chapters, etc.).
16
+ *
17
+ * @property src - URL of the track file.
18
+ * @property kind - Track type, maps directly to the `<track>` `kind` attribute.
19
+ * @property srclang - Language of the track content (e.g. `'fr'`, `'en'`).
20
+ * @property label - Human-readable label shown in the browser's track selector.
21
+ * @property default - When `true`, marks this track as the default selection.
22
+ */
23
+ type TrackData = {
24
+ src?: string;
25
+ kind?: 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata';
26
+ srclang?: string;
27
+ label?: string;
28
+ default?: boolean;
29
+ };
30
+ /**
31
+ * Callbacks for user actions on the video player controls.
32
+ * Allows you to intercept user actions on the player's buttons and sliders.
33
+ *
34
+ * @property playButtonClick - Called when the play button is clicked. Receives the event, isPlaying state and HTMLVideoElement before any change happens. If the time is not controlled by parent (no currentTime given as props), the component will play to the target time right after.
35
+ * @property pauseButtonClick - Called when the pause button is clicked. Receives the event, isPlaying state and HTMLVideoElement before any change happens. If the time is not controlled by parent (no currentTime given as props), the component will pause to the target time right after.
36
+ * @property loudButtonClick - Called when the "loud" (unmute) button is clicked. Receives the event, isLoud state and HTMLVideoElement before any change happens.
37
+ * @property muteButtonClick - Called when the mute button is clicked. Receives the event, isLoud state and HTMLVideoElement before any change happens.
38
+ * @property volumeRangeChange - Called when the volume slider is changed. Receives the event, target volume (0 to 1), current volume (0 to 1) and HTMLVideoElement before any change happens.
39
+ * @property fullscreenButtonClick - Called when the fullscreen button is clicked. Receives the event, isFullscreen state and HTMLVideoElement before any change happens.
40
+ * @property rateRangeChange - Called when the playback rate slider is changed. Receives the event, target rate, current rate and HTMLVideoElement before any change happens.
41
+ * @property timelineClick - Called when the timeline is clicked. Receives the event, target time (in seconds), current time (in seconds) and HTMLVideoElement before any change happens. If the time is not controlled by parent (no currentTime given as props), the component will update the video current time to the target time right after.
42
+ */
43
+ export type ActionHandlersProps = {
44
+ playButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isPlaying: boolean, video: HTMLVideoElement | null) => void;
45
+ pauseButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isPlaying: boolean, video: HTMLVideoElement | null) => void;
46
+ loudButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isLoud: boolean, video: HTMLVideoElement | null) => void;
47
+ muteButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isLoud: boolean, video: HTMLVideoElement | null) => void;
48
+ volumeRangeChange?: (e: React.ChangeEvent<HTMLInputElement>, targetVolumePercent: number, currentVolumePercent: number, video: HTMLVideoElement | null) => void;
49
+ fullscreenButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isFullscreen: boolean, video: HTMLVideoElement | null) => void;
50
+ rateRangeChange?: (e: React.ChangeEvent<HTMLInputElement>, targetRate: number, rate: number, video: HTMLVideoElement | null) => void;
51
+ timelineClick?: (e: React.MouseEvent<HTMLDivElement>, time: number, currentTime: number, video: HTMLVideoElement | null) => void;
52
+ };
53
+ /**
54
+ * Callbacks to synchronize the internal player state with the outside.
55
+ *
56
+ * @property isPlaying - Called whenever the play/pause state changes.
57
+ * @property isFullscreen - Called whenever the fullscreen state changes.
58
+ * @property isLoud - Called whenever the mute/unmute state changes (true = unmuted, false = muted).
59
+ * @property volume - Called whenever the volume changes (value between 0 and 1).
60
+ * @property playbackRate - Called whenever the playback speed changes.
61
+ * @property currentTime - Called on every change of the current time (in seconds).
62
+ */
63
+ export type StateHandlersProps = {
64
+ isPlaying?: (isPlaying: boolean) => void;
65
+ isFullscreen?: (isFullscreen: boolean) => void;
66
+ isLoud?: (isLoud: boolean) => void;
67
+ volume?: (volume: number) => void;
68
+ playbackRate?: (rate: number) => void;
69
+ currentTime?: (currentTime: number) => void;
70
+ };
71
+ /**
72
+ * Props for the ControlledVideo component.
73
+ *
74
+ * @property sources - List of video sources (string, array of strings, or SourceData objects).
75
+ * @property tracks - List of tracks (subtitles, captions, etc.), string, array of strings, or TrackData objects.
76
+ * @property subtitles - Props for the Subtitles component.
77
+ * @property playBtnContent - React content for the play button.
78
+ * @property pauseBtnContent - React content for the pause button.
79
+ * @property loudBtnContent - React content for the "loud" (unmute) button.
80
+ * @property muteBtnContent - React content for the mute button.
81
+ * @property fullscreenBtnContent - React content for the fullscreen button.
82
+ * @property play - External control of play state (true = play, false = pause).
83
+ * @property fullscreen - External control of fullscreen mode.
84
+ * @property volume - External control of volume (0 to 1).
85
+ * @property mute - External control of mute (true = muted).
86
+ * @property playbackRate - External control of playback speed.
87
+ * @property currentTimeMs - External control of current time (in ms). If given, the component considers the current time to be controlled by the parent, and will not attempt to update it internally on user interactions (play, timeline click, etc.), leaving it up to the parent to update this prop accordingly.
88
+ * @property actionHandlers - Callbacks for user actions on the controls.
89
+ * @property stateHandlers - Callbacks to synchronize internal state with the outside.
90
+ * @property onFullscreenChange - Callback for native fullscreen mode changes.
91
+ * @property _modifiers - Optional CSS modifiers for the root element.
92
+ * @property className - Additional CSS class for the root element.
93
+ * @property children - React content inserted into the <video> tag (fallback, etc).
94
+ *
95
+ * Also inherits all standard HTML props for a <video> element.
96
+ */
97
+ export type Props = PropsWithChildren<WithClassName<{
98
+ sources?: string | string[] | SourceData[];
99
+ tracks?: string | string[] | TrackData[];
100
+ subtitles?: SubsProps;
101
+ playBtnContent?: React.ReactNode;
102
+ pauseBtnContent?: React.ReactNode;
103
+ loudBtnContent?: React.ReactNode;
104
+ muteBtnContent?: React.ReactNode;
105
+ fullscreenBtnContent?: React.ReactNode;
106
+ play?: boolean;
107
+ fullscreen?: boolean;
108
+ volume?: number;
109
+ mute?: boolean;
110
+ playbackRate?: number;
111
+ currentTimeMs?: number;
112
+ onFullscreenChange?: ReactEventHandler<HTMLVideoElement>;
113
+ actionHandlers?: ActionHandlersProps;
114
+ stateHandlers?: StateHandlersProps;
115
+ _modifiers?: Record<string, boolean>;
116
+ }> & VideoHTMLAttributes<HTMLVideoElement>>;
117
+ /**
118
+ * Full-featured video player component. Wraps a native `<video>` element with
119
+ * playback controls, volume, playback rate, a timeline, optional subtitles
120
+ * and viewport-driven auto-play/mute behaviours.
121
+ *
122
+ * ### Root element modifiers
123
+ * The root `<figure>` receives the public class name defined by `video` and
124
+ * the following BEM-style modifier classes:
125
+ * - `--play-on` / `--play-off` — reflects current playback state.
126
+ * - `--fullscreen-on` / `--fullscreen-off` — reflects fullscreen state.
127
+ * - `--loud` / `--muted` — reflects mute state.
128
+ *
129
+ * ### Data attributes on the root element
130
+ * - `data-play-on` — present (empty string) when playing.
131
+ * - `data-play-off` — present (empty string) when paused.
132
+ * - `data-fullscreen-on` — present (empty string) when in fullscreen.
133
+ * - `data-fullscreen-off` — present (empty string) when not in fullscreen.
134
+ * - `data-loud` — present (empty string) when unmuted.
135
+ * - `data-muted` — present (empty string) when muted.
136
+ * - `data-volume` — current volume as a `0–1` float.
137
+ * - `data-volume-percent` — current volume as a `0–100` float.
138
+ * - `data-playback-rate` — current playback rate (e.g. `1`, `1.5`).
139
+ * - `data-current-time-ms` — current time in milliseconds, fixed to 2 decimals.
140
+ * - `data-current-time-ratio` — current / total ratio, fixed to 8 decimals.
141
+ * - `data-total-time-ms` — total duration in milliseconds.
142
+ *
143
+ * ### CSS custom properties on the root element
144
+ * - `--video-current-time-ratio` — current / total ratio, fixed to 8 decimals.
145
+ * Useful for driving progress-bar animations purely in CSS.
146
+ *
147
+ * @param props - Component properties.
148
+ * @see {@link Props}
149
+ * @returns A `<figure>` element containing the video, its controls, optional
150
+ * subtitles.
151
+ */
152
+ export declare const ControlledVideo: FunctionComponent<Props>;
153
+ export {};
@@ -0,0 +1,255 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useRef, useState, useCallback, useEffect } from 'react';
3
+ import { clss } from '../../agnostic/css/clss/index.js';
4
+ import { mergeClassNames } from '../utils/index.js';
5
+ import cssModule from './styles.module.css';
6
+ import { video as publicClassName } from '../public-classnames.js';
7
+ import { Subtitles } from '../Subtitles/index.js';
8
+ import { forceExitFullscreen, forceFullscreen, forceLoud, forceMute, forcePause, forcePlay, forcePlaybackRate, forceVolume, formatTime, getTimelineClickProgress, msToSeconds, secondsToMs } from './utils.js';
9
+ /**
10
+ * Full-featured video player component. Wraps a native `<video>` element with
11
+ * playback controls, volume, playback rate, a timeline, optional subtitles
12
+ * and viewport-driven auto-play/mute behaviours.
13
+ *
14
+ * ### Root element modifiers
15
+ * The root `<figure>` receives the public class name defined by `video` and
16
+ * the following BEM-style modifier classes:
17
+ * - `--play-on` / `--play-off` — reflects current playback state.
18
+ * - `--fullscreen-on` / `--fullscreen-off` — reflects fullscreen state.
19
+ * - `--loud` / `--muted` — reflects mute state.
20
+ *
21
+ * ### Data attributes on the root element
22
+ * - `data-play-on` — present (empty string) when playing.
23
+ * - `data-play-off` — present (empty string) when paused.
24
+ * - `data-fullscreen-on` — present (empty string) when in fullscreen.
25
+ * - `data-fullscreen-off` — present (empty string) when not in fullscreen.
26
+ * - `data-loud` — present (empty string) when unmuted.
27
+ * - `data-muted` — present (empty string) when muted.
28
+ * - `data-volume` — current volume as a `0–1` float.
29
+ * - `data-volume-percent` — current volume as a `0–100` float.
30
+ * - `data-playback-rate` — current playback rate (e.g. `1`, `1.5`).
31
+ * - `data-current-time-ms` — current time in milliseconds, fixed to 2 decimals.
32
+ * - `data-current-time-ratio` — current / total ratio, fixed to 8 decimals.
33
+ * - `data-total-time-ms` — total duration in milliseconds.
34
+ *
35
+ * ### CSS custom properties on the root element
36
+ * - `--video-current-time-ratio` — current / total ratio, fixed to 8 decimals.
37
+ * Useful for driving progress-bar animations purely in CSS.
38
+ *
39
+ * @param props - Component properties.
40
+ * @see {@link Props}
41
+ * @returns A `<figure>` element containing the video, its controls, optional
42
+ * subtitles.
43
+ */
44
+ export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pauseBtnContent, loudBtnContent, muteBtnContent, fullscreenBtnContent, play, fullscreen, mute, muted, volume = 1, playbackRate = 1, currentTimeMs: givenCurrentTimeMs, actionHandlers, stateHandlers, children, className, _modifiers, ...intrinsicVideoAttributes }) => {
45
+ const videoRef = useRef(null);
46
+ const [totalTime, setTotalTime] = useState(0);
47
+ const totalTimeMs = useMemo(() => secondsToMs(totalTime), [totalTime]);
48
+ const [currentTimeMs, setCurrentTimeMs] = useState(0);
49
+ const currentTime = useMemo(() => msToSeconds(currentTimeMs), [currentTimeMs]);
50
+ const isTimeControlled = useMemo(() => givenCurrentTimeMs !== undefined, [givenCurrentTimeMs]);
51
+ const volumePercent = useMemo(() => volume * 100, [volume]);
52
+ // Intrinsic event handler
53
+ const handleMetadataLoadEvent = useCallback((e) => {
54
+ if (videoRef.current === null)
55
+ return;
56
+ const video = videoRef.current;
57
+ setTotalTime(Number(video.duration));
58
+ intrinsicVideoAttributes.onLoadedMetadata?.(e);
59
+ }, [intrinsicVideoAttributes.onLoadedMetadata]);
60
+ const handleOnTimeUpdateEvent = useCallback((e) => {
61
+ const video = e.currentTarget;
62
+ const newTimeMs = secondsToMs(Number(video.currentTime));
63
+ setCurrentTimeMs(newTimeMs);
64
+ if (intrinsicVideoAttributes?.onTimeUpdate !== undefined) {
65
+ intrinsicVideoAttributes.onTimeUpdate(e);
66
+ }
67
+ }, [intrinsicVideoAttributes.onTimeUpdate]);
68
+ const handleOnPlayEvent = useCallback((e) => {
69
+ /* We must never play a video that has controlled time */
70
+ if (isTimeControlled) {
71
+ videoRef.current?.pause();
72
+ return;
73
+ }
74
+ intrinsicVideoAttributes.onPlay?.(e);
75
+ }, [isTimeControlled, intrinsicVideoAttributes.onPlay]);
76
+ // Custom action handlers
77
+ const handlePlayButtonClick = useCallback((e) => {
78
+ const isPlaying = videoRef.current !== null ? Boolean(videoRef.current.paused) : false;
79
+ actionHandlers?.playButtonClick?.(e, isPlaying, videoRef.current);
80
+ }, [actionHandlers?.playButtonClick]);
81
+ const handlePauseButtonClick = useCallback((e) => {
82
+ const isPlaying = videoRef.current !== null ? Boolean(videoRef.current.paused) : false;
83
+ actionHandlers?.pauseButtonClick?.(e, isPlaying, videoRef.current);
84
+ }, [actionHandlers?.pauseButtonClick]);
85
+ const handleLoudButtonClick = useCallback((e) => {
86
+ const isLoud = mute !== undefined ? !mute : false;
87
+ actionHandlers?.loudButtonClick?.(e, isLoud, videoRef.current);
88
+ }, [actionHandlers?.loudButtonClick, mute]);
89
+ const handleMuteButtonClick = useCallback((e) => {
90
+ const isLoud = mute !== undefined ? !mute : false;
91
+ actionHandlers?.muteButtonClick?.(e, isLoud, videoRef.current);
92
+ }, [actionHandlers?.muteButtonClick, mute]);
93
+ const handleFullscreenButtonClick = useCallback((e) => {
94
+ actionHandlers?.fullscreenButtonClick?.(e, fullscreen ?? false, videoRef.current);
95
+ }, [actionHandlers?.fullscreenButtonClick, fullscreen]);
96
+ const handleVolumeRangeChange = useCallback((e) => {
97
+ const targetVolume = Number(e.currentTarget.value) / 100;
98
+ actionHandlers?.volumeRangeChange?.(e, targetVolume, volume, videoRef.current);
99
+ }, [actionHandlers?.volumeRangeChange, volume]);
100
+ const handleRateRangeChange = useCallback((e) => {
101
+ actionHandlers?.rateRangeChange?.(e, Number(e.currentTarget.value), playbackRate, videoRef.current);
102
+ }, [actionHandlers?.rateRangeChange, playbackRate]);
103
+ const handleTimelineClick = useCallback((e) => {
104
+ if (videoRef.current === null)
105
+ return;
106
+ const progress = getTimelineClickProgress(e, e.currentTarget, videoRef.current);
107
+ const targetTime = progress * totalTime;
108
+ actionHandlers?.timelineClick?.(e, targetTime, currentTime, videoRef.current);
109
+ /* If we are given a currentTimeMs prop, we consider that the current time is controlled by the parent component, and we should not attempt to set it here on timeline click, as it would create conflicts. The parent comp should update the currentTimeMs prop itself */
110
+ if (!isTimeControlled) {
111
+ videoRef.current.currentTime = targetTime;
112
+ }
113
+ }, [actionHandlers?.timelineClick, totalTime, currentTime, isTimeControlled]);
114
+ // Rendering
115
+ const isPlaying = play ?? false;
116
+ const isLoud = mute ?? false;
117
+ const isFullscreen = fullscreen ?? false;
118
+ const c = clss(publicClassName, { cssModule });
119
+ const rootClss = mergeClassNames(c(null, {
120
+ 'play-on': isPlaying,
121
+ 'play-off': !isPlaying,
122
+ 'fullscreen-on': isFullscreen,
123
+ 'fullscreen-off': !isFullscreen,
124
+ 'loud': isLoud,
125
+ 'muted': !isLoud,
126
+ ..._modifiers
127
+ }), className);
128
+ const appliedVolume = volume ?? 0;
129
+ const rootAttributes = {
130
+ 'data-play-on': isPlaying ? '' : undefined,
131
+ 'data-play-off': !isPlaying ? '' : undefined,
132
+ 'data-fullscreen-on': isFullscreen ? '' : undefined,
133
+ 'data-fullscreen-off': !isFullscreen ? '' : undefined,
134
+ 'data-loud': isLoud ? '' : undefined,
135
+ 'data-muted': !isLoud ? '' : undefined,
136
+ 'data-volume': appliedVolume.toFixed(8),
137
+ 'data-volume-percent': volumePercent,
138
+ 'data-playback-rate': playbackRate,
139
+ 'data-current-time-ms': currentTimeMs.toFixed(2),
140
+ 'data-current-time-ratio': (currentTime / totalTime).toFixed(8),
141
+ 'data-total-time-ms': totalTimeMs
142
+ };
143
+ const rootStyles = {
144
+ [`--${publicClassName}-current-time-ratio`]: (currentTime / totalTime).toFixed(8)
145
+ };
146
+ const parsedSources = useMemo(() => {
147
+ if (sources === undefined)
148
+ return [];
149
+ if (typeof sources === 'string')
150
+ return [{ src: sources }];
151
+ if (Array.isArray(sources)) {
152
+ if (sources.length === 0)
153
+ return [];
154
+ if (typeof sources[0] === 'string')
155
+ return sources.map(src => ({ src }));
156
+ return sources;
157
+ }
158
+ return [];
159
+ }, [sources]);
160
+ const parsedTracks = useMemo(() => {
161
+ if (tracks === undefined)
162
+ return [];
163
+ if (typeof tracks === 'string')
164
+ return [{ src: tracks }];
165
+ if (Array.isArray(tracks)) {
166
+ if (tracks.length === 0)
167
+ return [];
168
+ if (typeof tracks[0] === 'string')
169
+ return tracks.map(src => ({ src }));
170
+ return tracks;
171
+ }
172
+ return [];
173
+ }, [tracks]);
174
+ const videoClss = c('video');
175
+ const videoControlsClss = c('video-controls');
176
+ const playBtnClss = c('play-btn');
177
+ const pauseBtnClss = c('pause-btn');
178
+ const loudBtnClss = c('loud-btn');
179
+ const muteBtnClss = c('mute-btn');
180
+ const volumePcntClss = c('volume-percent');
181
+ const fullscreenBtnClss = c('fullscreen-btn');
182
+ const volumeRangeClss = c('volume-range');
183
+ const playbackRateRangeClss = c('playback-rate-range');
184
+ const playbackRateClss = c('playback-rate');
185
+ const timeControlsClss = c('time-controls');
186
+ const currentTimeClss = c('current-time');
187
+ const totalTimeClss = c('total-time');
188
+ const timelineClss = c('timeline');
189
+ useEffect(() => {
190
+ forcePlaybackRate(videoRef.current, playbackRate);
191
+ }, [playbackRate]);
192
+ useEffect(() => {
193
+ if (fullscreen === true) {
194
+ void forceFullscreen(videoRef.current);
195
+ }
196
+ return () => {
197
+ void forceExitFullscreen(videoRef.current);
198
+ };
199
+ }, [fullscreen]);
200
+ useEffect(() => {
201
+ if (isTimeControlled)
202
+ return;
203
+ if (play === true) {
204
+ void forcePlay(videoRef.current);
205
+ }
206
+ else {
207
+ void forcePause(videoRef.current);
208
+ }
209
+ }, [play, isTimeControlled]);
210
+ useEffect(() => {
211
+ if (volume !== undefined) {
212
+ forceVolume(videoRef.current, volume);
213
+ }
214
+ }, [volume]);
215
+ useEffect(() => {
216
+ if (givenCurrentTimeMs !== undefined && videoRef.current !== null) {
217
+ void forcePause(videoRef.current);
218
+ videoRef.current.currentTime = msToSeconds(givenCurrentTimeMs);
219
+ }
220
+ }, [givenCurrentTimeMs]);
221
+ useEffect(() => {
222
+ if (mute === true) {
223
+ forceMute(videoRef.current);
224
+ }
225
+ else {
226
+ forceLoud(videoRef.current);
227
+ }
228
+ }, [mute]);
229
+ // State handlers
230
+ useEffect(() => {
231
+ if (stateHandlers?.currentTime !== undefined) {
232
+ stateHandlers.currentTime(msToSeconds(currentTimeMs));
233
+ }
234
+ }, [currentTimeMs, stateHandlers?.currentTime]);
235
+ useEffect(() => {
236
+ stateHandlers?.isPlaying?.(isPlaying);
237
+ }, [isPlaying, stateHandlers?.isPlaying]);
238
+ useEffect(() => {
239
+ stateHandlers?.isFullscreen?.(isFullscreen);
240
+ }, [isFullscreen, stateHandlers?.isFullscreen]);
241
+ useEffect(() => {
242
+ stateHandlers?.isLoud?.(isLoud);
243
+ }, [isLoud, stateHandlers?.isLoud]);
244
+ useEffect(() => {
245
+ stateHandlers?.volume?.(volume);
246
+ }, [volume, stateHandlers?.volume]);
247
+ useEffect(() => {
248
+ stateHandlers?.playbackRate?.(playbackRate);
249
+ }, [playbackRate, stateHandlers?.playbackRate]);
250
+ return _jsxs("figure", { className: rootClss, style: rootStyles, ...rootAttributes, children: [_jsxs("video", { ref: videoRef, className: videoClss, ...intrinsicVideoAttributes, onLoadedMetadata: handleMetadataLoadEvent, onPlay: handleOnPlayEvent, onTimeUpdate: handleOnTimeUpdateEvent, children: [parsedSources.map((source, index) => typeof source === 'string'
251
+ ? _jsx("source", { src: source }, index)
252
+ : _jsx("source", { src: source.src, type: source.type }, index)), parsedTracks.map((track, index) => typeof track === 'string'
253
+ ? _jsx("track", { src: track }, index)
254
+ : _jsx("track", { src: track.src, kind: track.kind, srcLang: track.srclang, label: track.label, default: track.default }, index)), children] }), _jsxs("div", { className: videoControlsClss, children: [_jsx("button", { className: playBtnClss, onClick: handlePlayButtonClick, children: playBtnContent }), _jsx("button", { className: pauseBtnClss, onClick: handlePauseButtonClick, children: pauseBtnContent }), _jsx("button", { className: loudBtnClss, onClick: handleLoudButtonClick, children: loudBtnContent }), _jsx("button", { className: muteBtnClss, onClick: handleMuteButtonClick, children: muteBtnContent }), _jsx("input", { type: "range", className: volumeRangeClss, value: volumePercent, onChange: handleVolumeRangeChange, min: 0, max: 100, step: 1 }), _jsx("span", { className: volumePcntClss, children: Math.round(volumePercent) }), _jsx("button", { className: fullscreenBtnClss, onClick: handleFullscreenButtonClick, children: fullscreenBtnContent }), _jsx("input", { type: "range", className: playbackRateRangeClss, value: playbackRate, onChange: handleRateRangeChange, min: 0.25, max: 4, step: 0.25 }), _jsx("span", { className: playbackRateClss, children: playbackRate })] }), _jsxs("div", { className: timeControlsClss, children: [_jsx("span", { className: currentTimeClss, children: formatTime(currentTimeMs, 'mm:ss:ms') }), _jsx("span", { className: totalTimeClss, children: formatTime(totalTimeMs, 'mm:ss:ms') }), _jsx("div", { className: timelineClss, onClick: handleTimelineClick })] }), subtitles !== undefined && _jsx(Subtitles, { ...subtitles, timecodeMs: currentTimeMs })] });
255
+ };
@@ -1,83 +1,10 @@
1
- import { type FunctionComponent, type PropsWithChildren, type VideoHTMLAttributes } from 'react';
1
+ import { type FunctionComponent } from 'react';
2
2
  import { type Props as DisclaimerProps } from '../Disclaimer/index.js';
3
- import { type Props as SubsProps } from '../Subtitles/index.js';
4
- import type { WithClassName } from '../utils/types.js';
5
- /**
6
- * Describes a single video source.
7
- *
8
- * @property src - URL of the video file.
9
- * @property type - MIME type of the source (e.g. `'video/mp4'`).
10
- */
11
- type SourceData = {
12
- src?: string;
13
- type?: string;
14
- };
15
- /**
16
- * Describes a single text track (subtitles, captions, chapters, etc.).
17
- *
18
- * @property src - URL of the track file.
19
- * @property kind - Track type, maps directly to the `<track>` `kind` attribute.
20
- * @property srclang - Language of the track content (e.g. `'fr'`, `'en'`).
21
- * @property label - Human-readable label shown in the browser's track selector.
22
- * @property default - When `true`, marks this track as the default selection.
23
- */
24
- type TrackData = {
25
- src?: string;
26
- kind?: 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata';
27
- srclang?: string;
28
- label?: string;
29
- default?: boolean;
30
- };
31
- type ActionHandlersProps = {
32
- playButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isPlaying: boolean, video: HTMLVideoElement | null) => void;
33
- pauseButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isPlaying: boolean, video: HTMLVideoElement | null) => void;
34
- loudButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isLoud: boolean, video: HTMLVideoElement | null) => void;
35
- muteButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isLoud: boolean, video: HTMLVideoElement | null) => void;
36
- volumeRangeChange?: (e: React.ChangeEvent<HTMLInputElement>, targetRangeVolume: number, volume: number, video: HTMLVideoElement | null) => void;
37
- fullscreenButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isFullscreen: boolean, video: HTMLVideoElement | null) => void;
38
- rateRangeChange?: (e: React.ChangeEvent<HTMLInputElement>, targetRangeRate: number, rate: number, video: HTMLVideoElement | null) => void;
39
- timelineClick?: (e: React.MouseEvent<HTMLDivElement>, targetTimeSec: number, timeSec: number, video: HTMLVideoElement | null) => void;
40
- };
41
- type StateHandlersProps = {
42
- isPlaying?: (isPlaying: boolean) => void;
43
- isFullScreen?: (isFullScreen: boolean) => void;
44
- isLoud?: (isLoud: boolean) => void;
45
- volume?: (volume: number) => void;
46
- currentTime?: (currentTime: number) => void;
47
- playbackRate?: (rate: number) => void;
48
- };
49
- /**
50
- * - play?: boolean
51
- * - mute?: boolean
52
- * - fullScreen?: boolean
53
- * - volume?: number
54
- * - playbackRate?: number
55
- * - currentTimeMs:? number
56
- * - PLUS TARD =
57
- * - onPlay, onLoud, etc...
58
- */
3
+ import { type Props as ControlledProps } from './index.controlled.js';
59
4
  /**
60
5
  * Props for the {@link Video} component.
61
6
  *
62
- * Extends all native `VideoHTMLAttributes<HTMLVideoElement>`, so any standard
63
- * video attribute (`autoPlay`, `muted`, `loop`, `poster`, etc.) can be passed
64
- * and will be forwarded to the underlying `<video>` element.
65
- *
66
- * @property sources - One or more video sources. Accepts:
67
- * - a single URL string,
68
- * - an array of URL strings,
69
- * - an array of {@link SourceData} objects for fine-grained `type` control.
70
- * @property tracks - One or more text tracks. Accepts:
71
- * - a single URL string,
72
- * - an array of URL strings,
73
- * - an array of {@link TrackData} objects.
74
- * @property playBtnContent - Custom content for the play button.
75
- * @property pauseBtnContent - Custom content for the pause button.
76
- * @property loudBtnContent - Custom content for the unmute button.
77
- * @property muteBtnContent - Custom content for the mute button.
78
- * @property fullScreenBtnContent - Custom content for the fullScreen button.
79
- * @property subtitles - Props forwarded to the internal {@link Subtitles} component.
80
- * `timecodeMs` is injected automatically from the current playback position.
7
+ * Extends all ControlledVideo props except play, mute, fullscreen, volume, playbackRate, and their associated event handlers
81
8
  * @property disclaimer - Props forwarded to the internal {@link Disclaimer} component.
82
9
  * While the disclaimer is active, `autoPlay` and `muted` are suppressed on the
83
10
  * underlying `<video>` element.
@@ -89,61 +16,30 @@ type StateHandlersProps = {
89
16
  * component intersects the viewport, provided no disclaimer is active.
90
17
  * @property autoMuteWhenHidden - When `true`, mutes the video whenever the component
91
18
  * leaves the viewport.
19
+ * @property currentTimeMs - When provided, forces the video's current time to the given value (in milliseconds). Giving the currentTimeMs prop puts the component in a controlled state for the current time, meaning that the parent component is responsible for updating the current time. In this mode, user interactions that would normally change the current time (play, pause, timeline click) will not have an effect unless the parent component updates the currentTimeMs prop accordingly.
20
+ * @property wrapperClassName - Optional additional class name(s) applied to the root wrapper element.
21
+ * @property stateHandlers - Optional callbacks invoked whenever the corresponding
22
+ * state changes. Useful for synchronizing external state with the internal video state.
92
23
  * @property className - Optional additional class name(s) applied to the root element.
93
24
  * @property children - React children rendered inside the `<video>` element itself
94
25
  * (e.g. fallback content).
95
26
  */
96
- export type Props = PropsWithChildren<WithClassName<{
97
- sources?: string | string[] | SourceData[];
98
- tracks?: string | string[] | TrackData[];
99
- playBtnContent?: React.ReactNode;
100
- pauseBtnContent?: React.ReactNode;
101
- loudBtnContent?: React.ReactNode;
102
- muteBtnContent?: React.ReactNode;
103
- fullScreenBtnContent?: React.ReactNode;
104
- subtitles?: SubsProps;
27
+ export type Props = Omit<ControlledProps, 'play' | 'fullscreen' | 'volume' | 'mute' | 'playbackRate'> & {
105
28
  disclaimer?: DisclaimerProps;
106
29
  autoPlayWhenVisible?: boolean;
107
30
  autoPauseWhenHidden?: boolean;
108
31
  autoLoudWhenVisible?: boolean;
109
32
  autoMuteWhenHidden?: boolean;
110
- actionHandlers: ActionHandlersProps;
111
- stateHandlers: StateHandlersProps;
112
- }> & VideoHTMLAttributes<HTMLVideoElement>>;
33
+ wrapperClassName?: string;
34
+ };
113
35
  /**
114
36
  * Full-featured video player component. Wraps a native `<video>` element with
115
37
  * playback controls, volume, playback rate, a timeline, optional subtitles,
116
38
  * an optional disclaimer gate, and viewport-driven auto-play/mute behaviours.
117
39
  *
118
- * ### Root element modifiers
119
- * The root `<figure>` receives the public class name defined by `video` and
120
- * the following BEM-style modifier classes:
121
- * - `--play-on` / `--play-off` — reflects current playback state.
122
- * - `--fullscreen-on` / `--fullscreen-off` — reflects fullscreen state.
123
- * - `--loud` / `--muted` — reflects mute state.
124
- *
125
- * ### Data attributes on the root element
126
- * - `data-play-on` — present (empty string) when playing.
127
- * - `data-play-off` — present (empty string) when paused.
128
- * - `data-fullscreen-on` — present (empty string) when in fullScreen.
129
- * - `data-fullscreen-off` — present (empty string) when not in fullScreen.
130
- * - `data-loud` — present (empty string) when unmuted.
131
- * - `data-muted` — present (empty string) when muted.
132
- * - `data-volume` — current volume as a `0–1` float.
133
- * - `data-volume-percent` — current volume as a `0–100` float.
134
- * - `data-playback-rate` — current playback rate (e.g. `1`, `1.5`).
135
- * - `data-current-time-ms` — current time in milliseconds, fixed to 2 decimals.
136
- * - `data-current-time-ratio` — current / total ratio, fixed to 8 decimals.
137
- * - `data-total-time-ms` — total duration in milliseconds.
138
- *
139
- * ### CSS custom properties on the root element
140
- * - `--video-current-time-ratio` — current / total ratio, fixed to 8 decimals.
141
- * Useful for driving progress-bar animations purely in CSS.
142
- *
143
40
  * @param props - Component properties.
144
41
  * @see {@link Props}
145
42
  * @returns A `<figure>` element containing the video, its controls, optional
146
43
  * subtitles, and an optional disclaimer overlay.
147
44
  */
148
45
  export declare const Video: FunctionComponent<Props>;
149
- export {};