@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.
Files changed (120) hide show
  1. package/agnostic/@react/effects/index.d.ts +1 -0
  2. package/agnostic/@react/effects/index.js +1 -0
  3. package/agnostic/@react/effects/use-state-change-dispatch/index.d.ts +22 -0
  4. package/agnostic/@react/effects/use-state-change-dispatch/index.js +31 -0
  5. package/agnostic/@react/index.d.ts +1 -0
  6. package/agnostic/@react/index.js +1 -0
  7. package/agnostic/arrays/index.d.ts +2 -1
  8. package/agnostic/arrays/index.js +2 -1
  9. package/agnostic/arrays/integers-between/index.d.ts +8 -0
  10. package/agnostic/arrays/integers-between/index.js +11 -0
  11. package/agnostic/arrays/integers-between/index.test.d.ts +1 -0
  12. package/agnostic/arrays/integers-between/index.test.js +16 -0
  13. package/agnostic/colors/index.d.ts +1 -1
  14. package/agnostic/colors/index.js +1 -1
  15. package/agnostic/css/index.d.ts +1 -0
  16. package/agnostic/css/index.js +1 -0
  17. package/agnostic/css/merge-class-names/index.d.ts +8 -0
  18. package/agnostic/css/merge-class-names/index.js +23 -0
  19. package/agnostic/css/merge-class-names/index.test.d.ts +1 -0
  20. package/agnostic/css/merge-class-names/index.test.js +52 -0
  21. package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +9 -9
  22. package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +9 -9
  23. package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +1 -1
  24. package/agnostic/html/hyper-json/smart-tags/isolated/index.js +1 -1
  25. package/agnostic/html/index.d.ts +2 -2
  26. package/agnostic/html/index.js +2 -2
  27. package/agnostic/index.d.ts +1 -0
  28. package/agnostic/index.js +1 -0
  29. package/agnostic/misc/index.d.ts +1 -1
  30. package/agnostic/misc/index.js +1 -1
  31. package/agnostic/numbers/index.d.ts +1 -1
  32. package/agnostic/numbers/index.js +1 -1
  33. package/agnostic/objects/index.d.ts +1 -1
  34. package/agnostic/objects/index.js +1 -1
  35. package/agnostic/time/index.d.ts +1 -1
  36. package/agnostic/time/index.js +1 -1
  37. package/components/BeforeAfter/index.controlled.js +1 -1
  38. package/components/BeforeAfter/index.js +2 -2
  39. package/components/Button/index.js +1 -1
  40. package/components/Clippable/index.js +1 -1
  41. package/components/Disclaimer/index.d.ts +2 -2
  42. package/components/Disclaimer/index.js +4 -3
  43. package/components/Drawer/index.js +4 -3
  44. package/components/EventListener/index.js +1 -1
  45. package/components/Gallery/index.js +5 -4
  46. package/components/Iframe/index.js +1 -1
  47. package/components/Image/index.d.ts +1 -1
  48. package/components/Image/index.js +3 -2
  49. package/components/Input/index.js +1 -1
  50. package/components/IntersectionObserver/index.js +1 -1
  51. package/components/JsonEditor/index.js +1 -1
  52. package/components/JsonEditor/types.d.ts +0 -6
  53. package/components/JsonEditor/utils.d.ts +0 -5
  54. package/components/JsonEditor/utils.js +0 -5
  55. package/components/Lightbox/index.d.ts +12 -12
  56. package/components/Lightbox/index.js +37 -35
  57. package/components/Lightbox/store.d.ts +9 -43
  58. package/components/Lightbox/store.js +51 -73
  59. package/components/Lightbox/types.d.ts +16 -0
  60. package/components/Lightbox/types.js +1 -0
  61. package/components/ListLoader/index.controlled.js +19 -13
  62. package/components/ListLoader/index.js +19 -3
  63. package/components/Overlayer/index.js +1 -1
  64. package/components/Paginator/index.js +4 -3
  65. package/components/ResizeObserver/index.js +1 -1
  66. package/components/Scrllgngn/index.d.ts +4 -4
  67. package/components/Scrllgngn/index.js +4 -3
  68. package/components/Scrllgngn/types.d.ts +55 -0
  69. package/components/Scrllgngn/types.js +24 -0
  70. package/components/Scrllgngn/utils.d.ts +2 -38
  71. package/components/ScrollListener/index.d.ts +1 -6
  72. package/components/ScrollListener/index.js +10 -9
  73. package/components/ScrollListener/types.d.ts +0 -3
  74. package/components/Select/index.js +1 -1
  75. package/components/Sequencer/index.d.ts +2 -9
  76. package/components/Sequencer/index.js +26 -128
  77. package/components/Sequencer/types.d.ts +30 -0
  78. package/components/Sequencer/types.js +30 -0
  79. package/components/Sequencer/utils.d.ts +4 -0
  80. package/components/Sequencer/utils.js +25 -0
  81. package/components/ShadowRoot/index.js +1 -1
  82. package/components/Subtitles/index.js +3 -5
  83. package/components/Subtitles/utils.d.ts +0 -51
  84. package/components/Subtitles/utils.js +0 -41
  85. package/components/Textarea/index.js +1 -1
  86. package/components/UIModule/index.js +5 -4
  87. package/components/Video/index.controlled.d.ts +1 -7
  88. package/components/Video/index.controlled.js +13 -12
  89. package/components/Video/index.d.ts +0 -8
  90. package/components/Video/index.js +11 -15
  91. package/components/Video/types.d.ts +33 -0
  92. package/components/Video/utils.d.ts +1 -67
  93. package/components/Video/utils.js +25 -44
  94. package/components/index.d.ts +2 -2
  95. package/components/index.js +2 -2
  96. package/components/utils/index.d.ts +1 -44
  97. package/components/utils/index.js +1 -83
  98. package/components/utils/viewport-behaviours/index.d.ts +8 -14
  99. package/components/utils/viewport-behaviours/index.js +15 -37
  100. package/components/utils/viewport-behaviours/types.d.ts +3 -9
  101. package/components/utils/viewport-behaviours/types.js +3 -4
  102. package/node/@aws-s3/storage/directory/index.d.ts +1 -1
  103. package/node/@aws-s3/storage/directory/index.js +1 -1
  104. package/node/@aws-s3/storage/file/index.d.ts +1 -1
  105. package/node/@aws-s3/storage/file/index.js +1 -1
  106. package/node/cloud-storage/operations/index.d.ts +1 -1
  107. package/node/cloud-storage/operations/index.js +1 -1
  108. package/node/encryption/@aes-256-gcm/index.d.ts +1 -1
  109. package/node/encryption/@aes-256-gcm/index.js +1 -1
  110. package/node/ftps/directory/index.d.ts +1 -1
  111. package/node/ftps/directory/index.js +1 -1
  112. package/node/images/index.d.ts +1 -1
  113. package/node/images/index.js +1 -1
  114. package/node/images/transform/operations/index.d.ts +3 -3
  115. package/node/images/transform/operations/index.js +3 -3
  116. package/node/index.d.ts +2 -2
  117. package/node/index.js +2 -2
  118. package/package.json +44 -1
  119. package/components/ListLoader/utils.d.ts +0 -8
  120. 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, behavioursSuspended, visibilityThreshold, visibilityRoot, visibilityRootMargin, visibilityOnAfterMs, visibilityOffAfterMs, whenVisible, whenHidden, onVisibilityChanged, onVideoClicked, onPlayButtonClicked, onPauseButtonClicked, onLoudButtonClicked, onMuteButtonClicked, onVolumeRangeChanged, onRateRangeChanged, onFullscreenButtonClicked, onSubtitlesButtonClicked, ...controlledProps }) => {
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: { kind: 'start', domain: 'playback', run: () => setPlay(true) },
77
- pause: { kind: 'stop', domain: 'playback', run: () => setPlay(false) },
78
- loud: { kind: 'start', domain: 'sound', run: () => setMute(false) },
79
- mute: { kind: 'stop', domain: 'sound', run: () => setMute(true) },
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': { kind: 'stop', domain: 'playback', run: () => forceJumpTo(videoRef.current, 0) },
93
- 'jump-end': { kind: 'stop', domain: 'playback', run: () => forceJumpTo(videoRef.current, -1) }
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, behavioursSuspended === true);
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
- // Un seul bouton pour les deux sens, contrairement à lecture/pause et son/muet qui en
189
- // ont un chacun : ceux-là existent en paire dans le DOM et la feuille en montre un,
190
- // parce qu'ils portent deux glyphes opposés. Les sous-titres n'en portent qu'un, dont
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
- * Asks the element to play, and reports what came of it.
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
- * Whether a viewport-driven behaviour should run on this crossing.
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
+ }
@@ -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,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 declare function mergeClassNames(...names: Array<string | null | undefined | Array<string | null | undefined>>): string;
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
- import { useEffect, useRef } from 'react';
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 consumers that receive their props as text.** lm-link reads an
11
- * article's XML, where nothing is typed, and has to reject `'jump-to:banana'` before it
12
- * reaches a component. Written on its side, the rule would exist twice and drift in
13
- * silence — a verb added here would be rejected there for no visible reason.
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 whole point. An `IntersectionObserver` reports an event, and
32
- * a behaviour hung on the event only ever runs when the screen is traversed. A
33
- * component already visible whose behaviour was held back — behind a lm-link gate,
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>, suspended?: boolean): ViewportBehavioursResult;
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 { useChangeDispatch } from '../index.js';
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 consumers that receive their props as text.** lm-link reads an
13
- * article's XML, where nothing is typed, and has to reject `'jump-to:banana'` before it
14
- * reaches a component. Written on its side, the rule would exist twice and drift in
15
- * silence — a verb added here would be rejected there for no visible reason.
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 whole point. An `IntersectionObserver` reports an event, and
44
- * a behaviour hung on the event only ever runs when the screen is traversed. A
45
- * component already visible whose behaviour was held back — behind a lm-link gate,
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, suspended = false) {
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 heldBackRef = useRef(new Set());
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
- useChangeDispatch(isVisible, next => {
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
- const crossed = last.isVisible !== isVisible;
133
- const released = !crossed && last.suspended && !suspended;
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, suspended, whenVisible, whenHidden]);
145
+ }, [isVisible, whenVisible, whenHidden]);
168
146
  return { isVisible, surrender };
169
147
  }
170
148
  /**