@design-edito/tools 0.5.2 → 0.5.3

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 (129) hide show
  1. package/agnostic/html/deep-select/index.js +5 -3
  2. package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +8 -8
  3. package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +8 -8
  4. package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +2 -2
  5. package/agnostic/html/hyper-json/smart-tags/isolated/index.js +2 -2
  6. package/agnostic/html/index.d.ts +1 -1
  7. package/agnostic/html/index.js +1 -1
  8. package/agnostic/misc/index.d.ts +3 -3
  9. package/agnostic/misc/index.js +3 -3
  10. package/agnostic/numbers/index.d.ts +1 -1
  11. package/agnostic/numbers/index.js +1 -1
  12. package/agnostic/optim/index.d.ts +1 -1
  13. package/agnostic/optim/index.js +1 -1
  14. package/agnostic/strings/index.d.ts +1 -1
  15. package/agnostic/strings/index.js +1 -1
  16. package/agnostic/time/dates/format-date/index.d.ts +4 -2
  17. package/agnostic/time/dates/format-date/index.js +43 -94
  18. package/agnostic/time/dates/format-date/index.test.js +154 -0
  19. package/agnostic/time/dates/format-date/parts.d.ts +28 -0
  20. package/agnostic/time/dates/format-date/parts.js +136 -0
  21. package/agnostic/time/dates/format-date/types.d.ts +84 -0
  22. package/agnostic/time/dates/format-date/types.js +1 -0
  23. package/agnostic/time/duration/format-duration/index.d.ts +42 -0
  24. package/agnostic/time/duration/format-duration/index.js +94 -0
  25. package/agnostic/time/duration/format-duration/index.test.d.ts +1 -0
  26. package/agnostic/time/duration/format-duration/index.test.js +148 -0
  27. package/agnostic/time/duration/format-duration/parts.d.ts +32 -0
  28. package/agnostic/time/duration/format-duration/parts.js +107 -0
  29. package/agnostic/time/duration/format-duration/types.d.ts +93 -0
  30. package/agnostic/time/duration/format-duration/types.js +1 -0
  31. package/agnostic/time/duration/index.d.ts +6 -1
  32. package/agnostic/time/duration/index.js +2 -2
  33. package/agnostic/time/duration/index.test.d.ts +1 -0
  34. package/agnostic/time/duration/index.test.js +168 -0
  35. package/agnostic/time/index.d.ts +1 -1
  36. package/agnostic/time/index.js +1 -1
  37. package/components/BeforeAfter/index.controlled.d.ts +25 -25
  38. package/components/BeforeAfter/index.controlled.js +47 -53
  39. package/components/BeforeAfter/index.d.ts +20 -23
  40. package/components/BeforeAfter/index.js +48 -40
  41. package/components/Button/index.js +1 -1
  42. package/components/Clippable/index.d.ts +17 -26
  43. package/components/Clippable/index.js +21 -23
  44. package/components/Disclaimer/index.d.ts +29 -22
  45. package/components/Disclaimer/index.js +29 -24
  46. package/components/Drawer/index.d.ts +45 -25
  47. package/components/Drawer/index.js +61 -52
  48. package/components/EventListener/index.d.ts +4 -5
  49. package/components/Gallery/index.d.ts +14 -16
  50. package/components/Gallery/index.js +25 -30
  51. package/components/Iframe/index.js +2 -2
  52. package/components/Image/index.d.ts +5 -12
  53. package/components/Image/index.js +11 -32
  54. package/components/Input/index.d.ts +23 -19
  55. package/components/Input/index.js +30 -20
  56. package/components/JsonEditor/index.d.ts +177 -9
  57. package/components/JsonEditor/index.js +133 -77
  58. package/components/JsonEditor/types.d.ts +11 -0
  59. package/components/JsonEditor/types.js +1 -0
  60. package/components/JsonEditor/utils.d.ts +8 -0
  61. package/components/JsonEditor/utils.js +23 -0
  62. package/components/ListLoader/index.controlled.d.ts +5 -5
  63. package/components/ListLoader/index.controlled.js +4 -4
  64. package/components/ListLoader/index.d.ts +11 -11
  65. package/components/ListLoader/index.js +12 -12
  66. package/components/Overlayer/index.js +4 -4
  67. package/components/Paginator/index.d.ts +7 -12
  68. package/components/Paginator/index.js +7 -11
  69. package/components/ResizeObserver/index.d.ts +14 -3
  70. package/components/ResizeObserver/index.js +49 -24
  71. package/components/Scrllgngn/index.d.ts +17 -13
  72. package/components/Scrllgngn/index.js +25 -84
  73. package/components/Scrllgngn/utils.d.ts +52 -0
  74. package/components/Scrllgngn/utils.js +102 -0
  75. package/components/ScrollListener/index.d.ts +43 -28
  76. package/components/ScrollListener/index.js +59 -79
  77. package/components/ScrollListener/utils.d.ts +37 -16
  78. package/components/ScrollListener/utils.js +141 -28
  79. package/components/Select/index.d.ts +24 -20
  80. package/components/Select/index.js +30 -20
  81. package/components/Sequencer/index.controlled.d.ts +16 -26
  82. package/components/Sequencer/index.controlled.js +15 -12
  83. package/components/Sequencer/index.d.ts +25 -30
  84. package/components/Sequencer/index.js +20 -34
  85. package/components/Subtitles/index.d.ts +29 -5
  86. package/components/Subtitles/index.js +90 -12
  87. package/components/Textarea/index.d.ts +26 -20
  88. package/components/Textarea/index.js +40 -21
  89. package/components/Theatre/index.d.ts +35 -39
  90. package/components/Theatre/index.js +46 -58
  91. package/components/UIModule/index.d.ts +25 -29
  92. package/components/UIModule/index.js +81 -110
  93. package/components/Video/index.controlled.d.ts +51 -51
  94. package/components/Video/index.controlled.js +75 -72
  95. package/components/Video/index.d.ts +12 -14
  96. package/components/Video/index.js +41 -86
  97. package/components/Video/utils.d.ts +35 -3
  98. package/components/Video/utils.js +37 -14
  99. package/components/index.d.ts +1 -1
  100. package/components/index.js +1 -1
  101. package/components/utils/index.d.ts +22 -0
  102. package/components/utils/index.js +31 -0
  103. package/index.d.ts +1 -1
  104. package/index.js +1 -1
  105. package/node/@aws-s3/storage/file/index.d.ts +1 -1
  106. package/node/@aws-s3/storage/file/index.js +1 -1
  107. package/node/@google-cloud/storage/index.d.ts +1 -1
  108. package/node/@google-cloud/storage/index.js +1 -1
  109. package/node/images/index.d.ts +1 -1
  110. package/node/images/index.js +1 -1
  111. package/node/images/transform/operations/index.d.ts +1 -1
  112. package/node/images/transform/operations/index.js +1 -1
  113. package/node/index.d.ts +1 -1
  114. package/node/index.js +1 -1
  115. package/package.json +20 -5
  116. package/TODO.md +0 -269
  117. package/components/BeforeAfter/utils.d.ts +0 -4
  118. package/components/BeforeAfter/utils.js +0 -9
  119. package/components/Input/index.controlled.d.ts +0 -42
  120. package/components/Input/index.controlled.js +0 -54
  121. package/components/Select/index.controlled.d.ts +0 -43
  122. package/components/Select/index.controlled.js +0 -54
  123. package/components/Subtitles/types.d.ts +0 -24
  124. package/components/Subtitles/utils.d.ts +0 -32
  125. package/components/Subtitles/utils.js +0 -107
  126. package/components/Textarea/index.controlled.d.ts +0 -50
  127. package/components/Textarea/index.controlled.js +0 -75
  128. /package/{components/Subtitles/types.js → agnostic/time/dates/format-date/index.test.d.ts} +0 -0
  129. /package/components/ResizeObserver/{style.module.css → styles.module.css} +0 -0
@@ -1,36 +1,33 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
3
  import { clss } from '../../agnostic/css/clss/index.js';
4
4
  import { unknownToString } from '../../agnostic/errors/unknown-to-string/index.js';
5
5
  import { isNonNullObject } from '../../agnostic/objects/is-object/index.js';
6
6
  import { randomHash } from '../../agnostic/random/uuid/index.js';
7
- import { mergeClassNames } from '../utils/index.js';
7
+ import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
8
8
  import { uiModule as publicClassName } from '../public-classnames.js';
9
9
  import cssModule from './styles.module.css';
10
10
  /**
11
- * Dynamic UI module host component. Asynchronously imports an ES module by URL,
12
- * validates its exported interface, calls its `init` lifecycle to obtain a DOM
13
- * `Element`, and appends that element to its own root `<div>`. Handles loading,
14
- * error, and teardown states automatically.
11
+ * Dynamic UI module host. Asynchronously imports an ES module by URL, validates
12
+ * its exported interface, calls its `init` lifecycle to obtain a DOM `Element`,
13
+ * and appends that element to its own root `<div>`.
15
14
  *
16
15
  * The imported module is expected to conform to the {@link ModuleData} interface.
17
16
  * Any violation (missing exports, wrong types, `init` not returning an `Element`)
18
17
  * transitions the component into the `--error` state and logs to `console.error`.
19
18
  *
20
- * ### Root element modifiers
21
- * The root `<div>` receives the public class name defined by `uiModule` and
22
- * the following BEM-style modifier classes reflecting the current load lifecycle:
23
- * - `--loading` — the module fetch is in progress.
24
- * - `--no-module` — no module has been loaded yet (`src` is undefined or the
25
- * effect has not run).
26
- * - `--error` — the import, validation, or `init` call failed.
27
- * - `--loaded` — the module passed validation and `init` returned successfully.
28
- * - `--initialized` — the `Element` returned by `init` has been appended to the
29
- * host `<div>`.
19
+ * ### CSS modifiers
20
+ * Reflecting the current load lifecycle:
21
+ * - `loading` the module fetch is in progress.
22
+ * - `no-module`nothing has been loaded yet (`src` is undefined, or the
23
+ * effect has not run).
24
+ * - `error` the import, the validation, or the `init` call failed.
25
+ * - `loaded` — the module passed validation and `init` returned successfully.
26
+ * - `initialized` — the `Element` returned by `init` has been appended.
30
27
  *
31
28
  * ### Root element attributes
32
- * - `id` — a stable randomly generated ID (prefixed `f`) assigned once on mount.
33
- * Used to scope the module's `css` entries to this specific instance.
29
+ * - `id` — a stable generated id, assigned once on mount and used to scope the
30
+ * module's `css` entries to this specific instance.
34
31
  *
35
32
  * @param props - Component properties.
36
33
  * @see {@link Props}
@@ -38,107 +35,81 @@ import cssModule from './styles.module.css';
38
35
  * @returns A host `<div>` into which the module's root `Element` is appended,
39
36
  * along with any `<style>` blocks exported by the module.
40
37
  */
41
- export const UIModule = ({ src, props, stateHandlers, className }) => {
38
+ export const UIModule = ({ src, props, onIdGenerated, onIsLoadingChanged, onLoadedModuleChanged, onModuleTargetChanged, className }) => {
42
39
  // State & refs
43
- const [id] = useState(`f${randomHash(10)}`);
44
- const [loading, setLoading] = useState(false);
45
- const [loadedModule, _setLoadedModule] = useState(null);
46
- const [moduleTarget, _setModuleTarget] = useState(null);
40
+ const [id] = useState(() => `f${randomHash(10)}`);
41
+ const [isLoading, setIsLoading] = useState(false);
42
+ const [loadedModule, setLoadedModule] = useState(null);
43
+ const [moduleTarget, setModuleTarget] = useState(null);
47
44
  const rootRef = useRef(null);
48
- const loadedModuleRef = useRef(null);
49
- const moduleTargetRef = useRef(null);
50
- const setLoadedModule = (data) => {
51
- loadedModuleRef.current = data;
52
- _setLoadedModule(data);
53
- };
54
- const setModuleTarget = (data) => {
55
- moduleTargetRef.current = data;
56
- _setModuleTarget(data);
57
- };
58
- // State changes dispatch
59
- useEffect(() => { stateHandlers?.idChanged?.(id); }, [id, stateHandlers]);
60
- useEffect(() => { stateHandlers?.isLoadingChanged?.(loading); }, [loading, stateHandlers]);
61
- useEffect(() => { stateHandlers?.loadedModuleChanged?.(loadedModule); }, [loadedModule, stateHandlers]);
62
- useEffect(() => { stateHandlers?.moduleTargetChanged?.(moduleTarget); }, [moduleTarget, stateHandlers]);
63
- // Fx dep. `src` - load & init module effect
45
+ // What the teardown needs, held outside state so the load effect can depend on
46
+ // `src` alone and still destroy whatever is actually live at cleanup time.
47
+ const liveInstanceRef = useRef(null);
48
+ // State dispatch
49
+ useChangeDispatch(isLoading, onIsLoadingChanged);
50
+ useChangeDispatch(loadedModule, onLoadedModuleChanged);
51
+ useChangeDispatch(moduleTarget, onModuleTargetChanged);
52
+ // Fx. no dep. - report the generated id, which never changes afterwards
53
+ useEffect(() => { onIdGenerated?.(id); }, []);
54
+ // Fx. dep. `src` - import, validate and initialize the module
64
55
  useEffect(() => {
65
56
  if (src === undefined)
66
57
  return;
67
- try {
68
- setLoading(true);
69
- import(src)
70
- .then(data => {
71
- setLoading(false);
72
- const errs = {
73
- notMod: new Error('Not a module'),
74
- initFunc: new Error('Module exported member `init` must be a function'),
75
- destroyFunc: new Error('Module exported member `destroy` must be a function'),
76
- cssStrArr: new Error('Module exported member `css` must be an array of strings'),
77
- updFunc: new Error('Module exported member `update` must be a function'),
78
- initRetElt: new Error('Module exported function `init` must return an Element'),
79
- initRetFirstElt: new Error('Module exported function `init` must return an array containing an Element in its first position')
80
- };
81
- if (!isNonNullObject(data))
82
- return setLoadedModule(errs.notMod);
83
- if (!('init' in data))
84
- return setLoadedModule(errs.initFunc);
85
- if (typeof data.init !== 'function')
86
- return setLoadedModule(errs.initFunc);
87
- if (!('destroy' in data) || typeof data.destroy !== 'function')
88
- return setLoadedModule(errs.destroyFunc);
89
- if ('css' in data) {
90
- if (!Array.isArray(data.css))
91
- return setLoadedModule(errs.cssStrArr);
92
- if (data.css.some(i => typeof i !== 'string'))
93
- return setLoadedModule(errs.cssStrArr);
94
- }
95
- if ('update' in data && typeof data.update !== 'function')
96
- return setLoadedModule(errs.updFunc);
97
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- shape fully validated by the checks above
98
- const module = data;
99
- setLoadedModule(module);
100
- try {
101
- const target = module.init(props ?? {});
102
- if (!(target instanceof Element))
103
- return setLoadedModule(errs.initRetElt);
104
- setModuleTarget(target);
105
- }
106
- catch (err) {
107
- setModuleTarget(null);
108
- const e = err instanceof Error
109
- ? err
110
- : new Error(unknownToString(err));
111
- setLoadedModule(e);
112
- }
113
- }).catch((err) => {
114
- setLoading(false);
115
- setLoadedModule(err instanceof Error ? err : new Error(unknownToString(err)));
58
+ setIsLoading(true);
59
+ void import(src)
60
+ .then(data => {
61
+ setIsLoading(false);
62
+ if (!isNonNullObject(data))
63
+ return setLoadedModule(new Error('Not a module'));
64
+ if (!('init' in data) || typeof data.init !== 'function')
65
+ return setLoadedModule(new Error('Module exported member `init` must be a function'));
66
+ if (!('destroy' in data) || typeof data.destroy !== 'function')
67
+ return setLoadedModule(new Error('Module exported member `destroy` must be a function'));
68
+ if ('css' in data) {
69
+ if (!Array.isArray(data.css))
70
+ return setLoadedModule(new Error('Module exported member `css` must be an array of strings'));
71
+ if (data.css.some(entry => typeof entry !== 'string'))
72
+ return setLoadedModule(new Error('Module exported member `css` must be an array of strings'));
73
+ }
74
+ if ('update' in data && typeof data.update !== 'function')
75
+ return setLoadedModule(new Error('Module exported member `update` must be a function'));
76
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- shape fully validated by the checks above
77
+ const module = data;
78
+ setLoadedModule(module);
79
+ try {
80
+ const target = module.init(props ?? {});
81
+ if (!(target instanceof Element))
82
+ return setLoadedModule(new Error('Module exported function `init` must return an Element'));
83
+ liveInstanceRef.current = { module, target };
84
+ setModuleTarget(target);
85
+ }
86
+ catch (err) {
87
+ liveInstanceRef.current = null;
116
88
  setModuleTarget(null);
117
- });
118
- }
119
- catch (err) {
120
- if (err instanceof Error)
121
- return setLoadedModule(err);
122
- const errStr = unknownToString(err);
123
- return setLoadedModule(new Error(errStr));
124
- }
89
+ setLoadedModule(err instanceof Error ? err : new Error(unknownToString(err)));
90
+ }
91
+ })
92
+ .catch((err) => {
93
+ setIsLoading(false);
94
+ liveInstanceRef.current = null;
95
+ setLoadedModule(err instanceof Error ? err : new Error(unknownToString(err)));
96
+ setModuleTarget(null);
97
+ });
125
98
  return () => {
126
- if (moduleTargetRef.current === null)
127
- return;
128
- if (loadedModuleRef.current instanceof Error)
129
- return;
130
- if (loadedModuleRef.current === null)
99
+ const liveInstance = liveInstanceRef.current;
100
+ if (liveInstance === null)
131
101
  return;
132
- loadedModuleRef.current.destroy(moduleTargetRef.current);
102
+ liveInstance.module.destroy(liveInstance.target);
103
+ liveInstanceRef.current = null;
133
104
  };
134
105
  }, [src]);
135
- // Fx dep. `loadedModule` - log load errors
106
+ // Fx. dep. `loadedModule` - surface load errors
136
107
  useEffect(() => {
137
108
  // eslint-disable-next-line no-console
138
109
  if (loadedModule instanceof Error)
139
110
  console.error(loadedModule);
140
111
  }, [loadedModule]);
141
- // Fx dep. `moduleTarget` - append the rendered module
112
+ // Fx. dep. `moduleTarget` - append the element the module built
142
113
  useEffect(() => {
143
114
  if (moduleTarget === null)
144
115
  return;
@@ -148,14 +119,14 @@ export const UIModule = ({ src, props, stateHandlers, className }) => {
148
119
  }, [moduleTarget]);
149
120
  // Rendering
150
121
  const c = clss(publicClassName, { cssModule });
122
+ const hasErrored = loadedModule instanceof Error;
151
123
  const rootClss = mergeClassNames(c(null, {
152
- loading,
124
+ 'loading': isLoading,
153
125
  'no-module': loadedModule === null,
154
- 'error': loadedModule instanceof Error,
155
- 'loaded': !loading && loadedModule !== null && !(loadedModule instanceof Error),
126
+ 'error': hasErrored,
127
+ 'loaded': !isLoading && loadedModule !== null && !hasErrored,
156
128
  'initialized': moduleTarget !== null
157
129
  }), className);
158
- return _jsxs("div", { className: rootClss, ref: rootRef, id: id, children: [loadedModule === null && '', loadedModule !== null
159
- && !(loadedModule instanceof Error)
160
- && loadedModule.css?.map(css => _jsx("style", { children: `.${publicClassName}#${id} { ${css} }` }))] });
130
+ const moduleCss = hasErrored || loadedModule === null ? [] : loadedModule.css ?? [];
131
+ return _jsx("div", { className: rootClss, ref: rootRef, id: id, children: moduleCss.map((css, cssPos) => _jsx("style", { children: `.${publicClassName}#${id} { ${css} }` }, cssPos)) });
161
132
  };
@@ -1,4 +1,4 @@
1
- import { type FunctionComponent, type PropsWithChildren, type VideoHTMLAttributes, type ReactEventHandler } from 'react';
1
+ import { type FunctionComponent, type PropsWithChildren, type VideoHTMLAttributes } from 'react';
2
2
  import type { WithClassName } from '../utils/types.js';
3
3
  import { type Props as SubsProps } from '../Subtitles/index.js';
4
4
  /**
@@ -27,47 +27,6 @@ type TrackData = {
27
27
  label?: string;
28
28
  default?: boolean;
29
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
30
  /**
72
31
  * Props for the ControlledVideo component.
73
32
  *
@@ -84,11 +43,41 @@ export type StateHandlersProps = {
84
43
  * @property volume - External control of volume (0 to 1).
85
44
  * @property mute - External control of mute (true = muted).
86
45
  * @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.
46
+ * @property currentTimeMs - External control of current time (in ms). Providing
47
+ * it hands ownership of the time to the parent: the value is displayed as given,
48
+ * the element is seeked to it on every change, and user interactions that would
49
+ * otherwise move the time (timeline click) no longer do — the parent is expected
50
+ * to update the prop instead. Because a playing element advances the time by
51
+ * itself, a controlled time also implies a stopped video: `play` and `autoPlay`
52
+ * are ignored for as long as this prop is provided.
53
+ * @property onPlayButtonClicked - Called when the play button is clicked, before
54
+ * the component reacts, with the playback state as it was.
55
+ * @property onPauseButtonClicked - Called when the pause button is clicked, before
56
+ * the component reacts, with the playback state as it was.
57
+ * @property onLoudButtonClicked - Called when the unmute button is clicked, before
58
+ * the component reacts, with the mute state as it was (`true` = unmuted).
59
+ * @property onMuteButtonClicked - Called when the mute button is clicked, before
60
+ * the component reacts, with the mute state as it was (`true` = unmuted).
61
+ * @property onVolumeRangeChanged - Called when the volume slider moves, before the
62
+ * component reacts, with the target and current volumes (both `0` to `1`).
63
+ * @property onRateRangeChanged - Called when the playback rate slider moves, before
64
+ * the component reacts, with the target and current rates.
65
+ * @property onFullscreenButtonClicked - Called when the fullscreen button is
66
+ * clicked, before the component reacts, with the fullscreen state as it was.
67
+ * @property onTimelineClicked - Called when the timeline is clicked, before the
68
+ * component reacts, with the target and current times (in seconds). The component
69
+ * seeks to the target right after, unless the time is controlled.
70
+ * @property onIsPlayingChanged - Called once the playback state has changed.
71
+ * @property onIsFullscreenChanged - Called once the fullscreen state has changed.
72
+ * @property onIsLoudChanged - Called once the mute state has changed (`true` = unmuted).
73
+ * @property onVolumeChanged - Called once the volume has changed (`0` to `1`).
74
+ * @property onPlaybackRateChanged - Called once the playback rate has changed.
75
+ * @property onCurrentTimeMsChanged - Called once the current time has changed, in
76
+ * milliseconds, to match the `currentTimeMs` prop.
77
+ * @property onFullscreenChange - Called when the *browser* enters or leaves
78
+ * fullscreen on its own — pressing Escape, typically — with the new state. This is
79
+ * the signal a parent needs to update its `fullscreen` prop, and it is distinct
80
+ * from `onIsFullscreenChanged`, which merely echoes that prop back once changed.
92
81
  * @property className - Additional CSS class for the root element.
93
82
  * @property children - React content inserted into the <video> tag (fallback, etc).
94
83
  *
@@ -109,10 +98,21 @@ export type Props = PropsWithChildren<WithClassName<{
109
98
  mute?: boolean;
110
99
  playbackRate?: number;
111
100
  currentTimeMs?: number;
112
- onFullscreenChange?: ReactEventHandler<HTMLVideoElement>;
113
- actionHandlers?: ActionHandlersProps;
114
- stateHandlers?: StateHandlersProps;
115
- _modifiers?: Record<string, boolean>;
101
+ onPlayButtonClicked?: (e: React.MouseEvent<HTMLButtonElement>, isPlaying: boolean, video: HTMLVideoElement | null) => void;
102
+ onPauseButtonClicked?: (e: React.MouseEvent<HTMLButtonElement>, isPlaying: boolean, video: HTMLVideoElement | null) => void;
103
+ onLoudButtonClicked?: (e: React.MouseEvent<HTMLButtonElement>, isLoud: boolean, video: HTMLVideoElement | null) => void;
104
+ onMuteButtonClicked?: (e: React.MouseEvent<HTMLButtonElement>, isLoud: boolean, video: HTMLVideoElement | null) => void;
105
+ onVolumeRangeChanged?: (e: React.ChangeEvent<HTMLInputElement>, targetVolume: number, currentVolume: number, video: HTMLVideoElement | null) => void;
106
+ onRateRangeChanged?: (e: React.ChangeEvent<HTMLInputElement>, targetRate: number, currentRate: number, video: HTMLVideoElement | null) => void;
107
+ onFullscreenButtonClicked?: (e: React.MouseEvent<HTMLButtonElement>, isFullscreen: boolean, video: HTMLVideoElement | null) => void;
108
+ onTimelineClicked?: (e: React.MouseEvent<HTMLDivElement>, targetTime: number, currentTime: number, video: HTMLVideoElement | null) => void;
109
+ onIsPlayingChanged?: (isPlaying: boolean) => void;
110
+ onIsFullscreenChanged?: (isFullscreen: boolean) => void;
111
+ onIsLoudChanged?: (isLoud: boolean) => void;
112
+ onVolumeChanged?: (volume: number) => void;
113
+ onPlaybackRateChanged?: (playbackRate: number) => void;
114
+ onCurrentTimeMsChanged?: (currentTimeMs: number) => void;
115
+ onFullscreenChange?: (isFullscreen: boolean) => void;
116
116
  }> & VideoHTMLAttributes<HTMLVideoElement>>;
117
117
  /**
118
118
  * Full-featured video player component. Wraps a native `<video>` element with
@@ -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
  };