@design-edito/tools 0.5.37 → 0.5.39
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/@react/effects/index.d.ts +1 -0
- package/agnostic/@react/effects/index.js +1 -0
- package/agnostic/@react/effects/use-state-change-dispatch/index.d.ts +22 -0
- package/agnostic/@react/effects/use-state-change-dispatch/index.js +31 -0
- package/agnostic/@react/index.d.ts +1 -0
- package/agnostic/@react/index.js +1 -0
- package/agnostic/arrays/index.d.ts +3 -2
- package/agnostic/arrays/index.js +3 -2
- package/agnostic/arrays/integers-between/index.d.ts +8 -0
- package/agnostic/arrays/integers-between/index.js +11 -0
- package/agnostic/arrays/integers-between/index.test.d.ts +1 -0
- package/agnostic/arrays/integers-between/index.test.js +16 -0
- package/agnostic/colors/index.d.ts +2 -2
- package/agnostic/colors/index.js +2 -2
- package/agnostic/css/index.d.ts +2 -1
- package/agnostic/css/index.js +2 -1
- package/agnostic/css/merge-class-names/index.d.ts +8 -0
- package/agnostic/css/merge-class-names/index.js +23 -0
- package/agnostic/css/merge-class-names/index.test.d.ts +1 -0
- package/agnostic/css/merge-class-names/index.test.js +52 -0
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +11 -11
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +11 -11
- 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 +2 -2
- package/agnostic/html/index.js +2 -2
- package/agnostic/index.d.ts +2 -1
- package/agnostic/index.js +2 -1
- package/agnostic/misc/index.d.ts +1 -1
- package/agnostic/misc/index.js +1 -1
- package/agnostic/numbers/index.d.ts +2 -2
- package/agnostic/numbers/index.js +2 -2
- package/agnostic/objects/index.d.ts +2 -2
- package/agnostic/objects/index.js +2 -2
- package/agnostic/random/index.d.ts +1 -1
- package/agnostic/random/index.js +1 -1
- package/agnostic/time/dates/index.d.ts +1 -1
- package/agnostic/time/dates/index.js +1 -1
- package/components/BeforeAfter/index.controlled.js +1 -1
- package/components/BeforeAfter/index.js +2 -2
- package/components/Button/index.js +1 -1
- package/components/Clippable/index.js +1 -1
- package/components/Disclaimer/index.d.ts +2 -2
- package/components/Disclaimer/index.js +4 -3
- package/components/Drawer/index.js +4 -3
- package/components/EventListener/index.js +1 -1
- package/components/Gallery/index.js +5 -4
- package/components/Iframe/index.js +1 -1
- package/components/Image/index.d.ts +1 -1
- package/components/Image/index.js +3 -2
- package/components/Input/index.js +1 -1
- package/components/IntersectionObserver/index.js +1 -1
- package/components/JsonEditor/index.js +1 -1
- package/components/JsonEditor/types.d.ts +0 -6
- package/components/JsonEditor/utils.d.ts +0 -5
- package/components/JsonEditor/utils.js +0 -5
- package/components/Lightbox/index.d.ts +12 -12
- package/components/Lightbox/index.js +37 -35
- package/components/Lightbox/store.d.ts +9 -43
- package/components/Lightbox/store.js +51 -73
- package/components/Lightbox/types.d.ts +16 -0
- package/components/Lightbox/types.js +1 -0
- package/components/ListLoader/index.controlled.js +19 -13
- package/components/ListLoader/index.js +19 -3
- package/components/Overlayer/index.js +1 -1
- package/components/Paginator/index.js +4 -3
- package/components/ResizeObserver/index.js +1 -1
- package/components/Scrllgngn/index.d.ts +4 -4
- package/components/Scrllgngn/index.js +4 -3
- package/components/Scrllgngn/types.d.ts +55 -0
- package/components/Scrllgngn/types.js +24 -0
- package/components/Scrllgngn/utils.d.ts +2 -38
- package/components/ScrollListener/index.d.ts +1 -6
- package/components/ScrollListener/index.js +10 -9
- package/components/ScrollListener/types.d.ts +0 -3
- package/components/Select/index.js +1 -1
- package/components/Sequencer/index.d.ts +2 -9
- package/components/Sequencer/index.js +26 -128
- package/components/Sequencer/types.d.ts +30 -0
- package/components/Sequencer/types.js +30 -0
- package/components/Sequencer/utils.d.ts +4 -0
- package/components/Sequencer/utils.js +25 -0
- package/components/ShadowRoot/index.js +1 -1
- package/components/Subtitles/index.js +3 -5
- package/components/Subtitles/utils.d.ts +0 -51
- package/components/Subtitles/utils.js +0 -41
- package/components/Textarea/index.js +1 -1
- package/components/UIModule/index.d.ts +8 -6
- package/components/UIModule/index.js +11 -14
- package/components/Video/index.controlled.d.ts +1 -7
- package/components/Video/index.controlled.js +13 -12
- package/components/Video/index.d.ts +0 -8
- package/components/Video/index.js +11 -15
- package/components/Video/types.d.ts +33 -0
- package/components/Video/utils.d.ts +1 -67
- package/components/Video/utils.js +25 -44
- package/components/index.d.ts +3 -3
- package/components/index.js +3 -3
- package/components/utils/index.d.ts +1 -44
- package/components/utils/index.js +1 -83
- package/components/utils/viewport-behaviours/index.d.ts +8 -14
- package/components/utils/viewport-behaviours/index.js +15 -37
- package/components/utils/viewport-behaviours/types.d.ts +3 -9
- package/components/utils/viewport-behaviours/types.js +3 -4
- package/node/ftps/directory/index.d.ts +1 -1
- package/node/ftps/directory/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 +3 -3
- package/node/images/transform/operations/index.js +3 -3
- package/node/index.d.ts +2 -2
- package/node/index.js +2 -2
- package/package.json +44 -1
- package/components/ListLoader/utils.d.ts +0 -8
- package/components/ListLoader/utils.js +0 -10
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useStateChangeDispatch } from '../../agnostic/@react/effects/use-state-change-dispatch/index.js';
|
|
3
4
|
import { clss } from '../../agnostic/css/clss/index.js';
|
|
4
5
|
import { unknownToString } from '../../agnostic/errors/unknown-to-string/index.js';
|
|
5
6
|
import { isNonNullObject } from '../../agnostic/objects/is-object/index.js';
|
|
6
7
|
import { randomHash } from '../../agnostic/random/uuid/index.js';
|
|
7
|
-
import { mergeClassNames
|
|
8
|
+
import { mergeClassNames } from '../../agnostic/css/merge-class-names/index.js';
|
|
8
9
|
import { uiModule as publicClassName } from '../public-classnames.js';
|
|
9
10
|
import cssModule from './styles.module.css';
|
|
10
11
|
/**
|
|
@@ -34,13 +35,13 @@ import cssModule from './styles.module.css';
|
|
|
34
35
|
*
|
|
35
36
|
* ### Root element attributes
|
|
36
37
|
* - `id` — a stable generated id, assigned once on mount and used to scope the
|
|
37
|
-
* module's `css`
|
|
38
|
+
* module's `css` to this specific instance.
|
|
38
39
|
*
|
|
39
40
|
* @param props - Component properties.
|
|
40
41
|
* @see {@link Props}
|
|
41
42
|
* @see {@link ModuleData}
|
|
42
43
|
* @returns A host `<div>` into which the module's root `Element` is appended,
|
|
43
|
-
* along with
|
|
44
|
+
* along with the `<style>` the module exported, if any.
|
|
44
45
|
*/
|
|
45
46
|
export const UIModule = ({ src, props, onIdGenerated, onIsLoadingChanged, onLoadedModuleChanged, onModuleTargetChanged, className }) => {
|
|
46
47
|
// State & refs
|
|
@@ -56,9 +57,9 @@ export const UIModule = ({ src, props, onIdGenerated, onIsLoadingChanged, onLoad
|
|
|
56
57
|
// the render it ran on. This is what `init` and `postInit` read instead.
|
|
57
58
|
const propsRef = useRef(props);
|
|
58
59
|
// State dispatch
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
useStateChangeDispatch(isLoading, onIsLoadingChanged);
|
|
61
|
+
useStateChangeDispatch(loadedModule, onLoadedModuleChanged);
|
|
62
|
+
useStateChangeDispatch(moduleTarget, onModuleTargetChanged);
|
|
62
63
|
// Fx. no dep. - keep the latest props reachable from the effects that run outside
|
|
63
64
|
// their own render, declared first so they read this render's value and not the last
|
|
64
65
|
useEffect(() => { propsRef.current = props; });
|
|
@@ -80,12 +81,8 @@ export const UIModule = ({ src, props, onIdGenerated, onIsLoadingChanged, onLoad
|
|
|
80
81
|
return setLoadedModule(new Error('Module exported member `destroy` must be a function'));
|
|
81
82
|
if ('postInit' in data && typeof data.postInit !== 'function')
|
|
82
83
|
return setLoadedModule(new Error('Module exported member `postInit` must be a function'));
|
|
83
|
-
if ('css' in data)
|
|
84
|
-
|
|
85
|
-
return setLoadedModule(new Error('Module exported member `css` must be an array of strings'));
|
|
86
|
-
if (data.css.some(entry => typeof entry !== 'string'))
|
|
87
|
-
return setLoadedModule(new Error('Module exported member `css` must be an array of strings'));
|
|
88
|
-
}
|
|
84
|
+
if ('css' in data && typeof data.css !== 'string')
|
|
85
|
+
return setLoadedModule(new Error('Module exported member `css` must be a string'));
|
|
89
86
|
if ('update' in data && typeof data.update !== 'function')
|
|
90
87
|
return setLoadedModule(new Error('Module exported member `update` must be a function'));
|
|
91
88
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- shape fully validated by the checks above
|
|
@@ -162,6 +159,6 @@ export const UIModule = ({ src, props, onIdGenerated, onIsLoadingChanged, onLoad
|
|
|
162
159
|
'loaded': !isLoading && loadedModule !== null && !hasErrored,
|
|
163
160
|
'initialized': moduleTarget !== null
|
|
164
161
|
}), className);
|
|
165
|
-
const moduleCss = hasErrored || loadedModule === null ?
|
|
166
|
-
return _jsx("div", { className: rootClss, ref: rootRef, id: id, children: moduleCss
|
|
162
|
+
const moduleCss = hasErrored || loadedModule === null ? undefined : loadedModule.css;
|
|
163
|
+
return _jsx("div", { className: rootClss, ref: rootRef, id: id, children: moduleCss !== undefined && _jsx("style", { children: `.${publicClassName}#${id} { ${moduleCss} }` }) });
|
|
167
164
|
};
|
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import { type FunctionComponent, type PropsWithChildren, type Ref, type RefObject, type VideoHTMLAttributes } from 'react';
|
|
2
2
|
import type { WithClassName } from '../utils/types.js';
|
|
3
3
|
import { type Props as SubsProps } from '../Subtitles/index.js';
|
|
4
|
-
import {
|
|
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
|
-
*/
|
|
4
|
+
import type { SourceData, TrackData } from './types.js';
|
|
11
5
|
/**
|
|
12
6
|
* Props for the ControlledVideo component.
|
|
13
7
|
*
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useRef, useState, useCallback, useEffect } from 'react';
|
|
3
|
+
import { useStateChangeDispatch } from '../../agnostic/@react/effects/use-state-change-dispatch/index.js';
|
|
3
4
|
import { clss } from '../../agnostic/css/clss/index.js';
|
|
4
5
|
import { formatDuration } from '../../agnostic/time/duration/format-duration/index.js';
|
|
5
|
-
import { mergeClassNames
|
|
6
|
+
import { mergeClassNames } from '../../agnostic/css/merge-class-names/index.js';
|
|
6
7
|
import cssModule from './styles.module.css';
|
|
7
8
|
import { video as publicClassName } from '../public-classnames.js';
|
|
8
9
|
import { Subtitles } from '../Subtitles/index.js';
|
|
9
|
-
import { forceExitFullscreen, forceFullscreen, forceLoud, forceMute, forcePause, forcePlay, forcePlaybackRate, forceVolume, getTimelineClickProgress, msToSeconds, secondsToMs } from './utils.js';
|
|
10
|
+
import { forceExitFullscreen, forceFullscreen, forceLoud, forceMute, forcePause, forcePlay, forcePlaybackRate, forceVolume, getTimelineClickProgress, msToSeconds, parseSrcSetSourcesList, secondsToMs } from './utils.js';
|
|
10
11
|
/**
|
|
11
12
|
* Full-featured video player component. Wraps a native `<video>` element with
|
|
12
13
|
* playback controls, volume, playback rate, a timeline, optional subtitles
|
|
@@ -226,8 +227,8 @@ timeFormat = '{{mm}}:{{ss}}:{{ms}}', onPlayButtonClicked, onPauseButtonClicked,
|
|
|
226
227
|
'--lm-video-volume-ratio': `${volume}`,
|
|
227
228
|
'--lm-video-playback-rate': `${playbackRate}`
|
|
228
229
|
};
|
|
229
|
-
const parsedSources = useMemo(() =>
|
|
230
|
-
const parsedTracks = useMemo(() =>
|
|
230
|
+
const parsedSources = useMemo(() => parseSrcSetSourcesList(sources, 'src'), [sources]);
|
|
231
|
+
const parsedTracks = useMemo(() => parseSrcSetSourcesList(tracks, 'src'), [tracks]);
|
|
231
232
|
const videoClss = c('video');
|
|
232
233
|
const videoControlsClss = c('video-controls');
|
|
233
234
|
const playBtnClss = c('play-btn');
|
|
@@ -305,13 +306,13 @@ timeFormat = '{{mm}}:{{ss}}:{{ms}}', onPlayButtonClicked, onPauseButtonClicked,
|
|
|
305
306
|
return () => { document.removeEventListener('fullscreenchange', onDocumentFullscreenChange); };
|
|
306
307
|
}, [onFullscreenChange]);
|
|
307
308
|
// State handlers
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
309
|
+
useStateChangeDispatch(currentTimeMs, onCurrentTimeMsChanged);
|
|
310
|
+
useStateChangeDispatch(isElementPlaying, onIsPlayingChanged);
|
|
311
|
+
useStateChangeDispatch(isFullscreen, onIsFullscreenChanged);
|
|
312
|
+
useStateChangeDispatch(isLoud, onIsLoudChanged);
|
|
313
|
+
useStateChangeDispatch(subtitlesOn, onIsSubtitlesOnChanged);
|
|
314
|
+
useStateChangeDispatch(isEnded, onIsEndedChanged);
|
|
315
|
+
useStateChangeDispatch(volume, onVolumeChanged);
|
|
316
|
+
useStateChangeDispatch(playbackRate, onPlaybackRateChanged);
|
|
316
317
|
return _jsxs("figure", { ref: rootRef, className: rootClss, style: rootStyles, ...rootAttributes, children: [_jsxs("video", { ref: videoRef, className: videoClss, ...intrinsicVideoAttributes, autoPlay: isTimeControlled ? false : intrinsicVideoAttributes.autoPlay, onClick: handleVideoClick, onLoadedMetadata: handleMetadataLoadEvent, onTimeUpdate: handleOnTimeUpdateEvent, onEnded: handleEndedEvent, onPlay: handlePlayEvent, onPause: handlePauseEvent, children: [parsedSources.map((source, index) => _jsx("source", { src: source.src, type: source.type }, index)), parsedTracks.map((track, index) => _jsx("track", { src: track.src, kind: track.kind, srcLang: track.srclang, label: track.label, default: track.default }, index)), children] }), _jsxs("div", { className: videoControlsClss, children: [_jsx("button", { className: playBtnClss, onClick: handlePlayButtonClick, children: playBtnContent }), _jsx("button", { className: pauseBtnClss, onClick: handlePauseButtonClick, children: pauseBtnContent }), _jsx("button", { className: loudBtnClss, onClick: handleLoudButtonClick, children: loudBtnContent }), _jsx("button", { className: muteBtnClss, onClick: handleMuteButtonClick, children: muteBtnContent }), _jsx("input", { type: 'range', className: volumeRangeClss, value: volumePercent, onChange: handleVolumeRangeChange, min: 0, max: 100, step: 1 }), _jsx("span", { className: volumePcntClss, children: Math.round(volumePercent) }), _jsx("button", { className: fullscreenBtnClss, onClick: handleFullscreenButtonClick, children: fullscreenBtnContent }), subtitles !== undefined && _jsx("button", { className: subtitlesBtnClss, onClick: handleSubtitlesButtonClick, children: subtitlesBtnContent }), _jsx("input", { type: 'range', className: playbackRateRangeClss, value: playbackRate, onChange: handleRateRangeChange, min: 0.25, max: 4, step: 0.25 }), _jsx("span", { className: playbackRateClss, children: playbackRate })] }), _jsxs("div", { className: timeControlsClss, children: [_jsx("span", { className: currentTimeClss, children: formatDuration(currentTimeMs, timeFormat) }), _jsx("span", { className: totalTimeClss, children: formatDuration(totalTimeMs, timeFormat) }), _jsx("div", { className: timelineClss, onClick: handleTimelineClick })] }), subtitles !== undefined && _jsx(Subtitles, { ...subtitles, timecodeMs: currentTimeMs, isEnded: isEnded })] });
|
|
317
318
|
};
|
|
@@ -6,13 +6,6 @@ import { type Props as ControlledProps } from './index.controlled.js';
|
|
|
6
6
|
* Props for the {@link Video} component.
|
|
7
7
|
*
|
|
8
8
|
* Extends all ControlledVideo props except play, mute, fullscreen, volume, playbackRate, and their associated event handlers
|
|
9
|
-
* @property behavioursSuspended - Holds back the instructions that **start** something,
|
|
10
|
-
* and lets through those that stop it. For a consumer withholding the video behind
|
|
11
|
-
* something — a warning to accept, typically: a video kept back that plays anyway is not
|
|
12
|
-
* withheld, it is merely hard to see. What was held back is replayed the moment this
|
|
13
|
-
* goes false, so lifting the veil on an already-visible video does what was asked, which
|
|
14
|
-
* a crossing-based trigger could never do. `':force'` does not override it: it speaks of
|
|
15
|
-
* consent not yet given, where a surrender speaks of an intention already expressed.
|
|
16
9
|
* @property visibilityThreshold - How much of the video has to be on screen to count as
|
|
17
10
|
* seen. See {@link VisibilityOptions} for this and the four that follow it.
|
|
18
11
|
* @property whenVisible - What to do each time it becomes visible.
|
|
@@ -39,7 +32,6 @@ import { type Props as ControlledProps } from './index.controlled.js';
|
|
|
39
32
|
*/
|
|
40
33
|
export type Props = Omit<ControlledProps, 'play' | 'fullscreen' | 'volume' | 'mute' | 'playbackRate' | 'subtitlesOn' | 'rootRef' | 'videoRef'> & ViewportBehaviours<VideoAction> & {
|
|
41
34
|
defaultSubtitlesOn?: boolean;
|
|
42
|
-
behavioursSuspended?: boolean;
|
|
43
35
|
};
|
|
44
36
|
/**
|
|
45
37
|
* Full-featured video player component. Wraps a native `<video>` element with
|
|
@@ -51,7 +51,7 @@ import { ControlledVideo } from './index.controlled.js';
|
|
|
51
51
|
* The media still won't play, though: autoplay muted, and leave unmuting to the
|
|
52
52
|
* reader.
|
|
53
53
|
*/
|
|
54
|
-
export const Video = ({ defaultSubtitlesOn = true,
|
|
54
|
+
export const Video = ({ defaultSubtitlesOn = true, visibilityThreshold, visibilityRoot, visibilityRootMargin, visibilityOnAfterMs, visibilityOffAfterMs, whenVisible, whenHidden, onVisibilityChanged, onVideoClicked, onPlayButtonClicked, onPauseButtonClicked, onLoudButtonClicked, onMuteButtonClicked, onVolumeRangeChanged, onRateRangeChanged, onFullscreenButtonClicked, onSubtitlesButtonClicked, ...controlledProps }) => {
|
|
55
55
|
// State & refs
|
|
56
56
|
const [play, setPlay] = useState(false);
|
|
57
57
|
const [volume, setVolume] = useState(1);
|
|
@@ -73,14 +73,11 @@ export const Video = ({ defaultSubtitlesOn = true, behavioursSuspended, visibili
|
|
|
73
73
|
// **Seeking belongs to `playback`.** A reader who has dragged the timeline has said
|
|
74
74
|
// where they want to be, and an automatic jump would take it back from them.
|
|
75
75
|
const actions = {
|
|
76
|
-
play: {
|
|
77
|
-
pause: {
|
|
78
|
-
loud: {
|
|
79
|
-
mute: {
|
|
80
|
-
// A seek starts nothing on its own — it moves a playhead, playing or not — so a gate
|
|
81
|
-
// has no reason to hold it back.
|
|
76
|
+
play: { domain: 'playback', run: () => setPlay(true) },
|
|
77
|
+
pause: { domain: 'playback', run: () => setPlay(false) },
|
|
78
|
+
loud: { domain: 'sound', run: () => setMute(false) },
|
|
79
|
+
mute: { domain: 'sound', run: () => setMute(true) },
|
|
82
80
|
'jump-to': {
|
|
83
|
-
kind: 'stop',
|
|
84
81
|
domain: 'playback',
|
|
85
82
|
run: arg => {
|
|
86
83
|
const targetMs = Number(arg);
|
|
@@ -89,8 +86,8 @@ export const Video = ({ defaultSubtitlesOn = true, behavioursSuspended, visibili
|
|
|
89
86
|
forceJumpTo(videoRef.current, targetMs);
|
|
90
87
|
}
|
|
91
88
|
},
|
|
92
|
-
'jump-start': {
|
|
93
|
-
'jump-end': {
|
|
89
|
+
'jump-start': { domain: 'playback', run: () => forceJumpTo(videoRef.current, 0) },
|
|
90
|
+
'jump-end': { domain: 'playback', run: () => forceJumpTo(videoRef.current, -1) }
|
|
94
91
|
};
|
|
95
92
|
const { surrender: surrenderAnyDomain } = useViewportBehaviours(rootRef, {
|
|
96
93
|
visibilityThreshold,
|
|
@@ -101,7 +98,7 @@ export const Video = ({ defaultSubtitlesOn = true, behavioursSuspended, visibili
|
|
|
101
98
|
whenVisible,
|
|
102
99
|
whenHidden,
|
|
103
100
|
onVisibilityChanged
|
|
104
|
-
}, actions
|
|
101
|
+
}, actions);
|
|
105
102
|
// Narrowed back to this component's own domains. The generic layer takes a `string`,
|
|
106
103
|
// having no vocabulary of its own; handing the handlers a typed door means a
|
|
107
104
|
// misspelled domain fails to compile instead of quietly never surrendering anything.
|
|
@@ -185,10 +182,9 @@ export const Video = ({ defaultSubtitlesOn = true, behavioursSuspended, visibili
|
|
|
185
182
|
onFullscreenButtonClicked?.(payload);
|
|
186
183
|
setFullscreen(!payload.isFullscreen);
|
|
187
184
|
}, [onFullscreenButtonClicked]);
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
// seul l'habillage change — inutile d'en rendre deux pour n'en montrer jamais qu'un.
|
|
185
|
+
// One button for both directions, where play/pause and loud/mute get one each: those
|
|
186
|
+
// carry two opposed glyphs, so both are rendered and the stylesheet shows one. The
|
|
187
|
+
// subtitles button carries a single glyph whose styling alone changes.
|
|
192
188
|
const handleSubtitlesButtonClick = useCallback(payload => {
|
|
193
189
|
onSubtitlesButtonClicked?.(payload);
|
|
194
190
|
setSubtitlesOn(!payload.isSubtitlesOn);
|
|
@@ -40,3 +40,36 @@ export type VideoAction = Exclude<VideoVerb, 'jump-to'> | `jump-to:${number}`;
|
|
|
40
40
|
* want to be, and an automatic jump would take it back from them.
|
|
41
41
|
*/
|
|
42
42
|
export type VideoDomain = 'playback' | 'sound';
|
|
43
|
+
/**
|
|
44
|
+
* One `<source>` of a `<video>`.
|
|
45
|
+
*
|
|
46
|
+
* **Not interchangeable with a picture source**: a `<source>` inside a `<video>` carries
|
|
47
|
+
* `src`, one inside a `<picture>` carries `srcSet`, `media` and `sizes`, and neither
|
|
48
|
+
* element accepts the other's attributes.
|
|
49
|
+
*
|
|
50
|
+
* @property src - URL of the video file.
|
|
51
|
+
* @property type - MIME type, so the browser can skip what it cannot play
|
|
52
|
+
* (`'video/mp4'`).
|
|
53
|
+
*/
|
|
54
|
+
export type SourceData = {
|
|
55
|
+
src?: string;
|
|
56
|
+
type?: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* One `<track>` of a `<video>` — subtitles, captions, chapters.
|
|
60
|
+
*
|
|
61
|
+
* @property src - URL of the WebVTT file.
|
|
62
|
+
* @property kind - What the track holds. Browsers default to `'subtitles'`.
|
|
63
|
+
* @property srclang - Its BCP 47 language tag, required by the element whenever `kind`
|
|
64
|
+
* is `'subtitles'`.
|
|
65
|
+
* @property label - The name a browser shows in its own track menu.
|
|
66
|
+
* @property default - Whether it is the one enabled when no preference says otherwise.
|
|
67
|
+
* At most one track per element may carry it.
|
|
68
|
+
*/
|
|
69
|
+
export type TrackData = {
|
|
70
|
+
src?: string;
|
|
71
|
+
kind?: 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata';
|
|
72
|
+
srclang?: string;
|
|
73
|
+
label?: string;
|
|
74
|
+
default?: boolean;
|
|
75
|
+
};
|
|
@@ -1,41 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The two list shapes a `<video>` accepts as children, as records.
|
|
3
|
-
*
|
|
4
|
-
* They live here and not next to the component because nothing about them is React:
|
|
5
|
-
* they describe a `<source>` and a `<track>` element.
|
|
6
|
-
*
|
|
7
|
-
* **A video source is not a picture source**, which is why `Image` keeps a shape of its
|
|
8
|
-
* own rather than sharing this one: a `<source>` inside a `<video>` carries `src`, one
|
|
9
|
-
* inside a `<picture>` carries `srcSet`, `media` and `sizes`, and neither element accepts
|
|
10
|
-
* the other's attributes. What the two do share is how a list of them is read — see
|
|
11
|
-
* `parseSourceList` in `components/utils`.
|
|
12
|
-
*/
|
|
13
|
-
export type SourceData = {
|
|
14
|
-
src?: string;
|
|
15
|
-
type?: string;
|
|
16
|
-
};
|
|
17
|
-
export type TrackData = {
|
|
18
|
-
src?: string;
|
|
19
|
-
kind?: 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata';
|
|
20
|
-
srclang?: string;
|
|
21
|
-
label?: string;
|
|
22
|
-
default?: boolean;
|
|
23
|
-
};
|
|
24
1
|
export declare const muteAttributeWorkaround: (video: HTMLVideoElement | null, shouldMute: boolean) => void;
|
|
25
2
|
export declare const forceMute: (video: HTMLVideoElement | null) => void;
|
|
26
3
|
export declare const forceLoud: (video: HTMLVideoElement | null) => void;
|
|
27
4
|
export declare const forceVolume: (video: HTMLVideoElement | null, volume: number) => void;
|
|
28
|
-
/**
|
|
29
|
-
* Asks the element to play, and reports what came of it.
|
|
30
|
-
*
|
|
31
|
-
* A browser may refuse — an unmuted media outside a user gesture, typically — and
|
|
32
|
-
* it refuses **silently**: the promise rejects and no event fires, so this is the
|
|
33
|
-
* only moment the outcome can be known. Hence the return value, and hence no
|
|
34
|
-
* logging: a refusal is not an anomaly to print, it is an answer to hand back.
|
|
35
|
-
*
|
|
36
|
-
* @param video - The element, or `null` before it mounts.
|
|
37
|
-
* @returns Whether it is playing now.
|
|
38
|
-
*/
|
|
39
5
|
export declare const forcePlay: (video: HTMLVideoElement | null) => Promise<boolean>;
|
|
40
6
|
export declare const forcePause: (video: HTMLVideoElement | null) => boolean;
|
|
41
7
|
export declare const forcePlaybackRate: (video: HTMLVideoElement | null, rate: number) => void;
|
|
@@ -43,38 +9,6 @@ export declare const forceFullscreen: (video: HTMLVideoElement | null) => Promis
|
|
|
43
9
|
export declare const forceExitFullscreen: (video: HTMLVideoElement | null) => Promise<boolean>;
|
|
44
10
|
export declare function secondsToMs(seconds: number): number;
|
|
45
11
|
export declare function msToSeconds(ms: number): number;
|
|
46
|
-
/**
|
|
47
|
-
* Where along the timeline a click landed.
|
|
48
|
-
*
|
|
49
|
-
* @param event - The click on the timeline element.
|
|
50
|
-
* @returns A ratio between `0` and `1`, clamped to the element's own bounds.
|
|
51
|
-
*/
|
|
52
12
|
export declare const getTimelineClickProgress: (event: React.MouseEvent<HTMLDivElement>) => number;
|
|
53
|
-
/**
|
|
54
|
-
* Whether a viewport-driven behaviour should run on this crossing.
|
|
55
|
-
*
|
|
56
|
-
* Each behaviour comes in two flavours: `…When…` on every crossing, `…Once…` on the
|
|
57
|
-
* first one only. Setting both is the same as setting `…When…` alone.
|
|
58
|
-
*
|
|
59
|
-
* @param whenCrossed - The `…When…` prop.
|
|
60
|
-
* @param onceOnly - The `…Once…` prop.
|
|
61
|
-
* @param hasFired - Whether this behaviour's own automatic trigger already ran. It
|
|
62
|
-
* tracks that behaviour and nothing else: a user pressing play must not spend an
|
|
63
|
-
* automatic mute the component still owed.
|
|
64
|
-
* @returns Whether to apply it now.
|
|
65
|
-
*/
|
|
66
|
-
/**
|
|
67
|
-
* Seeks the element, in milliseconds, counting from the end when asked negatively.
|
|
68
|
-
*
|
|
69
|
-
* **A negative target counts back from the duration**, so `-1` is the last reachable
|
|
70
|
-
* timecode. It is `-1` and not `-0` because `-0 === 0` in JavaScript, which would make
|
|
71
|
-
* the end of a video indistinguishable from its start.
|
|
72
|
-
*
|
|
73
|
-
* Nothing happens before the metadata lands: the duration is `NaN` until then, and a
|
|
74
|
-
* seek computed against it would land anywhere. An instruction asking for the end of a
|
|
75
|
-
* video whose length is still unknown is simply not yet answerable.
|
|
76
|
-
*
|
|
77
|
-
* @param video - The element, or `null` before it mounts.
|
|
78
|
-
* @param targetMs - Where to go. Negative counts from the end.
|
|
79
|
-
*/
|
|
80
13
|
export declare const forceJumpTo: (video: HTMLVideoElement | null, targetMs: number) => void;
|
|
14
|
+
export declare function parseSrcSetSourcesList<T extends Record<string, unknown>>(given: string | string[] | T[] | undefined, stringKey: keyof T & string): T[];
|
|
@@ -28,17 +28,8 @@ export const forceVolume = (video, volume) => {
|
|
|
28
28
|
// eslint-disable-next-line no-param-reassign
|
|
29
29
|
video.volume = volume;
|
|
30
30
|
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
*
|
|
34
|
-
* A browser may refuse — an unmuted media outside a user gesture, typically — and
|
|
35
|
-
* it refuses **silently**: the promise rejects and no event fires, so this is the
|
|
36
|
-
* only moment the outcome can be known. Hence the return value, and hence no
|
|
37
|
-
* logging: a refusal is not an anomaly to print, it is an answer to hand back.
|
|
38
|
-
*
|
|
39
|
-
* @param video - The element, or `null` before it mounts.
|
|
40
|
-
* @returns Whether it is playing now.
|
|
41
|
-
*/
|
|
31
|
+
// A browser refuses playback silently: the promise rejects and no event fires, so the
|
|
32
|
+
// return value is the only way the caller learns the outcome.
|
|
42
33
|
export const forcePlay = async (video) => {
|
|
43
34
|
if (video === null)
|
|
44
35
|
return false;
|
|
@@ -107,44 +98,13 @@ export function secondsToMs(seconds) {
|
|
|
107
98
|
export function msToSeconds(ms) {
|
|
108
99
|
return ms / 1000;
|
|
109
100
|
}
|
|
110
|
-
/**
|
|
111
|
-
* Where along the timeline a click landed.
|
|
112
|
-
*
|
|
113
|
-
* @param event - The click on the timeline element.
|
|
114
|
-
* @returns A ratio between `0` and `1`, clamped to the element's own bounds.
|
|
115
|
-
*/
|
|
116
101
|
export const getTimelineClickProgress = (event) => {
|
|
117
102
|
const timelineRect = event.currentTarget.getBoundingClientRect();
|
|
118
103
|
const position = event.clientX - timelineRect.left;
|
|
119
104
|
return Math.min(1, Math.max(0, position / timelineRect.width));
|
|
120
105
|
};
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
*
|
|
124
|
-
* Each behaviour comes in two flavours: `…When…` on every crossing, `…Once…` on the
|
|
125
|
-
* first one only. Setting both is the same as setting `…When…` alone.
|
|
126
|
-
*
|
|
127
|
-
* @param whenCrossed - The `…When…` prop.
|
|
128
|
-
* @param onceOnly - The `…Once…` prop.
|
|
129
|
-
* @param hasFired - Whether this behaviour's own automatic trigger already ran. It
|
|
130
|
-
* tracks that behaviour and nothing else: a user pressing play must not spend an
|
|
131
|
-
* automatic mute the component still owed.
|
|
132
|
-
* @returns Whether to apply it now.
|
|
133
|
-
*/
|
|
134
|
-
/**
|
|
135
|
-
* Seeks the element, in milliseconds, counting from the end when asked negatively.
|
|
136
|
-
*
|
|
137
|
-
* **A negative target counts back from the duration**, so `-1` is the last reachable
|
|
138
|
-
* timecode. It is `-1` and not `-0` because `-0 === 0` in JavaScript, which would make
|
|
139
|
-
* the end of a video indistinguishable from its start.
|
|
140
|
-
*
|
|
141
|
-
* Nothing happens before the metadata lands: the duration is `NaN` until then, and a
|
|
142
|
-
* seek computed against it would land anywhere. An instruction asking for the end of a
|
|
143
|
-
* video whose length is still unknown is simply not yet answerable.
|
|
144
|
-
*
|
|
145
|
-
* @param video - The element, or `null` before it mounts.
|
|
146
|
-
* @param targetMs - Where to go. Negative counts from the end.
|
|
147
|
-
*/
|
|
106
|
+
// A negative target counts back from the duration, `-1` being the last reachable
|
|
107
|
+
// timecode — not `-0`, which `===` makes indistinguishable from the start.
|
|
148
108
|
export const forceJumpTo = (video, targetMs) => {
|
|
149
109
|
if (video === null)
|
|
150
110
|
return;
|
|
@@ -155,3 +115,24 @@ export const forceJumpTo = (video, targetMs) => {
|
|
|
155
115
|
// eslint-disable-next-line no-param-reassign
|
|
156
116
|
video.currentTime = msToSeconds(Math.min(Math.max(absolute, 0), durationMs));
|
|
157
117
|
};
|
|
118
|
+
/* Source lists */
|
|
119
|
+
export function parseSrcSetSourcesList(given, stringKey) {
|
|
120
|
+
const fromString = (value) => {
|
|
121
|
+
const single = { [stringKey]: value };
|
|
122
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- a one-key record is the narrowest `T` a bare string can describe; every other field is optional by contract
|
|
123
|
+
return single;
|
|
124
|
+
};
|
|
125
|
+
if (given === undefined)
|
|
126
|
+
return [];
|
|
127
|
+
if (typeof given === 'string')
|
|
128
|
+
return [fromString(given)];
|
|
129
|
+
if (!Array.isArray(given))
|
|
130
|
+
return [];
|
|
131
|
+
if (given.length === 0)
|
|
132
|
+
return [];
|
|
133
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- first element sampled just above; array is read as homogeneous
|
|
134
|
+
if (typeof given[0] === 'string')
|
|
135
|
+
return given.map(fromString);
|
|
136
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- first element was checked not to be a string just above; array is read as homogeneous
|
|
137
|
+
return given;
|
|
138
|
+
}
|
package/components/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export * as beforeAfter from './BeforeAfter/index.js'
|
|
2
|
-
export * as clippable from './Clippable/index.js'
|
|
3
2
|
export * as button from './Button/index.js'
|
|
3
|
+
export * as clippable from './Clippable/index.js'
|
|
4
|
+
export * as disclaimer from './Disclaimer/index.js'
|
|
4
5
|
export * as drawer from './Drawer/index.js'
|
|
5
6
|
export * as eventListener from './EventListener/index.js'
|
|
6
|
-
export * as disclaimer from './Disclaimer/index.js'
|
|
7
7
|
export * as gallery from './Gallery/index.js'
|
|
8
8
|
export * as iframe from './Iframe/index.js'
|
|
9
9
|
export * as image from './Image/index.js'
|
|
@@ -16,8 +16,8 @@ export * as overlayer from './Overlayer/index.js'
|
|
|
16
16
|
export * as paginator from './Paginator/index.js'
|
|
17
17
|
export * as resizeObserver from './ResizeObserver/index.js'
|
|
18
18
|
export * as scrllgngn from './Scrllgngn/index.js'
|
|
19
|
-
export * as select from './Select/index.js'
|
|
20
19
|
export * as scrollListener from './ScrollListener/index.js'
|
|
20
|
+
export * as select from './Select/index.js'
|
|
21
21
|
export * as sequencer from './Sequencer/index.js'
|
|
22
22
|
export * as shadowRoot from './ShadowRoot/index.js'
|
|
23
23
|
export * as subtitles from './Subtitles/index.js'
|
package/components/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export * as beforeAfter from './BeforeAfter/index.js'
|
|
2
|
-
export * as clippable from './Clippable/index.js'
|
|
3
2
|
export * as button from './Button/index.js'
|
|
3
|
+
export * as clippable from './Clippable/index.js'
|
|
4
|
+
export * as disclaimer from './Disclaimer/index.js'
|
|
4
5
|
export * as drawer from './Drawer/index.js'
|
|
5
6
|
export * as eventListener from './EventListener/index.js'
|
|
6
|
-
export * as disclaimer from './Disclaimer/index.js'
|
|
7
7
|
export * as gallery from './Gallery/index.js'
|
|
8
8
|
export * as iframe from './Iframe/index.js'
|
|
9
9
|
export * as image from './Image/index.js'
|
|
@@ -16,8 +16,8 @@ export * as overlayer from './Overlayer/index.js'
|
|
|
16
16
|
export * as paginator from './Paginator/index.js'
|
|
17
17
|
export * as resizeObserver from './ResizeObserver/index.js'
|
|
18
18
|
export * as scrllgngn from './Scrllgngn/index.js'
|
|
19
|
-
export * as select from './Select/index.js'
|
|
20
19
|
export * as scrollListener from './ScrollListener/index.js'
|
|
20
|
+
export * as select from './Select/index.js'
|
|
21
21
|
export * as sequencer from './Sequencer/index.js'
|
|
22
22
|
export * as shadowRoot from './ShadowRoot/index.js'
|
|
23
23
|
export * as subtitles from './Subtitles/index.js'
|
|
@@ -1,44 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
/**
|
|
3
|
-
* Calls `onChange` whenever `value` changes, and only then — never on mount.
|
|
4
|
-
*
|
|
5
|
-
* This is how a component reports one of its state items to its consumer:
|
|
6
|
-
* a bare `useEffect` on the value would also fire on the first render, telling
|
|
7
|
-
* the consumer something changed when nothing did.
|
|
8
|
-
*
|
|
9
|
-
* @template T - Type of the watched value.
|
|
10
|
-
* @param value - The value to watch.
|
|
11
|
-
* @param onChange - Called with the new value, once the change has happened.
|
|
12
|
-
* @param isEqual - Decides whether the value actually changed. Defaults to
|
|
13
|
-
* `Object.is`, which covers booleans, numbers and strings. Pass a custom
|
|
14
|
-
* comparison for values rebuilt on every render, such as an array or an object
|
|
15
|
-
* derived from state, whose identity always differs.
|
|
16
|
-
*
|
|
17
|
-
* @remarks
|
|
18
|
-
* `onChange` is deliberately left out of the effect's dependencies. A consumer
|
|
19
|
-
* passing an inline arrow creates a new function on every render, which would
|
|
20
|
-
* re-run the effect each time and defeat the guard. The handler is read when the
|
|
21
|
-
* effect fires, so it is never a stale one.
|
|
22
|
-
*/
|
|
23
|
-
export declare function useChangeDispatch<T>(value: T, onChange?: (value: T) => void, isEqual?: (a: T, b: T) => boolean): void;
|
|
24
|
-
/**
|
|
25
|
-
* The three forms a `<source>` list is written in, reduced to the one a render uses.
|
|
26
|
-
*
|
|
27
|
-
* A bare string is a single source, an array of strings is several, an array of records
|
|
28
|
-
* is taken as given. **The array is read as homogeneous** — the first element decides for
|
|
29
|
-
* all of them —, which is what someone writing one by hand means anyway, and the only
|
|
30
|
-
* reading a static hyper-json array could support.
|
|
31
|
-
*
|
|
32
|
-
* `stringKey` is the whole reason this is shared rather than written twice. The parsing
|
|
33
|
-
* is identical for `Video` and `Image`, but **the shorthand does not name the same
|
|
34
|
-
* attribute**: a `<source>` inside a `<video>` carries `src`, one inside a `<picture>`
|
|
35
|
-
* carries `srcSet`. The two record shapes stay apart for the same reason — the picture
|
|
36
|
-
* source also takes `media` and `sizes`, which a video source has no use for, and neither
|
|
37
|
-
* element accepts the other's key. One element, one shape; only the reading is common.
|
|
38
|
-
*
|
|
39
|
-
* @template T - The record shape the caller's element accepts.
|
|
40
|
-
* @param given - What the consumer wrote.
|
|
41
|
-
* @param stringKey - Where a bare string goes.
|
|
42
|
-
* @returns The list as records, empty when there is nothing to render.
|
|
43
|
-
*/
|
|
44
|
-
export declare function parseSourceList<T extends Record<string, unknown>>(given: string | string[] | T[] | undefined, stringKey: keyof T & string): T[];
|
|
1
|
+
export * as viewportBehaviours from './viewport-behaviours/index.js'
|
|
@@ -1,83 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export function mergeClassNames(...names) {
|
|
3
|
-
return names.map(name => {
|
|
4
|
-
if (typeof name !== 'string' && !Array.isArray(name))
|
|
5
|
-
return false;
|
|
6
|
-
if (Array.isArray(name))
|
|
7
|
-
return mergeClassNames(...name);
|
|
8
|
-
if (name.trim() === '')
|
|
9
|
-
return false;
|
|
10
|
-
return name.trim();
|
|
11
|
-
}).filter((name) => typeof name === 'string')
|
|
12
|
-
.join(' ');
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Calls `onChange` whenever `value` changes, and only then — never on mount.
|
|
16
|
-
*
|
|
17
|
-
* This is how a component reports one of its state items to its consumer:
|
|
18
|
-
* a bare `useEffect` on the value would also fire on the first render, telling
|
|
19
|
-
* the consumer something changed when nothing did.
|
|
20
|
-
*
|
|
21
|
-
* @template T - Type of the watched value.
|
|
22
|
-
* @param value - The value to watch.
|
|
23
|
-
* @param onChange - Called with the new value, once the change has happened.
|
|
24
|
-
* @param isEqual - Decides whether the value actually changed. Defaults to
|
|
25
|
-
* `Object.is`, which covers booleans, numbers and strings. Pass a custom
|
|
26
|
-
* comparison for values rebuilt on every render, such as an array or an object
|
|
27
|
-
* derived from state, whose identity always differs.
|
|
28
|
-
*
|
|
29
|
-
* @remarks
|
|
30
|
-
* `onChange` is deliberately left out of the effect's dependencies. A consumer
|
|
31
|
-
* passing an inline arrow creates a new function on every render, which would
|
|
32
|
-
* re-run the effect each time and defeat the guard. The handler is read when the
|
|
33
|
-
* effect fires, so it is never a stale one.
|
|
34
|
-
*/
|
|
35
|
-
export function useChangeDispatch(value, onChange, isEqual = Object.is) {
|
|
36
|
-
const previousRef = useRef(value);
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
if (isEqual(previousRef.current, value))
|
|
39
|
-
return;
|
|
40
|
-
previousRef.current = value;
|
|
41
|
-
onChange?.(value);
|
|
42
|
-
}, [value]);
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* The three forms a `<source>` list is written in, reduced to the one a render uses.
|
|
46
|
-
*
|
|
47
|
-
* A bare string is a single source, an array of strings is several, an array of records
|
|
48
|
-
* is taken as given. **The array is read as homogeneous** — the first element decides for
|
|
49
|
-
* all of them —, which is what someone writing one by hand means anyway, and the only
|
|
50
|
-
* reading a static hyper-json array could support.
|
|
51
|
-
*
|
|
52
|
-
* `stringKey` is the whole reason this is shared rather than written twice. The parsing
|
|
53
|
-
* is identical for `Video` and `Image`, but **the shorthand does not name the same
|
|
54
|
-
* attribute**: a `<source>` inside a `<video>` carries `src`, one inside a `<picture>`
|
|
55
|
-
* carries `srcSet`. The two record shapes stay apart for the same reason — the picture
|
|
56
|
-
* source also takes `media` and `sizes`, which a video source has no use for, and neither
|
|
57
|
-
* element accepts the other's key. One element, one shape; only the reading is common.
|
|
58
|
-
*
|
|
59
|
-
* @template T - The record shape the caller's element accepts.
|
|
60
|
-
* @param given - What the consumer wrote.
|
|
61
|
-
* @param stringKey - Where a bare string goes.
|
|
62
|
-
* @returns The list as records, empty when there is nothing to render.
|
|
63
|
-
*/
|
|
64
|
-
export function parseSourceList(given, stringKey) {
|
|
65
|
-
const fromString = (value) => {
|
|
66
|
-
const single = { [stringKey]: value };
|
|
67
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- a one-key record is the narrowest `T` a bare string can describe; every other field is optional by contract
|
|
68
|
-
return single;
|
|
69
|
-
};
|
|
70
|
-
if (given === undefined)
|
|
71
|
-
return [];
|
|
72
|
-
if (typeof given === 'string')
|
|
73
|
-
return [fromString(given)];
|
|
74
|
-
if (!Array.isArray(given))
|
|
75
|
-
return [];
|
|
76
|
-
if (given.length === 0)
|
|
77
|
-
return [];
|
|
78
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- first element sampled just above; array is read as homogeneous
|
|
79
|
-
if (typeof given[0] === 'string')
|
|
80
|
-
return given.map(fromString);
|
|
81
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- first element was checked not to be a string just above; array is read as homogeneous
|
|
82
|
-
return given;
|
|
83
|
-
}
|
|
1
|
+
export * as viewportBehaviours from './viewport-behaviours/index.js'
|