@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,20 +1,28 @@
1
- import { type FunctionComponent } from 'react';
2
- import { type Props as ControlledProps } from './index.controlled.js';
1
+ import { type FunctionComponent, type PropsWithChildren, type ReactNode, type SelectHTMLAttributes } from 'react';
2
+ import type { WithClassName } from '../utils/types.js';
3
3
  /**
4
4
  * Props for the {@link Select} component.
5
5
  *
6
- * Alias of {@link ControlledProps}.
6
+ * Extends all native {@link SelectHTMLAttributes} and {@link WithClassName}
7
+ * with optional label, error content, and option children.
7
8
  *
8
- * All native select attributes remain available, including `value`,
9
- * `defaultValue`, and `onChange`, allowing the component to operate
10
- * in either controlled or hybrid mode.
9
+ * @property label - Content rendered as an associated `<label>`. When omitted, no label is rendered.
10
+ * @property error - Content rendered as an error message below the select. When omitted, no error is rendered.
11
+ * @property className - Additional class name(s) applied to the select element.
12
+ * @property children - `<option>` or `<optgroup>` elements rendered inside the select.
11
13
  */
12
- export type Props = ControlledProps;
14
+ export type Props = SelectHTMLAttributes<HTMLSelectElement> & PropsWithChildren<WithClassName<{
15
+ label?: ReactNode;
16
+ error?: ReactNode;
17
+ }>>;
13
18
  /**
14
- * Select field component supporting controlled and hybrid usage.
19
+ * Select field supporting controlled and uncontrolled usage.
15
20
  *
16
- * Wraps {@link ControlledSelect} and automatically manages the selected
17
- * value when no `value` prop is provided.
21
+ * Renders a native `<select>` with optional label and error feedback. All
22
+ * standard select attributes are forwarded to the underlying element.
23
+ *
24
+ * A stable auto-generated `id` is created on mount and used to associate the
25
+ * rendered label through the `htmlFor` attribute.
18
26
  *
19
27
  * ### CSS elements
20
28
  * - `label`
@@ -22,17 +30,13 @@ export type Props = ControlledProps;
22
30
  *
23
31
  * @param props - Component properties.
24
32
  * @see {@link Props}
25
- *
26
- * @returns A labelled select with optional internal selection management.
33
+ * @returns A labelled select with optional error feedback.
27
34
  *
28
35
  * @remarks
29
- * - In controlled mode (`value` defined), the selected value is fully driven
30
- * by the parent component and internal state is never updated.
31
- * - In hybrid mode, internal state is initialized from `defaultValue` and
32
- * subsequently manages selection updates itself.
33
- * - In hybrid mode, the internal value is updated before forwarding the
34
- * `onChange` callback.
35
- * - `defaultValue` is only used to initialize internal state and is not
36
- * forwarded to the underlying controlled component.
36
+ * - In controlled mode (`value` defined), the selection is fully driven by the
37
+ * parent and internal state is never updated.
38
+ * - In uncontrolled mode, internal state is initialized from `defaultValue` and
39
+ * updated before `onChange` is forwarded.
40
+ * - `onChange` fires in both modes.
37
41
  */
38
42
  export declare const Select: FunctionComponent<Props>;
@@ -1,11 +1,19 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
- import { ControlledSelect } from './index.controlled.js';
3
+ import { clss } from '../../agnostic/css/clss/index.js';
4
+ import { isNotFalsy } from '../../agnostic/booleans/is-falsy/index.js';
5
+ import { randomHash } from '../../agnostic/random/uuid/index.js';
6
+ import { mergeClassNames } from '../utils/index.js';
7
+ import { select as publicClassName } from '../public-classnames.js';
8
+ import cssModule from './styles.module.css';
4
9
  /**
5
- * Select field component supporting controlled and hybrid usage.
10
+ * Select field supporting controlled and uncontrolled usage.
6
11
  *
7
- * Wraps {@link ControlledSelect} and automatically manages the selected
8
- * value when no `value` prop is provided.
12
+ * Renders a native `<select>` with optional label and error feedback. All
13
+ * standard select attributes are forwarded to the underlying element.
14
+ *
15
+ * A stable auto-generated `id` is created on mount and used to associate the
16
+ * rendered label through the `htmlFor` attribute.
9
17
  *
10
18
  * ### CSS elements
11
19
  * - `label`
@@ -13,27 +21,29 @@ import { ControlledSelect } from './index.controlled.js';
13
21
  *
14
22
  * @param props - Component properties.
15
23
  * @see {@link Props}
16
- *
17
- * @returns A labelled select with optional internal selection management.
24
+ * @returns A labelled select with optional error feedback.
18
25
  *
19
26
  * @remarks
20
- * - In controlled mode (`value` defined), the selected value is fully driven
21
- * by the parent component and internal state is never updated.
22
- * - In hybrid mode, internal state is initialized from `defaultValue` and
23
- * subsequently manages selection updates itself.
24
- * - In hybrid mode, the internal value is updated before forwarding the
25
- * `onChange` callback.
26
- * - `defaultValue` is only used to initialize internal state and is not
27
- * forwarded to the underlying controlled component.
27
+ * - In controlled mode (`value` defined), the selection is fully driven by the
28
+ * parent and internal state is never updated.
29
+ * - In uncontrolled mode, internal state is initialized from `defaultValue` and
30
+ * updated before `onChange` is forwarded.
31
+ * - `onChange` fires in both modes.
28
32
  */
29
- export const Select = ({ defaultValue, value, onChange, ...rest }) => {
33
+ export const Select = ({ label, error, value, defaultValue, onChange, className, children, ...rest }) => {
34
+ // State
35
+ const [id] = useState(`_${randomHash(12)}`);
36
+ const [internalValue, setInternalValue] = useState(defaultValue ?? '');
30
37
  const isControlled = value !== undefined;
31
- const [internal, setInternal] = useState(defaultValue ?? '');
32
- const currentValue = isControlled ? value : internal;
38
+ const currentValue = isControlled ? value : internalValue;
39
+ // User action handlers
33
40
  const handleChange = (e) => {
34
41
  if (!isControlled)
35
- setInternal(e.target.value);
42
+ setInternalValue(e.target.value);
36
43
  onChange?.(e);
37
44
  };
38
- return _jsx(ControlledSelect, { ...rest, value: currentValue, onChange: handleChange });
45
+ // Rendering
46
+ const c = clss(publicClassName, { cssModule });
47
+ const rootClss = mergeClassNames(c(), className);
48
+ return _jsxs(_Fragment, { children: [isNotFalsy(label) && _jsx("label", { className: c('label'), htmlFor: id, children: label }), _jsx("select", { ...rest, id: id, className: rootClss, value: currentValue, onChange: handleChange, children: children }), isNotFalsy(error) && _jsx("span", { className: c('error'), children: error })] });
39
49
  };
@@ -1,7 +1,7 @@
1
1
  import { type FunctionComponent, type PropsWithChildren } from 'react';
2
2
  import type { WithClassName } from '../utils/types.js';
3
3
  /**
4
- * Props for the {@link SequencerControlled} component.
4
+ * Props for the {@link ControlledSequencer} component.
5
5
  *
6
6
  * This is the low-level controlled interface. All state is driven externally —
7
7
  * the component holds no internal state of its own. For the uncontrolled
@@ -16,32 +16,22 @@ import type { WithClassName } from '../utils/types.js';
16
16
  * child indices that should be active when `step === i`, allowing multiple
17
17
  * children to be simultaneously active on a given step. When omitted, exactly
18
18
  * one child is active at a time (the child at index `step`).
19
- * @property _modifiers - Internal modifier flags forwarded directly to the
20
- * BEM class builder on the root element. Intended to be injected by the
21
- * uncontrolled wrapper; avoid setting manually in consumer code.
22
- * - `playing` sequence is currently progressing.
23
- * - `at-start` current step is the first step.
24
- * - `at-end` current step is the last step.
25
- * @property _dataAttributes - Internal data attribute values forwarded to the
26
- * root element as `data-<key>`. Intended to be injected by the uncontrolled
27
- * wrapper; avoid setting manually in consumer code.
28
- * - `tempo` — exposed as `data-tempo`, reflects the current playback tempo.
19
+ * @property isPlaying - Whether the sequence is currently progressing. The
20
+ * controlled layer never advances on its own; this only drives the `--playing`
21
+ * modifier. Defaults to `false`.
22
+ * @property tempo - Playback speed in beats per minute, exposed as `data-tempo`
23
+ * for styling and scripting. Purely informational here — the interval itself
24
+ * lives in the uncontrolled wrapper. When omitted, no attribute is rendered.
29
25
  * @property className - Optional additional class name(s) applied to the root element.
30
26
  * @property children - The items to sequence. Each child is wrapped in a
31
27
  * classifier `<div>` and receives one of the `--active`, `--previous`, or
32
28
  * `--next` modifiers depending on its position relative to the current `step`.
33
29
  */
34
- export type ControlledProps = PropsWithChildren<WithClassName<{
30
+ export type Props = PropsWithChildren<WithClassName<{
35
31
  step?: number;
36
32
  activateOnStep?: number[][];
37
- _modifiers?: {
38
- playing?: boolean;
39
- 'at-start'?: boolean;
40
- 'at-end'?: boolean;
41
- };
42
- _dataAttributes?: {
43
- tempo?: number;
44
- };
33
+ isPlaying?: boolean;
34
+ tempo?: number;
45
35
  }>>;
46
36
  /**
47
37
  * Controlled sequencer component. Renders each child inside a classifier
@@ -54,14 +44,14 @@ export type ControlledProps = PropsWithChildren<WithClassName<{
54
44
  *
55
45
  * ### Root element modifiers
56
46
  * The root `<div>` receives the public class name defined by `sequencer` and
57
- * the following BEM-style modifier classes, sourced from `_modifiers`:
58
- * - `--playing` — when the sequence is actively progressing.
47
+ * the following BEM-style modifier classes:
48
+ * - `--playing` — when `isPlaying` is `true`.
59
49
  * - `--at-start` — when the current step is the first step.
60
50
  * - `--at-end` — when the current step is the last step.
61
51
  *
62
52
  * ### Data attributes on the root element
63
- * Derived from `_dataAttributes`, each key is prefixed with `data-`:
64
- * - `data-tempo` — current playback tempo, when provided.
53
+ * - `data-step` the current step index.
54
+ * - `data-tempo` — current playback tempo, when `tempo` is provided.
65
55
  *
66
56
  * ### Child wrapper elements
67
57
  * Each child is wrapped in a `<div>` with the `__child` element class and
@@ -72,7 +62,7 @@ export type ControlledProps = PropsWithChildren<WithClassName<{
72
62
  * - `--next` — this child has not yet been reached.
73
63
  *
74
64
  * @param props - Component properties.
75
- * @see {@link ControlledProps}
65
+ * @see {@link Props}
76
66
  * @returns A root `<div>` containing one classifier wrapper per child.
77
67
  */
78
- export declare const SequencerControlled: FunctionComponent<ControlledProps>;
68
+ export declare const ControlledSequencer: FunctionComponent<Props>;
@@ -15,14 +15,14 @@ import cssModule from './styles.module.css';
15
15
  *
16
16
  * ### Root element modifiers
17
17
  * The root `<div>` receives the public class name defined by `sequencer` and
18
- * the following BEM-style modifier classes, sourced from `_modifiers`:
19
- * - `--playing` — when the sequence is actively progressing.
18
+ * the following BEM-style modifier classes:
19
+ * - `--playing` — when `isPlaying` is `true`.
20
20
  * - `--at-start` — when the current step is the first step.
21
21
  * - `--at-end` — when the current step is the last step.
22
22
  *
23
23
  * ### Data attributes on the root element
24
- * Derived from `_dataAttributes`, each key is prefixed with `data-`:
25
- * - `data-tempo` — current playback tempo, when provided.
24
+ * - `data-step` the current step index.
25
+ * - `data-tempo` — current playback tempo, when `tempo` is provided.
26
26
  *
27
27
  * ### Child wrapper elements
28
28
  * Each child is wrapped in a `<div>` with the `__child` element class and
@@ -33,17 +33,20 @@ import cssModule from './styles.module.css';
33
33
  * - `--next` — this child has not yet been reached.
34
34
  *
35
35
  * @param props - Component properties.
36
- * @see {@link ControlledProps}
36
+ * @see {@link Props}
37
37
  * @returns A root `<div>` containing one classifier wrapper per child.
38
38
  */
39
- export const SequencerControlled = ({ step = 0, activateOnStep, _modifiers, _dataAttributes, className, children }) => {
39
+ export const ControlledSequencer = ({ step = 0, activateOnStep, isPlaying = false, tempo, className, children }) => {
40
+ const childrenArr = Children.toArray(children);
41
+ const stepsCount = activateOnStep?.length ?? childrenArr.length;
40
42
  // Rendering
41
43
  const c = clss(publicClassName, { cssModule });
42
- const rootClss = mergeClassNames(c(null, _modifiers), className);
43
- const rootAttributes = Object
44
- .entries({ ..._dataAttributes })
45
- .reduce((acc, [key, val]) => ({ ...acc, [`data-${key}`]: val }), { 'data-step': step });
46
- return _jsx("div", { className: rootClss, ...rootAttributes, children: Children.toArray(children)
44
+ const rootClss = mergeClassNames(c(null, {
45
+ playing: isPlaying,
46
+ 'at-start': step === 0,
47
+ 'at-end': step === stepsCount - 1
48
+ }), className);
49
+ return _jsx("div", { className: rootClss, "data-step": step, "data-tempo": tempo, children: childrenArr
47
50
  .map((child, childPos) => {
48
51
  const thisStepActivateOnStep = activateOnStep?.[step];
49
52
  const isPrevious = activateOnStep === undefined
@@ -62,6 +65,6 @@ export const SequencerControlled = ({ step = 0, activateOnStep, _modifiers, _dat
62
65
  prev: isPrevious,
63
66
  next: isNext
64
67
  });
65
- return _jsx("div", { className: childClss, children: child });
68
+ return _jsx("div", { className: childClss, children: child }, childPos);
66
69
  }) });
67
70
  };
@@ -1,11 +1,12 @@
1
1
  import { type FunctionComponent } from 'react';
2
2
  import { type Props as IOCompProps } from '../IntersectionObserver/index.js';
3
- import { type ControlledProps } from './index.controlled.js';
3
+ import { type Props as ControlledProps } from './index.controlled.js';
4
4
  /**
5
5
  * Props for the {@link Sequencer} component.
6
6
  *
7
- * Extends {@link ControlledProps} (minus `_modifiers`, which are derived
8
- * internally) with uncontrolled playback and viewport-driven behaviour.
7
+ * Extends {@link ControlledProps} minus `isPlaying` and `tempo`, which this
8
+ * component derives itself — with uncontrolled playback and viewport-driven
9
+ * behaviour.
9
10
  *
10
11
  * @property defaultStep - Initial step index when running in uncontrolled mode.
11
12
  * Ignored if `step` is provided. Defaults to `0`.
@@ -28,16 +29,15 @@ import { type ControlledProps } from './index.controlled.js';
28
29
  * component enters the viewport. No-op when `play` is controlled.
29
30
  * @property pauseOnHidden - When `true`, pauses internal playback when the
30
31
  * component leaves the viewport. No-op when `play` is controlled.
31
- * @property actionHandlers - Optional handlers for imperative actions triggered
32
- * by external events:
33
- * - `intersected` forwarded verbatim to the internal
34
- * {@link IntersectionObserverComponent}'s `onIntersected`, called on every
35
- * intersection change regardless of controlled state.
36
- * @property stateHandlers - Optional callbacks invoked when derived state changes:
37
- * - `isPlaying` — called with the new play state whenever it changes.
38
- * - `stepChanged` — called with the new forwarded step index whenever it changes.
32
+ * @property onIntersected - Forwarded verbatim to the internal
33
+ * {@link IntersectionObserverComponent}, and called on every intersection
34
+ * change whichever mode the sequencer runs in.
35
+ * @property onIsPlayingChanged - Called after the effective play state changed,
36
+ * with the new value.
37
+ * @property onStepChanged - Called after the forwarded step changed, with the
38
+ * new value.
39
39
  */
40
- export type Props = Omit<ControlledProps, '_modifiers'> & {
40
+ export type Props = Omit<ControlledProps, 'isPlaying' | 'tempo'> & {
41
41
  defaultStep?: number;
42
42
  tempo?: number;
43
43
  play?: boolean;
@@ -48,38 +48,33 @@ export type Props = Omit<ControlledProps, '_modifiers'> & {
48
48
  resetOnHidden?: boolean;
49
49
  playOnVisible?: boolean;
50
50
  pauseOnHidden?: boolean;
51
- actionHandlers?: {
52
- intersected?: IOCompProps['onIntersected'];
53
- };
54
- stateHandlers?: {
55
- isPlaying?: (isPlaying: boolean) => void;
56
- stepChanged?: (step: number) => void;
57
- };
51
+ onIntersected?: IOCompProps['onIntersected'];
52
+ onIsPlayingChanged?: (isPlaying: boolean) => void;
53
+ onStepChanged?: (step: number) => void;
58
54
  };
59
55
  /**
60
56
  * Uncontrolled, self-advancing sequencer component. Drives a
61
- * {@link SequencerControlled} instance with an internal tempo-based interval,
57
+ * {@link ControlledSequencer} instance with an internal tempo-based interval,
62
58
  * optional loop/clamp boundary behaviour, and viewport-driven play/reset triggers
63
59
  * via an {@link IntersectionObserverComponent}.
64
60
  *
65
61
  * Supports mixed controlled/uncontrolled usage: passing `step` disables the
66
62
  * internal interval while still applying loop/clamp arithmetic before forwarding
67
63
  * to the controlled layer. Passing `play` disables internal play state management
68
- * while still allowing viewport handlers to fire `actionHandlers.intersected`.
64
+ * while still allowing viewport handlers to fire `onIntersected`.
69
65
  *
70
- * ### Forwarded modifiers to {@link SequencerControlled}
71
- * The following `_modifiers` are computed and injected automatically:
72
- * - `playing` — `true` when the effective play state is active.
73
- * - `at-start` — `true` when the forwarded step is `0`.
74
- * - `at-end` — `true` when the forwarded step equals `stepsCount - 1`.
66
+ * ### Forwarded to {@link ControlledSequencer}
67
+ * - `step` the effective step, after loop/clamp arithmetic.
68
+ * - `isPlaying` — the effective play state, controlled or internal.
69
+ * - `tempo` — the current tempo, which the controlled layer exposes as `data-tempo`.
75
70
  *
76
- * ### Forwarded data attributes to {@link SequencerControlled}
77
- * - `data-tempo` the current `tempo` value.
71
+ * The `--at-start` and `--at-end` modifiers are derived by the controlled layer
72
+ * from `step` and the children count.
78
73
  *
79
74
  * @param props - Component properties.
80
75
  * @see {@link Props}
81
- * @see {@link SequencerControlled}
76
+ * @see {@link ControlledSequencer}
82
77
  * @returns An {@link IntersectionObserverComponent} wrapping a
83
- * {@link SequencerControlled} with the computed step and modifiers applied.
78
+ * {@link ControlledSequencer} with the computed step and modifiers applied.
84
79
  */
85
80
  export declare const Sequencer: FunctionComponent<Props>;
@@ -1,43 +1,42 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useState, useEffect, useCallback, Children, useRef } from 'react';
2
+ import { useState, useEffect, useCallback, Children } from 'react';
3
3
  import { absoluteModulo } from '../../agnostic/numbers/absolute-modulo/index.js';
4
4
  import { clamp } from '../../agnostic/numbers/clamp/index.js';
5
5
  import { IntersectionObserverComponent } from '../IntersectionObserver/index.js';
6
- import { SequencerControlled } from './index.controlled.js';
6
+ import { useChangeDispatch } from '../utils/index.js';
7
+ import { ControlledSequencer } from './index.controlled.js';
7
8
  /**
8
9
  * Uncontrolled, self-advancing sequencer component. Drives a
9
- * {@link SequencerControlled} instance with an internal tempo-based interval,
10
+ * {@link ControlledSequencer} instance with an internal tempo-based interval,
10
11
  * optional loop/clamp boundary behaviour, and viewport-driven play/reset triggers
11
12
  * via an {@link IntersectionObserverComponent}.
12
13
  *
13
14
  * Supports mixed controlled/uncontrolled usage: passing `step` disables the
14
15
  * internal interval while still applying loop/clamp arithmetic before forwarding
15
16
  * to the controlled layer. Passing `play` disables internal play state management
16
- * while still allowing viewport handlers to fire `actionHandlers.intersected`.
17
+ * while still allowing viewport handlers to fire `onIntersected`.
17
18
  *
18
- * ### Forwarded modifiers to {@link SequencerControlled}
19
- * The following `_modifiers` are computed and injected automatically:
20
- * - `playing` — `true` when the effective play state is active.
21
- * - `at-start` — `true` when the forwarded step is `0`.
22
- * - `at-end` — `true` when the forwarded step equals `stepsCount - 1`.
19
+ * ### Forwarded to {@link ControlledSequencer}
20
+ * - `step` the effective step, after loop/clamp arithmetic.
21
+ * - `isPlaying` — the effective play state, controlled or internal.
22
+ * - `tempo` — the current tempo, which the controlled layer exposes as `data-tempo`.
23
23
  *
24
- * ### Forwarded data attributes to {@link SequencerControlled}
25
- * - `data-tempo` the current `tempo` value.
24
+ * The `--at-start` and `--at-end` modifiers are derived by the controlled layer
25
+ * from `step` and the children count.
26
26
  *
27
27
  * @param props - Component properties.
28
28
  * @see {@link Props}
29
- * @see {@link SequencerControlled}
29
+ * @see {@link ControlledSequencer}
30
30
  * @returns An {@link IntersectionObserverComponent} wrapping a
31
- * {@link SequencerControlled} with the computed step and modifiers applied.
31
+ * {@link ControlledSequencer} with the computed step and modifiers applied.
32
32
  */
33
- export const Sequencer = ({ defaultStep, tempo = 60, play, loop, clampFirst, clampLast, resetOnVisible, resetOnHidden, playOnVisible, pauseOnHidden, actionHandlers, stateHandlers, ...controlledProps }) => {
33
+ export const Sequencer = ({ defaultStep, tempo = 60, play, loop, clampFirst, clampLast, resetOnVisible, resetOnHidden, playOnVisible, pauseOnHidden, onIntersected, onIsPlayingChanged, onStepChanged, ...controlledProps }) => {
34
34
  // State
35
35
  const { step, activateOnStep, children } = controlledProps;
36
36
  const [internalPlay, setInternalPlay] = useState(play ?? false);
37
37
  const [internalStep, setInternalStep] = useState(step ?? defaultStep ?? 0);
38
38
  const actualPlay = play ?? internalPlay;
39
39
  const actualStep = step ?? internalStep;
40
- const actualPlayRef = useRef(actualPlay);
41
40
  // Effects
42
41
  useEffect(() => {
43
42
  const clampedTempo = Math.max(tempo, 1);
@@ -61,24 +60,15 @@ export const Sequencer = ({ defaultStep, tempo = 60, play, loop, clampFirst, cla
61
60
  const rightClamp = clampLast === true ? stepsCount - 1 : Infinity;
62
61
  forwardedStep = clamp(actualStep, leftClamp, rightClamp);
63
62
  }
64
- const forwardedStepRef = useRef(forwardedStep);
65
- // State change handlers
66
- useEffect(() => {
67
- if (actualPlay !== actualPlayRef.current) {
68
- actualPlayRef.current = actualPlay;
69
- stateHandlers?.isPlaying?.(actualPlay);
70
- }
71
- if (forwardedStep !== forwardedStepRef.current) {
72
- forwardedStepRef.current = forwardedStep;
73
- stateHandlers?.stepChanged?.(forwardedStep);
74
- }
75
- }, [actualPlay, forwardedStep]);
63
+ // State dispatch
64
+ useChangeDispatch(actualPlay, onIsPlayingChanged);
65
+ useChangeDispatch(forwardedStep, onStepChanged);
76
66
  // Action handlers
77
67
  const handleIntersection = useCallback(({ ioEntry, observer }) => {
68
+ onIntersected?.({ ioEntry, observer });
78
69
  if (play === true || step !== undefined)
79
70
  return;
80
71
  const { isIntersecting } = ioEntry ?? {};
81
- actionHandlers?.intersected?.({ ioEntry, observer });
82
72
  if (isIntersecting === true) {
83
73
  if (resetOnVisible === true)
84
74
  setInternalStep(0);
@@ -98,12 +88,8 @@ export const Sequencer = ({ defaultStep, tempo = 60, play, loop, clampFirst, cla
98
88
  pauseOnHidden,
99
89
  play,
100
90
  step,
101
- actionHandlers
91
+ onIntersected
102
92
  ]);
103
93
  // Rendering
104
- return _jsx(IntersectionObserverComponent, { onIntersected: handleIntersection, children: _jsx(SequencerControlled, { ...controlledProps, step: forwardedStep, _modifiers: {
105
- playing: actualPlay,
106
- 'at-start': forwardedStep === 0,
107
- 'at-end': forwardedStep === stepsCount - 1
108
- }, _dataAttributes: { tempo } }) });
94
+ return _jsx(IntersectionObserverComponent, { onIntersected: handleIntersection, children: _jsx(ControlledSequencer, { ...controlledProps, step: forwardedStep, isPlaying: actualPlay, tempo: tempo }) });
109
95
  };
@@ -1,6 +1,29 @@
1
1
  import { type FunctionComponent, type PropsWithChildren } from 'react';
2
2
  import type { WithClassName } from '../utils/types.js';
3
- import type { ParsedSub } from './types.js';
3
+ /**
4
+ * A single parsed subtitle entry from an SRT file.
5
+ *
6
+ * @property id - Sequential subtitle number.
7
+ * @property start - Start time in milliseconds.
8
+ * @property end - End time in milliseconds.
9
+ * @property content - Subtitle text content.
10
+ */
11
+ export type ParsedSub = {
12
+ id: number;
13
+ start?: number;
14
+ end?: number;
15
+ content?: string;
16
+ };
17
+ /**
18
+ * The inclusive ID boundaries of a subtitle group.
19
+ *
20
+ * @property startId - ID of the first subtitle in the group.
21
+ * @property endId - ID of the last subtitle in the group.
22
+ */
23
+ export type SubGroupBoundaries = {
24
+ startId: number;
25
+ endId: number;
26
+ };
4
27
  /**
5
28
  * Props for the {@link Subtitles} component.
6
29
  *
@@ -16,10 +39,11 @@ import type { ParsedSub } from './types.js';
16
39
  * @property isEnded - When `true`, forces the last group to be treated as current,
17
40
  * regardless of `timecodeMs`. Useful to keep the final subtitle group visible after
18
41
  * media playback finishes.
19
- * @property onLoaded - Callback invoked with the raw SRT string after a successful
20
- * fetch and parse. Not called when `srtFileContent` is used directly.
21
- * @property onParsed - Callback invoked with the raw SRT string has been parsed.
22
- * @property onLoadFailed - Callback invoked with an `Error` if the fetch or parse step fails.
42
+ * @property onLoaded - Called with the raw SRT string once fetched. Not called
43
+ * when `srtFileContent` is used directly, since nothing is fetched then.
44
+ * @property onParsed - Called after the subtitles have been parsed, with the
45
+ * resulting entries.
46
+ * @property onLoadFailed - Called with an `Error` when the fetch or the parse fails.
23
47
  * @property className - Optional additional class name(s) applied to the root element.
24
48
  * @property children - React children rendered inside the root element, after the subtitle groups.
25
49
  */
@@ -1,12 +1,95 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { useCallback, useEffect, useState } from 'react';
3
3
  import { clss } from '../../agnostic/css/clss/index.js';
4
4
  import { toError } from '../../agnostic/misc/cast/index.js';
5
5
  import { unknownToString } from '../../agnostic/errors/unknown-to-string/index.js';
6
- import { mergeClassNames } from '../utils/index.js';
6
+ import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
7
7
  import { subtitles as publicClassName } from '../public-classnames.js';
8
- import { computeSubGroupsWithBoundaries, getCurrentGroup, parseSubs } from './utils.js';
9
8
  import cssModule from './styles.module.css';
9
+ /** Converts an SRT timecode (hh:mm:ss,ms) to milliseconds. */
10
+ const getTimecodeToMs = (timecode) => {
11
+ const [hours = '0', minutes = '0', secondsAndMs = '0,0'] = timecode.split(':');
12
+ const [seconds = '0', milliseconds = '0'] = secondsAndMs.split(',');
13
+ let result = parseInt(hours) * 60 * 60 * 1000;
14
+ result += parseInt(minutes) * 60 * 1000;
15
+ result += parseInt(seconds) * 1000;
16
+ result += parseInt(milliseconds);
17
+ return result;
18
+ };
19
+ /** Parses a raw SRT subtitle text into a list of {@link ParsedSub} entries. */
20
+ const parseSubs = (rawSubs) => {
21
+ const numberRegex = /^\d+$/v;
22
+ const timecodeRegex = /^[0-9]+:[0-9]+:[0-9]+,[0-9]+\s*-->\s*[0-9]+:[0-9]+:[0-9]+,[0-9]+$/v;
23
+ const parsedSubs = [];
24
+ rawSubs.split('\n').forEach(line => {
25
+ if (line.trim() === '')
26
+ return;
27
+ const lastParsedSub = parsedSubs[parsedSubs.length - 1];
28
+ const matchId = line.match(numberRegex);
29
+ const matchTimecode = line.match(timecodeRegex);
30
+ // id
31
+ if (matchId !== null) {
32
+ if (lastParsedSub === undefined
33
+ || lastParsedSub.content !== undefined) {
34
+ const parsedSub = { id: parseInt(line) };
35
+ parsedSubs.push(parsedSub);
36
+ return;
37
+ }
38
+ }
39
+ // timecode
40
+ if (matchTimecode !== null) {
41
+ if (lastParsedSub?.id !== undefined) {
42
+ const [rawStart = '', rawEnd = ''] = line.split('-->');
43
+ const startTime = rawStart.trim();
44
+ const endTime = rawEnd.trim();
45
+ lastParsedSub.start = getTimecodeToMs(startTime);
46
+ lastParsedSub.end = getTimecodeToMs(endTime);
47
+ return;
48
+ }
49
+ }
50
+ // content
51
+ if (lastParsedSub?.id !== undefined
52
+ && lastParsedSub.start !== undefined
53
+ && lastParsedSub.end !== undefined) {
54
+ if (lastParsedSub.content !== undefined) {
55
+ lastParsedSub.content += `\n${line}`;
56
+ return;
57
+ }
58
+ lastParsedSub.content = line;
59
+ }
60
+ });
61
+ return parsedSubs;
62
+ };
63
+ /** Computes subtitle groups with their inclusive `startId` / `endId` boundaries. */
64
+ const computeSubGroupsWithBoundaries = (subsGroups, highestSubId) => {
65
+ const fallback = [{ startId: 1, endId: highestSubId }];
66
+ if (subsGroups === undefined || subsGroups.length === 0)
67
+ return fallback;
68
+ const emptySubGroupBoundaries = [];
69
+ return subsGroups.reduce((acc, curr, currIndex) => {
70
+ const lastInAcc = acc[acc.length - 1];
71
+ const startId = lastInAcc === undefined ? 1 : lastInAcc.endId + 1;
72
+ const endId = curr;
73
+ if (currIndex === subsGroups.length - 1
74
+ && endId !== highestSubId) {
75
+ return [
76
+ ...acc,
77
+ { startId, endId },
78
+ { startId: endId + 1, endId: highestSubId }
79
+ ];
80
+ }
81
+ return [...acc, { startId, endId }];
82
+ }, emptySubGroupBoundaries);
83
+ };
84
+ /** Returns the group holding the last elapsed subtitle, or the last group once playback ended. */
85
+ const getCurrentGroup = (subsGroupsWithBoundaries, lastPrevSubId, isEnded) => {
86
+ const previousGroups = subsGroupsWithBoundaries.filter(group => group.startId <= (lastPrevSubId ?? 0));
87
+ if (previousGroups.length === 0)
88
+ return isEnded === true
89
+ ? subsGroupsWithBoundaries[subsGroupsWithBoundaries.length - 1]
90
+ : subsGroupsWithBoundaries[0];
91
+ return previousGroups[previousGroups.length - 1];
92
+ };
10
93
  /**
11
94
  * Subtitle synchronization component. Fetches or receives an SRT source, parses it,
12
95
  * and renders subtitle groups whose individual spans are styled according to the
@@ -34,19 +117,14 @@ export const Subtitles = ({ src, srtFileContent, subsGroups, timecodeMs, isEnded
34
117
  const [isLoading, setIsLoading] = useState(false);
35
118
  const [loadError, setLoadError] = useState(null);
36
119
  const [parsedSubs, setParsedSubs] = useState([]);
37
- const pParsedSubs = useRef(parsedSubs);
38
- // State change handlers
39
- useEffect(() => {
40
- if (pParsedSubs.current === parsedSubs)
41
- return;
42
- onParsed?.(parsedSubs);
43
- }, [parsedSubs]);
120
+ // State dispatch
121
+ useChangeDispatch(parsedSubs, onParsed);
44
122
  // Effects
45
123
  const fetchAndParseSubs = useCallback(async (src, srtFileContent) => {
46
- if (src === undefined)
47
- return;
48
124
  if (srtFileContent !== undefined)
49
125
  return setParsedSubs(parseSubs(srtFileContent));
126
+ if (src === undefined)
127
+ return;
50
128
  setIsLoading(true);
51
129
  setLoadError(null);
52
130
  try {