@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
@@ -1,80 +1,14 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Children, cloneElement, isValidElement, useEffect, useRef, useState } from 'react';
3
+ import { useStateChangeDispatch } from '../../agnostic/@react/effects/use-state-change-dispatch/index.js';
3
4
  import { absoluteModulo } from '../../agnostic/numbers/absolute-modulo/index.js';
4
5
  import { clamp } from '../../agnostic/numbers/clamp/index.js';
5
6
  import { clss } from '../../agnostic/css/clss/index.js';
6
- import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
7
+ import { mergeClassNames } from '../../agnostic/css/merge-class-names/index.js';
7
8
  import { useViewportBehaviours } from '../utils/viewport-behaviours/index.js';
8
9
  import { sequencer as publicClassName } from '../public-classnames.js';
9
10
  import cssModule from './styles.module.css';
10
- /**
11
- * The attribute a child uses to say which steps it belongs to.
12
- *
13
- * **Written on the child rather than gathered in a prop**, and that is the whole
14
- * difference with the `activateOnStep` this component used to take. A parallel array and
15
- * a children list derive from one another: inserting a child in the middle means
16
- * reindexing the array, and nothing signals the one time it is forgotten. Carried by the
17
- * child, the attachment travels with it.
18
- *
19
- * It stays in the DOM once read — it costs nothing, it is worth having while debugging,
20
- * and a stylesheet can select on it.
21
- */
22
- const STEPS_ATTRIBUTE = 'data-steps';
23
- /**
24
- * The attribute a child uses to say that the sequence ends on it.
25
- *
26
- * It answers a shape the positional fallback could not: an article writes the steps in
27
- * reading order, then — **after** the sequence proper — the children that belong to
28
- * several steps at once and name them with {@link STEPS_ATTRIBUTE}. Those trailing
29
- * children would each claim a step of their own in the count, and the sequence would run
30
- * on into steps where nothing but them is lit.
31
- *
32
- * Marking the last step is what separates the two halves, and it is written where the
33
- * separation is rather than recounted in a prop — the same reason `data-steps` lives on
34
- * the child. A child after the mark that names no step therefore never lights up, which
35
- * is the honest reading of « it is not part of the sequence ».
36
- *
37
- * Valueless, as an HTML boolean attribute: what matters is that it is there.
38
- */
39
- const LAST_STEP_ATTRIBUTE = 'data-last-step';
40
- /**
41
- * `data-steps="2, 6"` into the steps it names.
42
- *
43
- * Anything unreadable gives back `null`, which the caller reads as « the attribute was
44
- * not written » and falls back to the child's position. That is lm-link's rule applied
45
- * here — a typo costs the default behaviour, never a broken one.
46
- */
47
- function parseSteps(raw) {
48
- if (typeof raw !== 'string')
49
- return null;
50
- const parts = raw.split(',').map(part => part.trim()).filter(part => part !== '');
51
- if (parts.length === 0)
52
- return null;
53
- const steps = parts.map(Number);
54
- if (steps.some(step => !Number.isInteger(step)))
55
- return null;
56
- return steps;
57
- }
58
- /**
59
- * Whether a child carries {@link LAST_STEP_ATTRIBUTE}.
60
- *
61
- * Present is enough — `data-last-step` alone gives `''` through lm-html, and `true`
62
- * written in JSX. Only an explicit denial is read as one, so that a consumer computing
63
- * the attribute can write `false` without having to leave it out.
64
- */
65
- function isLastStep(child) {
66
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- a React element's props are a record, whatever else they are
67
- const raw = child.props[LAST_STEP_ATTRIBUTE];
68
- if (raw === undefined || raw === null)
69
- return false;
70
- return raw !== false && raw !== 'false';
71
- }
72
- /** The steps a child answers to: what it declares, or where it sits. */
73
- function stepsOf(child, elementIndex) {
74
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- a React element's props are a record, whatever else they are
75
- const declared = parseSteps(child.props[STEPS_ATTRIBUTE]);
76
- return declared ?? [elementIndex];
77
- }
11
+ import { getChildActiveSteps, hasDataLastStepAttribute } from './utils.js';
78
12
  /**
79
13
  * A sequencer over its children, which it classifies and never moves.
80
14
  *
@@ -118,7 +52,7 @@ function stepsOf(child, elementIndex) {
118
52
  * **Nothing surrenders here.** The layer lets a reader's hand switch an instruction off
119
53
  * for the rest of the mount, and this component has no control for a hand to touch — so
120
54
  * `':force'` has nothing to override, and the single `'playback'` domain is a formality.
121
- * A gate still holds `'play'` back, which is a different brake. @see {@link SequencerDomain}
55
+ * @see {@link SequencerDomain}
122
56
  *
123
57
  * ### On the root
124
58
  * `--playing`, `--at-start`, `--at-end`, `--ended`, plus `data-step`, `data-active-step`,
@@ -148,19 +82,14 @@ function stepsOf(child, elementIndex) {
148
82
  * therefore `--is-first` at 1 and `--not-first` at 6, and the answer is the same on every
149
83
  * lap without anything being reset.
150
84
  */
151
- export const Sequencer = ({ totalSteps, stepMap, step, defaultStep, play, defaultPlay, tempo = 60, loop, onStepChanged, onIsPlayingChanged, onIsEndedChanged, onLooped, onReachedFirstStep, onReachedLastStep, behavioursSuspended, visibilityThreshold, visibilityRoot, visibilityRootMargin, visibilityOnAfterMs, visibilityOffAfterMs, whenVisible, whenHidden, onVisibilityChanged, className, children }) => {
85
+ export const Sequencer = ({ totalSteps, stepMap, step, defaultStep, play, defaultPlay, tempo = 60, loop, onStepChanged, onIsPlayingChanged, onIsEndedChanged, onLooped, onReachedFirstStep, onReachedLastStep, visibilityThreshold, visibilityRoot, visibilityRootMargin, visibilityOnAfterMs, visibilityOffAfterMs, whenVisible, whenHidden, onVisibilityChanged, className, children }) => {
152
86
  const [internalStep, setInternalStep] = useState(step ?? defaultStep ?? 0);
153
87
  const [internalPlay, setInternalPlay] = useState(defaultPlay ?? false);
154
88
  const [hasLapped, setHasLapped] = useState(0);
155
89
  const rootRef = useRef(null);
156
- // Children, split once: what takes part and what merely renders.
157
90
  const childrenArr = Children.toArray(children);
158
91
  const elements = childrenArr.filter(isValidElement);
159
- // Three answers to « how long is the sequence », in decreasing order of explicitness:
160
- // the prop, then the child that says the sequence ends on it, then the count of
161
- // element children. The prop wins because it is the one a consumer writes knowing the
162
- // whole, where the mark only knows where it sits.
163
- const markedLast = elements.findIndex(isLastStep);
92
+ const markedLast = elements.findIndex(hasDataLastStepAttribute);
164
93
  const impliedSteps = markedLast === -1 ? elements.length : markedLast + 1;
165
94
  const stepsCount = Math.max(totalSteps ?? impliedSteps, 0);
166
95
  // The two numbers
@@ -171,12 +100,8 @@ export const Sequencer = ({ totalSteps, stepMap, step, defaultStep, play, defaul
171
100
  : clamp(rawStep, 0, stepsCount - 1))
172
101
  : 0;
173
102
  const activeStep = stepMap?.[position] ?? position;
174
- // **The end is the counter leaving the last step, not arriving on it.** The last step
175
- // is owed its beat like every other one, so the counter runs one past the end while
176
- // the position clamps to it — which is also what keeps the whole thing derived: nothing
177
- // has to be unset when `loop` or `step` changes under it, and `onReachedLastStep` stays
178
- // the arrival that this is not. A sequence merely paused on the last step has not
179
- // ended, and a consumer driving `step` says so by naming the position after the last.
103
+ // The counter runs one past the end so the last step gets its beat, and the position
104
+ // clamps to it. Ended is therefore leaving the last step, not arriving on it.
180
105
  const isEnded = loop !== true && stepsCount > 0 && rawStep >= stepsCount;
181
106
  const isPlaying = (play ?? internalPlay) && !isEnded;
182
107
  useEffect(() => {
@@ -197,18 +122,8 @@ export const Sequencer = ({ totalSteps, stepMap, step, defaultStep, play, defaul
197
122
  onLooped?.();
198
123
  }, [lap, loop]);
199
124
  // Viewport behaviours
200
- // A jump names a position and lands on it **within the current lap**: the counter is
201
- // absolute, so setting it to a bare position on a looping sequence would count as a
202
- // wrap backwards and fire `onLooped` for a move nobody made. `jump-by` is left alone
203
- // on purpose — stepping past the last position really is a wrap, and says so.
204
- //
205
- // **The lap offset is for looping sequences only, and adding it elsewhere was a bug.**
206
- // A sequence that does not loop runs its counter one past the end and leaves it there,
207
- // so `lap` reads `1` from that moment on — and `jump-start` landed on `1 * stepsCount`,
208
- // which is precisely the ended position it was asked to leave. The symptom: a sequence
209
- // with `whenVisible='jump-start'` played once, stopped on its last step, and stayed
210
- // there however many times it was scrolled past. Nothing is lost by dropping the
211
- // offset here: without `loop`, there is no wrap to misread and `onLooped` never fires.
125
+ // The lap offset only applies while looping: on a sequence that does not loop, `lap`
126
+ // reads `1` past the end, and adding it would land `jump-start` back on the end.
212
127
  const jumpToPosition = (target) => {
213
128
  if (step !== undefined || stepsCount <= 0)
214
129
  return;
@@ -223,18 +138,12 @@ export const Sequencer = ({ totalSteps, stepMap, step, defaultStep, play, defaul
223
138
  return;
224
139
  setInternalStep(current => current + offset);
225
140
  };
226
- // What each verb does. Rebuilt on every render — it closes over the setters — which is
227
- // why the generic layer reads it through a ref rather than a dependency list.
228
- //
229
141
  // `play` and `pause` need no guard against a controlled `play`: the effective state
230
- // reads the prop first, so the internal one they write is simply never consulted.
142
+ // reads the prop first, so the internal one they write is never consulted.
231
143
  const actions = {
232
- play: { kind: 'start', domain: 'playback', run: () => setInternalPlay(true) },
233
- pause: { kind: 'stop', domain: 'playback', run: () => setInternalPlay(false) },
234
- // A jump starts nothing on its own — it moves a counter, playing or not — so a gate
235
- // has no reason to hold it back.
144
+ play: { domain: 'playback', run: () => setInternalPlay(true) },
145
+ pause: { domain: 'playback', run: () => setInternalPlay(false) },
236
146
  'jump-to': {
237
- kind: 'stop',
238
147
  domain: 'playback',
239
148
  run: arg => {
240
149
  const target = Number(arg);
@@ -243,10 +152,9 @@ export const Sequencer = ({ totalSteps, stepMap, step, defaultStep, play, defaul
243
152
  jumpToPosition(target);
244
153
  }
245
154
  },
246
- 'jump-start': { kind: 'stop', domain: 'playback', run: () => jumpToPosition(0) },
247
- 'jump-end': { kind: 'stop', domain: 'playback', run: () => jumpToPosition(-1) },
155
+ 'jump-start': { domain: 'playback', run: () => jumpToPosition(0) },
156
+ 'jump-end': { domain: 'playback', run: () => jumpToPosition(-1) },
248
157
  'jump-by': {
249
- kind: 'stop',
250
158
  domain: 'playback',
251
159
  run: arg => {
252
160
  const offset = Number(arg);
@@ -256,8 +164,7 @@ export const Sequencer = ({ totalSteps, stepMap, step, defaultStep, play, defaul
256
164
  }
257
165
  }
258
166
  };
259
- // The surrender half of the result is dropped, and that is the whole of what this
260
- // component's lack of controls changes: nothing here can be taken over by hand.
167
+ // The surrender half is dropped: this component has no control a hand could take over.
261
168
  useViewportBehaviours(rootRef, {
262
169
  visibilityThreshold,
263
170
  visibilityRoot,
@@ -267,14 +174,14 @@ export const Sequencer = ({ totalSteps, stepMap, step, defaultStep, play, defaul
267
174
  whenVisible,
268
175
  whenHidden,
269
176
  onVisibilityChanged
270
- }, actions, behavioursSuspended === true);
177
+ }, actions);
271
178
  // State dispatch
272
- useChangeDispatch(position, () => onStepChanged?.({ step: position, activeStep }));
273
- useChangeDispatch(isPlaying, onIsPlayingChanged);
274
- useChangeDispatch(isEnded, onIsEndedChanged);
275
- useChangeDispatch(position === 0, atStart => { if (atStart)
179
+ useStateChangeDispatch(position, () => onStepChanged?.({ step: position, activeStep }));
180
+ useStateChangeDispatch(isPlaying, onIsPlayingChanged);
181
+ useStateChangeDispatch(isEnded, onIsEndedChanged);
182
+ useStateChangeDispatch(position === 0, atStart => { if (atStart)
276
183
  onReachedFirstStep?.(); });
277
- useChangeDispatch(stepsCount > 0 && position === stepsCount - 1, atEnd => { if (atEnd)
184
+ useStateChangeDispatch(stepsCount > 0 && position === stepsCount - 1, atEnd => { if (atEnd)
278
185
  onReachedLastStep?.(); });
279
186
  // Rendering
280
187
  const c = clss(publicClassName, { cssModule });
@@ -284,8 +191,6 @@ export const Sequencer = ({ totalSteps, stepMap, step, defaultStep, play, defaul
284
191
  'at-end': stepsCount > 0 && position === stepsCount - 1,
285
192
  ended: isEnded
286
193
  }), className);
287
- // Walked once per render: for each element, the positions of this lap at which it is
288
- // active. Cheap, and it is what makes the three pairs answerable without memory.
289
194
  const positionsOf = (steps) => {
290
195
  const positions = [];
291
196
  for (let i = 0; i < stepsCount; i += 1) {
@@ -294,14 +199,9 @@ export const Sequencer = ({ totalSteps, stepMap, step, defaultStep, play, defaul
294
199
  }
295
200
  return positions;
296
201
  };
297
- // **The lap count is the wraps, so it is zero without `loop`.** The internal counter
298
- // runs one past the end and stays there, which makes `lap` read `1` on a sequence that
299
- // never wrapped at all — true of the division, false of the thing it is named after,
300
- // and `onLooped` says the same by never firing without `loop`.
202
+ // A lap is a wrap, so there is none without `loop` — where the internal counter would
203
+ // report `1` past the end. Both numbers below are rebuilt from it rather than `rawStep`.
301
204
  const publishedLap = loop === true ? lap : 0;
302
- // Rebuilt from the published lap rather than read off `rawStep`, for that same reason:
303
- // at the end of a non-looping sequence the internal counter sits one step past the last
304
- // position, and a number meant to drive a transform must not overshoot the picture.
305
205
  const rawStepProp = publishedLap * stepsCount + position;
306
206
  const cssCustomProps = {
307
207
  '--lm-sequencer-step': `${position}`,
@@ -316,7 +216,7 @@ export const Sequencer = ({ totalSteps, stepMap, step, defaultStep, play, defaul
316
216
  if (!isValidElement(child))
317
217
  return child;
318
218
  elementIndex += 1;
319
- const steps = stepsOf(child, elementIndex);
219
+ const steps = getChildActiveSteps(child, elementIndex);
320
220
  const positions = positionsOf(steps);
321
221
  const isOn = steps.includes(activeStep);
322
222
  const hasEarlier = positions.some(p => p < position);
@@ -331,9 +231,7 @@ export const Sequencer = ({ totalSteps, stepMap, step, defaultStep, play, defaul
331
231
  });
332
232
  // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- a React element's props are a record, whatever else they are
333
233
  const ownClassName = child.props.className;
334
- // `className` is the whole of what a child is asked to honour, and the only prop
335
- // touched here — its own is kept, and comes second so a consumer's class wins a
336
- // shorthand conflict.
234
+ // The child's own class comes second, so a consumer wins a shorthand conflict.
337
235
  const nextProps = {
338
236
  key: childPos,
339
237
  className: mergeClassNames(childClss, typeof ownClassName === 'string' ? ownClassName : undefined)
@@ -55,3 +55,33 @@ export type SequencerAction = Exclude<SequencerVerb, 'jump-to' | 'jump-by'> | `j
55
55
  * there are none.
56
56
  */
57
57
  export type SequencerDomain = 'playback';
58
+ /**
59
+ * The attribute a child uses to say which steps it belongs to.
60
+ *
61
+ * **Written on the child rather than gathered in a prop**, and that is the whole
62
+ * difference with the `activateOnStep` this component used to take. A parallel array and
63
+ * a children list derive from one another: inserting a child in the middle means
64
+ * reindexing the array, and nothing signals the one time it is forgotten. Carried by the
65
+ * child, the attachment travels with it.
66
+ *
67
+ * It stays in the DOM once read — it costs nothing, it is worth having while debugging,
68
+ * and a stylesheet can select on it.
69
+ */
70
+ export declare const STEPS_ATTRIBUTE = "data-steps";
71
+ /**
72
+ * The attribute a child uses to say that the sequence ends on it.
73
+ *
74
+ * It answers a shape the positional fallback could not: an article writes the steps in
75
+ * reading order, then — **after** the sequence proper — the children that belong to
76
+ * several steps at once and name them with {@link STEPS_ATTRIBUTE}. Those trailing
77
+ * children would each claim a step of their own in the count, and the sequence would run
78
+ * on into steps where nothing but them is lit.
79
+ *
80
+ * Marking the last step is what separates the two halves, and it is written where the
81
+ * separation is rather than recounted in a prop — the same reason `data-steps` lives on
82
+ * the child. A child after the mark that names no step therefore never lights up, which
83
+ * is the honest reading of « it is not part of the sequence ».
84
+ *
85
+ * Valueless, as an HTML boolean attribute: what matters is that it is there.
86
+ */
87
+ export declare const LAST_STEP_ATTRIBUTE = "data-last-step";
@@ -30,3 +30,33 @@ export const SEQUENCER_VERB_ARGUMENTS = {
30
30
  'jump-to': (arg) => /^-?\d+$/v.test(arg),
31
31
  'jump-by': (arg) => /^-?\d+$/v.test(arg)
32
32
  };
33
+ /**
34
+ * The attribute a child uses to say which steps it belongs to.
35
+ *
36
+ * **Written on the child rather than gathered in a prop**, and that is the whole
37
+ * difference with the `activateOnStep` this component used to take. A parallel array and
38
+ * a children list derive from one another: inserting a child in the middle means
39
+ * reindexing the array, and nothing signals the one time it is forgotten. Carried by the
40
+ * child, the attachment travels with it.
41
+ *
42
+ * It stays in the DOM once read — it costs nothing, it is worth having while debugging,
43
+ * and a stylesheet can select on it.
44
+ */
45
+ export const STEPS_ATTRIBUTE = 'data-steps';
46
+ /**
47
+ * The attribute a child uses to say that the sequence ends on it.
48
+ *
49
+ * It answers a shape the positional fallback could not: an article writes the steps in
50
+ * reading order, then — **after** the sequence proper — the children that belong to
51
+ * several steps at once and name them with {@link STEPS_ATTRIBUTE}. Those trailing
52
+ * children would each claim a step of their own in the count, and the sequence would run
53
+ * on into steps where nothing but them is lit.
54
+ *
55
+ * Marking the last step is what separates the two halves, and it is written where the
56
+ * separation is rather than recounted in a prop — the same reason `data-steps` lives on
57
+ * the child. A child after the mark that names no step therefore never lights up, which
58
+ * is the honest reading of « it is not part of the sequence ».
59
+ *
60
+ * Valueless, as an HTML boolean attribute: what matters is that it is there.
61
+ */
62
+ export const LAST_STEP_ATTRIBUTE = 'data-last-step';
@@ -0,0 +1,4 @@
1
+ import type { ReactElement } from 'react';
2
+ export declare function parseDataStepsAttribute(raw: unknown): number[] | null;
3
+ export declare function hasDataLastStepAttribute(child: ReactElement): boolean;
4
+ export declare function getChildActiveSteps(child: ReactElement, elementIndex: number): number[];
@@ -0,0 +1,25 @@
1
+ import { LAST_STEP_ATTRIBUTE, STEPS_ATTRIBUTE } from './types.js';
2
+ export function parseDataStepsAttribute(raw) {
3
+ if (typeof raw !== 'string')
4
+ return null;
5
+ const parts = raw.split(',').map(part => part.trim()).filter(part => part !== '');
6
+ if (parts.length === 0)
7
+ return null;
8
+ const steps = parts.map(Number);
9
+ if (steps.some(step => !Number.isInteger(step)))
10
+ return null;
11
+ return steps;
12
+ }
13
+ export function hasDataLastStepAttribute(child) {
14
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- a React element's props are a record, whatever else they are
15
+ const raw = child.props[LAST_STEP_ATTRIBUTE];
16
+ if (raw === undefined || raw === null)
17
+ return false;
18
+ return raw !== false && raw !== 'false';
19
+ }
20
+ export function getChildActiveSteps(child, elementIndex) {
21
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- a React element's props are a record, whatever else they are
22
+ const props = child.props;
23
+ const declared = parseDataStepsAttribute(props[STEPS_ATTRIBUTE]);
24
+ return declared ?? [elementIndex];
25
+ }
@@ -2,8 +2,8 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import { useRef, useEffect, useState } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
4
  import { clss } from '../../agnostic/css/clss/index.js';
5
+ import { mergeClassNames } from '../../agnostic/css/merge-class-names/index.js';
5
6
  import { shadowRoot as publicClassName } from '../public-classnames.js';
6
- import { mergeClassNames } from '../utils/index.js';
7
7
  import cssModule from './styles.module.css';
8
8
  /**
9
9
  * Component that creates a Shadow Root on its host element and renders
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
+ import { useStateChangeDispatch } from '../../agnostic/@react/effects/use-state-change-dispatch/index.js';
3
4
  import { clss } from '../../agnostic/css/clss/index.js';
4
5
  import { toError } from '../../agnostic/misc/cast/index.js';
5
6
  import { parseSrt } from '../../agnostic/subtitles/parse-srt/index.js';
6
- import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
7
+ import { mergeClassNames } from '../../agnostic/css/merge-class-names/index.js';
7
8
  import { subtitles as publicClassName } from '../public-classnames.js';
8
9
  import { lastElapsedPos as toLastElapsedPos, normalizeGroupBoundaries, toCurrentGroup, toSubGroups } from './utils.js';
9
10
  import cssModule from './styles.module.css';
@@ -44,9 +45,6 @@ export const Subtitles = ({ cues, srtFileContent, src, subsGroups, timecodeMs, i
44
45
  const [isLoading, setIsLoading] = useState(false);
45
46
  const [loadError, setLoadError] = useState(null);
46
47
  const [parsedSubs, setParsedSubs] = useState([]);
47
- // The load handlers are read through refs so the effect below can depend on the
48
- // source alone. Depending on their identity re-fetches on every parent render —
49
- // and `Video` re-renders several times a second as the timecode moves.
50
48
  const onLoadedRef = useRef(onLoaded);
51
49
  const onLoadFailedRef = useRef(onLoadFailed);
52
50
  useEffect(() => {
@@ -54,7 +52,7 @@ export const Subtitles = ({ cues, srtFileContent, src, subsGroups, timecodeMs, i
54
52
  onLoadFailedRef.current = onLoadFailed;
55
53
  });
56
54
  // State dispatch
57
- useChangeDispatch(parsedSubs, onParsed);
55
+ useStateChangeDispatch(parsedSubs, onParsed);
58
56
  // Fx. dep. cues, srtFileContent, src - Load from whichever source is set
59
57
  useEffect(() => {
60
58
  setIsLoading(false);
@@ -1,60 +1,9 @@
1
1
  import type { SrtCue } from '../../agnostic/subtitles/parse-srt/types.js';
2
- /**
3
- * The inclusive bounds of a subtitle group, as **positions** in the cue array.
4
- *
5
- * Positions rather than file ids: an SRT concatenated from several files, or edited
6
- * by hand, numbers its cues however it likes, and nothing guarantees a run starting
7
- * at 1. A consumer shouldn't have to know a file's numbering to split it.
8
- *
9
- * @property startPos - Position of the group's first cue.
10
- * @property endPos - Position of its last cue.
11
- */
12
2
  export type SubGroupBounds = {
13
3
  startPos: number;
14
4
  endPos: number;
15
5
  };
16
- /**
17
- * Puts a consumer's group boundaries in a state the group builder can use.
18
- *
19
- * Out-of-range, duplicate and unordered boundaries all produce overlapping or
20
- * inverted groups downstream — the same cue rendered twice, colliding keys. Rather
21
- * than demand a sorted array and break quietly when it isn't, normalise here.
22
- *
23
- * @param boundaries - The positions a consumer asked to split on, in any order.
24
- * @param lastPos - Position of the last cue.
25
- * @returns The usable boundaries, ascending, without duplicates, and without the
26
- * last position — which ends the final group anyway.
27
- */
28
6
  export declare function normalizeGroupBoundaries(boundaries: number[] | undefined, lastPos: number): number[];
29
- /**
30
- * Cuts the cue array into groups.
31
- *
32
- * @param boundaries - Normalised boundaries, each the position of a group's last cue.
33
- * See {@link normalizeGroupBoundaries}.
34
- * @param lastPos - Position of the last cue.
35
- * @returns One group per section, in order. A single group spanning everything when
36
- * no boundary is given, and nothing at all when there is no cue to group.
37
- */
38
7
  export declare function toSubGroups(boundaries: number[], lastPos: number): SubGroupBounds[];
39
- /**
40
- * Finds the last cue the playhead has already reached.
41
- *
42
- * @param cues - The parsed cues, in order.
43
- * @param timecodeMs - Current media position, absent while nothing plays.
44
- * @returns Its position, or `-1` when the playhead sits before the first cue. One
45
- * number, computed once: `--prev` on a cue and the current group both read from it,
46
- * and deriving them separately lets them disagree on a file with duplicate or
47
- * out-of-order start times.
48
- */
49
8
  export declare function lastElapsedPos(cues: SrtCue[], timecodeMs?: number): number;
50
- /**
51
- * Picks the group the reader is in.
52
- *
53
- * @param groups - The groups, in order.
54
- * @param lastElapsedPos - Position of the last elapsed cue, `-1` before the first.
55
- * @param isEnded - Whether playback is over.
56
- * @returns The group holding that cue. Once playback has ended, the last group
57
- * whatever the timecode says — which is what `isEnded` is for. The first group
58
- * before anything has elapsed.
59
- */
60
9
  export declare function toCurrentGroup(groups: SubGroupBounds[], lastElapsedPos: number, isEnded?: boolean): SubGroupBounds | undefined;
@@ -1,30 +1,9 @@
1
- /**
2
- * Puts a consumer's group boundaries in a state the group builder can use.
3
- *
4
- * Out-of-range, duplicate and unordered boundaries all produce overlapping or
5
- * inverted groups downstream — the same cue rendered twice, colliding keys. Rather
6
- * than demand a sorted array and break quietly when it isn't, normalise here.
7
- *
8
- * @param boundaries - The positions a consumer asked to split on, in any order.
9
- * @param lastPos - Position of the last cue.
10
- * @returns The usable boundaries, ascending, without duplicates, and without the
11
- * last position — which ends the final group anyway.
12
- */
13
1
  export function normalizeGroupBoundaries(boundaries, lastPos) {
14
2
  if (boundaries === undefined)
15
3
  return [];
16
4
  const usable = boundaries.filter(pos => Number.isInteger(pos) && pos >= 0 && pos < lastPos);
17
5
  return [...new Set(usable)].sort((a, b) => a - b);
18
6
  }
19
- /**
20
- * Cuts the cue array into groups.
21
- *
22
- * @param boundaries - Normalised boundaries, each the position of a group's last cue.
23
- * See {@link normalizeGroupBoundaries}.
24
- * @param lastPos - Position of the last cue.
25
- * @returns One group per section, in order. A single group spanning everything when
26
- * no boundary is given, and nothing at all when there is no cue to group.
27
- */
28
7
  export function toSubGroups(boundaries, lastPos) {
29
8
  if (lastPos < 0)
30
9
  return [];
@@ -37,16 +16,6 @@ export function toSubGroups(boundaries, lastPos) {
37
16
  groups.push({ startPos, endPos: lastPos });
38
17
  return groups;
39
18
  }
40
- /**
41
- * Finds the last cue the playhead has already reached.
42
- *
43
- * @param cues - The parsed cues, in order.
44
- * @param timecodeMs - Current media position, absent while nothing plays.
45
- * @returns Its position, or `-1` when the playhead sits before the first cue. One
46
- * number, computed once: `--prev` on a cue and the current group both read from it,
47
- * and deriving them separately lets them disagree on a file with duplicate or
48
- * out-of-order start times.
49
- */
50
19
  export function lastElapsedPos(cues, timecodeMs) {
51
20
  if (timecodeMs === undefined)
52
21
  return -1;
@@ -57,16 +26,6 @@ export function lastElapsedPos(cues, timecodeMs) {
57
26
  });
58
27
  return found;
59
28
  }
60
- /**
61
- * Picks the group the reader is in.
62
- *
63
- * @param groups - The groups, in order.
64
- * @param lastElapsedPos - Position of the last elapsed cue, `-1` before the first.
65
- * @param isEnded - Whether playback is over.
66
- * @returns The group holding that cue. Once playback has ended, the last group
67
- * whatever the timecode says — which is what `isEnded` is for. The first group
68
- * before anything has elapsed.
69
- */
70
29
  export function toCurrentGroup(groups, lastElapsedPos, isEnded) {
71
30
  if (isEnded === true)
72
31
  return groups[groups.length - 1];
@@ -3,7 +3,7 @@ import { useLayoutEffect, useRef, useState } from 'react';
3
3
  import { clss } from '../../agnostic/css/clss/index.js';
4
4
  import { isNotFalsy } from '../../agnostic/booleans/is-falsy/index.js';
5
5
  import { randomHash } from '../../agnostic/random/uuid/index.js';
6
- import { mergeClassNames } from '../utils/index.js';
6
+ import { mergeClassNames } from '../../agnostic/css/merge-class-names/index.js';
7
7
  import { textarea as publicClassName } from '../public-classnames.js';
8
8
  import cssModule from './styles.module.css';
9
9
  /**
@@ -1,10 +1,11 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
+ import { useStateChangeDispatch } from '../../agnostic/@react/effects/use-state-change-dispatch/index.js';
3
4
  import { clss } from '../../agnostic/css/clss/index.js';
4
5
  import { unknownToString } from '../../agnostic/errors/unknown-to-string/index.js';
5
6
  import { isNonNullObject } from '../../agnostic/objects/is-object/index.js';
6
7
  import { randomHash } from '../../agnostic/random/uuid/index.js';
7
- import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
8
+ import { mergeClassNames } from '../../agnostic/css/merge-class-names/index.js';
8
9
  import { uiModule as publicClassName } from '../public-classnames.js';
9
10
  import cssModule from './styles.module.css';
10
11
  /**
@@ -56,9 +57,9 @@ export const UIModule = ({ src, props, onIdGenerated, onIsLoadingChanged, onLoad
56
57
  // the render it ran on. This is what `init` and `postInit` read instead.
57
58
  const propsRef = useRef(props);
58
59
  // State dispatch
59
- useChangeDispatch(isLoading, onIsLoadingChanged);
60
- useChangeDispatch(loadedModule, onLoadedModuleChanged);
61
- useChangeDispatch(moduleTarget, onModuleTargetChanged);
60
+ useStateChangeDispatch(isLoading, onIsLoadingChanged);
61
+ useStateChangeDispatch(loadedModule, onLoadedModuleChanged);
62
+ useStateChangeDispatch(moduleTarget, onModuleTargetChanged);
62
63
  // Fx. no dep. - keep the latest props reachable from the effects that run outside
63
64
  // their own render, declared first so they read this render's value and not the last
64
65
  useEffect(() => { propsRef.current = props; });
@@ -1,13 +1,7 @@
1
1
  import { type FunctionComponent, type PropsWithChildren, type Ref, type RefObject, type VideoHTMLAttributes } from 'react';
2
2
  import type { WithClassName } from '../utils/types.js';
3
3
  import { type Props as SubsProps } from '../Subtitles/index.js';
4
- import { type SourceData, type TrackData } from './utils.js';
5
- /**
6
- * Describes a single video source.
7
- *
8
- * @property src - URL of the video file.
9
- * @property type - MIME type of the source (e.g. `'video/mp4'`).
10
- */
4
+ import type { SourceData, TrackData } from './types.js';
11
5
  /**
12
6
  * Props for the ControlledVideo component.
13
7
  *
@@ -1,12 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo, useRef, useState, useCallback, useEffect } from 'react';
3
+ import { useStateChangeDispatch } from '../../agnostic/@react/effects/use-state-change-dispatch/index.js';
3
4
  import { clss } from '../../agnostic/css/clss/index.js';
4
5
  import { formatDuration } from '../../agnostic/time/duration/format-duration/index.js';
5
- import { mergeClassNames, parseSourceList, useChangeDispatch } from '../utils/index.js';
6
+ import { mergeClassNames } from '../../agnostic/css/merge-class-names/index.js';
6
7
  import cssModule from './styles.module.css';
7
8
  import { video as publicClassName } from '../public-classnames.js';
8
9
  import { Subtitles } from '../Subtitles/index.js';
9
- import { forceExitFullscreen, forceFullscreen, forceLoud, forceMute, forcePause, forcePlay, forcePlaybackRate, forceVolume, getTimelineClickProgress, msToSeconds, secondsToMs } from './utils.js';
10
+ import { forceExitFullscreen, forceFullscreen, forceLoud, forceMute, forcePause, forcePlay, forcePlaybackRate, forceVolume, getTimelineClickProgress, msToSeconds, parseSrcSetSourcesList, secondsToMs } from './utils.js';
10
11
  /**
11
12
  * Full-featured video player component. Wraps a native `<video>` element with
12
13
  * playback controls, volume, playback rate, a timeline, optional subtitles
@@ -226,8 +227,8 @@ timeFormat = '{{mm}}:{{ss}}:{{ms}}', onPlayButtonClicked, onPauseButtonClicked,
226
227
  '--lm-video-volume-ratio': `${volume}`,
227
228
  '--lm-video-playback-rate': `${playbackRate}`
228
229
  };
229
- const parsedSources = useMemo(() => parseSourceList(sources, 'src'), [sources]);
230
- const parsedTracks = useMemo(() => parseSourceList(tracks, 'src'), [tracks]);
230
+ const parsedSources = useMemo(() => parseSrcSetSourcesList(sources, 'src'), [sources]);
231
+ const parsedTracks = useMemo(() => parseSrcSetSourcesList(tracks, 'src'), [tracks]);
231
232
  const videoClss = c('video');
232
233
  const videoControlsClss = c('video-controls');
233
234
  const playBtnClss = c('play-btn');
@@ -305,13 +306,13 @@ timeFormat = '{{mm}}:{{ss}}:{{ms}}', onPlayButtonClicked, onPauseButtonClicked,
305
306
  return () => { document.removeEventListener('fullscreenchange', onDocumentFullscreenChange); };
306
307
  }, [onFullscreenChange]);
307
308
  // State handlers
308
- useChangeDispatch(currentTimeMs, onCurrentTimeMsChanged);
309
- useChangeDispatch(isElementPlaying, onIsPlayingChanged);
310
- useChangeDispatch(isFullscreen, onIsFullscreenChanged);
311
- useChangeDispatch(isLoud, onIsLoudChanged);
312
- useChangeDispatch(subtitlesOn, onIsSubtitlesOnChanged);
313
- useChangeDispatch(isEnded, onIsEndedChanged);
314
- useChangeDispatch(volume, onVolumeChanged);
315
- useChangeDispatch(playbackRate, onPlaybackRateChanged);
309
+ useStateChangeDispatch(currentTimeMs, onCurrentTimeMsChanged);
310
+ useStateChangeDispatch(isElementPlaying, onIsPlayingChanged);
311
+ useStateChangeDispatch(isFullscreen, onIsFullscreenChanged);
312
+ useStateChangeDispatch(isLoud, onIsLoudChanged);
313
+ useStateChangeDispatch(subtitlesOn, onIsSubtitlesOnChanged);
314
+ useStateChangeDispatch(isEnded, onIsEndedChanged);
315
+ useStateChangeDispatch(volume, onVolumeChanged);
316
+ useStateChangeDispatch(playbackRate, onPlaybackRateChanged);
316
317
  return _jsxs("figure", { ref: rootRef, className: rootClss, style: rootStyles, ...rootAttributes, children: [_jsxs("video", { ref: videoRef, className: videoClss, ...intrinsicVideoAttributes, autoPlay: isTimeControlled ? false : intrinsicVideoAttributes.autoPlay, onClick: handleVideoClick, onLoadedMetadata: handleMetadataLoadEvent, onTimeUpdate: handleOnTimeUpdateEvent, onEnded: handleEndedEvent, onPlay: handlePlayEvent, onPause: handlePauseEvent, children: [parsedSources.map((source, index) => _jsx("source", { src: source.src, type: source.type }, index)), parsedTracks.map((track, index) => _jsx("track", { src: track.src, kind: track.kind, srcLang: track.srclang, label: track.label, default: track.default }, index)), children] }), _jsxs("div", { className: videoControlsClss, children: [_jsx("button", { className: playBtnClss, onClick: handlePlayButtonClick, children: playBtnContent }), _jsx("button", { className: pauseBtnClss, onClick: handlePauseButtonClick, children: pauseBtnContent }), _jsx("button", { className: loudBtnClss, onClick: handleLoudButtonClick, children: loudBtnContent }), _jsx("button", { className: muteBtnClss, onClick: handleMuteButtonClick, children: muteBtnContent }), _jsx("input", { type: 'range', className: volumeRangeClss, value: volumePercent, onChange: handleVolumeRangeChange, min: 0, max: 100, step: 1 }), _jsx("span", { className: volumePcntClss, children: Math.round(volumePercent) }), _jsx("button", { className: fullscreenBtnClss, onClick: handleFullscreenButtonClick, children: fullscreenBtnContent }), subtitles !== undefined && _jsx("button", { className: subtitlesBtnClss, onClick: handleSubtitlesButtonClick, children: subtitlesBtnContent }), _jsx("input", { type: 'range', className: playbackRateRangeClss, value: playbackRate, onChange: handleRateRangeChange, min: 0.25, max: 4, step: 0.25 }), _jsx("span", { className: playbackRateClss, children: playbackRate })] }), _jsxs("div", { className: timeControlsClss, children: [_jsx("span", { className: currentTimeClss, children: formatDuration(currentTimeMs, timeFormat) }), _jsx("span", { className: totalTimeClss, children: formatDuration(totalTimeMs, timeFormat) }), _jsx("div", { className: timelineClss, onClick: handleTimelineClick })] }), subtitles !== undefined && _jsx(Subtitles, { ...subtitles, timecodeMs: currentTimeMs, isEnded: isEnded })] });
317
318
  };