@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.
- package/agnostic/html/deep-select/index.js +5 -3
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +8 -8
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +8 -8
- package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +2 -2
- package/agnostic/html/hyper-json/smart-tags/isolated/index.js +2 -2
- package/agnostic/html/index.d.ts +1 -1
- package/agnostic/html/index.js +1 -1
- package/agnostic/misc/index.d.ts +3 -3
- package/agnostic/misc/index.js +3 -3
- package/agnostic/numbers/index.d.ts +1 -1
- package/agnostic/numbers/index.js +1 -1
- package/agnostic/optim/index.d.ts +1 -1
- package/agnostic/optim/index.js +1 -1
- package/agnostic/strings/index.d.ts +1 -1
- package/agnostic/strings/index.js +1 -1
- package/agnostic/time/dates/format-date/index.d.ts +4 -2
- package/agnostic/time/dates/format-date/index.js +43 -94
- package/agnostic/time/dates/format-date/index.test.js +154 -0
- package/agnostic/time/dates/format-date/parts.d.ts +28 -0
- package/agnostic/time/dates/format-date/parts.js +136 -0
- package/agnostic/time/dates/format-date/types.d.ts +84 -0
- package/agnostic/time/dates/format-date/types.js +1 -0
- package/agnostic/time/duration/format-duration/index.d.ts +42 -0
- package/agnostic/time/duration/format-duration/index.js +94 -0
- package/agnostic/time/duration/format-duration/index.test.d.ts +1 -0
- package/agnostic/time/duration/format-duration/index.test.js +148 -0
- package/agnostic/time/duration/format-duration/parts.d.ts +32 -0
- package/agnostic/time/duration/format-duration/parts.js +107 -0
- package/agnostic/time/duration/format-duration/types.d.ts +93 -0
- package/agnostic/time/duration/format-duration/types.js +1 -0
- package/agnostic/time/duration/index.d.ts +6 -1
- package/agnostic/time/duration/index.js +2 -2
- package/agnostic/time/duration/index.test.d.ts +1 -0
- package/agnostic/time/duration/index.test.js +168 -0
- package/agnostic/time/index.d.ts +1 -1
- package/agnostic/time/index.js +1 -1
- package/components/BeforeAfter/index.controlled.d.ts +25 -25
- package/components/BeforeAfter/index.controlled.js +47 -53
- package/components/BeforeAfter/index.d.ts +20 -23
- package/components/BeforeAfter/index.js +48 -40
- package/components/Button/index.js +1 -1
- package/components/Clippable/index.d.ts +17 -26
- package/components/Clippable/index.js +21 -23
- package/components/Disclaimer/index.d.ts +29 -22
- package/components/Disclaimer/index.js +29 -24
- package/components/Drawer/index.d.ts +45 -25
- package/components/Drawer/index.js +61 -52
- package/components/EventListener/index.d.ts +4 -5
- package/components/Gallery/index.d.ts +14 -16
- package/components/Gallery/index.js +25 -30
- package/components/Iframe/index.js +2 -2
- package/components/Image/index.d.ts +5 -12
- package/components/Image/index.js +11 -32
- package/components/Input/index.d.ts +23 -19
- package/components/Input/index.js +30 -20
- package/components/JsonEditor/index.d.ts +177 -9
- package/components/JsonEditor/index.js +133 -77
- package/components/JsonEditor/types.d.ts +11 -0
- package/components/JsonEditor/types.js +1 -0
- package/components/JsonEditor/utils.d.ts +8 -0
- package/components/JsonEditor/utils.js +23 -0
- package/components/ListLoader/index.controlled.d.ts +5 -5
- package/components/ListLoader/index.controlled.js +4 -4
- package/components/ListLoader/index.d.ts +11 -11
- package/components/ListLoader/index.js +12 -12
- package/components/Overlayer/index.js +4 -4
- package/components/Paginator/index.d.ts +7 -12
- package/components/Paginator/index.js +7 -11
- package/components/ResizeObserver/index.d.ts +14 -3
- package/components/ResizeObserver/index.js +49 -24
- package/components/Scrllgngn/index.d.ts +17 -13
- package/components/Scrllgngn/index.js +25 -84
- package/components/Scrllgngn/utils.d.ts +52 -0
- package/components/Scrllgngn/utils.js +102 -0
- package/components/ScrollListener/index.d.ts +43 -28
- package/components/ScrollListener/index.js +59 -79
- package/components/ScrollListener/utils.d.ts +37 -16
- package/components/ScrollListener/utils.js +141 -28
- package/components/Select/index.d.ts +24 -20
- package/components/Select/index.js +30 -20
- package/components/Sequencer/index.controlled.d.ts +16 -26
- package/components/Sequencer/index.controlled.js +15 -12
- package/components/Sequencer/index.d.ts +25 -30
- package/components/Sequencer/index.js +20 -34
- package/components/Subtitles/index.d.ts +29 -5
- package/components/Subtitles/index.js +90 -12
- package/components/Textarea/index.d.ts +26 -20
- package/components/Textarea/index.js +40 -21
- package/components/Theatre/index.d.ts +35 -39
- package/components/Theatre/index.js +46 -58
- package/components/UIModule/index.d.ts +25 -29
- package/components/UIModule/index.js +81 -110
- package/components/Video/index.controlled.d.ts +51 -51
- package/components/Video/index.controlled.js +75 -72
- package/components/Video/index.d.ts +12 -14
- package/components/Video/index.js +41 -86
- package/components/Video/utils.d.ts +35 -3
- package/components/Video/utils.js +37 -14
- package/components/index.d.ts +1 -1
- package/components/index.js +1 -1
- package/components/utils/index.d.ts +22 -0
- package/components/utils/index.js +31 -0
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/node/@aws-s3/storage/file/index.d.ts +1 -1
- package/node/@aws-s3/storage/file/index.js +1 -1
- package/node/@google-cloud/storage/index.d.ts +1 -1
- package/node/@google-cloud/storage/index.js +1 -1
- package/node/images/index.d.ts +1 -1
- package/node/images/index.js +1 -1
- package/node/images/transform/operations/index.d.ts +1 -1
- package/node/images/transform/operations/index.js +1 -1
- package/node/index.d.ts +1 -1
- package/node/index.js +1 -1
- package/package.json +20 -5
- package/TODO.md +0 -269
- package/components/BeforeAfter/utils.d.ts +0 -4
- package/components/BeforeAfter/utils.js +0 -9
- package/components/Input/index.controlled.d.ts +0 -42
- package/components/Input/index.controlled.js +0 -54
- package/components/Select/index.controlled.d.ts +0 -43
- package/components/Select/index.controlled.js +0 -54
- package/components/Subtitles/types.d.ts +0 -24
- package/components/Subtitles/utils.d.ts +0 -32
- package/components/Subtitles/utils.js +0 -107
- package/components/Textarea/index.controlled.d.ts +0 -50
- package/components/Textarea/index.controlled.js +0 -75
- /package/{components/Subtitles/types.js → agnostic/time/dates/format-date/index.test.d.ts} +0 -0
- /package/components/ResizeObserver/{style.module.css → styles.module.css} +0 -0
|
@@ -1,20 +1,28 @@
|
|
|
1
|
-
import { type FunctionComponent } from 'react';
|
|
2
|
-
import
|
|
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
|
-
*
|
|
6
|
+
* Extends all native {@link SelectHTMLAttributes} and {@link WithClassName}
|
|
7
|
+
* with optional label, error content, and option children.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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 =
|
|
14
|
+
export type Props = SelectHTMLAttributes<HTMLSelectElement> & PropsWithChildren<WithClassName<{
|
|
15
|
+
label?: ReactNode;
|
|
16
|
+
error?: ReactNode;
|
|
17
|
+
}>>;
|
|
13
18
|
/**
|
|
14
|
-
* Select field
|
|
19
|
+
* Select field supporting controlled and uncontrolled usage.
|
|
15
20
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
30
|
-
*
|
|
31
|
-
* - In
|
|
32
|
-
*
|
|
33
|
-
* -
|
|
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 {
|
|
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
|
|
10
|
+
* Select field supporting controlled and uncontrolled usage.
|
|
6
11
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
21
|
-
*
|
|
22
|
-
* - In
|
|
23
|
-
*
|
|
24
|
-
* -
|
|
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 = ({
|
|
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
|
|
32
|
-
|
|
38
|
+
const currentValue = isControlled ? value : internalValue;
|
|
39
|
+
// User action handlers
|
|
33
40
|
const handleChange = (e) => {
|
|
34
41
|
if (!isControlled)
|
|
35
|
-
|
|
42
|
+
setInternalValue(e.target.value);
|
|
36
43
|
onChange?.(e);
|
|
37
44
|
};
|
|
38
|
-
|
|
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
|
|
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
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* -
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
|
30
|
+
export type Props = PropsWithChildren<WithClassName<{
|
|
35
31
|
step?: number;
|
|
36
32
|
activateOnStep?: number[][];
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
58
|
-
* - `--playing` — when
|
|
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
|
-
*
|
|
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
|
|
65
|
+
* @see {@link Props}
|
|
76
66
|
* @returns A root `<div>` containing one classifier wrapper per child.
|
|
77
67
|
*/
|
|
78
|
-
export declare const
|
|
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
|
|
19
|
-
* - `--playing` — when
|
|
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
|
-
*
|
|
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
|
|
36
|
+
* @see {@link Props}
|
|
37
37
|
* @returns A root `<div>` containing one classifier wrapper per child.
|
|
38
38
|
*/
|
|
39
|
-
export const
|
|
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,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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}
|
|
8
|
-
*
|
|
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
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* @property
|
|
37
|
-
*
|
|
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, '
|
|
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
|
-
|
|
52
|
-
|
|
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
|
|
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 `
|
|
64
|
+
* while still allowing viewport handlers to fire `onIntersected`.
|
|
69
65
|
*
|
|
70
|
-
* ### Forwarded
|
|
71
|
-
*
|
|
72
|
-
* - `
|
|
73
|
-
* - `
|
|
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
|
-
*
|
|
77
|
-
*
|
|
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
|
|
76
|
+
* @see {@link ControlledSequencer}
|
|
82
77
|
* @returns An {@link IntersectionObserverComponent} wrapping a
|
|
83
|
-
* {@link
|
|
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
|
|
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 {
|
|
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
|
|
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 `
|
|
17
|
+
* while still allowing viewport handlers to fire `onIntersected`.
|
|
17
18
|
*
|
|
18
|
-
* ### Forwarded
|
|
19
|
-
*
|
|
20
|
-
* - `
|
|
21
|
-
* - `
|
|
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
|
-
*
|
|
25
|
-
*
|
|
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
|
|
29
|
+
* @see {@link ControlledSequencer}
|
|
30
30
|
* @returns An {@link IntersectionObserverComponent} wrapping a
|
|
31
|
-
* {@link
|
|
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,
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
91
|
+
onIntersected
|
|
102
92
|
]);
|
|
103
93
|
// Rendering
|
|
104
|
-
return _jsx(IntersectionObserverComponent, { onIntersected: handleIntersection, children: _jsx(
|
|
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
|
-
|
|
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 -
|
|
20
|
-
*
|
|
21
|
-
* @property onParsed -
|
|
22
|
-
*
|
|
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,
|
|
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
|
-
|
|
38
|
-
|
|
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 {
|