@design-edito/tools 0.3.10 → 0.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/agnostic/colors/index.d.ts +2 -2
  2. package/agnostic/colors/index.js +2 -2
  3. package/agnostic/css/clss/index.d.ts +53 -1
  4. package/agnostic/css/clss/index.js +1 -1
  5. package/agnostic/css/index.d.ts +1 -1
  6. package/agnostic/css/index.js +1 -1
  7. package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +15 -15
  8. package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +15 -15
  9. package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +6 -6
  10. package/agnostic/html/hyper-json/smart-tags/isolated/index.js +6 -6
  11. package/agnostic/index.d.ts +4 -4
  12. package/agnostic/index.js +4 -4
  13. package/agnostic/misc/assert/index.d.ts +3 -0
  14. package/agnostic/misc/index.d.ts +2 -2
  15. package/agnostic/misc/index.js +2 -2
  16. package/agnostic/misc/logs/index.d.ts +1 -1
  17. package/agnostic/misc/logs/index.js +1 -1
  18. package/agnostic/misc/logs/logger/index.d.ts +10 -0
  19. package/agnostic/misc/logs/logger/index.js +40 -10
  20. package/agnostic/misc/logs/styles/index.d.ts +1 -0
  21. package/agnostic/misc/logs/styles/index.js +27 -9
  22. package/agnostic/numbers/index.d.ts +2 -2
  23. package/agnostic/numbers/index.js +2 -2
  24. package/agnostic/objects/index.d.ts +2 -2
  25. package/agnostic/objects/index.js +2 -2
  26. package/agnostic/optim/index.d.ts +1 -1
  27. package/agnostic/optim/index.js +1 -1
  28. package/agnostic/random/index.d.ts +1 -1
  29. package/agnostic/random/index.js +1 -1
  30. package/agnostic/sanitization/index.d.ts +2 -2
  31. package/agnostic/sanitization/index.js +2 -2
  32. package/agnostic/strings/index.d.ts +1 -1
  33. package/agnostic/strings/index.js +1 -1
  34. package/agnostic/time/index.d.ts +1 -1
  35. package/agnostic/time/index.js +1 -1
  36. package/agnostic/time/transitions/index.d.ts +3 -3
  37. package/agnostic/time/transitions/index.js +4 -4
  38. package/components/BeforeAfter/index.controlled.d.ts +46 -0
  39. package/components/BeforeAfter/index.d.ts +55 -0
  40. package/components/BeforeAfter/index.js +40 -0
  41. package/components/BeforeAfter/utils.d.ts +4 -0
  42. package/components/Disclaimer/index.d.ts +45 -0
  43. package/components/Disclaimer/index.js +70 -0
  44. package/components/Disclaimer/styles.module.css +0 -0
  45. package/components/Drawer/index.d.ts +45 -0
  46. package/components/Drawer/index.js +82 -0
  47. package/components/Drawer/styles.module.css +0 -0
  48. package/components/EventListener/index.d.ts +20 -3
  49. package/components/EventListener/index.js +15 -22
  50. package/components/Gallery/index.d.ts +67 -0
  51. package/components/Gallery/index.js +173 -0
  52. package/components/Gallery/styles.module.css +33 -0
  53. package/components/Gallery/utils.d.ts +1 -0
  54. package/components/Image/index.d.ts +60 -0
  55. package/components/Image/index.js +99 -0
  56. package/components/Image/styles.module.css +0 -0
  57. package/components/IntersectionObserver/index.d.ts +48 -11
  58. package/components/IntersectionObserver/index.js +13 -22
  59. package/components/Paginator/index.d.ts +72 -0
  60. package/components/Paginator/index.js +116 -0
  61. package/components/Paginator/styles.module.css +9 -0
  62. package/components/ResizeObserver/index.d.ts +27 -0
  63. package/components/ResizeObserver/index.js +81 -0
  64. package/components/Scrllgngn/index.d.ts +123 -0
  65. package/components/Scrllgngn/index.js +175 -0
  66. package/components/Scrllgngn/styles.module.css +74 -0
  67. package/components/ScrollListener/index.d.ts +47 -0
  68. package/components/ScrollListener/index.js +110 -0
  69. package/components/ScrollListener/styles.module.css +0 -0
  70. package/components/ScrollListener/utils.d.ts +41 -0
  71. package/components/Sequencer/index.controlled.d.ts +78 -0
  72. package/components/Sequencer/index.d.ts +85 -0
  73. package/components/Sequencer/index.js +109 -0
  74. package/components/Sequencer/styles.module.css +0 -0
  75. package/components/ShadowRoot/index.d.ts +35 -0
  76. package/components/ShadowRoot/index.js +56 -0
  77. package/components/ShadowRoot/styles.module.css +0 -0
  78. package/components/Subtitles/index.d.ts +58 -0
  79. package/components/Subtitles/index.js +111 -0
  80. package/components/Subtitles/styles.module.css +0 -0
  81. package/components/Subtitles/types.d.ts +10 -0
  82. package/components/Subtitles/types.js +0 -0
  83. package/components/Subtitles/utils.d.ts +28 -0
  84. package/components/Theatre/index.d.ts +64 -0
  85. package/components/Theatre/index.js +97 -0
  86. package/components/Theatre/styles.module.css +0 -0
  87. package/components/UIModule/index.d.ts +85 -0
  88. package/components/UIModule/index.js +134 -0
  89. package/components/UIModule/styles.module.css +0 -0
  90. package/components/Video/index.d.ts +139 -0
  91. package/components/Video/index.js +385 -0
  92. package/components/Video/styles.module.css +0 -0
  93. package/components/Video/utils.d.ts +14 -0
  94. package/components/_WIP_AudioQuote/index.d.ts +1 -0
  95. package/components/_WIP_AudioQuote/index.js +0 -0
  96. package/components/_WIP_Icon/index.d.ts +1 -0
  97. package/components/_WIP_Icon/index.js +0 -0
  98. package/components/index.d.ts +18 -1
  99. package/components/index.js +18 -1
  100. package/components/public-classnames.d.ts +17 -3
  101. package/components/utils/index.d.ts +1 -0
  102. package/components/utils/index.js +12 -0
  103. package/components/utils/types.d.ts +3 -0
  104. package/components/utils/types.js +0 -0
  105. package/index.d.ts +1 -1
  106. package/index.js +1 -1
  107. package/node/@aws-s3/index.test.d.ts +1 -0
  108. package/node/@aws-s3/storage/file/index.d.ts +3 -3
  109. package/node/@aws-s3/storage/file/index.js +3 -3
  110. package/node/@google-cloud/storage/directory/index.d.ts +1 -1
  111. package/node/@google-cloud/storage/directory/index.js +1 -1
  112. package/node/@google-cloud/storage/file/index.d.ts +4 -4
  113. package/node/@google-cloud/storage/file/index.js +4 -4
  114. package/node/cloud-storage/operations/index.d.ts +1 -1
  115. package/node/cloud-storage/operations/index.js +1 -1
  116. package/node/ftps/directory/index.d.ts +1 -1
  117. package/node/ftps/directory/index.js +1 -1
  118. package/node/ftps/file/index.d.ts +1 -1
  119. package/node/ftps/file/index.js +1 -1
  120. package/node/images/index.d.ts +1 -1
  121. package/node/images/index.js +1 -1
  122. package/node/images/transform/operations/index.d.ts +4 -4
  123. package/node/images/transform/operations/index.js +4 -4
  124. package/node/index.d.ts +3 -3
  125. package/node/index.js +3 -3
  126. package/node/process/spawner/index.d.ts +61 -2
  127. package/node/process/spawner/index.js +6 -6
  128. package/node/sftp/file/index.d.ts +3 -3
  129. package/node/sftp/file/index.js +3 -3
  130. package/package.json +1051 -13
  131. package/components/Input/index.d.ts +0 -7
  132. package/components/Input/index.js +0 -29
  133. /package/components/{Input → BeforeAfter}/styles.module.css +0 -0
@@ -0,0 +1,111 @@
1
+ // src/components/Subtitles/index.tsx
2
+ import {
3
+ useCallback,
4
+ useEffect,
5
+ useRef,
6
+ useState
7
+ } from "react";
8
+ import { clss } from "../../agnostic/css/clss/index.js";
9
+ import { toError } from "../../agnostic/misc/cast/index.js";
10
+ import { unknownToString } from "../../agnostic/errors/unknown-to-string/index.js";
11
+ import { mergeClassNames } from "../utils/index.js";
12
+ import { subtitles as publicClassName } from "../public-classnames.js";
13
+ import {
14
+ computeSubGroupsWithBoundaries,
15
+ getCurrentGroup,
16
+ parseSubs
17
+ } from "./utils.js";
18
+ import cssModule from "./styles.module.css";
19
+ import { jsx } from "react/jsx-runtime";
20
+ var Subtitles = ({
21
+ src,
22
+ srtFileContent,
23
+ subsGroups,
24
+ timecodeMs,
25
+ isEnded,
26
+ className,
27
+ onLoaded,
28
+ onParsed,
29
+ onLoadFailed
30
+ }) => {
31
+ const [isLoading, setIsLoading] = useState(false);
32
+ const [loadError, setLoadError] = useState(null);
33
+ const [parsedSubs, setParsedSubs] = useState([]);
34
+ const pParsedSubs = useRef(parsedSubs);
35
+ useEffect(() => {
36
+ if (pParsedSubs.current === parsedSubs) return;
37
+ onParsed?.(parsedSubs);
38
+ }, [parsedSubs]);
39
+ const fetchAndParseSubs = useCallback(async (src2, srtFileContent2) => {
40
+ if (src2 === void 0) return;
41
+ if (srtFileContent2 !== void 0) return setParsedSubs(parseSubs(srtFileContent2));
42
+ setIsLoading(true);
43
+ setLoadError(null);
44
+ try {
45
+ const response = await fetch(src2);
46
+ const srtContent = await response.text();
47
+ onLoaded?.(srtContent);
48
+ const parsedSubs2 = parseSubs(srtContent);
49
+ setParsedSubs(parsedSubs2);
50
+ } catch (error) {
51
+ setLoadError(error instanceof Error ? error : new Error(unknownToString(error)));
52
+ console.error(error);
53
+ onLoadFailed?.(toError(error));
54
+ } finally {
55
+ setIsLoading(false);
56
+ }
57
+ }, [onLoadFailed, onLoaded]);
58
+ useEffect(() => {
59
+ fetchAndParseSubs(src, srtFileContent).catch((error) => {
60
+ console.error(error);
61
+ });
62
+ }, [fetchAndParseSubs, src, srtFileContent]);
63
+ const c = clss(publicClassName, { cssModule });
64
+ const rootClss = mergeClassNames(
65
+ c(null, {
66
+ loading: isLoading,
67
+ error: loadError !== null
68
+ }),
69
+ className
70
+ );
71
+ const prevSubs = parsedSubs.filter(({ start }) => start != null && start < (timecodeMs ?? 0));
72
+ const lastPrevSub = prevSubs[prevSubs.length - 1];
73
+ const highestSubId = Math.max(...parsedSubs.map((sub) => sub.id));
74
+ const subsGroupsWithBoundaries = computeSubGroupsWithBoundaries(subsGroups, highestSubId);
75
+ const currentGroup = getCurrentGroup(subsGroupsWithBoundaries, lastPrevSub?.id, isEnded);
76
+ return /* @__PURE__ */ jsx("div", { className: rootClss, children: timecodeMs !== void 0 && parsedSubs.length > 0 && subsGroupsWithBoundaries.map((group) => {
77
+ const groupSubs = parsedSubs.filter((sub) => sub.id >= group.startId && sub.id <= group.endId);
78
+ const totalSubs = groupSubs.length;
79
+ const groupClass = c("group", { curr: currentGroup?.startId === group.startId });
80
+ const subsNodes = groupSubs.map((sub, subIndex) => {
81
+ let subText = sub.content?.trim() ?? "";
82
+ if (subIndex !== totalSubs - 1) subText += " ";
83
+ const subClass = c("sub", {
84
+ prev: sub.start !== void 0 && lastPrevSub?.start !== void 0 && sub.start <= lastPrevSub.start,
85
+ curr: sub.start !== void 0 && timecodeMs >= sub.start && sub.end !== void 0 && timecodeMs <= sub.end
86
+ });
87
+ return /* @__PURE__ */ jsx(
88
+ "span",
89
+ {
90
+ className: subClass,
91
+ "data-sub-pos": sub.id,
92
+ children: subText
93
+ },
94
+ sub.id
95
+ );
96
+ });
97
+ return /* @__PURE__ */ jsx(
98
+ "div",
99
+ {
100
+ className: groupClass,
101
+ "data-start-sub-pos": group.startId,
102
+ "data-end-sub-pos": group.endId,
103
+ children: subsNodes
104
+ },
105
+ group.startId
106
+ );
107
+ }) });
108
+ };
109
+ export {
110
+ Subtitles
111
+ };
File without changes
@@ -0,0 +1,10 @@
1
+ export type ParsedSub = {
2
+ id: number;
3
+ start?: number;
4
+ end?: number;
5
+ content?: string;
6
+ };
7
+ export type SubGroupBoundaries = {
8
+ startId: number;
9
+ endId: number;
10
+ };
File without changes
@@ -0,0 +1,28 @@
1
+ import type { ParsedSub, SubGroupBoundaries } from './types.js';
2
+ /**
3
+ * Convertit un timecode au format SRT (hh:mm:ss,ms) en millisecondes.
4
+ * @param timecode - Timecode sous forme de chaîne (ex: '00:01:23,456')
5
+ * @returns Le temps en millisecondes
6
+ */
7
+ export declare const getTimecodeToMs: (timecode: string) => number;
8
+ /**
9
+ * Parse un texte brut de sous-titres SRT en une liste d'objets ParsedSub.
10
+ * @param rawSubs - Sous-titres bruts au format SRT
11
+ * @returns Tableau d'objets ParsedSub
12
+ */
13
+ export declare const parseSubs: (rawSubs: string) => ParsedSub[];
14
+ /**
15
+ * Calcule les groupes de sous-titres avec leurs bornes (startId, endId).
16
+ * @param subsGroups - Tableau d'IDs de fin de groupe
17
+ * @param highestSubId - ID le plus élevé des sous-titres
18
+ * @returns Tableau de SubGroupBoundaries
19
+ */
20
+ export declare const computeSubGroupsWithBoundaries: (subsGroups: number[] | undefined, highestSubId: number) => SubGroupBoundaries[];
21
+ /**
22
+ * Retourne le groupe de sous-titres courant selon l'ID du dernier sous-titre précédent et l'état.
23
+ * @param subsGroupsWithBoundaries - Tableau des groupes avec bornes
24
+ * @param lastPrevSubId - ID du dernier sous-titre précédent
25
+ * @param isEnded - Indique si la lecture est terminée
26
+ * @returns Le groupe courant ou undefined
27
+ */
28
+ export declare const getCurrentGroup: (subsGroupsWithBoundaries: SubGroupBoundaries[], lastPrevSubId: number | undefined, isEnded: boolean | undefined) => SubGroupBoundaries | undefined;
@@ -0,0 +1,64 @@
1
+ import { type FunctionComponent, type PropsWithChildren, type ReactNode } from 'react';
2
+ import type { WithClassName } from '../utils/types.js';
3
+ /**
4
+ * Props for the {@link Theatre} component.
5
+ *
6
+ * @property closeBtnContent - Custom content rendered inside the close/exit button.
7
+ * @property openBtnContent - Custom content rendered inside the open/enter button.
8
+ * @property isOn - Controlled theatre mode state. When provided, overrides the
9
+ * internal state. Use together with {@link Props.onToggleClick} for fully
10
+ * controlled usage.
11
+ * @property defaultIsOn - Default state for the theatre mode.
12
+ * @property exitOnEscape — When uncontrolled and on, toggles internal state to off when 'esc' key is pressed
13
+ * @property exitOnBgClick — When uncontrolled and on, toggles internal state to off when the background is clicked
14
+ * @property stateHandlers - Callbacks called after the internal state changed
15
+ * @property stateHandlers.toggled - Callback invoked after the state changed
16
+ * @property actionHandlers - Callbacks called after a user action on children elements
17
+ * @property actionHandlers.toggleClick - Callback invoked when either the open or close
18
+ * button is clicked, the 'esc' key pressed or the background clicked. Receives the theatre state value (`isOn`) at the time of the click,
19
+ * i.e. the previous state before the toggle.
20
+ * @property className - Optional additional class name(s) applied to the root element.
21
+ * @property children - Content rendered both in the default slot and, when theatre
22
+ * mode is active, duplicated inside the stage element.
23
+ */
24
+ export type Props = PropsWithChildren<WithClassName<{
25
+ closeBtnContent?: ReactNode;
26
+ openBtnContent?: ReactNode;
27
+ isOn?: boolean;
28
+ defaultIsOn?: boolean;
29
+ exitOnEscape?: boolean;
30
+ exitOnBgClick?: boolean;
31
+ stateHandlers?: {
32
+ toggled?: (isOn: boolean) => void;
33
+ };
34
+ actionHandlers?: {
35
+ toggleClick?: (prevIsOn: boolean) => void;
36
+ };
37
+ }>>;
38
+ /**
39
+ * Theatre mode component. Wraps content in a toggleable fullscreen-like "stage"
40
+ * overlay. Supports both controlled and uncontrolled usage.
41
+ *
42
+ * When `isOn` is not provided the component manages its own open/closed state
43
+ * internally. When `isOn` is provided it acts as the source of truth and the
44
+ * internal state is ignored.
45
+ *
46
+ * ### Root element modifiers
47
+ * The root `<div>` receives the public class name defined by `theatre` and the
48
+ * following BEM-style modifier classes:
49
+ * - `--on` — when theatre mode is active.
50
+ * - `--off` — when theatre mode is inactive.
51
+ *
52
+ * ### Child elements
53
+ * - `__stage` — container rendered inside the root that holds the duplicated
54
+ * `children` when theatre mode is active. Only mounted when `isOn` is `true`.
55
+ * - `__open-btn` — clickable element that activates theatre mode.
56
+ * - `__close-btn` — clickable element that deactivates theatre mode.
57
+ *
58
+ * @param props - Component properties.
59
+ * @see {@link Props}
60
+ * @returns A root `<div>` containing the children in their original position,
61
+ * a stage overlay with the duplicated children (when active), and the open/close
62
+ * toggle buttons.
63
+ */
64
+ export declare const Theatre: FunctionComponent<Props>;
@@ -0,0 +1,97 @@
1
+ // src/components/Theatre/index.tsx
2
+ import {
3
+ useEffect,
4
+ useState,
5
+ useRef
6
+ } from "react";
7
+ import { clss } from "../../agnostic/css/clss/index.js";
8
+ import { mergeClassNames } from "../utils/index.js";
9
+ import { theatre as publicClassName } from "../public-classnames.js";
10
+ import cssModule from "./styles.module.css";
11
+ import { jsx, jsxs } from "react/jsx-runtime";
12
+ var Theatre = ({
13
+ closeBtnContent,
14
+ openBtnContent,
15
+ isOn,
16
+ defaultIsOn,
17
+ exitOnEscape,
18
+ exitOnBgClick,
19
+ stateHandlers,
20
+ actionHandlers,
21
+ children,
22
+ className
23
+ }) => {
24
+ const [internalIsOn, setInternalIsOn] = useState(defaultIsOn ?? false);
25
+ const stageRef = useRef(null);
26
+ const isTheatreOn = isOn ?? internalIsOn;
27
+ const prevIsTheatreOnRef = useRef(isTheatreOn);
28
+ const handleCloseBtnClick = () => {
29
+ actionHandlers?.toggleClick?.(isTheatreOn);
30
+ if (isOn === void 0) setInternalIsOn(false);
31
+ };
32
+ const handleOpenBtnClick = () => {
33
+ actionHandlers?.toggleClick?.(isTheatreOn);
34
+ if (isOn === void 0) setInternalIsOn(true);
35
+ };
36
+ const handleStageBgClick = (e) => {
37
+ if (exitOnBgClick !== true) return;
38
+ if (e.target !== stageRef.current) return;
39
+ actionHandlers?.toggleClick?.(isTheatreOn);
40
+ if (isOn === void 0) setInternalIsOn(false);
41
+ };
42
+ useEffect(() => {
43
+ if (prevIsTheatreOnRef.current !== isTheatreOn) {
44
+ stateHandlers?.toggled?.(isTheatreOn);
45
+ prevIsTheatreOnRef.current = isTheatreOn;
46
+ }
47
+ }, [isTheatreOn, stateHandlers]);
48
+ useEffect(() => {
49
+ if (exitOnEscape === true || !isTheatreOn || isOn !== void 0) return;
50
+ const listener = (e) => {
51
+ if (e.key !== "Escape") return;
52
+ actionHandlers?.toggleClick?.(isTheatreOn);
53
+ setInternalIsOn(false);
54
+ };
55
+ window.addEventListener("keydown", listener);
56
+ return () => window.removeEventListener("keydown", listener);
57
+ }, [exitOnEscape, isTheatreOn, isOn]);
58
+ const c = clss(publicClassName, { cssModule });
59
+ const rootClss = mergeClassNames(c(null, {
60
+ "on": isTheatreOn,
61
+ "off": !isTheatreOn
62
+ }), className);
63
+ const stageClass = c("stage");
64
+ const openBtnClass = c("open-btn");
65
+ const closeBtnClass = c("close-btn");
66
+ return /* @__PURE__ */ jsxs("div", { className: rootClss, children: [
67
+ children,
68
+ /* @__PURE__ */ jsx(
69
+ "div",
70
+ {
71
+ className: stageClass,
72
+ onClick: handleStageBgClick,
73
+ ref: stageRef,
74
+ children: isTheatreOn && children
75
+ }
76
+ ),
77
+ /* @__PURE__ */ jsx(
78
+ "div",
79
+ {
80
+ className: closeBtnClass,
81
+ onClick: handleCloseBtnClick,
82
+ children: closeBtnContent
83
+ }
84
+ ),
85
+ /* @__PURE__ */ jsx(
86
+ "div",
87
+ {
88
+ className: openBtnClass,
89
+ onClick: handleOpenBtnClick,
90
+ children: openBtnContent
91
+ }
92
+ )
93
+ ] });
94
+ };
95
+ export {
96
+ Theatre
97
+ };
File without changes
@@ -0,0 +1,85 @@
1
+ import { type FunctionComponent } from 'react';
2
+ import type { WithClassName } from '../utils/types.js';
3
+ /**
4
+ * Describes the contract a dynamically imported UI module must satisfy.
5
+ * Every member is validated at runtime after the import resolves.
6
+ *
7
+ * @property init - Called once after the module loads. Receives the current
8
+ * `props` and must return the root `Element` that will be appended to the
9
+ * host `<div>`. Throwing inside `init` is caught and surfaced as an error state.
10
+ * @property destroy - Called when the component unmounts or `src` changes.
11
+ * Receives the `Element` previously returned by `init`. Use it to tear down
12
+ * event listeners, timers, or third-party instances.
13
+ * @property update - Optional. Called when `props` change after the module is
14
+ * already initialized. Receives the live `Element` and the new props object.
15
+ * @property css - Optional array of raw CSS strings scoped automatically to
16
+ * the host element via `.<publicClassName>#<id> { … }` and injected as
17
+ * `<style>` elements.
18
+ */
19
+ type ModuleData = {
20
+ init: (props: Record<string, unknown>) => Element;
21
+ destroy: (target: Element) => void;
22
+ update?: (target: Element, props: Record<string, unknown>) => void;
23
+ css?: string[];
24
+ };
25
+ /**
26
+ * Props for the {@link UIModule} component.
27
+ *
28
+ * @property src - URL of the ES module to import dynamically. The module must
29
+ * satisfy the {@link ModuleData} interface — `init` and `destroy` are required,
30
+ * `update` and `css` are optional. When `undefined`, nothing is loaded and the
31
+ * component stays in the `--no-module` state.
32
+ * @property props - Arbitrary key-value object forwarded verbatim to the
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.
41
+ * @property className - Optional additional class name(s) applied to the root element.
42
+ */
43
+ export type Props = WithClassName<{
44
+ src?: string;
45
+ 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
+ };
52
+ }>;
53
+ /**
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.
58
+ *
59
+ * The imported module is expected to conform to the {@link ModuleData} interface.
60
+ * Any violation (missing exports, wrong types, `init` not returning an `Element`)
61
+ * transitions the component into the `--error` state and logs to `console.error`.
62
+ *
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>`.
73
+ *
74
+ * ### 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.
77
+ *
78
+ * @param props - Component properties.
79
+ * @see {@link Props}
80
+ * @see {@link ModuleData}
81
+ * @returns A host `<div>` into which the module's root `Element` is appended,
82
+ * along with any `<style>` blocks exported by the module.
83
+ */
84
+ export declare const UIModule: FunctionComponent<Props>;
85
+ export {};
@@ -0,0 +1,134 @@
1
+ // src/components/UIModule/index.tsx
2
+ import {
3
+ useEffect,
4
+ useRef,
5
+ useState
6
+ } from "react";
7
+ import { clss } from "../../agnostic/css/clss/index.js";
8
+ import { unknownToString } from "../../agnostic/errors/unknown-to-string/index.js";
9
+ import { isNonNullObject } from "../../agnostic/objects/is-object/index.js";
10
+ import { randomHash } from "../../agnostic/random/uuid/index.js";
11
+ import { mergeClassNames } from "../utils/index.js";
12
+ import { uiModule as publicClassName } from "../public-classnames.js";
13
+ import cssModule from "./styles.module.css";
14
+ import { jsx, jsxs } from "react/jsx-runtime";
15
+ var UIModule = ({
16
+ src,
17
+ props,
18
+ stateHandlers,
19
+ className
20
+ }) => {
21
+ const [id] = useState(`f${randomHash(10)}`);
22
+ const [loading, setLoading] = useState(false);
23
+ const [loadedModule, _setLoadedModule] = useState(null);
24
+ const [moduleTarget, _setModuleTarget] = useState(null);
25
+ const rootRef = useRef(null);
26
+ const loadedModuleRef = useRef(null);
27
+ const moduleTargetRef = useRef(null);
28
+ const setLoadedModule = (data) => {
29
+ loadedModuleRef.current = data;
30
+ _setLoadedModule(data);
31
+ };
32
+ const setModuleTarget = (data) => {
33
+ moduleTargetRef.current = data;
34
+ _setModuleTarget(data);
35
+ };
36
+ useEffect(() => {
37
+ stateHandlers?.idChanged?.(id);
38
+ }, [id, stateHandlers]);
39
+ useEffect(() => {
40
+ stateHandlers?.isLoadingChanged?.(loading);
41
+ }, [loading, stateHandlers]);
42
+ useEffect(() => {
43
+ stateHandlers?.loadedModuleChanged?.(loadedModule);
44
+ }, [loadedModule, stateHandlers]);
45
+ useEffect(() => {
46
+ stateHandlers?.moduleTargetChanged?.(moduleTarget);
47
+ }, [moduleTarget, stateHandlers]);
48
+ useEffect(() => {
49
+ if (src === void 0) return;
50
+ try {
51
+ setLoading(true);
52
+ import(src).then((data) => {
53
+ setLoading(false);
54
+ const errs = {
55
+ notMod: new Error("Not a module"),
56
+ initFunc: new Error("Module exported member `init` must be a function"),
57
+ destroyFunc: new Error("Module exported member `destroy` must be a function"),
58
+ cssStrArr: new Error("Module exported member `css` must be an array of strings"),
59
+ updFunc: new Error("Module exported member `update` must be a function"),
60
+ initRetElt: new Error("Module exported function `init` must return an Element"),
61
+ initRetFirstElt: new Error("Module exported function `init` must return an array containing an Element in its first position")
62
+ };
63
+ if (!isNonNullObject(data)) return setLoadedModule(errs.notMod);
64
+ if (!("init" in data)) return setLoadedModule(errs.initFunc);
65
+ if (typeof data.init !== "function") return setLoadedModule(errs.initFunc);
66
+ if (!("destroy" in data) || typeof data.destroy !== "function") return setLoadedModule(errs.destroyFunc);
67
+ if ("css" in data) {
68
+ if (!Array.isArray(data.css)) return setLoadedModule(errs.cssStrArr);
69
+ if (data.css.some((i) => typeof i !== "string")) return setLoadedModule(errs.cssStrArr);
70
+ }
71
+ if ("update" in data && typeof data.update !== "function") return setLoadedModule(errs.updFunc);
72
+ const module = data;
73
+ setLoadedModule(module);
74
+ try {
75
+ const target = module.init(props ?? {});
76
+ if (!(target instanceof Element)) return setLoadedModule(errs.initRetElt);
77
+ setModuleTarget(target);
78
+ } catch (err) {
79
+ setModuleTarget(null);
80
+ const e = err instanceof Error ? err : new Error(unknownToString(err));
81
+ setLoadedModule(e);
82
+ }
83
+ }).catch((err) => {
84
+ setLoading(false);
85
+ setLoadedModule(err instanceof Error ? err : new Error(unknownToString(err)));
86
+ setModuleTarget(null);
87
+ });
88
+ } catch (err) {
89
+ if (err instanceof Error) return setLoadedModule(err);
90
+ const errStr = unknownToString(err);
91
+ return setLoadedModule(new Error(errStr));
92
+ }
93
+ return () => {
94
+ if (moduleTargetRef.current === null) return;
95
+ if (loadedModuleRef.current instanceof Error) return;
96
+ if (loadedModuleRef.current === null) return;
97
+ loadedModuleRef.current.destroy(moduleTargetRef.current);
98
+ };
99
+ }, [src]);
100
+ useEffect(() => {
101
+ if (loadedModule instanceof Error) console.error(loadedModule);
102
+ }, [loadedModule]);
103
+ useEffect(() => {
104
+ if (moduleTarget === null) return;
105
+ if (rootRef.current === null) return;
106
+ rootRef.current.appendChild(moduleTarget);
107
+ }, [moduleTarget]);
108
+ const c = clss(publicClassName, { cssModule });
109
+ const rootClss = mergeClassNames(
110
+ c(null, {
111
+ loading,
112
+ "no-module": loadedModule === null,
113
+ "error": loadedModule instanceof Error,
114
+ "loaded": !loading && loadedModule !== null && !(loadedModule instanceof Error),
115
+ "initialized": moduleTarget !== null
116
+ }),
117
+ className
118
+ );
119
+ return /* @__PURE__ */ jsxs(
120
+ "div",
121
+ {
122
+ className: rootClss,
123
+ ref: rootRef,
124
+ id,
125
+ children: [
126
+ loadedModule === null && "",
127
+ loadedModule !== null && !(loadedModule instanceof Error) && loadedModule.css?.map((css) => /* @__PURE__ */ jsx("style", { children: `.${publicClassName}#${id} { ${css} }` }))
128
+ ]
129
+ }
130
+ );
131
+ };
132
+ export {
133
+ UIModule
134
+ };
File without changes
@@ -0,0 +1,139 @@
1
+ import { type FunctionComponent, type PropsWithChildren, type VideoHTMLAttributes } from 'react';
2
+ import { type Props as DisclaimerProps } from '../Disclaimer/index.js';
3
+ import { type Props as SubsProps } from '../Subtitles/index.js';
4
+ import type { WithClassName } from '../utils/types.js';
5
+ /**
6
+ * Describes a single video source.
7
+ *
8
+ * @property src - URL of the video file.
9
+ * @property type - MIME type of the source (e.g. `'video/mp4'`).
10
+ */
11
+ type SourceData = {
12
+ src?: string;
13
+ type?: string;
14
+ };
15
+ /**
16
+ * Describes a single text track (subtitles, captions, chapters, etc.).
17
+ *
18
+ * @property src - URL of the track file.
19
+ * @property kind - Track type, maps directly to the `<track>` `kind` attribute.
20
+ * @property srclang - Language of the track content (e.g. `'fr'`, `'en'`).
21
+ * @property label - Human-readable label shown in the browser's track selector.
22
+ * @property default - When `true`, marks this track as the default selection.
23
+ */
24
+ type TrackData = {
25
+ src?: string;
26
+ kind?: 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata';
27
+ srclang?: string;
28
+ label?: string;
29
+ default?: boolean;
30
+ };
31
+ type ActionHandlersProps = {
32
+ playButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isPlaying: boolean, video: HTMLVideoElement | null) => void;
33
+ pauseButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isPlaying: boolean, video: HTMLVideoElement | null) => void;
34
+ loudButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isLoud: boolean, video: HTMLVideoElement | null) => void;
35
+ muteButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isLoud: boolean, video: HTMLVideoElement | null) => void;
36
+ volumeRangeChange?: (e: React.ChangeEvent<HTMLInputElement>, targetRangeVolume: number, volume: number, video: HTMLVideoElement | null) => void;
37
+ fullscreenButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isFullscreen: boolean, video: HTMLVideoElement | null) => void;
38
+ rateRangeChange?: (e: React.ChangeEvent<HTMLInputElement>, targetRangeRate: number, rate: number, video: HTMLVideoElement | null) => void;
39
+ timelineClick?: (e: React.MouseEvent<HTMLDivElement>, targetTimeSec: number, timeSec: number, video: HTMLVideoElement | null) => void;
40
+ };
41
+ type StateHandlersProps = {
42
+ isPlaying?: (isPlaying: boolean) => void;
43
+ isFullScreen?: (isFullScreen: boolean) => void;
44
+ isLoud?: (isLoud: boolean) => void;
45
+ volume?: (volume: number) => void;
46
+ currentTime?: (currentTime: number) => void;
47
+ playbackRate?: (rate: number) => void;
48
+ };
49
+ /**
50
+ * Props for the {@link Video} component.
51
+ *
52
+ * Extends all native `VideoHTMLAttributes<HTMLVideoElement>`, so any standard
53
+ * video attribute (`autoPlay`, `muted`, `loop`, `poster`, etc.) can be passed
54
+ * and will be forwarded to the underlying `<video>` element.
55
+ *
56
+ * @property sources - One or more video sources. Accepts:
57
+ * - a single URL string,
58
+ * - an array of URL strings,
59
+ * - an array of {@link SourceData} objects for fine-grained `type` control.
60
+ * @property tracks - One or more text tracks. Accepts:
61
+ * - a single URL string,
62
+ * - an array of URL strings,
63
+ * - an array of {@link TrackData} objects.
64
+ * @property playBtnContent - Custom content for the play button.
65
+ * @property pauseBtnContent - Custom content for the pause button.
66
+ * @property loudBtnContent - Custom content for the unmute button.
67
+ * @property muteBtnContent - Custom content for the mute button.
68
+ * @property fullScreenBtnContent - Custom content for the fullScreen button.
69
+ * @property subtitles - Props forwarded to the internal {@link Subtitles} component.
70
+ * `timecodeMs` is injected automatically from the current playback position.
71
+ * @property disclaimer - Props forwarded to the internal {@link Disclaimer} component.
72
+ * While the disclaimer is active, `autoPlay` and `muted` are suppressed on the
73
+ * underlying `<video>` element.
74
+ * @property autoPlayWhenVisible - When `true`, triggers playback the first time the
75
+ * component intersects the viewport, provided no disclaimer is active.
76
+ * @property autoPauseWhenHidden - When `true`, pauses playback whenever the component
77
+ * leaves the viewport.
78
+ * @property autoLoudWhenVisible - When `true`, unmutes the video the first time the
79
+ * component intersects the viewport, provided no disclaimer is active.
80
+ * @property autoMuteWhenHidden - When `true`, mutes the video whenever the component
81
+ * leaves the viewport.
82
+ * @property className - Optional additional class name(s) applied to the root element.
83
+ * @property children - React children rendered inside the `<video>` element itself
84
+ * (e.g. fallback content).
85
+ */
86
+ export type Props = PropsWithChildren<WithClassName<{
87
+ sources?: string | string[] | SourceData[];
88
+ tracks?: string | string[] | TrackData[];
89
+ playBtnContent?: React.ReactNode;
90
+ pauseBtnContent?: React.ReactNode;
91
+ loudBtnContent?: React.ReactNode;
92
+ muteBtnContent?: React.ReactNode;
93
+ fullScreenBtnContent?: React.ReactNode;
94
+ subtitles?: SubsProps;
95
+ disclaimer?: DisclaimerProps;
96
+ autoPlayWhenVisible?: boolean;
97
+ autoPauseWhenHidden?: boolean;
98
+ autoLoudWhenVisible?: boolean;
99
+ autoMuteWhenHidden?: boolean;
100
+ actionHandlers: ActionHandlersProps;
101
+ stateHandlers: StateHandlersProps;
102
+ }> & VideoHTMLAttributes<HTMLVideoElement>>;
103
+ /**
104
+ * Full-featured video player component. Wraps a native `<video>` element with
105
+ * playback controls, volume, playback rate, a timeline, optional subtitles,
106
+ * an optional disclaimer gate, and viewport-driven auto-play/mute behaviours.
107
+ *
108
+ * ### Root element modifiers
109
+ * The root `<figure>` receives the public class name defined by `video` and
110
+ * the following BEM-style modifier classes:
111
+ * - `--play-on` / `--play-off` — reflects current playback state.
112
+ * - `--fullscreen-on` / `--fullscreen-off` — reflects fullscreen state.
113
+ * - `--loud` / `--muted` — reflects mute state.
114
+ *
115
+ * ### Data attributes on the root element
116
+ * - `data-play-on` — present (empty string) when playing.
117
+ * - `data-play-off` — present (empty string) when paused.
118
+ * - `data-fullscreen-on` — present (empty string) when in fullScreen.
119
+ * - `data-fullscreen-off` — present (empty string) when not in fullScreen.
120
+ * - `data-loud` — present (empty string) when unmuted.
121
+ * - `data-muted` — present (empty string) when muted.
122
+ * - `data-volume` — current volume as a `0–1` float.
123
+ * - `data-volume-percent` — current volume as a `0–100` float.
124
+ * - `data-playback-rate` — current playback rate (e.g. `1`, `1.5`).
125
+ * - `data-current-time-ms` — current time in milliseconds, fixed to 2 decimals.
126
+ * - `data-current-time-ratio` — current / total ratio, fixed to 8 decimals.
127
+ * - `data-total-time-ms` — total duration in milliseconds.
128
+ *
129
+ * ### CSS custom properties on the root element
130
+ * - `--video-current-time-ratio` — current / total ratio, fixed to 8 decimals.
131
+ * Useful for driving progress-bar animations purely in CSS.
132
+ *
133
+ * @param props - Component properties.
134
+ * @see {@link Props}
135
+ * @returns A `<figure>` element containing the video, its controls, optional
136
+ * subtitles, and an optional disclaimer overlay.
137
+ */
138
+ export declare const Video: FunctionComponent<Props>;
139
+ export {};