@design-edito/tools 0.5.17 → 0.5.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/agnostic/colors/index.d.ts +2 -2
  2. package/agnostic/colors/index.js +2 -2
  3. package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +13 -13
  4. package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +13 -13
  5. package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +3 -3
  6. package/agnostic/html/hyper-json/smart-tags/isolated/index.js +3 -3
  7. package/agnostic/html/index.d.ts +2 -2
  8. package/agnostic/html/index.js +2 -2
  9. package/agnostic/index.d.ts +4 -4
  10. package/agnostic/index.js +4 -4
  11. package/agnostic/misc/index.d.ts +4 -4
  12. package/agnostic/misc/index.js +4 -4
  13. package/agnostic/numbers/index.d.ts +2 -2
  14. package/agnostic/numbers/index.js +2 -2
  15. package/agnostic/objects/index.d.ts +2 -2
  16. package/agnostic/objects/index.js +2 -2
  17. package/agnostic/optim/index.d.ts +1 -1
  18. package/agnostic/optim/index.js +1 -1
  19. package/agnostic/sanitization/index.d.ts +1 -1
  20. package/agnostic/sanitization/index.js +1 -1
  21. package/agnostic/strings/index.d.ts +2 -2
  22. package/agnostic/strings/index.js +2 -2
  23. package/agnostic/subtitles/index.d.ts +1 -1
  24. package/agnostic/subtitles/index.js +1 -1
  25. package/agnostic/time/index.d.ts +3 -3
  26. package/agnostic/time/index.js +3 -3
  27. package/components/Image/index.d.ts +6 -0
  28. package/components/Image/index.js +3 -18
  29. package/components/Video/index.controlled.d.ts +5 -22
  30. package/components/Video/index.controlled.js +28 -42
  31. package/components/Video/index.d.ts +28 -37
  32. package/components/Video/index.js +83 -88
  33. package/components/Video/types.d.ts +26 -0
  34. package/components/Video/types.js +1 -0
  35. package/components/Video/utils.d.ts +38 -1
  36. package/components/Video/utils.js +23 -4
  37. package/components/index.d.ts +5 -5
  38. package/components/index.js +5 -5
  39. package/components/utils/index.d.ts +21 -0
  40. package/components/utils/index.js +40 -0
  41. package/components/utils/viewport-behaviours/index.d.ts +54 -0
  42. package/components/utils/viewport-behaviours/index.js +163 -0
  43. package/components/utils/viewport-behaviours/types.d.ts +109 -0
  44. package/components/utils/viewport-behaviours/types.js +14 -0
  45. package/components/utils/viewport-behaviours/utils.d.ts +26 -0
  46. package/components/utils/viewport-behaviours/utils.js +55 -0
  47. package/node/@aws-s3/storage/file/index.d.ts +1 -1
  48. package/node/@aws-s3/storage/file/index.js +1 -1
  49. package/node/@google-cloud/storage/file/index.d.ts +1 -1
  50. package/node/@google-cloud/storage/file/index.js +1 -1
  51. package/node/images/index.d.ts +1 -1
  52. package/node/images/index.js +1 -1
  53. package/node/images/transform/operations/index.d.ts +2 -2
  54. package/node/images/transform/operations/index.js +2 -2
  55. package/package.json +16 -1
@@ -1,11 +1,11 @@
1
1
  export * as beforeAfter from './BeforeAfter/index.js'
2
2
  export * as button from './Button/index.js'
3
- export * as clippable from './Clippable/index.js'
4
3
  export * as disclaimer from './Disclaimer/index.js'
5
- export * as drawer from './Drawer/index.js'
6
- export * as gallery from './Gallery/index.js'
4
+ export * as clippable from './Clippable/index.js'
7
5
  export * as eventListener from './EventListener/index.js'
6
+ export * as drawer from './Drawer/index.js'
8
7
  export * as iframe from './Iframe/index.js'
8
+ export * as gallery from './Gallery/index.js'
9
9
  export * as image from './Image/index.js'
10
10
  export * as input from './Input/index.js'
11
11
  export * as intersectionObserver from './IntersectionObserver/index.js'
@@ -13,15 +13,15 @@ export * as jsonEditor from './JsonEditor/index.js'
13
13
  export * as lightbox from './Lightbox/index.js'
14
14
  export * as listLoader from './ListLoader/index.js'
15
15
  export * as overlayer from './Overlayer/index.js'
16
- export * as paginator from './Paginator/index.js'
17
16
  export * as resizeObserver from './ResizeObserver/index.js'
17
+ export * as paginator from './Paginator/index.js'
18
18
  export * as scrllgngn from './Scrllgngn/index.js'
19
19
  export * as scrollListener from './ScrollListener/index.js'
20
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
- export * as subtitles from './Subtitles/index.js'
24
23
  export * as textarea from './Textarea/index.js'
24
+ export * as subtitles from './Subtitles/index.js'
25
25
  export * as uiModule from './UIModule/index.js'
26
26
  export * as video from './Video/index.js'
27
27
  export * as wipAudioQuote from './_WIP_AudioQuote/index.js'
@@ -21,3 +21,24 @@ export declare function mergeClassNames(...names: Array<string | null | undefine
21
21
  * effect fires, so it is never a stale one.
22
22
  */
23
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[];
@@ -41,3 +41,43 @@ export function useChangeDispatch(value, onChange, isEqual = Object.is) {
41
41
  onChange?.(value);
42
42
  }, [value]);
43
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
+ }
@@ -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,26 @@
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>>;
@@ -0,0 +1,55 @@
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
+ }
@@ -1,7 +1,7 @@
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'
4
3
  export * as move from './move/index.js'
5
4
  export * as remove from './remove/index.js'
6
5
  export * as stat from './stat/index.js'
7
6
  export * as upload from './upload/index.js'
7
+ export * as download from './download/index.js'
@@ -1,7 +1,7 @@
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'
4
3
  export * as move from './move/index.js'
5
4
  export * as remove from './remove/index.js'
6
5
  export * as stat from './stat/index.js'
7
6
  export * as upload from './upload/index.js'
7
+ export * as download from './download/index.js'
@@ -1,8 +1,8 @@
1
1
  export * as copy from './copy/index.js'
2
2
  export * as download from './download/index.js'
3
3
  export * as exists from './exists/index.js'
4
- export * as generateSignedUrl from './generate-signed-url/index.js'
5
4
  export * as getMetadata from './get-metadata/index.js'
5
+ export * as generateSignedUrl from './generate-signed-url/index.js'
6
6
  export * as getPermissions from './get-permissions/index.js'
7
7
  export * as move from './move/index.js'
8
8
  export * as remove from './remove/index.js'
@@ -1,8 +1,8 @@
1
1
  export * as copy from './copy/index.js'
2
2
  export * as download from './download/index.js'
3
3
  export * as exists from './exists/index.js'
4
- export * as generateSignedUrl from './generate-signed-url/index.js'
5
4
  export * as getMetadata from './get-metadata/index.js'
5
+ export * as generateSignedUrl from './generate-signed-url/index.js'
6
6
  export * as getPermissions from './get-permissions/index.js'
7
7
  export * as move from './move/index.js'
8
8
  export * as remove from './remove/index.js'
@@ -1,5 +1,5 @@
1
1
  export * as create from './create/index.js'
2
- export * as format from './format/index.js'
3
2
  export * as metadata from './metadata/index.js'
4
3
  export * as transform from './transform/index.js'
5
4
  export * as utils from './utils/index.js'
5
+ export * as format from './format/index.js'
@@ -1,5 +1,5 @@
1
1
  export * as create from './create/index.js'
2
- export * as format from './format/index.js'
3
2
  export * as metadata from './metadata/index.js'
4
3
  export * as transform from './transform/index.js'
5
4
  export * as utils from './utils/index.js'
5
+ export * as format from './format/index.js'
@@ -1,15 +1,15 @@
1
1
  export * as blur from './blur/index.js'
2
- export * as brighten from './brighten/index.js'
3
2
  export * as extend from './extend/index.js'
4
3
  export * as extract from './extract/index.js'
5
4
  export * as flatten from './flatten/index.js'
6
5
  export * as flip from './flip/index.js'
7
6
  export * as flop from './flop/index.js'
7
+ export * as brighten from './brighten/index.js'
8
8
  export * as hue from './hue/index.js'
9
9
  export * as level from './level/index.js'
10
- export * as lighten from './lighten/index.js'
11
10
  export * as normalize from './normalize/index.js'
12
11
  export * as overlay from './overlay/index.js'
12
+ export * as lighten from './lighten/index.js'
13
13
  export * as resize from './resize/index.js'
14
14
  export * as rotate from './rotate/index.js'
15
15
  export * as saturate from './saturate/index.js'
@@ -1,15 +1,15 @@
1
1
  export * as blur from './blur/index.js'
2
- export * as brighten from './brighten/index.js'
3
2
  export * as extend from './extend/index.js'
4
3
  export * as extract from './extract/index.js'
5
4
  export * as flatten from './flatten/index.js'
6
5
  export * as flip from './flip/index.js'
7
6
  export * as flop from './flop/index.js'
7
+ export * as brighten from './brighten/index.js'
8
8
  export * as hue from './hue/index.js'
9
9
  export * as level from './level/index.js'
10
- export * as lighten from './lighten/index.js'
11
10
  export * as normalize from './normalize/index.js'
12
11
  export * as overlay from './overlay/index.js'
12
+ export * as lighten from './lighten/index.js'
13
13
  export * as resize from './resize/index.js'
14
14
  export * as rotate from './rotate/index.js'
15
15
  export * as saturate from './saturate/index.js'