@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,90 +1,78 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState, useRef } 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 { theatre as publicClassName } from '../public-classnames.js';
6
6
  import cssModule from './styles.module.css';
7
7
  /**
8
8
  * Theatre mode component. Wraps content in a toggleable fullscreen-like "stage"
9
- * overlay. Supports both controlled and uncontrolled usage.
9
+ * overlay.
10
10
  *
11
- * When `isOn` is not provided the component manages its own open/closed state
12
- * internally. When `isOn` is provided it acts as the source of truth and the
13
- * internal state is ignored.
11
+ * ### CSS modifiers
12
+ * - `on` theatre mode is active.
13
+ * - `off` — theatre mode is inactive.
14
14
  *
15
- * ### Root element modifiers
16
- * The root `<div>` receives the public class name defined by `theatre` and the
17
- * following BEM-style modifier classes:
18
- * - `--on` — when theatre mode is active.
19
- * - `--off` — when theatre mode is inactive.
20
- *
21
- * ### Child elements
22
- * - `__stage` — container rendered inside the root that holds the duplicated
23
- * `children` when theatre mode is active. Only mounted when `isOn` is `true`.
24
- * - `__open-btn` — clickable element that activates theatre mode.
25
- * - `__close-btn` — clickable element that deactivates theatre mode.
15
+ * ### CSS elements
16
+ * - `stage` holds the duplicated `children`, mounted only when `isOn`.
17
+ * - `open-btn`
18
+ * - `close-btn`
26
19
  *
27
20
  * @param props - Component properties.
28
21
  * @see {@link Props}
29
22
  * @returns A root `<div>` containing the children in their original position,
30
- * a stage overlay with the duplicated children (when active), and the open/close
31
- * toggle buttons.
23
+ * a stage overlay with the duplicated children when active, and both toggles.
24
+ *
25
+ * @remarks
26
+ * - In controlled mode (`isOn` defined), the state is fully driven by the
27
+ * parent and internal state is never updated.
28
+ * - `onToggleClicked` fires in both modes, for all four toggle sources — a
29
+ * controlled parent needs it to know a toggle was requested at all.
30
+ * - `onIsOnChanged` fires in both modes too, and never on mount.
32
31
  */
33
- export const Theatre = ({ closeBtnContent, openBtnContent, isOn, defaultIsOn, exitOnEscape, exitOnBgClick, stateHandlers, actionHandlers, children, className }) => {
32
+ export const Theatre = ({ closeBtnContent, openBtnContent, isOn: isOnProp, defaultIsOn = false, exitOnEscape, exitOnBgClick, onToggleClicked, onIsOnChanged, children, className }) => {
34
33
  // State & refs
35
- const [internalIsOn, setInternalIsOn] = useState(defaultIsOn ?? false);
34
+ const [internalIsOn, setInternalIsOn] = useState(defaultIsOn);
36
35
  const stageRef = useRef(null);
37
- const isTheatreOn = isOn ?? internalIsOn;
38
- const prevIsTheatreOnRef = useRef(isTheatreOn);
39
- // Handlers
40
- const handleCloseBtnClick = () => {
41
- actionHandlers?.toggleClick?.(isTheatreOn);
42
- if (isOn === undefined)
43
- setInternalIsOn(false);
44
- };
45
- const handleOpenBtnClick = () => {
46
- actionHandlers?.toggleClick?.(isTheatreOn);
47
- if (isOn === undefined)
48
- setInternalIsOn(true);
36
+ const isControlled = isOnProp !== undefined;
37
+ const isOn = isOnProp ?? internalIsOn;
38
+ // State dispatch
39
+ useChangeDispatch(isOn, onIsOnChanged);
40
+ // User action handlers
41
+ const requestToggle = (targetIsOn) => {
42
+ onToggleClicked?.(isOn);
43
+ if (isControlled)
44
+ return;
45
+ setInternalIsOn(targetIsOn);
49
46
  };
47
+ const handleOpenBtnClick = () => requestToggle(true);
48
+ const handleCloseBtnClick = () => requestToggle(false);
50
49
  const handleStageBgClick = e => {
51
50
  if (exitOnBgClick !== true)
52
51
  return;
53
52
  if (e.target !== stageRef.current)
54
53
  return;
55
- actionHandlers?.toggleClick?.(isTheatreOn);
56
- if (isOn === undefined)
57
- setInternalIsOn(false);
54
+ requestToggle(false);
58
55
  };
59
- // Effects
60
- useEffect(() => {
61
- if (prevIsTheatreOnRef.current !== isTheatreOn) {
62
- stateHandlers?.toggled?.(isTheatreOn);
63
- prevIsTheatreOnRef.current = isTheatreOn;
64
- }
65
- }, [isTheatreOn, stateHandlers]);
56
+ // Fx. dep. `exitOnEscape`, `isOn` - close the stage on the Escape key
66
57
  useEffect(() => {
67
- if (exitOnEscape === true
68
- || !isTheatreOn
69
- || isOn !== undefined)
58
+ if (exitOnEscape !== true || !isOn)
70
59
  return;
71
- const listener = (e) => {
60
+ const handleKeyDown = (e) => {
72
61
  if (e.key !== 'Escape')
73
62
  return;
74
- actionHandlers?.toggleClick?.(isTheatreOn);
75
- setInternalIsOn(false);
63
+ requestToggle(false);
76
64
  };
77
- window.addEventListener('keydown', listener);
78
- return () => window.removeEventListener('keydown', listener);
79
- }, [exitOnEscape, isTheatreOn, isOn]);
80
- // Render
65
+ window.addEventListener('keydown', handleKeyDown);
66
+ return () => window.removeEventListener('keydown', handleKeyDown);
67
+ }, [exitOnEscape, isOn, isControlled]);
68
+ // Rendering
81
69
  const c = clss(publicClassName, { cssModule });
82
70
  const rootClss = mergeClassNames(c(null, {
83
- 'on': isTheatreOn,
84
- 'off': !isTheatreOn
71
+ 'on': isOn,
72
+ 'off': !isOn
85
73
  }), className);
86
- const stageClass = c('stage');
87
- const openBtnClass = c('open-btn');
88
- const closeBtnClass = c('close-btn');
89
- return _jsxs("div", { className: rootClss, children: [children, _jsx("div", { className: stageClass, onClick: handleStageBgClick, ref: stageRef, children: isTheatreOn && children }), _jsx("div", { className: closeBtnClass, onClick: handleCloseBtnClick, children: closeBtnContent }), _jsx("div", { className: openBtnClass, onClick: handleOpenBtnClick, children: openBtnContent })] });
74
+ const stageClss = c('stage');
75
+ const openBtnClss = c('open-btn');
76
+ const closeBtnClss = c('close-btn');
77
+ return _jsxs("div", { className: rootClss, children: [children, _jsx("div", { className: stageClss, onClick: handleStageBgClick, ref: stageRef, children: isOn && children }), _jsx("div", { className: closeBtnClss, onClick: handleCloseBtnClick, children: closeBtnContent }), _jsx("div", { className: openBtnClss, onClick: handleOpenBtnClick, children: openBtnContent })] });
90
78
  };
@@ -31,49 +31,45 @@ type ModuleData = {
31
31
  * component stays in the `--no-module` state.
32
32
  * @property props - Arbitrary key-value object forwarded verbatim to the
33
33
  * module's `init` call and, on subsequent changes, to `update` (if exported).
34
- * @property stateHandlers - Optional callbacks invoked whenever internal state changes:
35
- * - `idChanged` called with the stable instance ID once on mount.
36
- * - `isLoadingChanged` called with the new loading state on every transition.
37
- * - `loadedModuleChanged` — called with the new module value (`ModuleData`, `Error`, or `null`)
38
- * after each load attempt or teardown.
39
- * - `moduleTargetChanged` called with the `Element` returned by `init`, or `null`
40
- * when the module is unloaded or errored.
34
+ * @property onIdGenerated - Called once on mount with the instance's generated
35
+ * `id`. The id never changes afterwards, so this fires exactly once.
36
+ * @property onIsLoadingChanged - Called after the loading state changed, with
37
+ * the new value.
38
+ * @property onLoadedModuleChanged - Called after the loaded module changed, with
39
+ * the new value: the validated {@link ModuleData}, an `Error`, or `null`.
40
+ * @property onModuleTargetChanged - Called after the hosted element changed,
41
+ * with the `Element` returned by `init`, or `null` once unloaded or errored.
41
42
  * @property className - Optional additional class name(s) applied to the root element.
42
43
  */
43
44
  export type Props = WithClassName<{
44
45
  src?: string;
45
46
  props?: Record<string, unknown>;
46
- stateHandlers?: {
47
- idChanged?: (id: string) => void;
48
- isLoadingChanged?: (isLoading: boolean) => void;
49
- loadedModuleChanged?: (loadedModule: ModuleData | Error | null) => void;
50
- moduleTargetChanged?: (moduleTarget: Element | null) => void;
51
- };
47
+ onIdGenerated?: (id: string) => void;
48
+ onIsLoadingChanged?: (isLoading: boolean) => void;
49
+ onLoadedModuleChanged?: (loadedModule: ModuleData | Error | null) => void;
50
+ onModuleTargetChanged?: (moduleTarget: Element | null) => void;
52
51
  }>;
53
52
  /**
54
- * Dynamic UI module host component. Asynchronously imports an ES module by URL,
55
- * validates its exported interface, calls its `init` lifecycle to obtain a DOM
56
- * `Element`, and appends that element to its own root `<div>`. Handles loading,
57
- * error, and teardown states automatically.
53
+ * Dynamic UI module host. Asynchronously imports an ES module by URL, validates
54
+ * its exported interface, calls its `init` lifecycle to obtain a DOM `Element`,
55
+ * and appends that element to its own root `<div>`.
58
56
  *
59
57
  * The imported module is expected to conform to the {@link ModuleData} interface.
60
58
  * Any violation (missing exports, wrong types, `init` not returning an `Element`)
61
59
  * transitions the component into the `--error` state and logs to `console.error`.
62
60
  *
63
- * ### Root element modifiers
64
- * The root `<div>` receives the public class name defined by `uiModule` and
65
- * the following BEM-style modifier classes reflecting the current load lifecycle:
66
- * - `--loading` — the module fetch is in progress.
67
- * - `--no-module` — no module has been loaded yet (`src` is undefined or the
68
- * effect has not run).
69
- * - `--error` — the import, validation, or `init` call failed.
70
- * - `--loaded` — the module passed validation and `init` returned successfully.
71
- * - `--initialized` — the `Element` returned by `init` has been appended to the
72
- * host `<div>`.
61
+ * ### CSS modifiers
62
+ * Reflecting the current load lifecycle:
63
+ * - `loading` the module fetch is in progress.
64
+ * - `no-module`nothing has been loaded yet (`src` is undefined, or the
65
+ * effect has not run).
66
+ * - `error` the import, the validation, or the `init` call failed.
67
+ * - `loaded` — the module passed validation and `init` returned successfully.
68
+ * - `initialized` — the `Element` returned by `init` has been appended.
73
69
  *
74
70
  * ### Root element attributes
75
- * - `id` — a stable randomly generated ID (prefixed `f`) assigned once on mount.
76
- * Used to scope the module's `css` entries to this specific instance.
71
+ * - `id` — a stable generated id, assigned once on mount and used to scope the
72
+ * module's `css` entries to this specific instance.
77
73
  *
78
74
  * @param props - Component properties.
79
75
  * @see {@link Props}
@@ -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