@design-edito/tools 0.5.17 → 0.5.19
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/colors/index.d.ts +2 -2
- package/agnostic/colors/index.js +2 -2
- 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 +6 -6
- package/agnostic/html/hyper-json/smart-tags/isolated/index.js +6 -6
- package/agnostic/html/index.d.ts +2 -2
- package/agnostic/html/index.js +2 -2
- package/agnostic/misc/index.d.ts +3 -3
- package/agnostic/misc/index.js +3 -3
- package/agnostic/misc/logs/index.d.ts +1 -1
- package/agnostic/misc/logs/index.js +1 -1
- package/agnostic/numbers/index.d.ts +3 -3
- package/agnostic/numbers/index.js +3 -3
- package/agnostic/objects/index.d.ts +1 -1
- package/agnostic/objects/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 +3 -3
- package/agnostic/strings/index.js +3 -3
- package/agnostic/time/index.d.ts +2 -2
- package/agnostic/time/index.js +2 -2
- package/components/Image/index.d.ts +6 -0
- package/components/Image/index.js +3 -18
- package/components/Video/index.controlled.d.ts +5 -22
- package/components/Video/index.controlled.js +28 -42
- package/components/Video/index.d.ts +36 -37
- package/components/Video/index.js +83 -88
- package/components/Video/types.d.ts +42 -0
- package/components/Video/types.js +30 -0
- package/components/Video/utils.d.ts +38 -1
- package/components/Video/utils.js +23 -4
- package/components/index.d.ts +1 -1
- package/components/index.js +1 -1
- package/components/utils/index.d.ts +21 -0
- package/components/utils/index.js +40 -0
- package/components/utils/viewport-behaviours/index.d.ts +54 -0
- package/components/utils/viewport-behaviours/index.js +163 -0
- package/components/utils/viewport-behaviours/types.d.ts +109 -0
- package/components/utils/viewport-behaviours/types.js +14 -0
- package/components/utils/viewport-behaviours/utils.d.ts +47 -0
- package/components/utils/viewport-behaviours/utils.js +86 -0
- 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/package.json +16 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import type { ActionTable, ViewportBehaviours, VisibilityOptions } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Whether the element counts as visible — **as a state, not as a crossing**.
|
|
5
|
+
*
|
|
6
|
+
* That distinction is the whole point. An `IntersectionObserver` reports an event, and
|
|
7
|
+
* a behaviour hung on the event only ever runs when the screen is traversed. A
|
|
8
|
+
* component already visible whose behaviour was held back — behind a lm-link gate,
|
|
9
|
+
* typically — never gets a second chance, because nothing crosses when the gate lifts.
|
|
10
|
+
* Held as a state, the condition can be recombined with whatever else gates it.
|
|
11
|
+
*
|
|
12
|
+
* The delays are a debounce **on this state**: the element has to hold its new value
|
|
13
|
+
* for that long before the value is committed. A component crossed while scrolling fast
|
|
14
|
+
* therefore never counts as seen at all, rather than counting and being undone.
|
|
15
|
+
*
|
|
16
|
+
* @param targetRef - The element to watch.
|
|
17
|
+
* @param options - The observer's settings, plus the two delays.
|
|
18
|
+
* @param enabled - `false` mounts no observer. A hook can't be skipped; this is how
|
|
19
|
+
* a caller with nothing to watch for says so.
|
|
20
|
+
* @returns `true` or `false` once the first observation has settled, `undefined` before.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useVisibilityState(targetRef: RefObject<Element | null>, options?: VisibilityOptions, enabled?: boolean): boolean | undefined;
|
|
23
|
+
/** The shape `useViewportBehaviours` hands back. */
|
|
24
|
+
export type ViewportBehavioursResult = {
|
|
25
|
+
isVisible: boolean | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Tells the layer that the reader has taken over a domain, and that its instructions
|
|
28
|
+
* are to stop firing. Call it from the handlers that count as taking over — and only
|
|
29
|
+
* those: seeking on a timeline or going fullscreen is not deciding about playback.
|
|
30
|
+
*/
|
|
31
|
+
surrender: (domain: string) => void;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Runs a component's visibility instructions, and holds everything that decides whether
|
|
35
|
+
* they may run at all.
|
|
36
|
+
*
|
|
37
|
+
* @template A - The component's vocabulary.
|
|
38
|
+
* @param targetRef - The element to watch.
|
|
39
|
+
* @param props - The component's visibility props.
|
|
40
|
+
* @param table - What each of its verbs does. @see {@link ActionTable}
|
|
41
|
+
* @param suspended - Holds back the verbs that **start** something, and lets through
|
|
42
|
+
* those that stop something. This is a capability's doing — a lm-link gate — and it is
|
|
43
|
+
* not the same as a surrender: a gate speaks of consent not yet given, so `':force'`
|
|
44
|
+
* does not override it.
|
|
45
|
+
*
|
|
46
|
+
* @remarks
|
|
47
|
+
* **An instruction yields to the reader by default.** The failure modes are not
|
|
48
|
+
* symmetrical: not restarting on its own is a disappointment, restarting against a
|
|
49
|
+
* reader who has just pressed pause is an hostility they will meet again at every pass.
|
|
50
|
+
* `':force'` opts out, and is expected to be rare — which is why it is the written form
|
|
51
|
+
* and yielding is the silent one.
|
|
52
|
+
*/
|
|
53
|
+
export declare function useViewportBehaviours<A extends string>(targetRef: RefObject<Element | null>, props: ViewportBehaviours<A>, table: ActionTable<A>, suspended?: boolean): ViewportBehavioursResult;
|
|
54
|
+
export type { ActionSpec, ActionTable, Instruction, Modifier, ParsedInstruction, ViewportBehaviours, VisibilityOptions } from './types.js';
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useIntersectionObserver } from '../../IntersectionObserver/index.js';
|
|
3
|
+
import { useChangeDispatch } from '../index.js';
|
|
4
|
+
import { parseInstruction, toInstructionList } from './utils.js';
|
|
5
|
+
/**
|
|
6
|
+
* Whether the element counts as visible — **as a state, not as a crossing**.
|
|
7
|
+
*
|
|
8
|
+
* That distinction is the whole point. An `IntersectionObserver` reports an event, and
|
|
9
|
+
* a behaviour hung on the event only ever runs when the screen is traversed. A
|
|
10
|
+
* component already visible whose behaviour was held back — behind a lm-link gate,
|
|
11
|
+
* typically — never gets a second chance, because nothing crosses when the gate lifts.
|
|
12
|
+
* Held as a state, the condition can be recombined with whatever else gates it.
|
|
13
|
+
*
|
|
14
|
+
* The delays are a debounce **on this state**: the element has to hold its new value
|
|
15
|
+
* for that long before the value is committed. A component crossed while scrolling fast
|
|
16
|
+
* therefore never counts as seen at all, rather than counting and being undone.
|
|
17
|
+
*
|
|
18
|
+
* @param targetRef - The element to watch.
|
|
19
|
+
* @param options - The observer's settings, plus the two delays.
|
|
20
|
+
* @param enabled - `false` mounts no observer. A hook can't be skipped; this is how
|
|
21
|
+
* a caller with nothing to watch for says so.
|
|
22
|
+
* @returns `true` or `false` once the first observation has settled, `undefined` before.
|
|
23
|
+
*/
|
|
24
|
+
export function useVisibilityState(targetRef, options = {}, enabled = true) {
|
|
25
|
+
const { visibilityThreshold: threshold, visibilityRoot: root, visibilityRootMargin: rootMargin, visibilityOnAfterMs, visibilityOffAfterMs } = options;
|
|
26
|
+
const ioEntry = useIntersectionObserver(targetRef, { threshold, root, rootMargin }, undefined, enabled);
|
|
27
|
+
const reported = ioEntry?.isIntersecting;
|
|
28
|
+
const [settled, setSettled] = useState(undefined);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (reported === undefined)
|
|
31
|
+
return;
|
|
32
|
+
const delay = reported ? visibilityOnAfterMs : visibilityOffAfterMs;
|
|
33
|
+
if (delay === undefined || delay <= 0) {
|
|
34
|
+
setSettled(reported);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const timeout = window.setTimeout(() => setSettled(reported), delay);
|
|
38
|
+
// The cancellation *is* the debounce: a value that flips back before its timer
|
|
39
|
+
// fires is never committed, so the short crossing leaves no trace.
|
|
40
|
+
return () => window.clearTimeout(timeout);
|
|
41
|
+
}, [reported, visibilityOnAfterMs, visibilityOffAfterMs]);
|
|
42
|
+
return settled;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Runs a component's visibility instructions, and holds everything that decides whether
|
|
46
|
+
* they may run at all.
|
|
47
|
+
*
|
|
48
|
+
* @template A - The component's vocabulary.
|
|
49
|
+
* @param targetRef - The element to watch.
|
|
50
|
+
* @param props - The component's visibility props.
|
|
51
|
+
* @param table - What each of its verbs does. @see {@link ActionTable}
|
|
52
|
+
* @param suspended - Holds back the verbs that **start** something, and lets through
|
|
53
|
+
* those that stop something. This is a capability's doing — a lm-link gate — and it is
|
|
54
|
+
* not the same as a surrender: a gate speaks of consent not yet given, so `':force'`
|
|
55
|
+
* does not override it.
|
|
56
|
+
*
|
|
57
|
+
* @remarks
|
|
58
|
+
* **An instruction yields to the reader by default.** The failure modes are not
|
|
59
|
+
* symmetrical: not restarting on its own is a disappointment, restarting against a
|
|
60
|
+
* reader who has just pressed pause is an hostility they will meet again at every pass.
|
|
61
|
+
* `':force'` opts out, and is expected to be rare — which is why it is the written form
|
|
62
|
+
* and yielding is the silent one.
|
|
63
|
+
*/
|
|
64
|
+
export function useViewportBehaviours(targetRef, props, table, suspended = false) {
|
|
65
|
+
const { whenVisible, whenHidden, onVisibilityChanged } = props;
|
|
66
|
+
// The table closes over the component's state, so it is a new object on every render
|
|
67
|
+
// and has no business in a dependency list. Read through a ref, it is never stale.
|
|
68
|
+
//
|
|
69
|
+
// Held as a `Map` and not as the record itself: a verb parsed out of a string is a
|
|
70
|
+
// plain `string`, and looking one up in a `Record<A, …>` would mean asserting it into
|
|
71
|
+
// the vocabulary before knowing whether it belongs to it. A map answers `undefined`,
|
|
72
|
+
// which is the honest answer for an instruction nobody recognises.
|
|
73
|
+
const tableRef = useRef(new Map());
|
|
74
|
+
tableRef.current = new Map(Object.entries(table));
|
|
75
|
+
const surrenderedRef = useRef(new Set());
|
|
76
|
+
const spentRef = useRef(new Set());
|
|
77
|
+
const heldBackRef = useRef(new Set());
|
|
78
|
+
const lastRunRef = useRef({ suspended });
|
|
79
|
+
const surrender = useCallback((domain) => {
|
|
80
|
+
surrenderedRef.current.add(domain);
|
|
81
|
+
}, []);
|
|
82
|
+
const needsObserve = whenVisible !== undefined
|
|
83
|
+
|| whenHidden !== undefined
|
|
84
|
+
|| onVisibilityChanged !== undefined;
|
|
85
|
+
const isVisible = useVisibilityState(targetRef, props, needsObserve);
|
|
86
|
+
// Reported on the **settled** state, delays included: a consumer watching visibility
|
|
87
|
+
// and a consumer running instructions have to be told the same story.
|
|
88
|
+
useChangeDispatch(isVisible, next => {
|
|
89
|
+
if (next === undefined)
|
|
90
|
+
return;
|
|
91
|
+
onVisibilityChanged?.(next);
|
|
92
|
+
});
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (isVisible === undefined)
|
|
95
|
+
return;
|
|
96
|
+
const last = lastRunRef.current;
|
|
97
|
+
const crossed = last.isVisible !== isVisible;
|
|
98
|
+
const released = !crossed && last.suspended && !suspended;
|
|
99
|
+
lastRunRef.current = { isVisible, suspended };
|
|
100
|
+
// Neither the state nor the suspension moved: nothing to do. Without this, any
|
|
101
|
+
// unrelated re-render would replay the list.
|
|
102
|
+
if (!crossed && !released)
|
|
103
|
+
return;
|
|
104
|
+
const trigger = isVisible ? 'visible' : 'hidden';
|
|
105
|
+
const list = toInstructionList(isVisible ? whenVisible : whenHidden);
|
|
106
|
+
const runnable = dedupe(list);
|
|
107
|
+
if (released)
|
|
108
|
+
heldBackRef.current.clear();
|
|
109
|
+
for (const parsed of runnable) {
|
|
110
|
+
const key = `${trigger}|${parsed.verb}|${parsed.arg ?? ''}`;
|
|
111
|
+
// A suspension only reaches a verb that *starts* something, and it is the only
|
|
112
|
+
// one of the two brakes that can be lifted — hence the held-back list, replayed
|
|
113
|
+
// on release so a gate opening on a visible component does what it was asked.
|
|
114
|
+
const spec = tableRef.current.get(parsed.verb);
|
|
115
|
+
if (spec === undefined)
|
|
116
|
+
continue;
|
|
117
|
+
if (released && !heldBackRef.current.has(key))
|
|
118
|
+
continue;
|
|
119
|
+
if (suspended && spec.kind === 'start') {
|
|
120
|
+
heldBackRef.current.add(key);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!parsed.force && surrenderedRef.current.has(spec.domain))
|
|
124
|
+
continue;
|
|
125
|
+
if (parsed.once && spentRef.current.has(key))
|
|
126
|
+
continue;
|
|
127
|
+
if (parsed.once)
|
|
128
|
+
spentRef.current.add(key);
|
|
129
|
+
heldBackRef.current.delete(key);
|
|
130
|
+
spec.run(parsed.arg);
|
|
131
|
+
}
|
|
132
|
+
}, [isVisible, suspended, whenVisible, whenHidden]);
|
|
133
|
+
return { isVisible, surrender };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* One entry per verb, keeping the **widest** reading when the same verb is written more
|
|
137
|
+
* than once in a list.
|
|
138
|
+
*
|
|
139
|
+
* `['play', 'play:once']` runs on every crossing — the bare form wins, which is the
|
|
140
|
+
* long-standing rule that declaring both frequencies is declaring the recurring one.
|
|
141
|
+
* `['play', 'play:force']` is forced, by the same logic read the other way: a consumer
|
|
142
|
+
* who wrote `force` somewhere asked for it.
|
|
143
|
+
*/
|
|
144
|
+
function dedupe(list) {
|
|
145
|
+
const byVerb = new Map();
|
|
146
|
+
for (const instruction of list) {
|
|
147
|
+
const parsed = parseInstruction(instruction);
|
|
148
|
+
if (parsed.verb === '')
|
|
149
|
+
continue;
|
|
150
|
+
const key = `${parsed.verb}|${parsed.arg ?? ''}`;
|
|
151
|
+
const seen = byVerb.get(key);
|
|
152
|
+
if (seen === undefined) {
|
|
153
|
+
byVerb.set(key, parsed);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
byVerb.set(key, {
|
|
157
|
+
...parsed,
|
|
158
|
+
once: seen.once && parsed.once,
|
|
159
|
+
force: seen.force || parsed.force
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return [...byVerb.values()];
|
|
163
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stop conditions an instruction can carry, written as suffixes.
|
|
3
|
+
*
|
|
4
|
+
* - `once` — runs on the first crossing only. The credit is per (verb, trigger) pair
|
|
5
|
+
* and lives for the mount.
|
|
6
|
+
* - `force` — runs even after the reader has taken over the verb's domain. It
|
|
7
|
+
* overrides the **surrender**, never a suspension: a gate speaks of consent not yet
|
|
8
|
+
* given, a surrender of an intention already expressed, and an article able to force
|
|
9
|
+
* its way past a disclaimer would empty it of its purpose.
|
|
10
|
+
*
|
|
11
|
+
* **They are a set, not a sequence.** `'play:once:force'` and `'play:force:once'` are
|
|
12
|
+
* the same instruction, which is what spares the contract an ordering rule.
|
|
13
|
+
*/
|
|
14
|
+
export declare const MODIFIERS: readonly ["once", "force"];
|
|
15
|
+
export type Modifier = typeof MODIFIERS[number];
|
|
16
|
+
/**
|
|
17
|
+
* A verb, optionally suffixed by its stop conditions.
|
|
18
|
+
*
|
|
19
|
+
* A verb may carry its own argument — `'jump-to:500'` — in which case the argument is
|
|
20
|
+
* part of `A`, declared by the component that owns the vocabulary. The modifiers then
|
|
21
|
+
* suffix that whole form: `'jump-to:500:once'` needs nothing written here.
|
|
22
|
+
*
|
|
23
|
+
* **No verb may be named after a modifier.** The trailing segments are read as
|
|
24
|
+
* modifiers first, so a verb called `once` would parse as an instruction with no verb
|
|
25
|
+
* at all.
|
|
26
|
+
*
|
|
27
|
+
* @template A - The component's vocabulary.
|
|
28
|
+
*/
|
|
29
|
+
export type Instruction<A extends string> = A | `${A}:${Modifier}` | `${A}:${Modifier}:${Modifier}`;
|
|
30
|
+
/** An instruction taken apart. @see {@link Instruction} */
|
|
31
|
+
export type ParsedInstruction = {
|
|
32
|
+
verb: string;
|
|
33
|
+
/** What followed the verb, when the verb takes one. `'500'` in `'jump-to:500'`. */
|
|
34
|
+
arg?: string;
|
|
35
|
+
once: boolean;
|
|
36
|
+
force: boolean;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* What a component says about one of its verbs.
|
|
40
|
+
*
|
|
41
|
+
* @property kind - Whether the verb starts something or stops it. A suspension — a
|
|
42
|
+
* lm-link gate, typically — holds back `'start'` and lets `'stop'` through, since a
|
|
43
|
+
* verb that quietens a component can never work against a reader who hasn't consented
|
|
44
|
+
* yet.
|
|
45
|
+
* @property domain - Which of the component's controls this verb competes with.
|
|
46
|
+
* `play` and `pause` share `'playback'`, `loud` and `mute` share `'sound'` — so pausing
|
|
47
|
+
* by hand doesn't stop the sound from being cut on the way out. One flag for all of
|
|
48
|
+
* them would confound four questions, which is a mistake this library has made once.
|
|
49
|
+
* @property run - Does the thing. Receives the verb's argument, when it takes one.
|
|
50
|
+
*/
|
|
51
|
+
export type ActionSpec<D extends string = string> = {
|
|
52
|
+
kind: 'start' | 'stop';
|
|
53
|
+
domain: D;
|
|
54
|
+
run: (arg?: string) => void;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* A component's whole vocabulary.
|
|
58
|
+
*
|
|
59
|
+
* @template A - Its verbs.
|
|
60
|
+
* @template D - Its domains, when it has a type for them. Naming them is what makes a
|
|
61
|
+
* typo in `domain` — or in the matching `surrender` call — a compile error rather than
|
|
62
|
+
* a behaviour that silently never fires.
|
|
63
|
+
*
|
|
64
|
+
* @see {@link ActionSpec}
|
|
65
|
+
*/
|
|
66
|
+
export type ActionTable<A extends string, D extends string = string> = Record<A, ActionSpec<D>>;
|
|
67
|
+
/**
|
|
68
|
+
* Everything that decides **when** a component counts as visible.
|
|
69
|
+
*
|
|
70
|
+
* Flat and prefixed rather than grouped in a record: a record reads well in a type and
|
|
71
|
+
* badly at a call site, where it costs a pair of braces to set one value. The shared
|
|
72
|
+
* `visibility` prefix does the grouping that a record would have done, and it does it
|
|
73
|
+
* in the autocomplete list too.
|
|
74
|
+
*
|
|
75
|
+
* @property visibilityThreshold - How much of the element must be on screen. `0.3` asks
|
|
76
|
+
* for a third; omitted, a pixel is enough.
|
|
77
|
+
* @property visibilityRoot - The box visibility is measured against. Defaults to the
|
|
78
|
+
* viewport, which is the only one a static structure could name.
|
|
79
|
+
* @property visibilityRootMargin - Grows or shrinks that box before measuring, in
|
|
80
|
+
* `IntersectionObserver` syntax.
|
|
81
|
+
* @property visibilityOnAfterMs - How long it must stay visible to count as visible.
|
|
82
|
+
* This is a debounce on the state and not a delay on the action: a component crossed
|
|
83
|
+
* while scrolling fast never counts as seen, so nothing runs and no `once` credit is
|
|
84
|
+
* spent.
|
|
85
|
+
* @property visibilityOffAfterMs - The same, on the way out.
|
|
86
|
+
*/
|
|
87
|
+
export type VisibilityOptions = {
|
|
88
|
+
visibilityThreshold?: number | number[];
|
|
89
|
+
visibilityRoot?: HTMLElement;
|
|
90
|
+
visibilityRootMargin?: string;
|
|
91
|
+
visibilityOnAfterMs?: number;
|
|
92
|
+
visibilityOffAfterMs?: number;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* The visibility surface of a component: when it counts as seen, and what that does.
|
|
96
|
+
*
|
|
97
|
+
* @property whenVisible - Instructions run each time it becomes visible.
|
|
98
|
+
* @property whenHidden - The same, on the way out.
|
|
99
|
+
* @property onVisibilityChanged - Fired on the **settled** state, delays included: a
|
|
100
|
+
* consumer watching visibility and a consumer running instructions are told the same
|
|
101
|
+
* story.
|
|
102
|
+
*
|
|
103
|
+
* @template Action - The component's vocabulary.
|
|
104
|
+
*/
|
|
105
|
+
export type ViewportBehaviours<Action extends string> = VisibilityOptions & {
|
|
106
|
+
whenVisible?: Instruction<Action> | Array<Instruction<Action>>;
|
|
107
|
+
whenHidden?: Instruction<Action> | Array<Instruction<Action>>;
|
|
108
|
+
onVisibilityChanged?: (isVisible: boolean) => void;
|
|
109
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stop conditions an instruction can carry, written as suffixes.
|
|
3
|
+
*
|
|
4
|
+
* - `once` — runs on the first crossing only. The credit is per (verb, trigger) pair
|
|
5
|
+
* and lives for the mount.
|
|
6
|
+
* - `force` — runs even after the reader has taken over the verb's domain. It
|
|
7
|
+
* overrides the **surrender**, never a suspension: a gate speaks of consent not yet
|
|
8
|
+
* given, a surrender of an intention already expressed, and an article able to force
|
|
9
|
+
* its way past a disclaimer would empty it of its purpose.
|
|
10
|
+
*
|
|
11
|
+
* **They are a set, not a sequence.** `'play:once:force'` and `'play:force:once'` are
|
|
12
|
+
* the same instruction, which is what spares the contract an ordering rule.
|
|
13
|
+
*/
|
|
14
|
+
export const MODIFIERS = ['once', 'force'];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type Instruction, type ParsedInstruction } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Takes an instruction apart: its verb, its argument if it has one, its modifiers.
|
|
4
|
+
*
|
|
5
|
+
* **Trailing segments are read as modifiers until one isn't**, and everything left is
|
|
6
|
+
* the verb — whatever its own internal shape. That single rule is what lets a verb
|
|
7
|
+
* carry an argument without the grammar having two levels: `'jump-to:500:once'` gives
|
|
8
|
+
* back `jump-to`, `'500'` and `once`, and the parser never needs to know that `jump-to`
|
|
9
|
+
* is the kind of verb that takes a number.
|
|
10
|
+
*
|
|
11
|
+
* @param instruction - What the consumer wrote.
|
|
12
|
+
* @returns Its parts. `verb` is empty when the instruction was nothing but modifiers.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseInstruction(instruction: string): ParsedInstruction;
|
|
15
|
+
/**
|
|
16
|
+
* The one-or-many shape of a list prop, reduced to the many.
|
|
17
|
+
*
|
|
18
|
+
* A single instruction is written bare — `whenVisible='play'` — because that is what an
|
|
19
|
+
* article writes nine times out of ten, and a one-item array in hyper-json is four
|
|
20
|
+
* lines to say one word.
|
|
21
|
+
*
|
|
22
|
+
* @template A - The component's vocabulary.
|
|
23
|
+
* @param given - What the consumer wrote.
|
|
24
|
+
* @returns The instructions in the order they were given, which is the order they run.
|
|
25
|
+
*/
|
|
26
|
+
export declare function toInstructionList<A extends string>(given: Instruction<A> | Array<Instruction<A>> | undefined): Array<Instruction<A>>;
|
|
27
|
+
/**
|
|
28
|
+
* Whether a string is an instruction this vocabulary answers to.
|
|
29
|
+
*
|
|
30
|
+
* **Exported for the consumers that receive their props as text.** lm-link reads an
|
|
31
|
+
* article's XML, where nothing is typed, and has to reject `'jump-to:banana'` before it
|
|
32
|
+
* reaches a component. Written on its side, the rule would exist twice and drift in
|
|
33
|
+
* silence — a verb added here would be rejected there for no visible reason.
|
|
34
|
+
*
|
|
35
|
+
* Returns a plain boolean rather than a type predicate, and that is not an oversight: a
|
|
36
|
+
* verb taking an argument is written `'jump-to:500'`, which is not a member of the verb
|
|
37
|
+
* list this checks against. Narrowing to `Instruction<A>` would therefore claim more than
|
|
38
|
+
* the list can support, and the caller that needs the narrow type is better off asserting
|
|
39
|
+
* it once, where it can say why.
|
|
40
|
+
*
|
|
41
|
+
* @param value - What the consumer wrote.
|
|
42
|
+
* @param verbs - The vocabulary, bare — `VIDEO_VERBS` and its like.
|
|
43
|
+
* @param withArgument - Those of them that take one, and what a valid one looks like. A
|
|
44
|
+
* verb given an argument it does not take, or denied one it needs, fails either way:
|
|
45
|
+
* both are a consumer meaning something the component cannot do.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isInstruction(value: unknown, verbs: readonly string[], withArgument?: Readonly<Partial<Record<string, (arg: string) => boolean>>>): boolean;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { MODIFIERS } from './types.js';
|
|
2
|
+
const isModifier = (segment) => MODIFIERS.includes(segment);
|
|
3
|
+
/**
|
|
4
|
+
* Takes an instruction apart: its verb, its argument if it has one, its modifiers.
|
|
5
|
+
*
|
|
6
|
+
* **Trailing segments are read as modifiers until one isn't**, and everything left is
|
|
7
|
+
* the verb — whatever its own internal shape. That single rule is what lets a verb
|
|
8
|
+
* carry an argument without the grammar having two levels: `'jump-to:500:once'` gives
|
|
9
|
+
* back `jump-to`, `'500'` and `once`, and the parser never needs to know that `jump-to`
|
|
10
|
+
* is the kind of verb that takes a number.
|
|
11
|
+
*
|
|
12
|
+
* @param instruction - What the consumer wrote.
|
|
13
|
+
* @returns Its parts. `verb` is empty when the instruction was nothing but modifiers.
|
|
14
|
+
*/
|
|
15
|
+
export function parseInstruction(instruction) {
|
|
16
|
+
const segments = instruction.trim().split(':');
|
|
17
|
+
const modifiers = new Set();
|
|
18
|
+
// Walked from the end: the modifiers are a set, so their order carries nothing and
|
|
19
|
+
// only the boundary with the verb matters.
|
|
20
|
+
while (segments.length > 0) {
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- length checked by the loop
|
|
22
|
+
const last = segments[segments.length - 1];
|
|
23
|
+
if (!isModifier(last))
|
|
24
|
+
break;
|
|
25
|
+
modifiers.add(last);
|
|
26
|
+
segments.pop();
|
|
27
|
+
}
|
|
28
|
+
const [verb, ...rest] = segments;
|
|
29
|
+
return {
|
|
30
|
+
verb: verb ?? '',
|
|
31
|
+
// Rejoined rather than taken as one segment: an argument holding a colon of its
|
|
32
|
+
// own stays whole, and nothing in the grammar has to forbid it.
|
|
33
|
+
...(rest.length > 0 ? { arg: rest.join(':') } : {}),
|
|
34
|
+
once: modifiers.has('once'),
|
|
35
|
+
force: modifiers.has('force')
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The one-or-many shape of a list prop, reduced to the many.
|
|
40
|
+
*
|
|
41
|
+
* A single instruction is written bare — `whenVisible='play'` — because that is what an
|
|
42
|
+
* article writes nine times out of ten, and a one-item array in hyper-json is four
|
|
43
|
+
* lines to say one word.
|
|
44
|
+
*
|
|
45
|
+
* @template A - The component's vocabulary.
|
|
46
|
+
* @param given - What the consumer wrote.
|
|
47
|
+
* @returns The instructions in the order they were given, which is the order they run.
|
|
48
|
+
*/
|
|
49
|
+
export function toInstructionList(given) {
|
|
50
|
+
if (given === undefined)
|
|
51
|
+
return [];
|
|
52
|
+
if (Array.isArray(given))
|
|
53
|
+
return given;
|
|
54
|
+
return [given];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Whether a string is an instruction this vocabulary answers to.
|
|
58
|
+
*
|
|
59
|
+
* **Exported for the consumers that receive their props as text.** lm-link reads an
|
|
60
|
+
* article's XML, where nothing is typed, and has to reject `'jump-to:banana'` before it
|
|
61
|
+
* reaches a component. Written on its side, the rule would exist twice and drift in
|
|
62
|
+
* silence — a verb added here would be rejected there for no visible reason.
|
|
63
|
+
*
|
|
64
|
+
* Returns a plain boolean rather than a type predicate, and that is not an oversight: a
|
|
65
|
+
* verb taking an argument is written `'jump-to:500'`, which is not a member of the verb
|
|
66
|
+
* list this checks against. Narrowing to `Instruction<A>` would therefore claim more than
|
|
67
|
+
* the list can support, and the caller that needs the narrow type is better off asserting
|
|
68
|
+
* it once, where it can say why.
|
|
69
|
+
*
|
|
70
|
+
* @param value - What the consumer wrote.
|
|
71
|
+
* @param verbs - The vocabulary, bare — `VIDEO_VERBS` and its like.
|
|
72
|
+
* @param withArgument - Those of them that take one, and what a valid one looks like. A
|
|
73
|
+
* verb given an argument it does not take, or denied one it needs, fails either way:
|
|
74
|
+
* both are a consumer meaning something the component cannot do.
|
|
75
|
+
*/
|
|
76
|
+
export function isInstruction(value, verbs, withArgument = {}) {
|
|
77
|
+
if (typeof value !== 'string')
|
|
78
|
+
return false;
|
|
79
|
+
const { verb, arg } = parseInstruction(value);
|
|
80
|
+
if (!verbs.includes(verb))
|
|
81
|
+
return false;
|
|
82
|
+
const validate = withArgument[verb];
|
|
83
|
+
if (validate === undefined)
|
|
84
|
+
return arg === undefined;
|
|
85
|
+
return arg !== undefined && validate(arg);
|
|
86
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * as copy from './copy/index.js'
|
|
2
|
-
export * as download from './download/index.js'
|
|
3
2
|
export * as exists from './exists/index.js'
|
|
3
|
+
export * as download from './download/index.js'
|
|
4
4
|
export * as move from './move/index.js'
|
|
5
5
|
export * as remove from './remove/index.js'
|
|
6
6
|
export * as stat from './stat/index.js'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * as copy from './copy/index.js'
|
|
2
|
-
export * as download from './download/index.js'
|
|
3
2
|
export * as exists from './exists/index.js'
|
|
3
|
+
export * as download from './download/index.js'
|
|
4
4
|
export * as move from './move/index.js'
|
|
5
5
|
export * as remove from './remove/index.js'
|
|
6
6
|
export * as stat from './stat/index.js'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * as copyDir from './copy-dir/index.js'
|
|
2
|
-
export * as copyFile from './copy-file/index.js'
|
|
3
2
|
export * as downloadFile from './download-file/index.js'
|
|
3
|
+
export * as copyFile from './copy-file/index.js'
|
|
4
4
|
export * as existsFile from './exists-file/index.js'
|
|
5
5
|
export * as listDir from './list-dir/index.js'
|
|
6
6
|
export * as moveDir from './move-dir/index.js'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * as copyDir from './copy-dir/index.js'
|
|
2
|
-
export * as copyFile from './copy-file/index.js'
|
|
3
2
|
export * as downloadFile from './download-file/index.js'
|
|
3
|
+
export * as copyFile from './copy-file/index.js'
|
|
4
4
|
export * as existsFile from './exists-file/index.js'
|
|
5
5
|
export * as listDir from './list-dir/index.js'
|
|
6
6
|
export * as moveDir from './move-dir/index.js'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@design-edito/tools",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.19",
|
|
4
4
|
"description": "",
|
|
5
5
|
"author": "Maxime Fabas",
|
|
6
6
|
"license": "ISC",
|
|
@@ -1597,6 +1597,17 @@
|
|
|
1597
1597
|
"import": "./components/utils/types.js",
|
|
1598
1598
|
"types": "./components/utils/types.d.ts"
|
|
1599
1599
|
},
|
|
1600
|
+
"./components/utils/viewport-behaviours": {
|
|
1601
|
+
"import": "./components/utils/viewport-behaviours/index.js",
|
|
1602
|
+
"types": "./components/utils/viewport-behaviours/index.d.ts"
|
|
1603
|
+
},
|
|
1604
|
+
"./components/utils/viewport-behaviours/index.js": {
|
|
1605
|
+
"import": "./components/utils/viewport-behaviours/index.js"
|
|
1606
|
+
},
|
|
1607
|
+
"./components/utils/viewport-behaviours/types.js": {
|
|
1608
|
+
"import": "./components/utils/viewport-behaviours/types.js",
|
|
1609
|
+
"types": "./components/utils/viewport-behaviours/types.d.ts"
|
|
1610
|
+
},
|
|
1600
1611
|
"./components/Video": {
|
|
1601
1612
|
"import": "./components/Video/index.js",
|
|
1602
1613
|
"types": "./components/Video/index.d.ts"
|
|
@@ -1604,6 +1615,10 @@
|
|
|
1604
1615
|
"./components/Video/index.js": {
|
|
1605
1616
|
"import": "./components/Video/index.js"
|
|
1606
1617
|
},
|
|
1618
|
+
"./components/Video/types.js": {
|
|
1619
|
+
"import": "./components/Video/types.js",
|
|
1620
|
+
"types": "./components/Video/types.d.ts"
|
|
1621
|
+
},
|
|
1607
1622
|
"./index.js": {
|
|
1608
1623
|
"import": "./index.js"
|
|
1609
1624
|
},
|