@design-edito/tools 0.5.5 → 0.5.7

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 (67) hide show
  1. package/agnostic/css/index.d.ts +1 -1
  2. package/agnostic/css/index.js +1 -1
  3. package/agnostic/errors/index.d.ts +1 -1
  4. package/agnostic/errors/index.js +1 -1
  5. package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +8 -8
  6. package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +8 -8
  7. package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +2 -2
  8. package/agnostic/html/hyper-json/smart-tags/isolated/index.js +2 -2
  9. package/agnostic/index.d.ts +1 -0
  10. package/agnostic/index.js +1 -0
  11. package/agnostic/misc/index.d.ts +1 -1
  12. package/agnostic/misc/index.js +1 -1
  13. package/agnostic/objects/index.d.ts +2 -2
  14. package/agnostic/objects/index.js +2 -2
  15. package/agnostic/random/index.d.ts +1 -1
  16. package/agnostic/random/index.js +1 -1
  17. package/agnostic/strings/index.d.ts +1 -1
  18. package/agnostic/strings/index.js +1 -1
  19. package/agnostic/subtitles/index.d.ts +2 -0
  20. package/agnostic/subtitles/index.js +2 -0
  21. package/agnostic/subtitles/parse-srt/index.d.ts +17 -0
  22. package/agnostic/subtitles/parse-srt/index.js +67 -0
  23. package/agnostic/subtitles/parse-srt/index.test.d.ts +1 -0
  24. package/agnostic/subtitles/parse-srt/index.test.js +94 -0
  25. package/agnostic/subtitles/parse-srt/types.d.ts +22 -0
  26. package/agnostic/subtitles/parse-srt/types.js +1 -0
  27. package/agnostic/subtitles/srt-timecode-to-ms/index.d.ts +14 -0
  28. package/agnostic/subtitles/srt-timecode-to-ms/index.js +36 -0
  29. package/agnostic/subtitles/srt-timecode-to-ms/index.test.d.ts +1 -0
  30. package/agnostic/subtitles/srt-timecode-to-ms/index.test.js +44 -0
  31. package/agnostic/time/dates/index.d.ts +1 -1
  32. package/agnostic/time/dates/index.js +1 -1
  33. package/components/ListLoader/index.controlled.d.ts +8 -3
  34. package/components/ListLoader/index.controlled.js +2 -2
  35. package/components/ListLoader/index.d.ts +1 -1
  36. package/components/ListLoader/index.js +2 -2
  37. package/components/Paginator/index.d.ts +5 -0
  38. package/components/Paginator/index.js +2 -1
  39. package/components/Scrllgngn/index.d.ts +68 -8
  40. package/components/Scrllgngn/index.js +231 -27
  41. package/components/Scrllgngn/styles.module.css +30 -5
  42. package/components/Scrllgngn/utils.d.ts +222 -4
  43. package/components/Scrllgngn/utils.js +335 -8
  44. package/components/ScrollListener/index.d.ts +7 -3
  45. package/components/ScrollListener/index.js +2 -2
  46. package/components/Sequencer/index.d.ts +10 -1
  47. package/components/Sequencer/index.js +7 -3
  48. package/components/Subtitles/index.d.ts +53 -62
  49. package/components/Subtitles/index.js +90 -158
  50. package/components/Subtitles/utils.d.ts +60 -0
  51. package/components/Subtitles/utils.js +76 -0
  52. package/components/Video/index.controlled.d.ts +5 -0
  53. package/components/Video/index.controlled.js +22 -3
  54. package/components/Video/index.d.ts +47 -9
  55. package/components/Video/index.js +71 -20
  56. package/components/Video/utils.d.ts +14 -0
  57. package/components/Video/utils.js +18 -0
  58. package/components/utils/types.d.ts +32 -0
  59. package/index.d.ts +1 -1
  60. package/index.js +1 -1
  61. package/node/@google-cloud/storage/file/index.d.ts +1 -1
  62. package/node/@google-cloud/storage/file/index.js +1 -1
  63. package/node/files/index.d.ts +1 -1
  64. package/node/files/index.js +1 -1
  65. package/node/images/transform/operations/index.d.ts +1 -1
  66. package/node/images/transform/operations/index.js +1 -1
  67. package/package.json +26 -1
@@ -1,4 +1,4 @@
1
- import type { PropsPage, PropsStickyBlock } from './index.js';
1
+ import type { PropsBlock, PropsPage, PropsStickyBlock } from './index.js';
2
2
  /** The subset of a bounding rect a {@link Scrllgngn} tracks and exposes. */
3
3
  export type ScreenRect = {
4
4
  left: number;
@@ -33,9 +33,11 @@ export declare function consolidateStickyBlocks(pages?: PropsPage[]): Map<string
33
33
  * @param currentPagePos - Zero-based position of the page in view.
34
34
  * @param lazyLoadDistance - How many pages around the current one still mount
35
35
  * their blocks.
36
- * @returns The blocks to render, stacked by ascending `zIndex`.
36
+ * @returns The blocks to render with the id they were consolidated under, stacked
37
+ * by ascending `zIndex`. The id comes along because it is what identifies the block
38
+ * across renders — its own `id` is optional, a consolidation key never is.
37
39
  */
38
- export declare function lazyLoadedBlocks(blocks: Map<string, ConsolidatedStickyBlock>, depth: 'back' | 'front', currentPagePos: number, lazyLoadDistance: number): ConsolidatedStickyBlock[];
40
+ export declare function lazyLoadedBlocks(blocks: Map<string, ConsolidatedStickyBlock>, depth: 'back' | 'front', currentPagePos: number, lazyLoadDistance: number): Array<[string, ConsolidatedStickyBlock]>;
39
41
  /**
40
42
  * Builds the CSS custom properties exposed on a {@link Scrllgngn} root.
41
43
  *
@@ -46,7 +48,223 @@ export declare function lazyLoadedBlocks(blocks: Map<string, ConsolidatedStickyB
46
48
  * never inherit the same names from an ancestor component. `initial` leaves them
47
49
  * behaving exactly as an unset variable would, without opening the hole.
48
50
  *
51
+ * The public set describes the component's own box and nothing else. The private
52
+ * one is narrowed to what the fixed layers may actually occupy — the component's
53
+ * box **intersected** with the visible zone — so a viewport offset pushes them off
54
+ * whatever covers that edge without the stylesheet doing arithmetic.
55
+ *
49
56
  * @param rect - The measured bounding rect, absent until the first resize lands.
57
+ * @param zone - The measured visible zone, absent until the probe reports.
58
+ * @returns The custom properties, keyed by their full name.
59
+ */
60
+ export declare function toScreenCssProps(rect?: ScreenRect, zone?: VisibleZoneRect): Record<string, string>;
61
+ /**
62
+ * Where one tracked block stands in the scroll, as handed to its `onScrolled`.
63
+ *
64
+ * Every progression is a `0`–`1` ratio measured against the threshold line, and
65
+ * weighted **by pixel height**: a zone of unequal pages is not the same as
66
+ * `(indexOfCurrentPage + currentPageProgression) / displayZone.length`.
67
+ *
68
+ * @property currentPage - Zero-based position of the page in view.
69
+ * @property currentPageProgression - `0` when the current page's top meets the
70
+ * threshold, `1` when its bottom does.
71
+ * @property displayZone - Every page the block's `id` appears on. A block without
72
+ * an `id` shows on one page, so its zone holds that page alone and its three
73
+ * progressions are equal.
74
+ * @property indexOfCurrentPageInDisplayZone - Where `currentPage` sits in
75
+ * `displayZone`.
76
+ * @property displayZoneProgression - Progression across the whole zone. On a zone
77
+ * like `[2, 3, 5]` it runs 0 → ~0.66 over pages 2–3, pauses while page 4 shows the
78
+ * block nowhere, then resumes to 1 on page 5.
79
+ * @property contiguousDisplayZone - The unbroken run of `displayZone` containing
80
+ * `currentPage` — `[2, 3]` for the zone above while on page 2 or 3.
81
+ * @property indexOfCurrentPageInContiguousDisplayZone - Where `currentPage` sits in
82
+ * `contiguousDisplayZone`.
83
+ * @property contiguousDisplayZoneProgression - Progression across that run alone,
84
+ * so it always spans a full 0 → 1.
85
+ */
86
+ export type TrackedBlockContext = {
87
+ currentPage: number;
88
+ currentPageProgression: number;
89
+ displayZone: number[];
90
+ indexOfCurrentPageInDisplayZone: number;
91
+ displayZoneProgression: number;
92
+ contiguousDisplayZone: number[];
93
+ indexOfCurrentPageInContiguousDisplayZone: number;
94
+ contiguousDisplayZoneProgression: number;
95
+ };
96
+ /** How far the threshold line has travelled through one page, in pixels. */
97
+ export type PageScrollMetrics = {
98
+ scrolled: number;
99
+ height: number;
100
+ };
101
+ /**
102
+ * Lists the pages each block id shows on.
103
+ *
104
+ * @param pages - The pages to walk, in order.
105
+ * @returns Zero-based page positions per id, ordered. Blocks without an `id` are
106
+ * left out: they show on their own page and nowhere else.
107
+ */
108
+ export declare function blockDisplayZones(pages?: PropsPage[]): Map<string, number[]>;
109
+ /**
110
+ * Isolates the unbroken run of pages around one page.
111
+ *
112
+ * @param zone - The pages a block shows on, in any order.
113
+ * @param page - The page the run must contain.
114
+ * @returns The run holding `page`, ascending, or an empty array when `zone` misses
115
+ * it entirely.
116
+ */
117
+ export declare function contiguousRunContaining(zone: number[], page: number): number[];
118
+ /**
119
+ * Measures how far the threshold line has travelled through each of the pages
120
+ * asked for.
121
+ *
122
+ * Only those pages are measured, not all of them: a long sequence would otherwise
123
+ * pay for a forced layout per page on every frame, where the tracked blocks rarely
124
+ * span more than a handful.
125
+ *
126
+ * @param pageElements - Every page slot, indexed by position.
127
+ * @param pagePositions - The positions worth measuring.
128
+ * @param thresholdY - The threshold line, in pixels from the viewport top. See
129
+ * {@link toThresholdY}.
130
+ * @returns The metrics of each measurable page, keyed by position.
131
+ */
132
+ export declare function measurePages(pageElements: HTMLElement[], pagePositions: Iterable<number>, thresholdY: number): Map<number, PageScrollMetrics>;
133
+ /**
134
+ * Assembles what one tracked block is told about the scroll.
135
+ *
136
+ * @param displayZone - The pages the block shows on.
137
+ * @param currentPage - Zero-based position of the page in view.
138
+ * @param metrics - Measurements covering at least `displayZone`.
139
+ * @returns The context to hand to `onScrolled`.
140
+ */
141
+ export declare function toTrackedBlockContext(displayZone: number[], currentPage: number, metrics: Map<number, PageScrollMetrics>): TrackedBlockContext;
142
+ /** Whether two contexts hold the same discrete fields, ignoring the progressions. */
143
+ export declare function sameDiscretePart(a: TrackedBlockContext, b?: TrackedBlockContext): boolean;
144
+ /** Whether two contexts are equal down to the progressions. */
145
+ export declare function contextsAreEqual(a: TrackedBlockContext, b?: TrackedBlockContext): boolean;
146
+ /**
147
+ * Builds the CSS custom properties carrying a tracked block's progressions.
148
+ *
149
+ * Only the continuous fields land here — the discrete ones are `data-` attributes,
150
+ * written by {@link toTrackedBlockDataAttributes}.
151
+ *
152
+ * @param context - The block's current context.
50
153
  * @returns The custom properties, keyed by their full name.
51
154
  */
52
- export declare function toScreenCssProps(rect?: ScreenRect): Record<string, string>;
155
+ export declare function toTrackedBlockCssProps(context: TrackedBlockContext): Record<string, string>;
156
+ /**
157
+ * Builds the `data-` attributes carrying a tracked block's discrete position.
158
+ *
159
+ * @param context - The block's current context.
160
+ * @returns The attributes, keyed by their full name.
161
+ */
162
+ export declare function toTrackedBlockDataAttributes(context: TrackedBlockContext): Record<string, string>;
163
+ /** What the per-frame pass needs to know about one tracked block. */
164
+ export type TrackedBlock = {
165
+ displayZone: number[];
166
+ onScrolled: (context: TrackedBlockContext) => void;
167
+ };
168
+ /**
169
+ * Keys a tracked sticky block.
170
+ *
171
+ * Sticky and scroll blocks share one map of wrappers, and a sticky block is one
172
+ * element for the whole sequence where a scroll block is one per page — hence two
173
+ * key shapes rather than a single ambiguous one.
174
+ *
175
+ * @param blockId - The id the block was consolidated under.
176
+ * @returns The key its wrapper is registered as.
177
+ */
178
+ export declare function stickyKey(blockId: string): string;
179
+ /**
180
+ * Keys a tracked scroll block.
181
+ *
182
+ * @param pagePos - Zero-based position of the page it belongs to.
183
+ * @param blockPos - Its position among that page's scroll blocks.
184
+ * @returns The key its wrapper is registered as.
185
+ */
186
+ export declare function scrollKey(pagePos: number, blockPos: number): string;
187
+ /**
188
+ * Picks the blocks of a page that scroll with the content.
189
+ *
190
+ * @param page - The page to read.
191
+ * @returns Its scroll blocks, in order. `depth` left out means `'scroll'`.
192
+ */
193
+ export declare function scrollBlocksOf(page: PropsPage): PropsBlock[];
194
+ /** How much of each viewport edge is covered by something else, in pixels. */
195
+ export type VisibleZoneRect = {
196
+ top: number;
197
+ right: number;
198
+ bottom: number;
199
+ left: number;
200
+ width: number;
201
+ height: number;
202
+ };
203
+ /** A length as authored: a bare number means pixels, a string is CSS's problem. */
204
+ export type ViewportOffset = number | string;
205
+ /** The four edges, as authored on the component. */
206
+ export type ViewportOffsets = {
207
+ top?: ViewportOffset;
208
+ right?: ViewportOffset;
209
+ bottom?: ViewportOffset;
210
+ left?: ViewportOffset;
211
+ };
212
+ /**
213
+ * Builds the custom properties carrying the visible zone's insets.
214
+ *
215
+ * They are emitted whatever the props hold, `0px` standing in for an edge left
216
+ * unset: the probe and the fixed layers position themselves against them, and an
217
+ * absent name would let an enclosing component's value inherit through.
218
+ *
219
+ * @param offsets - The edges as authored.
220
+ * @returns The custom properties, keyed by their full name.
221
+ */
222
+ export declare function toViewportOffsetCssProps(offsets: ViewportOffsets): Record<string, string>;
223
+ /**
224
+ * Reads the visible zone off the probe element.
225
+ *
226
+ * The probe is fixed and inset by the four offsets, so its rect *is* the zone —
227
+ * which is how a `var()`, a `clamp()` or a breakpoint-dependent length reaches
228
+ * JavaScript without anyone parsing a CSS length.
229
+ *
230
+ * @param probeRect - The probe's bounding rect.
231
+ * @returns The zone, as insets from each viewport edge plus its own size.
232
+ */
233
+ export declare function toVisibleZoneRect(probeRect: DOMRect): VisibleZoneRect;
234
+ /**
235
+ * Shrinks an {@link IntersectionObserver} root down to the visible zone.
236
+ *
237
+ * @param zone - The measured zone, absent until the probe reports.
238
+ * @returns A `rootMargin`, or `undefined` to leave the observer on the full
239
+ * viewport.
240
+ */
241
+ export declare function toVisibleZoneRootMargin(zone?: VisibleZoneRect): string | undefined;
242
+ /**
243
+ * Places the threshold line, in pixels from the viewport top.
244
+ *
245
+ * `thresholdOffsetPercent` reads as a percentage **of the visible zone**, not of
246
+ * the viewport: a threshold at 50% has to sit halfway down what the reader can
247
+ * actually see, or it lands under whatever covers the top of the screen.
248
+ *
249
+ * @param zone - The measured zone, absent until the probe reports.
250
+ * @param thresholdOffsetPercent - The offset as authored.
251
+ * @returns The line's distance from the viewport top.
252
+ */
253
+ export declare function toThresholdY(zone?: VisibleZoneRect, thresholdOffsetPercent?: number): number;
254
+ /**
255
+ * Restates the threshold as the percentage of the **viewport** an
256
+ * {@link IntersectionObserver} needs, since that is the only frame `rootMargin`
257
+ * knows.
258
+ *
259
+ * With no offsets set the zone spans the viewport and the number comes back
260
+ * unchanged, so nothing moves for a component that never asked for any of this.
261
+ *
262
+ * @param zone - The measured zone, absent until the probe reports.
263
+ * @param thresholdOffsetPercent - The offset as authored.
264
+ * @returns The equivalent viewport percentage, to hand to the internal `Paginator`.
265
+ */
266
+ export declare function toPaginatorThresholdPercent(zone?: VisibleZoneRect, thresholdOffsetPercent?: number): number | undefined;
267
+ /** How often the visible zone is re-read while the component is on screen, in ms. */
268
+ export declare const visibleZonePollInterval = 100;
269
+ /** Whether two zones hold the same six measurements. */
270
+ export declare function visibleZonesAreEqual(a?: VisibleZoneRect, b?: VisibleZoneRect): boolean;
@@ -1,3 +1,4 @@
1
+ import { clamp } from '../../agnostic/numbers/clamp/index.js';
1
2
  import { randomHash } from '../../agnostic/random/uuid/index.js';
2
3
  /**
3
4
  * Merges the sticky blocks of every page into a single entry per block id.
@@ -50,18 +51,19 @@ export function consolidateStickyBlocks(pages) {
50
51
  * @param currentPagePos - Zero-based position of the page in view.
51
52
  * @param lazyLoadDistance - How many pages around the current one still mount
52
53
  * their blocks.
53
- * @returns The blocks to render, stacked by ascending `zIndex`.
54
+ * @returns The blocks to render with the id they were consolidated under, stacked
55
+ * by ascending `zIndex`. The id comes along because it is what identifies the block
56
+ * across renders — its own `id` is optional, a consolidation key never is.
54
57
  */
55
58
  export function lazyLoadedBlocks(blocks, depth, currentPagePos, lazyLoadDistance) {
56
59
  return Array
57
60
  .from(blocks)
58
- .map(([, block]) => block)
59
- .filter(block => block.depth === depth
61
+ .filter(([, block]) => block.depth === depth
60
62
  && block.displayOnPages.some(dispPage => {
61
63
  const absDiff = Math.abs(dispPage - currentPagePos);
62
64
  return absDiff <= lazyLoadDistance;
63
65
  }))
64
- .sort((a, b) => (a.zIndex ?? -Infinity) - (b.zIndex ?? -Infinity));
66
+ .sort(([, a], [, b]) => (a.zIndex ?? -Infinity) - (b.zIndex ?? -Infinity));
65
67
  }
66
68
  /**
67
69
  * Builds the CSS custom properties exposed on a {@link Scrllgngn} root.
@@ -73,10 +75,16 @@ export function lazyLoadedBlocks(blocks, depth, currentPagePos, lazyLoadDistance
73
75
  * never inherit the same names from an ancestor component. `initial` leaves them
74
76
  * behaving exactly as an unset variable would, without opening the hole.
75
77
  *
78
+ * The public set describes the component's own box and nothing else. The private
79
+ * one is narrowed to what the fixed layers may actually occupy — the component's
80
+ * box **intersected** with the visible zone — so a viewport offset pushes them off
81
+ * whatever covers that edge without the stylesheet doing arithmetic.
82
+ *
76
83
  * @param rect - The measured bounding rect, absent until the first resize lands.
84
+ * @param zone - The measured visible zone, absent until the probe reports.
77
85
  * @returns The custom properties, keyed by their full name.
78
86
  */
79
- export function toScreenCssProps(rect) {
87
+ export function toScreenCssProps(rect, zone) {
80
88
  const publicProps = rect === undefined
81
89
  ? {}
82
90
  : {
@@ -89,14 +97,333 @@ export function toScreenCssProps(rect) {
89
97
  '--lm-scrllgngn-screen-height': `${rect.height}px`,
90
98
  '--lm-scrllgngn-screen-height-raw': `${rect.height}`
91
99
  };
100
+ const inZone = toZoneClampedRect(rect, zone);
92
101
  return {
93
102
  ...publicProps,
94
- '--PRIVATE-left': toPrivateLength(rect?.left),
95
- '--PRIVATE-right': toPrivateLength(rect?.right),
96
- '--PRIVATE-width': toPrivateLength(rect?.width),
103
+ '--PRIVATE-left': toPrivateLength(inZone?.left),
104
+ '--PRIVATE-right': toPrivateLength(inZone?.right),
105
+ '--PRIVATE-width': toPrivateLength(inZone?.width),
97
106
  '--PRIVATE-height': toPrivateLength(rect?.height)
98
107
  };
99
108
  }
109
+ /** The component's box, cut down to the horizontal span of the visible zone. */
110
+ function toZoneClampedRect(rect, zone) {
111
+ if (rect === undefined)
112
+ return undefined;
113
+ if (zone === undefined)
114
+ return rect;
115
+ const left = Math.max(rect.left, zone.left);
116
+ const right = Math.min(rect.right, window.innerWidth - zone.right);
117
+ return {
118
+ left,
119
+ right,
120
+ width: Math.max(right - left, 0),
121
+ height: rect.height
122
+ };
123
+ }
100
124
  function toPrivateLength(value) {
101
125
  return value === undefined ? 'initial' : `${value}px`;
102
126
  }
127
+ /**
128
+ * Lists the pages each block id shows on.
129
+ *
130
+ * @param pages - The pages to walk, in order.
131
+ * @returns Zero-based page positions per id, ordered. Blocks without an `id` are
132
+ * left out: they show on their own page and nowhere else.
133
+ */
134
+ export function blockDisplayZones(pages) {
135
+ const zones = new Map();
136
+ (pages ?? []).forEach((page, pagePos) => {
137
+ for (const block of page.blocks ?? []) {
138
+ if (block.id === undefined)
139
+ continue;
140
+ const zone = zones.get(block.id) ?? [];
141
+ if (!zone.includes(pagePos))
142
+ zone.push(pagePos);
143
+ zones.set(block.id, zone);
144
+ }
145
+ });
146
+ return zones;
147
+ }
148
+ /**
149
+ * Isolates the unbroken run of pages around one page.
150
+ *
151
+ * @param zone - The pages a block shows on, in any order.
152
+ * @param page - The page the run must contain.
153
+ * @returns The run holding `page`, ascending, or an empty array when `zone` misses
154
+ * it entirely.
155
+ */
156
+ export function contiguousRunContaining(zone, page) {
157
+ const ascending = [...zone].sort((a, b) => a - b);
158
+ let run = [];
159
+ for (const pagePos of ascending) {
160
+ const previous = run[run.length - 1];
161
+ if (previous !== undefined && pagePos !== previous + 1) {
162
+ if (run.includes(page))
163
+ return run;
164
+ run = [];
165
+ }
166
+ run.push(pagePos);
167
+ }
168
+ return run.includes(page) ? run : [];
169
+ }
170
+ /**
171
+ * Measures how far the threshold line has travelled through each of the pages
172
+ * asked for.
173
+ *
174
+ * Only those pages are measured, not all of them: a long sequence would otherwise
175
+ * pay for a forced layout per page on every frame, where the tracked blocks rarely
176
+ * span more than a handful.
177
+ *
178
+ * @param pageElements - Every page slot, indexed by position.
179
+ * @param pagePositions - The positions worth measuring.
180
+ * @param thresholdY - The threshold line, in pixels from the viewport top. See
181
+ * {@link toThresholdY}.
182
+ * @returns The metrics of each measurable page, keyed by position.
183
+ */
184
+ export function measurePages(pageElements, pagePositions, thresholdY) {
185
+ const measured = new Map();
186
+ for (const pagePos of pagePositions) {
187
+ const element = pageElements[pagePos];
188
+ if (element === undefined)
189
+ continue;
190
+ const { top, height } = element.getBoundingClientRect();
191
+ measured.set(pagePos, {
192
+ scrolled: clamp(thresholdY - top, 0, height),
193
+ height
194
+ });
195
+ }
196
+ return measured;
197
+ }
198
+ /** Pixel-weighted progression across a set of pages, `0` when none is measured. */
199
+ function zoneProgression(zone, metrics) {
200
+ let scrolled = 0;
201
+ let height = 0;
202
+ for (const pagePos of zone) {
203
+ const pageMetrics = metrics.get(pagePos);
204
+ if (pageMetrics === undefined)
205
+ continue;
206
+ scrolled += pageMetrics.scrolled;
207
+ height += pageMetrics.height;
208
+ }
209
+ return height === 0 ? 0 : scrolled / height;
210
+ }
211
+ /**
212
+ * Assembles what one tracked block is told about the scroll.
213
+ *
214
+ * @param displayZone - The pages the block shows on.
215
+ * @param currentPage - Zero-based position of the page in view.
216
+ * @param metrics - Measurements covering at least `displayZone`.
217
+ * @returns The context to hand to `onScrolled`.
218
+ */
219
+ export function toTrackedBlockContext(displayZone, currentPage, metrics) {
220
+ const contiguousDisplayZone = contiguousRunContaining(displayZone, currentPage);
221
+ return {
222
+ currentPage,
223
+ currentPageProgression: zoneProgression([currentPage], metrics),
224
+ displayZone,
225
+ indexOfCurrentPageInDisplayZone: displayZone.indexOf(currentPage),
226
+ displayZoneProgression: zoneProgression(displayZone, metrics),
227
+ contiguousDisplayZone,
228
+ indexOfCurrentPageInContiguousDisplayZone: contiguousDisplayZone.indexOf(currentPage),
229
+ contiguousDisplayZoneProgression: zoneProgression(contiguousDisplayZone, metrics)
230
+ };
231
+ }
232
+ /** The fields that only move when the page does, so they can be written sparingly. */
233
+ function discretePart(context) {
234
+ return [
235
+ context.currentPage,
236
+ context.indexOfCurrentPageInDisplayZone,
237
+ context.indexOfCurrentPageInContiguousDisplayZone,
238
+ context.displayZone.join(','),
239
+ context.contiguousDisplayZone.join(',')
240
+ ].join('|');
241
+ }
242
+ /** Whether two contexts hold the same discrete fields, ignoring the progressions. */
243
+ export function sameDiscretePart(a, b) {
244
+ return b !== undefined && discretePart(a) === discretePart(b);
245
+ }
246
+ /** Whether two contexts are equal down to the progressions. */
247
+ export function contextsAreEqual(a, b) {
248
+ return sameDiscretePart(a, b)
249
+ && b !== undefined
250
+ && a.currentPageProgression === b.currentPageProgression
251
+ && a.displayZoneProgression === b.displayZoneProgression
252
+ && a.contiguousDisplayZoneProgression === b.contiguousDisplayZoneProgression;
253
+ }
254
+ /**
255
+ * Builds the CSS custom properties carrying a tracked block's progressions.
256
+ *
257
+ * Only the continuous fields land here — the discrete ones are `data-` attributes,
258
+ * written by {@link toTrackedBlockDataAttributes}.
259
+ *
260
+ * @param context - The block's current context.
261
+ * @returns The custom properties, keyed by their full name.
262
+ */
263
+ export function toTrackedBlockCssProps(context) {
264
+ return {
265
+ '--lm-scrllgngn-block-current-page-progression-ratio': `${context.currentPageProgression}`,
266
+ '--lm-scrllgngn-block-display-zone-progression-ratio': `${context.displayZoneProgression}`,
267
+ '--lm-scrllgngn-block-contiguous-display-zone-progression-ratio': `${context.contiguousDisplayZoneProgression}`
268
+ };
269
+ }
270
+ /**
271
+ * Builds the `data-` attributes carrying a tracked block's discrete position.
272
+ *
273
+ * @param context - The block's current context.
274
+ * @returns The attributes, keyed by their full name.
275
+ */
276
+ export function toTrackedBlockDataAttributes(context) {
277
+ return {
278
+ 'data-current-page': `${context.currentPage}`,
279
+ 'data-display-zone': context.displayZone.join(','),
280
+ 'data-index-of-current-page-in-display-zone': `${context.indexOfCurrentPageInDisplayZone}`,
281
+ 'data-contiguous-display-zone': context.contiguousDisplayZone.join(','),
282
+ 'data-index-of-current-page-in-contiguous-display-zone': `${context.indexOfCurrentPageInContiguousDisplayZone}`
283
+ };
284
+ }
285
+ /**
286
+ * Keys a tracked sticky block.
287
+ *
288
+ * Sticky and scroll blocks share one map of wrappers, and a sticky block is one
289
+ * element for the whole sequence where a scroll block is one per page — hence two
290
+ * key shapes rather than a single ambiguous one.
291
+ *
292
+ * @param blockId - The id the block was consolidated under.
293
+ * @returns The key its wrapper is registered as.
294
+ */
295
+ export function stickyKey(blockId) {
296
+ return `sticky:${blockId}`;
297
+ }
298
+ /**
299
+ * Keys a tracked scroll block.
300
+ *
301
+ * @param pagePos - Zero-based position of the page it belongs to.
302
+ * @param blockPos - Its position among that page's scroll blocks.
303
+ * @returns The key its wrapper is registered as.
304
+ */
305
+ export function scrollKey(pagePos, blockPos) {
306
+ return `scroll:${pagePos}:${blockPos}`;
307
+ }
308
+ /**
309
+ * Picks the blocks of a page that scroll with the content.
310
+ *
311
+ * @param page - The page to read.
312
+ * @returns Its scroll blocks, in order. `depth` left out means `'scroll'`.
313
+ */
314
+ export function scrollBlocksOf(page) {
315
+ return page.blocks?.filter(block => block.depth === 'scroll' || block.depth === undefined) ?? [];
316
+ }
317
+ function toCssLength(offset) {
318
+ if (offset === undefined)
319
+ return '0px';
320
+ return typeof offset === 'number' ? `${offset}px` : offset;
321
+ }
322
+ /**
323
+ * Builds the custom properties carrying the visible zone's insets.
324
+ *
325
+ * They are emitted whatever the props hold, `0px` standing in for an edge left
326
+ * unset: the probe and the fixed layers position themselves against them, and an
327
+ * absent name would let an enclosing component's value inherit through.
328
+ *
329
+ * @param offsets - The edges as authored.
330
+ * @returns The custom properties, keyed by their full name.
331
+ */
332
+ export function toViewportOffsetCssProps(offsets) {
333
+ const top = toCssLength(offsets.top);
334
+ const right = toCssLength(offsets.right);
335
+ const bottom = toCssLength(offsets.bottom);
336
+ const left = toCssLength(offsets.left);
337
+ return {
338
+ '--lm-scrllgngn-viewport-offset-top': top,
339
+ '--lm-scrllgngn-viewport-offset-right': right,
340
+ '--lm-scrllgngn-viewport-offset-bottom': bottom,
341
+ '--lm-scrllgngn-viewport-offset-left': left,
342
+ '--PRIVATE-viewport-offset-top': top,
343
+ '--PRIVATE-viewport-offset-right': right,
344
+ '--PRIVATE-viewport-offset-bottom': bottom,
345
+ '--PRIVATE-viewport-offset-left': left
346
+ };
347
+ }
348
+ /**
349
+ * Reads the visible zone off the probe element.
350
+ *
351
+ * The probe is fixed and inset by the four offsets, so its rect *is* the zone —
352
+ * which is how a `var()`, a `clamp()` or a breakpoint-dependent length reaches
353
+ * JavaScript without anyone parsing a CSS length.
354
+ *
355
+ * @param probeRect - The probe's bounding rect.
356
+ * @returns The zone, as insets from each viewport edge plus its own size.
357
+ */
358
+ export function toVisibleZoneRect(probeRect) {
359
+ return {
360
+ top: probeRect.top,
361
+ right: window.innerWidth - probeRect.right,
362
+ bottom: window.innerHeight - probeRect.bottom,
363
+ left: probeRect.left,
364
+ width: probeRect.width,
365
+ height: probeRect.height
366
+ };
367
+ }
368
+ /**
369
+ * Shrinks an {@link IntersectionObserver} root down to the visible zone.
370
+ *
371
+ * @param zone - The measured zone, absent until the probe reports.
372
+ * @returns A `rootMargin`, or `undefined` to leave the observer on the full
373
+ * viewport.
374
+ */
375
+ export function toVisibleZoneRootMargin(zone) {
376
+ if (zone === undefined)
377
+ return undefined;
378
+ return `${-zone.top}px ${-zone.right}px ${-zone.bottom}px ${-zone.left}px`;
379
+ }
380
+ /**
381
+ * Places the threshold line, in pixels from the viewport top.
382
+ *
383
+ * `thresholdOffsetPercent` reads as a percentage **of the visible zone**, not of
384
+ * the viewport: a threshold at 50% has to sit halfway down what the reader can
385
+ * actually see, or it lands under whatever covers the top of the screen.
386
+ *
387
+ * @param zone - The measured zone, absent until the probe reports.
388
+ * @param thresholdOffsetPercent - The offset as authored.
389
+ * @returns The line's distance from the viewport top.
390
+ */
391
+ export function toThresholdY(zone, thresholdOffsetPercent) {
392
+ const percent = thresholdOffsetPercent ?? 0;
393
+ if (zone === undefined)
394
+ return window.innerHeight * percent / 100;
395
+ return zone.top + zone.height * percent / 100;
396
+ }
397
+ /**
398
+ * Restates the threshold as the percentage of the **viewport** an
399
+ * {@link IntersectionObserver} needs, since that is the only frame `rootMargin`
400
+ * knows.
401
+ *
402
+ * With no offsets set the zone spans the viewport and the number comes back
403
+ * unchanged, so nothing moves for a component that never asked for any of this.
404
+ *
405
+ * @param zone - The measured zone, absent until the probe reports.
406
+ * @param thresholdOffsetPercent - The offset as authored.
407
+ * @returns The equivalent viewport percentage, to hand to the internal `Paginator`.
408
+ */
409
+ export function toPaginatorThresholdPercent(zone, thresholdOffsetPercent) {
410
+ if (zone === undefined)
411
+ return thresholdOffsetPercent;
412
+ const viewportHeight = window.innerHeight;
413
+ if (viewportHeight === 0)
414
+ return thresholdOffsetPercent;
415
+ return toThresholdY(zone, thresholdOffsetPercent) / viewportHeight * 100;
416
+ }
417
+ /** How often the visible zone is re-read while the component is on screen, in ms. */
418
+ export const visibleZonePollInterval = 100;
419
+ /** Whether two zones hold the same six measurements. */
420
+ export function visibleZonesAreEqual(a, b) {
421
+ if (a === undefined || b === undefined)
422
+ return a === b;
423
+ return a.top === b.top
424
+ && a.right === b.right
425
+ && a.bottom === b.bottom
426
+ && a.left === b.left
427
+ && a.width === b.width
428
+ && a.height === b.height;
429
+ }
@@ -1,5 +1,5 @@
1
1
  import { type FunctionComponent, type PropsWithChildren } from 'react';
2
- import type { WithClassName } from '../utils/types.js';
2
+ import type { WithClassName, WithViewportObservation } from '../utils/types.js';
3
3
  import { type ScrollState } from './utils.js';
4
4
  /**
5
5
  * Props for the {@link ScrollListener} component.
@@ -17,17 +17,21 @@ import { type ScrollState } from './utils.js';
17
17
  * it has fully left it. Never on mount.
18
18
  * @property onScrollDirectionChanged - Called after the document scroll
19
19
  * direction changed, with `'up'` or `'down'`. Never on mount.
20
+ * @property threshold - How much of the component has to be in view before it
21
+ * counts as visible, forwarded to the internal {@link IntersectionObserver}.
22
+ * @property root - The observer's root. Defaults to the viewport.
23
+ * @property rootMargin - Grows or shrinks that root before measuring.
20
24
  * @property className - Optional additional class name(s) applied to the root element.
21
25
  * @property children - React nodes rendered inside the scroll listener container.
22
26
  */
23
- export type Props = PropsWithChildren<WithClassName<{
27
+ export type Props = PropsWithChildren<WithClassName<WithViewportObservation<{
24
28
  startOnVisible?: boolean;
25
29
  stopOnHidden?: boolean;
26
30
  onScrollStateChanged?: (scrollState?: ScrollState) => void;
27
31
  onVisibilityChanged?: (isVisible: boolean) => void;
28
32
  onScrollProgressChanged?: (progress: number) => void;
29
33
  onScrollDirectionChanged?: (direction: 'up' | 'down') => void;
30
- }>>;
34
+ }>>>;
31
35
  /**
32
36
  * Exposes scroll metrics — both the document's and its own — as CSS custom
33
37
  * properties on its root element, so scroll-driven styling needs no JavaScript
@@ -45,7 +45,7 @@ import cssModule from './styles.module.css';
45
45
  * for everyone, each element only for itself. The listeners exist only while at
46
46
  * least one instance is tracking.
47
47
  */
48
- export const ScrollListener = ({ startOnVisible, stopOnHidden, onScrollStateChanged, onVisibilityChanged, onScrollProgressChanged, onScrollDirectionChanged, className, children }) => {
48
+ export const ScrollListener = ({ startOnVisible, stopOnHidden, threshold, root, rootMargin, onScrollStateChanged, onVisibilityChanged, onScrollProgressChanged, onScrollDirectionChanged, className, children }) => {
49
49
  // State & refs
50
50
  const [subscriberId] = useState(() => randomHash(6));
51
51
  const [scrollState, setScrollState] = useState();
@@ -96,5 +96,5 @@ export const ScrollListener = ({ startOnVisible, stopOnHidden, onScrollStateChan
96
96
  const customProps = scrollState === undefined
97
97
  ? {}
98
98
  : toScrollCssProps(scrollState);
99
- return _jsx("div", { className: rootClss, ref: rootRef, style: { ...customProps }, children: _jsx(IntersectionObserverComponent, { onIntersected: handleIntersection, children: children }) });
99
+ return _jsx("div", { className: rootClss, ref: rootRef, style: { ...customProps }, children: _jsx(IntersectionObserverComponent, { threshold: threshold, root: root, rootMargin: rootMargin, onIntersected: handleIntersection, children: children }) });
100
100
  };