@design-edito/tools 0.5.38 → 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 +2 -1
- package/agnostic/arrays/index.js +2 -1
- 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 +1 -1
- package/agnostic/colors/index.js +1 -1
- package/agnostic/css/index.d.ts +1 -0
- package/agnostic/css/index.js +1 -0
- 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 +9 -9
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +9 -9
- package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +1 -1
- package/agnostic/html/hyper-json/smart-tags/isolated/index.js +1 -1
- package/agnostic/html/index.d.ts +2 -2
- package/agnostic/html/index.js +2 -2
- package/agnostic/index.d.ts +1 -0
- package/agnostic/index.js +1 -0
- package/agnostic/misc/index.d.ts +1 -1
- package/agnostic/misc/index.js +1 -1
- package/agnostic/numbers/index.d.ts +1 -1
- package/agnostic/numbers/index.js +1 -1
- package/agnostic/objects/index.d.ts +1 -1
- package/agnostic/objects/index.js +1 -1
- package/agnostic/time/index.d.ts +1 -1
- package/agnostic/time/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.js +5 -4
- 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 +2 -2
- package/components/index.js +2 -2
- 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/@aws-s3/storage/directory/index.d.ts +1 -1
- package/node/@aws-s3/storage/directory/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/cloud-storage/operations/index.d.ts +1 -1
- package/node/cloud-storage/operations/index.js +1 -1
- package/node/encryption/@aes-256-gcm/index.d.ts +1 -1
- package/node/encryption/@aes-256-gcm/index.js +1 -1
- 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
|
@@ -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,6 +1,6 @@
|
|
|
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
4
|
export * as disclaimer from './Disclaimer/index.js'
|
|
5
5
|
export * as drawer from './Drawer/index.js'
|
|
6
6
|
export * as eventListener from './EventListener/index.js'
|
|
@@ -11,8 +11,8 @@ export * as input from './Input/index.js'
|
|
|
11
11
|
export * as intersectionObserver from './IntersectionObserver/index.js'
|
|
12
12
|
export * as jsonEditor from './JsonEditor/index.js'
|
|
13
13
|
export * as lightbox from './Lightbox/index.js'
|
|
14
|
-
export * as overlayer from './Overlayer/index.js'
|
|
15
14
|
export * as listLoader from './ListLoader/index.js'
|
|
15
|
+
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'
|
package/components/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
4
|
export * as disclaimer from './Disclaimer/index.js'
|
|
5
5
|
export * as drawer from './Drawer/index.js'
|
|
6
6
|
export * as eventListener from './EventListener/index.js'
|
|
@@ -11,8 +11,8 @@ export * as input from './Input/index.js'
|
|
|
11
11
|
export * as intersectionObserver from './IntersectionObserver/index.js'
|
|
12
12
|
export * as jsonEditor from './JsonEditor/index.js'
|
|
13
13
|
export * as lightbox from './Lightbox/index.js'
|
|
14
|
-
export * as overlayer from './Overlayer/index.js'
|
|
15
14
|
export * as listLoader from './ListLoader/index.js'
|
|
15
|
+
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'
|
|
@@ -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'
|
|
@@ -7,10 +7,10 @@ import type { ActionTable, ViewportBehaviours, VisibilityOptions } from './types
|
|
|
7
7
|
* belongs by subject, because the publish step only maps `index.js` and `types.js` into
|
|
8
8
|
* the package's exports — a `utils.js` is unreachable from outside, whatever it holds.
|
|
9
9
|
*
|
|
10
|
-
* **Exported for the
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* **Exported for the callers that build an instruction at runtime**, out of a string
|
|
11
|
+
* nothing has typed, and have to reject `'jump-to:banana'` before it reaches a
|
|
12
|
+
* component. Written on their side, the rule would exist twice and drift in silence —
|
|
13
|
+
* a verb added here would be rejected there for no visible reason.
|
|
14
14
|
*
|
|
15
15
|
* Returns a plain boolean rather than a type predicate, and that is not an oversight: a
|
|
16
16
|
* verb taking an argument is written `'jump-to:500'`, which is not a member of the verb
|
|
@@ -28,11 +28,9 @@ export declare function isInstruction(value: unknown, verbs: readonly string[],
|
|
|
28
28
|
/**
|
|
29
29
|
* Whether the element counts as visible — **as a state, not as a crossing**.
|
|
30
30
|
*
|
|
31
|
-
* That distinction is the
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* typically — never gets a second chance, because nothing crosses when the gate lifts.
|
|
35
|
-
* Held as a state, the condition can be recombined with whatever else gates it.
|
|
31
|
+
* That distinction is what the delays below are built on, and what lets a caller
|
|
32
|
+
* recombine visibility with whatever else it tracks: an `IntersectionObserver` reports
|
|
33
|
+
* an event, which is readable once, where a state is readable at any moment.
|
|
36
34
|
*
|
|
37
35
|
* The delays are a debounce **on this state**: the element has to hold its new value
|
|
38
36
|
* for that long before the value is committed. A component crossed while scrolling fast
|
|
@@ -63,10 +61,6 @@ export type ViewportBehavioursResult = {
|
|
|
63
61
|
* @param targetRef - The element to watch.
|
|
64
62
|
* @param props - The component's visibility props.
|
|
65
63
|
* @param table - What each of its verbs does. @see {@link ActionTable}
|
|
66
|
-
* @param suspended - Holds back the verbs that **start** something, and lets through
|
|
67
|
-
* those that stop something. This is a capability's doing — a lm-link gate — and it is
|
|
68
|
-
* not the same as a surrender: a gate speaks of consent not yet given, so `':force'`
|
|
69
|
-
* does not override it.
|
|
70
64
|
*
|
|
71
65
|
* @remarks
|
|
72
66
|
* **An instruction yields to the reader by default.** The failure modes are not
|
|
@@ -75,4 +69,4 @@ export type ViewportBehavioursResult = {
|
|
|
75
69
|
* `':force'` opts out, and is expected to be rare — which is why it is the written form
|
|
76
70
|
* and yielding is the silent one.
|
|
77
71
|
*/
|
|
78
|
-
export declare function useViewportBehaviours<A extends string>(targetRef: RefObject<Element | null>, props: ViewportBehaviours<A>, table: ActionTable<A
|
|
72
|
+
export declare function useViewportBehaviours<A extends string>(targetRef: RefObject<Element | null>, props: ViewportBehaviours<A>, table: ActionTable<A>): ViewportBehavioursResult;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { useIntersectionObserver } from '../../IntersectionObserver/index.js';
|
|
3
|
-
import {
|
|
3
|
+
import { useStateChangeDispatch } from '../../../agnostic/@react/effects/use-state-change-dispatch/index.js';
|
|
4
4
|
import { parseInstruction, toInstructionList } from './utils.js';
|
|
5
5
|
/**
|
|
6
6
|
* Whether a string is an instruction this vocabulary answers to.
|
|
@@ -9,10 +9,10 @@ import { parseInstruction, toInstructionList } from './utils.js';
|
|
|
9
9
|
* belongs by subject, because the publish step only maps `index.js` and `types.js` into
|
|
10
10
|
* the package's exports — a `utils.js` is unreachable from outside, whatever it holds.
|
|
11
11
|
*
|
|
12
|
-
* **Exported for the
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* **Exported for the callers that build an instruction at runtime**, out of a string
|
|
13
|
+
* nothing has typed, and have to reject `'jump-to:banana'` before it reaches a
|
|
14
|
+
* component. Written on their side, the rule would exist twice and drift in silence —
|
|
15
|
+
* a verb added here would be rejected there for no visible reason.
|
|
16
16
|
*
|
|
17
17
|
* Returns a plain boolean rather than a type predicate, and that is not an oversight: a
|
|
18
18
|
* verb taking an argument is written `'jump-to:500'`, which is not a member of the verb
|
|
@@ -40,11 +40,9 @@ export function isInstruction(value, verbs, withArgument = {}) {
|
|
|
40
40
|
/**
|
|
41
41
|
* Whether the element counts as visible — **as a state, not as a crossing**.
|
|
42
42
|
*
|
|
43
|
-
* That distinction is the
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* typically — never gets a second chance, because nothing crosses when the gate lifts.
|
|
47
|
-
* Held as a state, the condition can be recombined with whatever else gates it.
|
|
43
|
+
* That distinction is what the delays below are built on, and what lets a caller
|
|
44
|
+
* recombine visibility with whatever else it tracks: an `IntersectionObserver` reports
|
|
45
|
+
* an event, which is readable once, where a state is readable at any moment.
|
|
48
46
|
*
|
|
49
47
|
* The delays are a debounce **on this state**: the element has to hold its new value
|
|
50
48
|
* for that long before the value is committed. A component crossed while scrolling fast
|
|
@@ -84,10 +82,6 @@ export function useVisibilityState(targetRef, options = {}, enabled = true) {
|
|
|
84
82
|
* @param targetRef - The element to watch.
|
|
85
83
|
* @param props - The component's visibility props.
|
|
86
84
|
* @param table - What each of its verbs does. @see {@link ActionTable}
|
|
87
|
-
* @param suspended - Holds back the verbs that **start** something, and lets through
|
|
88
|
-
* those that stop something. This is a capability's doing — a lm-link gate — and it is
|
|
89
|
-
* not the same as a surrender: a gate speaks of consent not yet given, so `':force'`
|
|
90
|
-
* does not override it.
|
|
91
85
|
*
|
|
92
86
|
* @remarks
|
|
93
87
|
* **An instruction yields to the reader by default.** The failure modes are not
|
|
@@ -96,7 +90,7 @@ export function useVisibilityState(targetRef, options = {}, enabled = true) {
|
|
|
96
90
|
* `':force'` opts out, and is expected to be rare — which is why it is the written form
|
|
97
91
|
* and yielding is the silent one.
|
|
98
92
|
*/
|
|
99
|
-
export function useViewportBehaviours(targetRef, props, table
|
|
93
|
+
export function useViewportBehaviours(targetRef, props, table) {
|
|
100
94
|
const { whenVisible, whenHidden, onVisibilityChanged } = props;
|
|
101
95
|
// The table closes over the component's state, so it is a new object on every render
|
|
102
96
|
// and has no business in a dependency list. Read through a ref, it is never stale.
|
|
@@ -109,8 +103,7 @@ export function useViewportBehaviours(targetRef, props, table, suspended = false
|
|
|
109
103
|
tableRef.current = new Map(Object.entries(table));
|
|
110
104
|
const surrenderedRef = useRef(new Set());
|
|
111
105
|
const spentRef = useRef(new Set());
|
|
112
|
-
const
|
|
113
|
-
const lastRunRef = useRef({ suspended });
|
|
106
|
+
const lastRunRef = useRef({});
|
|
114
107
|
const surrender = useCallback((domain) => {
|
|
115
108
|
surrenderedRef.current.add(domain);
|
|
116
109
|
}, []);
|
|
@@ -120,7 +113,7 @@ export function useViewportBehaviours(targetRef, props, table, suspended = false
|
|
|
120
113
|
const isVisible = useVisibilityState(targetRef, props, needsObserve);
|
|
121
114
|
// Reported on the **settled** state, delays included: a consumer watching visibility
|
|
122
115
|
// and a consumer running instructions have to be told the same story.
|
|
123
|
-
|
|
116
|
+
useStateChangeDispatch(isVisible, next => {
|
|
124
117
|
if (next === undefined)
|
|
125
118
|
return;
|
|
126
119
|
onVisibilityChanged?.(next);
|
|
@@ -129,42 +122,27 @@ export function useViewportBehaviours(targetRef, props, table, suspended = false
|
|
|
129
122
|
if (isVisible === undefined)
|
|
130
123
|
return;
|
|
131
124
|
const last = lastRunRef.current;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
lastRunRef.current = { isVisible, suspended };
|
|
135
|
-
// Neither the state nor the suspension moved: nothing to do. Without this, any
|
|
136
|
-
// unrelated re-render would replay the list.
|
|
137
|
-
if (!crossed && !released)
|
|
125
|
+
// Without this, any unrelated re-render would replay the list.
|
|
126
|
+
if (last.isVisible === isVisible)
|
|
138
127
|
return;
|
|
128
|
+
lastRunRef.current = { isVisible };
|
|
139
129
|
const trigger = isVisible ? 'visible' : 'hidden';
|
|
140
130
|
const list = toInstructionList(isVisible ? whenVisible : whenHidden);
|
|
141
131
|
const runnable = dedupe(list);
|
|
142
|
-
if (released)
|
|
143
|
-
heldBackRef.current.clear();
|
|
144
132
|
for (const parsed of runnable) {
|
|
145
133
|
const key = `${trigger}|${parsed.verb}|${parsed.arg ?? ''}`;
|
|
146
|
-
// A suspension only reaches a verb that *starts* something, and it is the only
|
|
147
|
-
// one of the two brakes that can be lifted — hence the held-back list, replayed
|
|
148
|
-
// on release so a gate opening on a visible component does what it was asked.
|
|
149
134
|
const spec = tableRef.current.get(parsed.verb);
|
|
150
135
|
if (spec === undefined)
|
|
151
136
|
continue;
|
|
152
|
-
if (released && !heldBackRef.current.has(key))
|
|
153
|
-
continue;
|
|
154
|
-
if (suspended && spec.kind === 'start') {
|
|
155
|
-
heldBackRef.current.add(key);
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
137
|
if (!parsed.force && surrenderedRef.current.has(spec.domain))
|
|
159
138
|
continue;
|
|
160
139
|
if (parsed.once && spentRef.current.has(key))
|
|
161
140
|
continue;
|
|
162
141
|
if (parsed.once)
|
|
163
142
|
spentRef.current.add(key);
|
|
164
|
-
heldBackRef.current.delete(key);
|
|
165
143
|
spec.run(parsed.arg);
|
|
166
144
|
}
|
|
167
|
-
}, [isVisible,
|
|
145
|
+
}, [isVisible, whenVisible, whenHidden]);
|
|
168
146
|
return { isVisible, surrender };
|
|
169
147
|
}
|
|
170
148
|
/**
|