@design-edito/tools 0.5.2 → 0.5.4

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 (152) hide show
  1. package/agnostic/html/deep-select/index.js +5 -3
  2. package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +9 -9
  3. package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +9 -9
  4. package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +3 -3
  5. package/agnostic/html/hyper-json/smart-tags/isolated/index.js +3 -3
  6. package/agnostic/html/index.d.ts +1 -1
  7. package/agnostic/html/index.js +1 -1
  8. package/agnostic/index.d.ts +2 -2
  9. package/agnostic/index.js +2 -2
  10. package/agnostic/misc/index.d.ts +3 -3
  11. package/agnostic/misc/index.js +3 -3
  12. package/agnostic/misc/logs/index.d.ts +1 -1
  13. package/agnostic/misc/logs/index.js +1 -1
  14. package/agnostic/numbers/index.d.ts +1 -1
  15. package/agnostic/numbers/index.js +1 -1
  16. package/agnostic/optim/index.d.ts +1 -1
  17. package/agnostic/optim/index.js +1 -1
  18. package/agnostic/strings/index.d.ts +2 -2
  19. package/agnostic/strings/index.js +2 -2
  20. package/agnostic/time/dates/format-date/index.d.ts +4 -2
  21. package/agnostic/time/dates/format-date/index.js +43 -94
  22. package/agnostic/time/dates/format-date/index.test.js +154 -0
  23. package/agnostic/time/dates/format-date/parts.d.ts +28 -0
  24. package/agnostic/time/dates/format-date/parts.js +136 -0
  25. package/agnostic/time/dates/format-date/types.d.ts +84 -0
  26. package/agnostic/time/dates/format-date/types.js +1 -0
  27. package/agnostic/time/duration/format-duration/index.d.ts +42 -0
  28. package/agnostic/time/duration/format-duration/index.js +94 -0
  29. package/agnostic/time/duration/format-duration/index.test.d.ts +1 -0
  30. package/agnostic/time/duration/format-duration/index.test.js +148 -0
  31. package/agnostic/time/duration/format-duration/parts.d.ts +32 -0
  32. package/agnostic/time/duration/format-duration/parts.js +107 -0
  33. package/agnostic/time/duration/format-duration/types.d.ts +93 -0
  34. package/agnostic/time/duration/format-duration/types.js +1 -0
  35. package/agnostic/time/duration/index.d.ts +6 -1
  36. package/agnostic/time/duration/index.js +2 -2
  37. package/agnostic/time/duration/index.test.d.ts +1 -0
  38. package/agnostic/time/duration/index.test.js +168 -0
  39. package/agnostic/time/index.d.ts +1 -1
  40. package/agnostic/time/index.js +1 -1
  41. package/components/BeforeAfter/index.controlled.d.ts +25 -25
  42. package/components/BeforeAfter/index.controlled.js +47 -53
  43. package/components/BeforeAfter/index.d.ts +20 -23
  44. package/components/BeforeAfter/index.js +48 -40
  45. package/components/Button/index.js +1 -1
  46. package/components/Clippable/index.d.ts +17 -26
  47. package/components/Clippable/index.js +21 -23
  48. package/components/Disclaimer/index.d.ts +32 -24
  49. package/components/Disclaimer/index.js +29 -24
  50. package/components/Drawer/index.d.ts +45 -25
  51. package/components/Drawer/index.js +61 -52
  52. package/components/EventListener/index.d.ts +4 -5
  53. package/components/Gallery/index.d.ts +14 -16
  54. package/components/Gallery/index.js +25 -30
  55. package/components/Iframe/index.js +2 -2
  56. package/components/Image/index.d.ts +5 -15
  57. package/components/Image/index.js +9 -34
  58. package/components/Input/index.d.ts +23 -19
  59. package/components/Input/index.js +30 -20
  60. package/components/JsonEditor/index.d.ts +177 -9
  61. package/components/JsonEditor/index.js +133 -77
  62. package/components/JsonEditor/types.d.ts +11 -0
  63. package/components/JsonEditor/types.js +1 -0
  64. package/components/JsonEditor/utils.d.ts +8 -0
  65. package/components/JsonEditor/utils.js +23 -0
  66. package/components/ListLoader/index.controlled.d.ts +5 -5
  67. package/components/ListLoader/index.controlled.js +4 -4
  68. package/components/ListLoader/index.d.ts +11 -11
  69. package/components/ListLoader/index.js +12 -12
  70. package/components/Overlayer/index.d.ts +4 -4
  71. package/components/Overlayer/index.js +6 -6
  72. package/components/Paginator/index.d.ts +7 -12
  73. package/components/Paginator/index.js +7 -11
  74. package/components/ResizeObserver/index.d.ts +14 -3
  75. package/components/ResizeObserver/index.js +49 -24
  76. package/components/Scrllgngn/index.d.ts +17 -13
  77. package/components/Scrllgngn/index.js +25 -84
  78. package/components/Scrllgngn/utils.d.ts +52 -0
  79. package/components/Scrllgngn/utils.js +102 -0
  80. package/components/ScrollListener/index.d.ts +43 -28
  81. package/components/ScrollListener/index.js +59 -79
  82. package/components/ScrollListener/utils.d.ts +37 -16
  83. package/components/ScrollListener/utils.js +141 -28
  84. package/components/Select/index.d.ts +24 -20
  85. package/components/Select/index.js +30 -20
  86. package/components/Sequencer/index.controlled.d.ts +16 -26
  87. package/components/Sequencer/index.controlled.js +15 -12
  88. package/components/Sequencer/index.d.ts +25 -30
  89. package/components/Sequencer/index.js +20 -34
  90. package/components/Subtitles/index.d.ts +29 -5
  91. package/components/Subtitles/index.js +90 -12
  92. package/components/Textarea/index.d.ts +26 -20
  93. package/components/Textarea/index.js +40 -21
  94. package/components/Theatre/index.d.ts +35 -39
  95. package/components/Theatre/index.js +46 -58
  96. package/components/UIModule/index.d.ts +25 -29
  97. package/components/UIModule/index.js +81 -110
  98. package/components/Video/index.controlled.d.ts +51 -51
  99. package/components/Video/index.controlled.js +75 -72
  100. package/components/Video/index.d.ts +12 -14
  101. package/components/Video/index.js +41 -86
  102. package/components/Video/utils.d.ts +35 -3
  103. package/components/Video/utils.js +37 -14
  104. package/components/index.d.ts +6 -6
  105. package/components/index.js +6 -6
  106. package/components/utils/index.d.ts +22 -0
  107. package/components/utils/index.js +31 -0
  108. package/index.d.ts +1 -1
  109. package/index.js +1 -1
  110. package/node/@aws-s3/storage/directory/index.d.ts +2 -2
  111. package/node/@aws-s3/storage/directory/index.js +2 -2
  112. package/node/@aws-s3/storage/file/index.d.ts +1 -1
  113. package/node/@aws-s3/storage/file/index.js +1 -1
  114. package/node/@aws-s3/storage/index.d.ts +1 -1
  115. package/node/@aws-s3/storage/index.js +1 -1
  116. package/node/@google-cloud/storage/directory/index.d.ts +2 -2
  117. package/node/@google-cloud/storage/directory/index.js +2 -2
  118. package/node/@google-cloud/storage/file/index.d.ts +2 -2
  119. package/node/@google-cloud/storage/file/index.js +2 -2
  120. package/node/@google-cloud/storage/index.d.ts +1 -1
  121. package/node/@google-cloud/storage/index.js +1 -1
  122. package/node/cloud-storage/operations/index.d.ts +2 -2
  123. package/node/cloud-storage/operations/index.js +2 -2
  124. package/node/ftps/file/index.d.ts +2 -2
  125. package/node/ftps/file/index.js +2 -2
  126. package/node/images/index.d.ts +1 -1
  127. package/node/images/index.js +1 -1
  128. package/node/images/transform/operations/index.d.ts +2 -2
  129. package/node/images/transform/operations/index.js +2 -2
  130. package/node/index.d.ts +1 -1
  131. package/node/index.js +1 -1
  132. package/node/process/index.d.ts +1 -1
  133. package/node/process/index.js +1 -1
  134. package/node/sftp/file/index.d.ts +2 -2
  135. package/node/sftp/file/index.js +2 -2
  136. package/node/sftp/index.d.ts +1 -1
  137. package/node/sftp/index.js +1 -1
  138. package/package.json +20 -5
  139. package/TODO.md +0 -269
  140. package/components/BeforeAfter/utils.d.ts +0 -4
  141. package/components/BeforeAfter/utils.js +0 -9
  142. package/components/Input/index.controlled.d.ts +0 -42
  143. package/components/Input/index.controlled.js +0 -54
  144. package/components/Select/index.controlled.d.ts +0 -43
  145. package/components/Select/index.controlled.js +0 -54
  146. package/components/Subtitles/types.d.ts +0 -24
  147. package/components/Subtitles/utils.d.ts +0 -32
  148. package/components/Subtitles/utils.js +0 -107
  149. package/components/Textarea/index.controlled.d.ts +0 -50
  150. package/components/Textarea/index.controlled.js +0 -75
  151. /package/{components/Subtitles/types.js → agnostic/time/dates/format-date/index.test.d.ts} +0 -0
  152. /package/components/ResizeObserver/{style.module.css → styles.module.css} +0 -0
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo, useRef, useState, useCallback, useEffect } from 'react';
3
3
  import { clss } from '../../agnostic/css/clss/index.js';
4
- import { mergeClassNames } from '../utils/index.js';
4
+ import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
5
5
  import cssModule from './styles.module.css';
6
6
  import { video as publicClassName } from '../public-classnames.js';
7
7
  import { Subtitles } from '../Subtitles/index.js';
@@ -41,79 +41,79 @@ import { forceExitFullscreen, forceFullscreen, forceLoud, forceMute, forcePause,
41
41
  * @returns A `<figure>` element containing the video, its controls, optional
42
42
  * subtitles.
43
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 }) => {
44
+ export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pauseBtnContent, loudBtnContent, muteBtnContent, fullscreenBtnContent, play, fullscreen, mute, muted, volume = 1, playbackRate = 1, currentTimeMs: givenCurrentTimeMs, onPlayButtonClicked, onPauseButtonClicked, onLoudButtonClicked, onMuteButtonClicked, onVolumeRangeChanged, onRateRangeChanged, onFullscreenButtonClicked, onTimelineClicked, onIsPlayingChanged, onIsFullscreenChanged, onIsLoudChanged, onVolumeChanged, onPlaybackRateChanged, onCurrentTimeMsChanged, onFullscreenChange, children, className, ...intrinsicVideoAttributes }) => {
45
45
  const videoRef = useRef(null);
46
46
  const [totalTime, setTotalTime] = useState(0);
47
47
  const totalTimeMs = useMemo(() => secondsToMs(totalTime), [totalTime]);
48
- const [currentTimeMs, setCurrentTimeMs] = useState(0);
48
+ const [internalCurrentTimeMs, setInternalCurrentTimeMs] = useState(0);
49
+ const isTimeControlled = givenCurrentTimeMs !== undefined;
50
+ // The parent owns the time as soon as it provides one, so that is what gets
51
+ // displayed — not what the element reported one render later.
52
+ const currentTimeMs = givenCurrentTimeMs ?? internalCurrentTimeMs;
49
53
  const currentTime = useMemo(() => msToSeconds(currentTimeMs), [currentTimeMs]);
50
- const isTimeControlled = useMemo(() => givenCurrentTimeMs !== undefined, [givenCurrentTimeMs]);
51
54
  const volumePercent = useMemo(() => volume * 100, [volume]);
55
+ // Every state item this component displays is read from its props, never from
56
+ // the element: it is the parent's job to keep them in sync.
57
+ const isPlaying = play ?? false;
58
+ const isLoud = !(mute ?? false);
59
+ const isFullscreen = fullscreen ?? false;
52
60
  // Intrinsic event handler
53
61
  const handleMetadataLoadEvent = useCallback((e) => {
54
62
  if (videoRef.current === null)
55
63
  return;
56
64
  const video = videoRef.current;
57
65
  setTotalTime(video.duration);
66
+ // A seek asked for before the metadata landed was silently dropped by the
67
+ // element — replay it now that it can be honoured.
68
+ if (givenCurrentTimeMs !== undefined)
69
+ video.currentTime = msToSeconds(givenCurrentTimeMs);
58
70
  intrinsicVideoAttributes.onLoadedMetadata?.(e);
59
- }, [intrinsicVideoAttributes.onLoadedMetadata]);
71
+ }, [intrinsicVideoAttributes.onLoadedMetadata, givenCurrentTimeMs]);
60
72
  const handleOnTimeUpdateEvent = useCallback((e) => {
61
73
  const video = e.currentTarget;
62
74
  const newTimeMs = secondsToMs(video.currentTime);
63
- setCurrentTimeMs(newTimeMs);
75
+ setInternalCurrentTimeMs(newTimeMs);
64
76
  if (intrinsicVideoAttributes.onTimeUpdate !== undefined)
65
77
  intrinsicVideoAttributes.onTimeUpdate(e);
66
78
  }, [intrinsicVideoAttributes.onTimeUpdate]);
67
- const handleOnPlayEvent = useCallback((e) => {
68
- /* We must never play a video that has controlled time */
69
- if (isTimeControlled) {
70
- videoRef.current?.pause();
71
- return;
72
- }
73
- intrinsicVideoAttributes.onPlay?.(e);
74
- }, [isTimeControlled, intrinsicVideoAttributes.onPlay]);
75
79
  // Custom action handlers
76
80
  const handlePlayButtonClick = useCallback((e) => {
77
- const isPlaying = videoRef.current !== null ? videoRef.current.paused : false;
78
- actionHandlers?.playButtonClick?.(e, isPlaying, videoRef.current);
79
- }, [actionHandlers?.playButtonClick]);
81
+ const wasPlaying = videoRef.current?.paused === false;
82
+ onPlayButtonClicked?.(e, wasPlaying, videoRef.current);
83
+ }, [onPlayButtonClicked]);
80
84
  const handlePauseButtonClick = useCallback((e) => {
81
- const isPlaying = videoRef.current !== null ? videoRef.current.paused : false;
82
- actionHandlers?.pauseButtonClick?.(e, isPlaying, videoRef.current);
83
- }, [actionHandlers?.pauseButtonClick]);
85
+ const wasPlaying = videoRef.current?.paused === false;
86
+ onPauseButtonClicked?.(e, wasPlaying, videoRef.current);
87
+ }, [onPauseButtonClicked]);
84
88
  const handleLoudButtonClick = useCallback((e) => {
85
- const isLoud = mute !== undefined ? !mute : false;
86
- actionHandlers?.loudButtonClick?.(e, isLoud, videoRef.current);
87
- }, [actionHandlers?.loudButtonClick, mute]);
89
+ onLoudButtonClicked?.(e, isLoud, videoRef.current);
90
+ }, [onLoudButtonClicked, isLoud]);
88
91
  const handleMuteButtonClick = useCallback((e) => {
89
- const isLoud = mute !== undefined ? !mute : false;
90
- actionHandlers?.muteButtonClick?.(e, isLoud, videoRef.current);
91
- }, [actionHandlers?.muteButtonClick, mute]);
92
+ onMuteButtonClicked?.(e, isLoud, videoRef.current);
93
+ }, [onMuteButtonClicked, isLoud]);
92
94
  const handleFullscreenButtonClick = useCallback((e) => {
93
- actionHandlers?.fullscreenButtonClick?.(e, fullscreen ?? false, videoRef.current);
94
- }, [actionHandlers?.fullscreenButtonClick, fullscreen]);
95
+ onFullscreenButtonClicked?.(e, isFullscreen, videoRef.current);
96
+ }, [onFullscreenButtonClicked, isFullscreen]);
95
97
  const handleVolumeRangeChange = useCallback((e) => {
96
98
  const targetVolume = Number(e.currentTarget.value) / 100;
97
- actionHandlers?.volumeRangeChange?.(e, targetVolume, volume, videoRef.current);
98
- }, [actionHandlers?.volumeRangeChange, volume]);
99
+ onVolumeRangeChanged?.(e, targetVolume, volume, videoRef.current);
100
+ }, [onVolumeRangeChanged, volume]);
99
101
  const handleRateRangeChange = useCallback((e) => {
100
- actionHandlers?.rateRangeChange?.(e, Number(e.currentTarget.value), playbackRate, videoRef.current);
101
- }, [actionHandlers?.rateRangeChange, playbackRate]);
102
+ onRateRangeChanged?.(e, Number(e.currentTarget.value), playbackRate, videoRef.current);
103
+ }, [onRateRangeChanged, playbackRate]);
102
104
  const handleTimelineClick = useCallback((e) => {
103
105
  if (videoRef.current === null)
104
106
  return;
105
- const progress = getTimelineClickProgress(e, e.currentTarget, videoRef.current);
107
+ const progress = getTimelineClickProgress(e);
106
108
  const targetTime = progress * totalTime;
107
- actionHandlers?.timelineClick?.(e, targetTime, currentTime, videoRef.current);
108
- /* 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 */
109
+ onTimelineClicked?.(e, targetTime, currentTime, videoRef.current);
110
+ // A controlled time is the parent's to move: it is expected to update the
111
+ // prop in response to this very handler.
109
112
  if (!isTimeControlled) {
110
113
  videoRef.current.currentTime = targetTime;
111
114
  }
112
- }, [actionHandlers?.timelineClick, totalTime, currentTime, isTimeControlled]);
115
+ }, [onTimelineClicked, totalTime, currentTime, isTimeControlled]);
113
116
  // Rendering
114
- const isPlaying = play ?? false;
115
- const isLoud = mute ?? false;
116
- const isFullscreen = fullscreen ?? false;
117
117
  const c = clss(publicClassName, { cssModule });
118
118
  const rootClss = mergeClassNames(c(null, {
119
119
  'play-on': isPlaying,
@@ -121,10 +121,11 @@ export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pa
121
121
  'fullscreen-on': isFullscreen,
122
122
  'fullscreen-off': !isFullscreen,
123
123
  'loud': isLoud,
124
- 'muted': !isLoud,
125
- ..._modifiers
124
+ 'muted': !isLoud
126
125
  }), className);
127
- const appliedVolume = volume;
126
+ // Guarded: the duration is unknown until the metadata lands, and an unguarded
127
+ // division would expose the string 'NaN' on every render until then.
128
+ const currentTimeRatio = totalTime > 0 ? currentTime / totalTime : 0;
128
129
  const rootAttributes = {
129
130
  'data-play-on': isPlaying ? '' : undefined,
130
131
  'data-play-off': !isPlaying ? '' : undefined,
@@ -132,15 +133,15 @@ export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pa
132
133
  'data-fullscreen-off': !isFullscreen ? '' : undefined,
133
134
  'data-loud': isLoud ? '' : undefined,
134
135
  'data-muted': !isLoud ? '' : undefined,
135
- 'data-volume': appliedVolume.toFixed(8),
136
+ 'data-volume': volume.toFixed(8),
136
137
  'data-volume-percent': volumePercent,
137
138
  'data-playback-rate': playbackRate,
138
139
  'data-current-time-ms': currentTimeMs.toFixed(2),
139
- 'data-current-time-ratio': (currentTime / totalTime).toFixed(8),
140
+ 'data-current-time-ratio': currentTimeRatio.toFixed(8),
140
141
  'data-total-time-ms': totalTimeMs
141
142
  };
142
143
  const rootStyles = {
143
- [`--${publicClassName}-current-time-ratio`]: (currentTime / totalTime).toFixed(8)
144
+ [`--${publicClassName}-current-time-ratio`]: currentTimeRatio.toFixed(8)
144
145
  };
145
146
  const parsedSources = useMemo(() => {
146
147
  if (sources === undefined)
@@ -201,8 +202,13 @@ export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pa
201
202
  };
202
203
  }, [fullscreen]);
203
204
  useEffect(() => {
204
- if (isTimeControlled)
205
+ // A controlled time belongs to the parent, and a playing element advances
206
+ // the time on its own — so the two cannot coexist. Applied once, on entering
207
+ // the mode, rather than fought on every tick.
208
+ if (isTimeControlled) {
209
+ void forcePause(videoRef.current);
205
210
  return;
211
+ }
206
212
  if (play === true) {
207
213
  void forcePlay(videoRef.current);
208
214
  }
@@ -214,10 +220,9 @@ export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pa
214
220
  forceVolume(videoRef.current, volume);
215
221
  }, [volume]);
216
222
  useEffect(() => {
217
- if (givenCurrentTimeMs !== undefined && videoRef.current !== null) {
218
- void forcePause(videoRef.current);
219
- videoRef.current.currentTime = msToSeconds(givenCurrentTimeMs);
220
- }
223
+ if (givenCurrentTimeMs === undefined || videoRef.current === null)
224
+ return;
225
+ videoRef.current.currentTime = msToSeconds(givenCurrentTimeMs);
221
226
  }, [givenCurrentTimeMs]);
222
227
  useEffect(() => {
223
228
  if (mute === true) {
@@ -227,30 +232,28 @@ export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pa
227
232
  forceLoud(videoRef.current);
228
233
  }
229
234
  }, [mute]);
230
- // State handlers
231
- useEffect(() => {
232
- if (stateHandlers?.currentTime !== undefined) {
233
- stateHandlers.currentTime(msToSeconds(currentTimeMs));
234
- }
235
- }, [currentTimeMs, stateHandlers?.currentTime]);
236
- useEffect(() => {
237
- stateHandlers?.isPlaying?.(isPlaying);
238
- }, [isPlaying, stateHandlers?.isPlaying]);
239
- useEffect(() => {
240
- stateHandlers?.isFullscreen?.(isFullscreen);
241
- }, [isFullscreen, stateHandlers?.isFullscreen]);
235
+ // The browser can leave fullscreen without going through our button — Escape,
236
+ // or the platform's own exit gesture. Nothing in React reports it, so the real
237
+ // event is the only way for a parent to keep its `fullscreen` prop truthful.
242
238
  useEffect(() => {
243
- stateHandlers?.isLoud?.(isLoud);
244
- }, [isLoud, stateHandlers?.isLoud]);
245
- useEffect(() => {
246
- stateHandlers?.volume?.(volume);
247
- }, [volume, stateHandlers?.volume]);
248
- useEffect(() => {
249
- stateHandlers?.playbackRate?.(playbackRate);
250
- }, [playbackRate, stateHandlers?.playbackRate]);
251
- 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'
239
+ if (onFullscreenChange === undefined)
240
+ return;
241
+ const onDocumentFullscreenChange = () => {
242
+ onFullscreenChange(document.fullscreenElement !== null);
243
+ };
244
+ document.addEventListener('fullscreenchange', onDocumentFullscreenChange);
245
+ return () => { document.removeEventListener('fullscreenchange', onDocumentFullscreenChange); };
246
+ }, [onFullscreenChange]);
247
+ // State handlers
248
+ useChangeDispatch(currentTimeMs, onCurrentTimeMsChanged);
249
+ useChangeDispatch(isPlaying, onIsPlayingChanged);
250
+ useChangeDispatch(isFullscreen, onIsFullscreenChanged);
251
+ useChangeDispatch(isLoud, onIsLoudChanged);
252
+ useChangeDispatch(volume, onVolumeChanged);
253
+ useChangeDispatch(playbackRate, onPlaybackRateChanged);
254
+ return _jsxs("figure", { className: rootClss, style: rootStyles, ...rootAttributes, children: [_jsxs("video", { ref: videoRef, className: videoClss, ...intrinsicVideoAttributes, autoPlay: isTimeControlled ? false : intrinsicVideoAttributes.autoPlay, onLoadedMetadata: handleMetadataLoadEvent, onTimeUpdate: handleOnTimeUpdateEvent, children: [parsedSources.map((source, index) => typeof source === 'string'
252
255
  ? _jsx("source", { src: source }, index)
253
256
  : _jsx("source", { src: source.src, type: source.type }, index)), parsedTracks.map((track, index) => typeof track === 'string'
254
257
  ? _jsx("track", { src: track }, index)
255
- : _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 })] });
258
+ : _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 })] });
256
259
  };
@@ -1,31 +1,29 @@
1
1
  import { type FunctionComponent } from 'react';
2
- import { type Props as DisclaimerProps } from '../Disclaimer/index.js';
3
2
  import { type Props as ControlledProps } from './index.controlled.js';
4
3
  /**
5
4
  * Props for the {@link Video} component.
6
5
  *
7
6
  * Extends all ControlledVideo props except play, mute, fullscreen, volume, playbackRate, and their associated event handlers
8
- * @property disclaimer - Props forwarded to the internal {@link Disclaimer} component.
9
- * While the disclaimer is active, `autoPlay` and `muted` are suppressed on the
10
- * underlying `<video>` element.
11
7
  * @property autoPlayWhenVisible - When `true`, triggers playback the first time the
12
- * component intersects the viewport, provided no disclaimer is active.
8
+ * component intersects the viewport.
13
9
  * @property autoPauseWhenHidden - When `true`, pauses playback whenever the component
14
10
  * leaves the viewport.
15
11
  * @property autoLoudWhenVisible - When `true`, unmutes the video the first time the
16
- * component intersects the viewport, provided no disclaimer is active.
12
+ * component intersects the viewport.
17
13
  * @property autoMuteWhenHidden - When `true`, mutes the video whenever the component
18
14
  * 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.
15
+ * @property currentTimeMs - When provided, hands ownership of the current time
16
+ * (in milliseconds) to the parent, which is then responsible for updating it —
17
+ * typically to scrub the video from scroll position. A controlled time implies a
18
+ * stopped video, since a playing element would advance a value it does not own:
19
+ * `autoPlay`, `autoPlayWhenVisible` and the play button have no effect for as
20
+ * long as this prop is provided.
20
21
  * @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.
23
22
  * @property className - Optional additional class name(s) applied to the root element.
24
23
  * @property children - React children rendered inside the `<video>` element itself
25
24
  * (e.g. fallback content).
26
25
  */
27
26
  export type Props = Omit<ControlledProps, 'play' | 'fullscreen' | 'volume' | 'mute' | 'playbackRate'> & {
28
- disclaimer?: DisclaimerProps;
29
27
  autoPlayWhenVisible?: boolean;
30
28
  autoPauseWhenHidden?: boolean;
31
29
  autoLoudWhenVisible?: boolean;
@@ -34,12 +32,12 @@ export type Props = Omit<ControlledProps, 'play' | 'fullscreen' | 'volume' | 'mu
34
32
  };
35
33
  /**
36
34
  * Full-featured video player component. Wraps a native `<video>` element with
37
- * playback controls, volume, playback rate, a timeline, optional subtitles,
38
- * an optional disclaimer gate, and viewport-driven auto-play/mute behaviours.
35
+ * playback controls, volume, playback rate, a timeline, optional subtitles, and
36
+ * viewport-driven auto-play/mute behaviours.
39
37
  *
40
38
  * @param props - Component properties.
41
39
  * @see {@link Props}
42
- * @returns A `<figure>` element containing the video, its controls, optional
43
- * subtitles, and an optional disclaimer overlay.
40
+ * @returns A `<figure>` element containing the video, its controls and optional
41
+ * subtitles.
44
42
  */
45
43
  export declare const Video: FunctionComponent<Props>;
@@ -1,7 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { clss } from '../../agnostic/css/clss/index.js';
4
- import { Disclaimer } from '../Disclaimer/index.js';
5
4
  import { IntersectionObserverComponent } from '../IntersectionObserver/index.js';
6
5
  import { mergeClassNames } from '../utils/index.js';
7
6
  import { videoWrapper as publicClassName } from '../public-classnames.js';
@@ -10,33 +9,27 @@ import cssModule from './styles.module.css';
10
9
  import { ControlledVideo } from './index.controlled.js';
11
10
  /**
12
11
  * Full-featured video player component. Wraps a native `<video>` element with
13
- * playback controls, volume, playback rate, a timeline, optional subtitles,
14
- * an optional disclaimer gate, and viewport-driven auto-play/mute behaviours.
12
+ * playback controls, volume, playback rate, a timeline, optional subtitles, and
13
+ * viewport-driven auto-play/mute behaviours.
15
14
  *
16
15
  * @param props - Component properties.
17
16
  * @see {@link Props}
18
- * @returns A `<figure>` element containing the video, its controls, optional
19
- * subtitles, and an optional disclaimer overlay.
17
+ * @returns A `<figure>` element containing the video, its controls and optional
18
+ * subtitles.
20
19
  */
21
- export const Video = ({ loop, disclaimer, autoPlayWhenVisible, autoPauseWhenHidden, autoMuteWhenHidden, autoLoudWhenVisible, wrapperClassName, ...controlledProps }) => {
20
+ export const Video = ({ loop, autoPlayWhenVisible, autoPauseWhenHidden, autoMuteWhenHidden, autoLoudWhenVisible, wrapperClassName, onPlayButtonClicked, onPauseButtonClicked, onLoudButtonClicked, onMuteButtonClicked, onVolumeRangeChanged, onRateRangeChanged, onFullscreenButtonClicked, ...controlledProps }) => {
22
21
  // State & refs
23
22
  const [play, setPlay] = useState(false);
24
23
  const [volume, setVolume] = useState(1);
25
24
  const [mute, setMute] = useState(false);
26
25
  const [playbackRate, setPlaybackRate] = useState(1);
27
26
  const [fullscreen, setFullscreen] = useState(false);
28
- const [isDisclaimerOn, setIsDisclaimerOn] = useState(disclaimer?.isOn === true
29
- || disclaimer?.defaultIsOn === true
30
- || (disclaimer !== undefined && disclaimer.defaultIsOn === undefined));
31
27
  const hasBeenAutoPlayed = useRef(false);
32
- const needsDisclaimer = useMemo(() => disclaimer !== undefined, [disclaimer]);
33
- const shouldDisclaimerBeOn = useMemo(() => {
34
- if (disclaimer?.isOn === false)
35
- return false;
36
- if (disclaimer?.isOn === true)
37
- return true;
38
- return isDisclaimerOn;
39
- }, [disclaimer, isDisclaimerOn]);
28
+ // Several paths below ask for playback — the play button, autoPlayWhenVisible,
29
+ // autoPlay itself. None of them may win over a parent-owned time, so the
30
+ // invariant is applied where the state is forwarded rather than guarded at each
31
+ // of those call sites.
32
+ const isTimeControlled = controlledProps.currentTimeMs !== undefined;
40
33
  const needsObserve = useMemo(() => autoLoudWhenVisible === true
41
34
  || autoMuteWhenHidden === true
42
35
  || autoPlayWhenVisible === true
@@ -49,13 +42,9 @@ export const Video = ({ loop, disclaimer, autoPlayWhenVisible, autoPauseWhenHidd
49
42
  // Intrisic event handlers
50
43
  const handleOnPlayEvent = useCallback((e) => {
51
44
  controlledProps.onPlay?.(e);
52
- if (shouldDisclaimerBeOn) {
53
- setPlay(false);
54
- return;
55
- }
56
45
  setPlay(true);
57
46
  hasBeenAutoPlayed.current = true;
58
- }, [controlledProps.onPlay, shouldDisclaimerBeOn]);
47
+ }, [controlledProps.onPlay]);
59
48
  const handleOnPauseEvent = useCallback((e) => {
60
49
  setPlay(false);
61
50
  controlledProps.onPause?.(e);
@@ -69,45 +58,44 @@ export const Video = ({ loop, disclaimer, autoPlayWhenVisible, autoPauseWhenHidd
69
58
  setPlaybackRate(e.currentTarget.playbackRate);
70
59
  controlledProps.onRateChange?.(e);
71
60
  }, [controlledProps.onRateChange]);
72
- const handleOnFullscreenChangeEvent = useCallback((e) => {
73
- if (document.fullscreenElement === null || shouldDisclaimerBeOn) {
61
+ const handleFullscreenChange = useCallback((isFullscreen) => {
62
+ if (!isFullscreen)
74
63
  setFullscreen(false);
75
- }
76
- }, [shouldDisclaimerBeOn]);
64
+ }, []);
77
65
  const handleOnLoadedMetadataEvent = useCallback((e) => {
78
66
  muteAttributeWorkaround(e.currentTarget, controlledProps.muted ?? false);
79
67
  controlledProps.onLoadedMetadata?.(e);
80
68
  }, [controlledProps.onLoadedMetadata, controlledProps.muted]);
81
69
  // User actions
82
70
  const handlePlayButtonClick = useCallback((e, isPlaying, video) => {
83
- controlledProps.actionHandlers?.playButtonClick?.(e, isPlaying, video);
84
- setPlay(!shouldDisclaimerBeOn);
85
- }, [controlledProps.actionHandlers, shouldDisclaimerBeOn]);
71
+ onPlayButtonClicked?.(e, isPlaying, video);
72
+ setPlay(true);
73
+ }, [onPlayButtonClicked]);
86
74
  const handlePauseButtonClick = useCallback((e, isPlaying, video) => {
87
- controlledProps.actionHandlers?.pauseButtonClick?.(e, isPlaying, video);
75
+ onPauseButtonClicked?.(e, isPlaying, video);
88
76
  setPlay(false);
89
- }, [controlledProps.actionHandlers]);
77
+ }, [onPauseButtonClicked]);
90
78
  const handleLoudButtonClick = useCallback((e, isLoud, video) => {
91
- controlledProps.actionHandlers?.loudButtonClick?.(e, isLoud, video);
79
+ onLoudButtonClicked?.(e, isLoud, video);
92
80
  setMute(false);
93
- }, [controlledProps.actionHandlers]);
81
+ }, [onLoudButtonClicked]);
94
82
  const handleMuteButtonClick = useCallback((e, isLoud, video) => {
95
- controlledProps.actionHandlers?.muteButtonClick?.(e, isLoud, video);
83
+ onMuteButtonClicked?.(e, isLoud, video);
96
84
  setMute(true);
97
- }, [controlledProps.actionHandlers]);
85
+ }, [onMuteButtonClicked]);
98
86
  const handleRateRangeChange = useCallback((e, targetRate, currentRate, video) => {
99
- controlledProps.actionHandlers?.rateRangeChange?.(e, targetRate, currentRate, video);
87
+ onRateRangeChanged?.(e, targetRate, currentRate, video);
100
88
  setPlaybackRate(targetRate);
101
- }, [controlledProps.actionHandlers]);
89
+ }, [onRateRangeChanged]);
102
90
  const handleVolumeRangeChange = useCallback((e, targetVolume, currentVolume, video) => {
103
- controlledProps.actionHandlers?.volumeRangeChange?.(e, targetVolume, currentVolume, video);
91
+ onVolumeRangeChanged?.(e, targetVolume, currentVolume, video);
104
92
  setVolume(targetVolume);
105
- }, [controlledProps.actionHandlers]);
93
+ }, [onVolumeRangeChanged]);
106
94
  const handleFullscreenButtonClick = useCallback((e, isFullscreen, video) => {
107
- controlledProps.actionHandlers?.fullscreenButtonClick?.(e, isFullscreen, video);
108
- setFullscreen((prev) => shouldDisclaimerBeOn ? false : !isFullscreen);
109
- }, [controlledProps.actionHandlers, shouldDisclaimerBeOn]);
110
- // Intersection Observer + Disclaimer
95
+ onFullscreenButtonClicked?.(e, isFullscreen, video);
96
+ setFullscreen(!isFullscreen);
97
+ }, [onFullscreenButtonClicked]);
98
+ // Intersection Observer
111
99
  const onIntersected = useCallback(({ ioEntry }) => {
112
100
  if (ioEntry === undefined)
113
101
  return;
@@ -117,12 +105,10 @@ export const Video = ({ loop, disclaimer, autoPlayWhenVisible, autoPauseWhenHidd
117
105
  if (autoLoudWhenVisible === true && isIntersecting)
118
106
  setMute(false);
119
107
  if (autoPlayWhenVisible === true
120
- && !shouldDisclaimerBeOn
121
108
  && !hasBeenAutoPlayed.current
122
109
  && isIntersecting)
123
110
  setPlay(true);
124
111
  if (autoMuteWhenHidden === true
125
- && !shouldDisclaimerBeOn
126
112
  && !hasBeenAutoPlayed.current
127
113
  && !isIntersecting)
128
114
  setMute(true);
@@ -130,50 +116,19 @@ export const Video = ({ loop, disclaimer, autoPlayWhenVisible, autoPauseWhenHidd
130
116
  autoPlayWhenVisible,
131
117
  autoPauseWhenHidden,
132
118
  autoMuteWhenHidden,
133
- autoLoudWhenVisible,
134
- shouldDisclaimerBeOn
119
+ autoLoudWhenVisible
135
120
  ]);
136
- const handleDiclaimerDismiss = useCallback((prevIsOn) => {
137
- setIsDisclaimerOn(false);
138
- if (controlledProps.autoPlay === true && !hasBeenAutoPlayed.current) {
139
- setPlay(true);
140
- }
141
- disclaimer?.actionHandlers?.dismissClick?.(prevIsOn);
142
- }, [controlledProps.autoPlay, disclaimer?.actionHandlers?.dismissClick]);
121
+ // `autoPlay` is forwarded to the element, but the play state is owned here, so
122
+ // it has to be seeded once on mount for the controls to agree with the element.
143
123
  useEffect(() => {
144
- if (isDisclaimerOn !== shouldDisclaimerBeOn)
145
- setIsDisclaimerOn(shouldDisclaimerBeOn);
146
- }, [isDisclaimerOn, shouldDisclaimerBeOn]);
147
- useEffect(() => {
148
- if (shouldDisclaimerBeOn) {
149
- setMute(true);
150
- setPlay(false);
151
- setFullscreen(false);
152
- }
153
- else if (controlledProps.autoPlay === true && !hasBeenAutoPlayed.current) {
124
+ if (controlledProps.autoPlay === true && !hasBeenAutoPlayed.current)
154
125
  setPlay(true);
155
- }
156
- }, [shouldDisclaimerBeOn]);
126
+ }, []);
157
127
  // Render
158
128
  const c = clss(publicClassName, { cssModule });
159
- const rootClss = mergeClassNames(c(null, {}), wrapperClassName);
160
- const sensitiveContent = _jsx(ControlledVideo, { ...controlledProps, play: play, volume: volume, mute: mute, playbackRate: playbackRate, fullscreen: fullscreen, onPlay: handleOnPlayEvent, onPause: handleOnPauseEvent, onVolumeChange: handleOnVolumeChangeEvent, onRateChange: handleOnRateChangeEvent, onLoadedMetadata: handleOnLoadedMetadataEvent, onFullscreenChange: handleOnFullscreenChangeEvent, actionHandlers: {
161
- ...controlledProps.actionHandlers,
162
- playButtonClick: handlePlayButtonClick,
163
- pauseButtonClick: handlePauseButtonClick,
164
- loudButtonClick: handleLoudButtonClick,
165
- muteButtonClick: handleMuteButtonClick,
166
- volumeRangeChange: handleVolumeRangeChange,
167
- rateRangeChange: handleRateRangeChange,
168
- fullscreenButtonClick: handleFullscreenButtonClick
169
- } });
170
- const disclaimedContent = needsDisclaimer
171
- ? _jsx(Disclaimer, { ...disclaimer, actionHandlers: {
172
- ...disclaimer?.actionHandlers,
173
- dismissClick: handleDiclaimerDismiss
174
- }, children: sensitiveContent })
175
- : sensitiveContent;
176
- return (_jsx("div", { className: rootClss, children: needsObserve
177
- ? _jsx(IntersectionObserverComponent, { onIntersected: onIntersected, children: disclaimedContent })
178
- : disclaimedContent }));
129
+ const rootClss = mergeClassNames(c(), wrapperClassName);
130
+ const videoContent = _jsx(ControlledVideo, { ...controlledProps, play: play && !isTimeControlled, volume: volume, mute: mute, playbackRate: playbackRate, fullscreen: fullscreen, onPlay: handleOnPlayEvent, onPause: handleOnPauseEvent, onVolumeChange: handleOnVolumeChangeEvent, onRateChange: handleOnRateChangeEvent, onLoadedMetadata: handleOnLoadedMetadataEvent, onFullscreenChange: handleFullscreenChange, onPlayButtonClicked: handlePlayButtonClick, onPauseButtonClicked: handlePauseButtonClick, onLoudButtonClicked: handleLoudButtonClick, onMuteButtonClicked: handleMuteButtonClick, onVolumeRangeChanged: handleVolumeRangeChange, onRateRangeChanged: handleRateRangeChange, onFullscreenButtonClicked: handleFullscreenButtonClick });
131
+ return _jsx("div", { className: rootClss, children: needsObserve
132
+ ? _jsx(IntersectionObserverComponent, { onIntersected: onIntersected, children: videoContent })
133
+ : videoContent });
179
134
  };
@@ -1,9 +1,7 @@
1
- import type { Dispatch, SetStateAction } from 'react';
2
1
  export declare const muteAttributeWorkaround: (video: HTMLVideoElement | null, shouldMute: boolean) => void;
3
2
  export declare const forceMute: (video: HTMLVideoElement | null) => void;
4
3
  export declare const forceLoud: (video: HTMLVideoElement | null) => void;
5
4
  export declare const forceVolume: (video: HTMLVideoElement | null, volume: number) => void;
6
- export declare const forceCurrentTime: (video: HTMLVideoElement | null, time: number, setCurrentTime: Dispatch<SetStateAction<number>>) => void;
7
5
  export declare const forcePlay: (video: HTMLVideoElement | null) => Promise<boolean>;
8
6
  export declare const forcePause: (video: HTMLVideoElement | null) => boolean;
9
7
  export declare const forcePlaybackRate: (video: HTMLVideoElement | null, rate: number) => void;
@@ -11,5 +9,39 @@ export declare const forceFullscreen: (video: HTMLVideoElement | null) => Promis
11
9
  export declare const forceExitFullscreen: (video: HTMLVideoElement | null) => Promise<boolean>;
12
10
  export declare function secondsToMs(seconds: number): number;
13
11
  export declare function msToSeconds(ms: number): number;
12
+ /**
13
+ * Renders a duration into a caller-defined pattern.
14
+ *
15
+ * The pattern is a free-form string in which the following tokens are replaced,
16
+ * longest first, so that `mm` is consumed before `m`:
17
+ *
18
+ * - `hh`, `mm`, `ss` — hours, minutes and seconds, zero-padded to two digits.
19
+ * - `ms` — the remaining milliseconds, zero-padded to three digits.
20
+ * - `frame` — the remaining milliseconds expressed in frames, zero-padded to two
21
+ * digits.
22
+ * - `h`, `m`, `s`, `f` — the same four values, unpadded.
23
+ *
24
+ * @param ms - The duration, in milliseconds.
25
+ * @param format - The pattern to fill, e.g. `'mm:ss:ms'`.
26
+ * @param fps - Frame rate used to derive the `frame` and `f` tokens. Defaults to `25`.
27
+ * @returns The formatted duration.
28
+ *
29
+ * @remarks
30
+ * Every field is a remainder of the one above it, never a total: at one hour and
31
+ * a half, `mm` is `30`, not `90`. A pattern that omits `hh` therefore loses the
32
+ * hours rather than folding them into the minutes.
33
+ *
34
+ * Since `h`, `m`, `s` and `f` are tokens on their own, **every** occurrence of
35
+ * those letters is substituted, wherever it sits: `'mm min ss'` renders as
36
+ * `'01 1in 01'`. Separate the fields with punctuation. This is what
37
+ * `agnostic/time/dates/format-date` avoids by delimiting its tokens with
38
+ * `{{…}}` — worth aligning on the day this one is made public.
39
+ */
14
40
  export declare function formatTime(ms: number, format: string, fps?: number): string;
15
- export declare const getTimelineClickProgress: (event: React.MouseEvent<HTMLDivElement>, timeline: HTMLDivElement | null, video: HTMLVideoElement | null) => number;
41
+ /**
42
+ * Where along the timeline a click landed.
43
+ *
44
+ * @param event - The click on the timeline element.
45
+ * @returns A ratio between `0` and `1`, clamped to the element's own bounds.
46
+ */
47
+ export declare const getTimelineClickProgress: (event: React.MouseEvent<HTMLDivElement>) => number;
@@ -28,13 +28,6 @@ export const forceVolume = (video, volume) => {
28
28
  // eslint-disable-next-line no-param-reassign
29
29
  video.volume = volume;
30
30
  };
31
- export const forceCurrentTime = (video, time, setCurrentTime) => {
32
- if (video === null)
33
- return;
34
- // eslint-disable-next-line no-param-reassign
35
- video.currentTime = time;
36
- setCurrentTime(time);
37
- };
38
31
  export const forcePlay = async (video) => {
39
32
  if (video === null)
40
33
  return false;
@@ -105,6 +98,34 @@ export function secondsToMs(seconds) {
105
98
  export function msToSeconds(ms) {
106
99
  return ms / 1000;
107
100
  }
101
+ /**
102
+ * Renders a duration into a caller-defined pattern.
103
+ *
104
+ * The pattern is a free-form string in which the following tokens are replaced,
105
+ * longest first, so that `mm` is consumed before `m`:
106
+ *
107
+ * - `hh`, `mm`, `ss` — hours, minutes and seconds, zero-padded to two digits.
108
+ * - `ms` — the remaining milliseconds, zero-padded to three digits.
109
+ * - `frame` — the remaining milliseconds expressed in frames, zero-padded to two
110
+ * digits.
111
+ * - `h`, `m`, `s`, `f` — the same four values, unpadded.
112
+ *
113
+ * @param ms - The duration, in milliseconds.
114
+ * @param format - The pattern to fill, e.g. `'mm:ss:ms'`.
115
+ * @param fps - Frame rate used to derive the `frame` and `f` tokens. Defaults to `25`.
116
+ * @returns The formatted duration.
117
+ *
118
+ * @remarks
119
+ * Every field is a remainder of the one above it, never a total: at one hour and
120
+ * a half, `mm` is `30`, not `90`. A pattern that omits `hh` therefore loses the
121
+ * hours rather than folding them into the minutes.
122
+ *
123
+ * Since `h`, `m`, `s` and `f` are tokens on their own, **every** occurrence of
124
+ * those letters is substituted, wherever it sits: `'mm min ss'` renders as
125
+ * `'01 1in 01'`. Separate the fields with punctuation. This is what
126
+ * `agnostic/time/dates/format-date` avoids by delimiting its tokens with
127
+ * `{{…}}` — worth aligning on the day this one is made public.
128
+ */
108
129
  export function formatTime(ms, format, fps = 25) {
109
130
  const totalSeconds = Math.floor(ms / 1000);
110
131
  const hours = Math.floor(totalSeconds / 3600);
@@ -123,17 +144,19 @@ export function formatTime(ms, format, fps = 25) {
123
144
  s: seconds,
124
145
  f: frames
125
146
  };
126
- const result = Object
147
+ return Object
127
148
  .keys(tokens)
128
149
  .sort((a, b) => b.length - a.length)
129
150
  .reduce((acc, t) => acc.replace(new RegExp(t, 'gv'), String(tokens[t])), format);
130
- return result;
131
151
  }
132
- export const getTimelineClickProgress = (event, timeline, video) => {
133
- if (video === null || timeline === null)
134
- return 0;
152
+ /**
153
+ * Where along the timeline a click landed.
154
+ *
155
+ * @param event - The click on the timeline element.
156
+ * @returns A ratio between `0` and `1`, clamped to the element's own bounds.
157
+ */
158
+ export const getTimelineClickProgress = (event) => {
135
159
  const timelineRect = event.currentTarget.getBoundingClientRect();
136
160
  const position = event.clientX - timelineRect.left;
137
- const progress = Math.min(1, Math.max(0, position / timelineRect.width));
138
- return progress;
161
+ return Math.min(1, Math.max(0, position / timelineRect.width));
139
162
  };