@design-edito/tools 0.5.2 → 0.5.4

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 (152) hide show
  1. package/agnostic/html/deep-select/index.js +5 -3
  2. package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +9 -9
  3. package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +9 -9
  4. package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +3 -3
  5. package/agnostic/html/hyper-json/smart-tags/isolated/index.js +3 -3
  6. package/agnostic/html/index.d.ts +1 -1
  7. package/agnostic/html/index.js +1 -1
  8. package/agnostic/index.d.ts +2 -2
  9. package/agnostic/index.js +2 -2
  10. package/agnostic/misc/index.d.ts +3 -3
  11. package/agnostic/misc/index.js +3 -3
  12. package/agnostic/misc/logs/index.d.ts +1 -1
  13. package/agnostic/misc/logs/index.js +1 -1
  14. package/agnostic/numbers/index.d.ts +1 -1
  15. package/agnostic/numbers/index.js +1 -1
  16. package/agnostic/optim/index.d.ts +1 -1
  17. package/agnostic/optim/index.js +1 -1
  18. package/agnostic/strings/index.d.ts +2 -2
  19. package/agnostic/strings/index.js +2 -2
  20. package/agnostic/time/dates/format-date/index.d.ts +4 -2
  21. package/agnostic/time/dates/format-date/index.js +43 -94
  22. package/agnostic/time/dates/format-date/index.test.js +154 -0
  23. package/agnostic/time/dates/format-date/parts.d.ts +28 -0
  24. package/agnostic/time/dates/format-date/parts.js +136 -0
  25. package/agnostic/time/dates/format-date/types.d.ts +84 -0
  26. package/agnostic/time/dates/format-date/types.js +1 -0
  27. package/agnostic/time/duration/format-duration/index.d.ts +42 -0
  28. package/agnostic/time/duration/format-duration/index.js +94 -0
  29. package/agnostic/time/duration/format-duration/index.test.d.ts +1 -0
  30. package/agnostic/time/duration/format-duration/index.test.js +148 -0
  31. package/agnostic/time/duration/format-duration/parts.d.ts +32 -0
  32. package/agnostic/time/duration/format-duration/parts.js +107 -0
  33. package/agnostic/time/duration/format-duration/types.d.ts +93 -0
  34. package/agnostic/time/duration/format-duration/types.js +1 -0
  35. package/agnostic/time/duration/index.d.ts +6 -1
  36. package/agnostic/time/duration/index.js +2 -2
  37. package/agnostic/time/duration/index.test.d.ts +1 -0
  38. package/agnostic/time/duration/index.test.js +168 -0
  39. package/agnostic/time/index.d.ts +1 -1
  40. package/agnostic/time/index.js +1 -1
  41. package/components/BeforeAfter/index.controlled.d.ts +25 -25
  42. package/components/BeforeAfter/index.controlled.js +47 -53
  43. package/components/BeforeAfter/index.d.ts +20 -23
  44. package/components/BeforeAfter/index.js +48 -40
  45. package/components/Button/index.js +1 -1
  46. package/components/Clippable/index.d.ts +17 -26
  47. package/components/Clippable/index.js +21 -23
  48. package/components/Disclaimer/index.d.ts +32 -24
  49. package/components/Disclaimer/index.js +29 -24
  50. package/components/Drawer/index.d.ts +45 -25
  51. package/components/Drawer/index.js +61 -52
  52. package/components/EventListener/index.d.ts +4 -5
  53. package/components/Gallery/index.d.ts +14 -16
  54. package/components/Gallery/index.js +25 -30
  55. package/components/Iframe/index.js +2 -2
  56. package/components/Image/index.d.ts +5 -15
  57. package/components/Image/index.js +9 -34
  58. package/components/Input/index.d.ts +23 -19
  59. package/components/Input/index.js +30 -20
  60. package/components/JsonEditor/index.d.ts +177 -9
  61. package/components/JsonEditor/index.js +133 -77
  62. package/components/JsonEditor/types.d.ts +11 -0
  63. package/components/JsonEditor/types.js +1 -0
  64. package/components/JsonEditor/utils.d.ts +8 -0
  65. package/components/JsonEditor/utils.js +23 -0
  66. package/components/ListLoader/index.controlled.d.ts +5 -5
  67. package/components/ListLoader/index.controlled.js +4 -4
  68. package/components/ListLoader/index.d.ts +11 -11
  69. package/components/ListLoader/index.js +12 -12
  70. package/components/Overlayer/index.d.ts +4 -4
  71. package/components/Overlayer/index.js +6 -6
  72. package/components/Paginator/index.d.ts +7 -12
  73. package/components/Paginator/index.js +7 -11
  74. package/components/ResizeObserver/index.d.ts +14 -3
  75. package/components/ResizeObserver/index.js +49 -24
  76. package/components/Scrllgngn/index.d.ts +17 -13
  77. package/components/Scrllgngn/index.js +25 -84
  78. package/components/Scrllgngn/utils.d.ts +52 -0
  79. package/components/Scrllgngn/utils.js +102 -0
  80. package/components/ScrollListener/index.d.ts +43 -28
  81. package/components/ScrollListener/index.js +59 -79
  82. package/components/ScrollListener/utils.d.ts +37 -16
  83. package/components/ScrollListener/utils.js +141 -28
  84. package/components/Select/index.d.ts +24 -20
  85. package/components/Select/index.js +30 -20
  86. package/components/Sequencer/index.controlled.d.ts +16 -26
  87. package/components/Sequencer/index.controlled.js +15 -12
  88. package/components/Sequencer/index.d.ts +25 -30
  89. package/components/Sequencer/index.js +20 -34
  90. package/components/Subtitles/index.d.ts +29 -5
  91. package/components/Subtitles/index.js +90 -12
  92. package/components/Textarea/index.d.ts +26 -20
  93. package/components/Textarea/index.js +40 -21
  94. package/components/Theatre/index.d.ts +35 -39
  95. package/components/Theatre/index.js +46 -58
  96. package/components/UIModule/index.d.ts +25 -29
  97. package/components/UIModule/index.js +81 -110
  98. package/components/Video/index.controlled.d.ts +51 -51
  99. package/components/Video/index.controlled.js +75 -72
  100. package/components/Video/index.d.ts +12 -14
  101. package/components/Video/index.js +41 -86
  102. package/components/Video/utils.d.ts +35 -3
  103. package/components/Video/utils.js +37 -14
  104. package/components/index.d.ts +6 -6
  105. package/components/index.js +6 -6
  106. package/components/utils/index.d.ts +22 -0
  107. package/components/utils/index.js +31 -0
  108. package/index.d.ts +1 -1
  109. package/index.js +1 -1
  110. package/node/@aws-s3/storage/directory/index.d.ts +2 -2
  111. package/node/@aws-s3/storage/directory/index.js +2 -2
  112. package/node/@aws-s3/storage/file/index.d.ts +1 -1
  113. package/node/@aws-s3/storage/file/index.js +1 -1
  114. package/node/@aws-s3/storage/index.d.ts +1 -1
  115. package/node/@aws-s3/storage/index.js +1 -1
  116. package/node/@google-cloud/storage/directory/index.d.ts +2 -2
  117. package/node/@google-cloud/storage/directory/index.js +2 -2
  118. package/node/@google-cloud/storage/file/index.d.ts +2 -2
  119. package/node/@google-cloud/storage/file/index.js +2 -2
  120. package/node/@google-cloud/storage/index.d.ts +1 -1
  121. package/node/@google-cloud/storage/index.js +1 -1
  122. package/node/cloud-storage/operations/index.d.ts +2 -2
  123. package/node/cloud-storage/operations/index.js +2 -2
  124. package/node/ftps/file/index.d.ts +2 -2
  125. package/node/ftps/file/index.js +2 -2
  126. package/node/images/index.d.ts +1 -1
  127. package/node/images/index.js +1 -1
  128. package/node/images/transform/operations/index.d.ts +2 -2
  129. package/node/images/transform/operations/index.js +2 -2
  130. package/node/index.d.ts +1 -1
  131. package/node/index.js +1 -1
  132. package/node/process/index.d.ts +1 -1
  133. package/node/process/index.js +1 -1
  134. package/node/sftp/file/index.d.ts +2 -2
  135. package/node/sftp/file/index.js +2 -2
  136. package/node/sftp/index.d.ts +1 -1
  137. package/node/sftp/index.js +1 -1
  138. package/package.json +20 -5
  139. package/TODO.md +0 -269
  140. package/components/BeforeAfter/utils.d.ts +0 -4
  141. package/components/BeforeAfter/utils.js +0 -9
  142. package/components/Input/index.controlled.d.ts +0 -42
  143. package/components/Input/index.controlled.js +0 -54
  144. package/components/Select/index.controlled.d.ts +0 -43
  145. package/components/Select/index.controlled.js +0 -54
  146. package/components/Subtitles/types.d.ts +0 -24
  147. package/components/Subtitles/utils.d.ts +0 -32
  148. package/components/Subtitles/utils.js +0 -107
  149. package/components/Textarea/index.controlled.d.ts +0 -50
  150. package/components/Textarea/index.controlled.js +0 -75
  151. /package/{components/Subtitles/types.js → agnostic/time/dates/format-date/index.test.d.ts} +0 -0
  152. /package/components/ResizeObserver/{style.module.css → styles.module.css} +0 -0
@@ -0,0 +1,52 @@
1
+ import type { PropsPage, PropsStickyBlock } from './index.js';
2
+ /** The subset of a bounding rect a {@link Scrllgngn} tracks and exposes. */
3
+ export type ScreenRect = {
4
+ left: number;
5
+ right: number;
6
+ width: number;
7
+ height: number;
8
+ };
9
+ /** Zero-based positions of the pages a consolidated block is displayed on. */
10
+ export type BlockConsolidatedData = {
11
+ displayOnPages: number[];
12
+ };
13
+ /** A sticky block, merged across every page it appears on. */
14
+ export type ConsolidatedStickyBlock = PropsStickyBlock & BlockConsolidatedData;
15
+ /**
16
+ * Merges the sticky blocks of every page into a single entry per block id.
17
+ *
18
+ * A block repeated across pages under the same `id` becomes one sticky block
19
+ * listing all the pages it shows on, so it stays mounted across them instead of
20
+ * being torn down and rebuilt at every page change. Blocks without an `id` get a
21
+ * random one and are therefore never merged.
22
+ *
23
+ * @param pages - The pages to walk, in order.
24
+ * @returns The sticky blocks, keyed by id. Scroll blocks are left out.
25
+ */
26
+ export declare function consolidateStickyBlocks(pages?: PropsPage[]): Map<string, ConsolidatedStickyBlock>;
27
+ /**
28
+ * Picks the sticky blocks of one layer close enough to the current page to be
29
+ * worth mounting.
30
+ *
31
+ * @param blocks - Every consolidated sticky block.
32
+ * @param depth - The layer to pick from.
33
+ * @param currentPagePos - Zero-based position of the page in view.
34
+ * @param lazyLoadDistance - How many pages around the current one still mount
35
+ * their blocks.
36
+ * @returns The blocks to render, stacked by ascending `zIndex`.
37
+ */
38
+ export declare function lazyLoadedBlocks(blocks: Map<string, ConsolidatedStickyBlock>, depth: 'back' | 'front', currentPagePos: number, lazyLoadDistance: number): ConsolidatedStickyBlock[];
39
+ /**
40
+ * Builds the CSS custom properties exposed on a {@link Scrllgngn} root.
41
+ *
42
+ * The public `screen-*` set follows the usual convention and stays absent until
43
+ * the first measurement lands. The `--PRIVATE-*` set is what the component's own
44
+ * stylesheet positions its fixed layers with, and it is emitted from the very
45
+ * first render — as `initial`, the guaranteed-invalid value — so those layers can
46
+ * never inherit the same names from an ancestor component. `initial` leaves them
47
+ * behaving exactly as an unset variable would, without opening the hole.
48
+ *
49
+ * @param rect - The measured bounding rect, absent until the first resize lands.
50
+ * @returns The custom properties, keyed by their full name.
51
+ */
52
+ export declare function toScreenCssProps(rect?: ScreenRect): Record<string, string>;
@@ -0,0 +1,102 @@
1
+ import { randomHash } from '../../agnostic/random/uuid/index.js';
2
+ /**
3
+ * Merges the sticky blocks of every page into a single entry per block id.
4
+ *
5
+ * A block repeated across pages under the same `id` becomes one sticky block
6
+ * listing all the pages it shows on, so it stays mounted across them instead of
7
+ * being torn down and rebuilt at every page change. Blocks without an `id` get a
8
+ * random one and are therefore never merged.
9
+ *
10
+ * @param pages - The pages to walk, in order.
11
+ * @returns The sticky blocks, keyed by id. Scroll blocks are left out.
12
+ */
13
+ export function consolidateStickyBlocks(pages) {
14
+ const consolidatedBlocks = new Map();
15
+ for (const page of pages ?? []) {
16
+ const pageIndex = pages?.indexOf(page) ?? -1;
17
+ for (const block of page.blocks ?? []) {
18
+ const blockId = block.id ?? randomHash(12);
19
+ const found = consolidatedBlocks.get(blockId);
20
+ if (found !== undefined)
21
+ consolidatedBlocks.set(blockId, {
22
+ ...found,
23
+ ...block,
24
+ displayOnPages: [
25
+ ...found.displayOnPages,
26
+ pageIndex
27
+ ]
28
+ });
29
+ else
30
+ consolidatedBlocks.set(blockId, {
31
+ ...block,
32
+ displayOnPages: [pageIndex]
33
+ });
34
+ }
35
+ }
36
+ return new Map(Array
37
+ .from(consolidatedBlocks)
38
+ .filter((e) => {
39
+ const block = e[1];
40
+ return block.depth === 'back'
41
+ || block.depth === 'front';
42
+ }));
43
+ }
44
+ /**
45
+ * Picks the sticky blocks of one layer close enough to the current page to be
46
+ * worth mounting.
47
+ *
48
+ * @param blocks - Every consolidated sticky block.
49
+ * @param depth - The layer to pick from.
50
+ * @param currentPagePos - Zero-based position of the page in view.
51
+ * @param lazyLoadDistance - How many pages around the current one still mount
52
+ * their blocks.
53
+ * @returns The blocks to render, stacked by ascending `zIndex`.
54
+ */
55
+ export function lazyLoadedBlocks(blocks, depth, currentPagePos, lazyLoadDistance) {
56
+ return Array
57
+ .from(blocks)
58
+ .map(([, block]) => block)
59
+ .filter(block => block.depth === depth
60
+ && block.displayOnPages.some(dispPage => {
61
+ const absDiff = Math.abs(dispPage - currentPagePos);
62
+ return absDiff <= lazyLoadDistance;
63
+ }))
64
+ .sort((a, b) => (a.zIndex ?? -Infinity) - (b.zIndex ?? -Infinity));
65
+ }
66
+ /**
67
+ * Builds the CSS custom properties exposed on a {@link Scrllgngn} root.
68
+ *
69
+ * The public `screen-*` set follows the usual convention and stays absent until
70
+ * the first measurement lands. The `--PRIVATE-*` set is what the component's own
71
+ * stylesheet positions its fixed layers with, and it is emitted from the very
72
+ * first render — as `initial`, the guaranteed-invalid value — so those layers can
73
+ * never inherit the same names from an ancestor component. `initial` leaves them
74
+ * behaving exactly as an unset variable would, without opening the hole.
75
+ *
76
+ * @param rect - The measured bounding rect, absent until the first resize lands.
77
+ * @returns The custom properties, keyed by their full name.
78
+ */
79
+ export function toScreenCssProps(rect) {
80
+ const publicProps = rect === undefined
81
+ ? {}
82
+ : {
83
+ '--lm-scrllgngn-screen-left': `${rect.left}px`,
84
+ '--lm-scrllgngn-screen-left-raw': `${rect.left}`,
85
+ '--lm-scrllgngn-screen-right': `${rect.right}px`,
86
+ '--lm-scrllgngn-screen-right-raw': `${rect.right}`,
87
+ '--lm-scrllgngn-screen-width': `${rect.width}px`,
88
+ '--lm-scrllgngn-screen-width-raw': `${rect.width}`,
89
+ '--lm-scrllgngn-screen-height': `${rect.height}px`,
90
+ '--lm-scrllgngn-screen-height-raw': `${rect.height}`
91
+ };
92
+ return {
93
+ ...publicProps,
94
+ '--PRIVATE-left': toPrivateLength(rect?.left),
95
+ '--PRIVATE-right': toPrivateLength(rect?.right),
96
+ '--PRIVATE-width': toPrivateLength(rect?.width),
97
+ '--PRIVATE-height': toPrivateLength(rect?.height)
98
+ };
99
+ }
100
+ function toPrivateLength(value) {
101
+ return value === undefined ? 'initial' : `${value}px`;
102
+ }
@@ -2,46 +2,61 @@ import { type FunctionComponent, type PropsWithChildren } from 'react';
2
2
  import type { WithClassName } from '../utils/types.js';
3
3
  import { type ScrollState } from './utils.js';
4
4
  /**
5
- * Props for the ScrollListener component.
5
+ * Props for the {@link ScrollListener} component.
6
6
  *
7
- * @property startOnVisible - When `true`, scroll tracking starts only when the component
8
- * becomes visible in the viewport. Visibility is detected using an IntersectionObserver.
9
- * @property stopOnHidden - When `true`, scroll tracking stops when the component leaves
10
- * the viewport.
11
- * @property stateHandlers - Optional callbacks invoked when internal state changes.
12
- * @property stateHandlers.scrollDataChanged - Called whenever the computed scroll data
13
- * changes. Receives the current {@link ScrollState} or `undefined`.
14
- * @property stateHandlers.visibilityChanged - Called when the visibility state of the
15
- * component changes. Receives `true` when the component is intersecting the viewport,
16
- * otherwise `false`.
7
+ * @property startOnVisible - When `true`, scroll tracking only starts once the
8
+ * component enters the viewport, instead of on mount.
9
+ * @property stopOnHidden - When `true`, scroll tracking stops when the component
10
+ * leaves the viewport.
11
+ * @property onScrollStateChanged - Called after the measured {@link ScrollState}
12
+ * changed. Receives `undefined` until the first measurement lands.
13
+ * @property onVisibilityChanged - Called on every intersection change, with
14
+ * `true` when the component intersects the viewport.
17
15
  * @property className - Optional additional class name(s) applied to the root element.
18
16
  * @property children - React nodes rendered inside the scroll listener container.
19
17
  */
20
18
  export type Props = PropsWithChildren<WithClassName<{
21
- stopOnHidden?: boolean;
22
19
  startOnVisible?: boolean;
23
- stateHandlers?: {
24
- scrollDataChanged?: (scrollData?: ScrollState) => void;
25
- visibilityChanged?: (isVisible: boolean) => void;
26
- };
20
+ stopOnHidden?: boolean;
21
+ onScrollStateChanged?: (scrollState?: ScrollState) => void;
22
+ onVisibilityChanged?: (isVisible: boolean) => void;
27
23
  }>>;
28
24
  /**
29
- * Component that listens to global scroll events and exposes scroll-related data
30
- * through CSS custom properties and optional state callbacks.
25
+ * Exposes scroll metrics both the document's and its own — as CSS custom
26
+ * properties on its root element, so scroll-driven styling needs no JavaScript
27
+ * of its own.
31
28
  *
32
- * The component measures both global scroll metrics (window size, document size,
33
- * scroll offsets) and local metrics relative to the component (dimensions and
34
- * position within the document).
29
+ * ### CSS custom properties on the root element
30
+ * Each measurement comes as a `px` length under its bare name, and as a `-raw`
31
+ * twin holding the plain number:
32
+ * - `--lm-scroll-listener-window-width`, `--lm-scroll-listener-window-height`
33
+ * - `--lm-scroll-listener-html-width`, `--lm-scroll-listener-html-height`
34
+ * - `--lm-scroll-listener-scroll-x`, `--lm-scroll-listener-scroll-y`
35
+ * - `--lm-scroll-listener-width`, `--lm-scroll-listener-height`
36
+ * - `--lm-scroll-listener-offset-x`, `--lm-scroll-listener-offset-y`
35
37
  *
36
- * Computed values are exposed as CSS variables on the root element, enabling
37
- * scroll-driven styling without additional JavaScript.
38
- *
39
- * Scroll observation can optionally start only when the component becomes visible
40
- * and stop when it leaves the viewport.
38
+ * Ratios are unitless and have no twin:
39
+ * - `--lm-scroll-listener-window-scrolled-x-ratio`,
40
+ * `--lm-scroll-listener-window-scrolled-y-ratio` — how far the document is
41
+ * scrolled.
42
+ * - `--lm-scroll-listener-self-inner-scrolled-x-ratio`,
43
+ * `--lm-scroll-listener-self-inner-scrolled-y-ratio` — `0` when the element's
44
+ * start edge meets the viewport's start edge, `1` when its end edge meets the
45
+ * viewport's end edge. The span where it is fully visible.
46
+ * - `--lm-scroll-listener-self-outer-scrolled-x-ratio`,
47
+ * `--lm-scroll-listener-self-outer-scrolled-y-ratio` — `0` when the element is
48
+ * about to enter the viewport, `1` once it has fully left it. The span where it
49
+ * overlaps the viewport at all.
41
50
  *
42
51
  * @param props - Component properties.
43
52
  * @see {@link Props}
44
- * @returns A container element exposing scroll metrics through CSS variables and
45
- * wrapping its children inside an IntersectionObserver boundary.
53
+ * @returns A container exposing the metrics, wrapping its children inside an
54
+ * {@link IntersectionObserverComponent}.
55
+ *
56
+ * @remarks
57
+ * All mounted instances share a single pair of `scroll` / `resize` listeners and
58
+ * a single measurement pass per animation frame — the document is measured once
59
+ * for everyone, each element only for itself. The listeners exist only while at
60
+ * least one instance is tracking.
46
61
  */
47
62
  export declare const ScrollListener: FunctionComponent<Props>;
@@ -1,100 +1,80 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState, useCallback } from 'react';
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { clss } from '../../agnostic/css/clss/index.js';
4
4
  import { randomHash } from '../../agnostic/random/uuid/index.js';
5
- import { mergeClassNames } from '../utils/index.js';
6
5
  import { IntersectionObserverComponent } from '../IntersectionObserver/index.js';
6
+ import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
7
7
  import { scrollListener as publicClassName } from '../public-classnames.js';
8
- import { register, unregister } from './utils.js';
8
+ import { subscribe, toScrollCssProps, unsubscribe } from './utils.js';
9
9
  import cssModule from './styles.module.css';
10
10
  /**
11
- * Component that listens to global scroll events and exposes scroll-related data
12
- * through CSS custom properties and optional state callbacks.
11
+ * Exposes scroll metrics both the document's and its own — as CSS custom
12
+ * properties on its root element, so scroll-driven styling needs no JavaScript
13
+ * of its own.
13
14
  *
14
- * The component measures both global scroll metrics (window size, document size,
15
- * scroll offsets) and local metrics relative to the component (dimensions and
16
- * position within the document).
15
+ * ### CSS custom properties on the root element
16
+ * Each measurement comes as a `px` length under its bare name, and as a `-raw`
17
+ * twin holding the plain number:
18
+ * - `--lm-scroll-listener-window-width`, `--lm-scroll-listener-window-height`
19
+ * - `--lm-scroll-listener-html-width`, `--lm-scroll-listener-html-height`
20
+ * - `--lm-scroll-listener-scroll-x`, `--lm-scroll-listener-scroll-y`
21
+ * - `--lm-scroll-listener-width`, `--lm-scroll-listener-height`
22
+ * - `--lm-scroll-listener-offset-x`, `--lm-scroll-listener-offset-y`
17
23
  *
18
- * Computed values are exposed as CSS variables on the root element, enabling
19
- * scroll-driven styling without additional JavaScript.
20
- *
21
- * Scroll observation can optionally start only when the component becomes visible
22
- * and stop when it leaves the viewport.
24
+ * Ratios are unitless and have no twin:
25
+ * - `--lm-scroll-listener-window-scrolled-x-ratio`,
26
+ * `--lm-scroll-listener-window-scrolled-y-ratio` — how far the document is
27
+ * scrolled.
28
+ * - `--lm-scroll-listener-self-inner-scrolled-x-ratio`,
29
+ * `--lm-scroll-listener-self-inner-scrolled-y-ratio` — `0` when the element's
30
+ * start edge meets the viewport's start edge, `1` when its end edge meets the
31
+ * viewport's end edge. The span where it is fully visible.
32
+ * - `--lm-scroll-listener-self-outer-scrolled-x-ratio`,
33
+ * `--lm-scroll-listener-self-outer-scrolled-y-ratio` — `0` when the element is
34
+ * about to enter the viewport, `1` once it has fully left it. The span where it
35
+ * overlaps the viewport at all.
23
36
  *
24
37
  * @param props - Component properties.
25
38
  * @see {@link Props}
26
- * @returns A container element exposing scroll metrics through CSS variables and
27
- * wrapping its children inside an IntersectionObserver boundary.
39
+ * @returns A container exposing the metrics, wrapping its children inside an
40
+ * {@link IntersectionObserverComponent}.
41
+ *
42
+ * @remarks
43
+ * All mounted instances share a single pair of `scroll` / `resize` listeners and
44
+ * a single measurement pass per animation frame — the document is measured once
45
+ * for everyone, each element only for itself. The listeners exist only while at
46
+ * least one instance is tracking.
28
47
  */
29
- export const ScrollListener = ({ startOnVisible, stopOnHidden, stateHandlers, className, children }) => {
30
- const [privateId] = useState(randomHash(6));
31
- const [scrollData, setScrollData] = useState();
48
+ export const ScrollListener = ({ startOnVisible, stopOnHidden, onScrollStateChanged, onVisibilityChanged, className, children }) => {
49
+ // State & refs
50
+ const [subscriberId] = useState(() => randomHash(6));
51
+ const [scrollState, setScrollState] = useState();
32
52
  const rootRef = useRef(null);
33
- // Fx. dep. `scrollData` - State dispatcher
34
- useEffect(() => {
35
- stateHandlers?.scrollDataChanged?.(scrollData);
36
- }, [scrollData]);
37
- // Fx. no dep. - register / unregister to scroll listeners
53
+ // State dispatch
54
+ useChangeDispatch(scrollState, onScrollStateChanged);
55
+ // Fx. no dep. - track from mount, unless waiting for the component to show up.
56
+ // The cleanup runs whichever way the subscription was opened.
38
57
  useEffect(() => {
39
- register({ id: privateId, rootRef, setData: setScrollData });
40
- return () => { unregister(privateId); };
58
+ if (startOnVisible !== true) {
59
+ subscribe(subscriberId, { rootRef, onScrollStateChange: setScrollState });
60
+ }
61
+ return () => unsubscribe(subscriberId);
41
62
  }, []);
42
- const handleIntersection = useCallback(({ ioEntry, observer }) => {
43
- stateHandlers?.visibilityChanged?.(ioEntry?.isIntersecting ?? false);
44
- if (startOnVisible !== true && stopOnHidden !== true)
45
- return;
46
- if (ioEntry?.isIntersecting === true && startOnVisible === true)
47
- register({ id: privateId, rootRef, setData: setScrollData });
48
- if (ioEntry?.isIntersecting !== true && stopOnHidden === true)
49
- unregister(privateId);
50
- }, [startOnVisible, stopOnHidden]);
51
- // Rendering
52
- const customProps = Object
53
- .entries(scrollData !== undefined
54
- ? {
55
- // Global
56
- [`--${publicClassName}-window-width`]: scrollData.global.win.width,
57
- [`--${publicClassName}-window-height`]: scrollData.global.win.height,
58
- [`--${publicClassName}-html-width`]: scrollData.global.html.width,
59
- [`--${publicClassName}-html-height`]: scrollData.global.html.height,
60
- [`--${publicClassName}-scroll-x`]: scrollData.global.scroll.x,
61
- [`--${publicClassName}-scroll-y`]: scrollData.global.scroll.y,
62
- // Local
63
- [`--${publicClassName}-width`]: scrollData.local.width,
64
- [`--${publicClassName}-height`]: scrollData.local.height,
65
- [`--${publicClassName}-offset-x`]: scrollData.local.offsetX,
66
- [`--${publicClassName}-offset-y`]: scrollData.local.offsetY
63
+ // User action handlers
64
+ const handleIntersection = useCallback(({ ioEntry }) => {
65
+ const isVisible = ioEntry?.isIntersecting ?? false;
66
+ onVisibilityChanged?.(isVisible);
67
+ if (isVisible && startOnVisible === true) {
68
+ subscribe(subscriberId, { rootRef, onScrollStateChange: setScrollState });
67
69
  }
68
- : {}).reduce((acc, [key, val]) => ({
69
- ...acc,
70
- [key]: val,
71
- [`${key}-px`]: `${val}px`
72
- }), {});
73
- if (scrollData !== undefined) {
74
- customProps[`--${publicClassName}-window-scrolled-x-ratio`] = (scrollData.global.scroll.x) / Math.max((scrollData.global.html.width - scrollData.global.win.width), 1);
75
- customProps[`--${publicClassName}-window-scrolled-y-ratio`] = (scrollData.global.scroll.y) / Math.max((scrollData.global.html.height - scrollData.global.win.height), 1);
76
- const topTouchesTop = scrollData.local.offsetY;
77
- const topTouchesBottom = scrollData.local.offsetY - scrollData.global.win.height;
78
- const bottomTouchesTop = scrollData.local.offsetY + scrollData.local.height;
79
- const bottomTouchesBottom = (scrollData.local.offsetY + scrollData.local.height) - scrollData.global.win.height;
80
- const leftTouchesLeft = scrollData.local.offsetX;
81
- const leftTouchesRight = scrollData.local.offsetX - scrollData.global.win.width;
82
- const rightTouchesLeft = scrollData.local.offsetX + scrollData.local.width;
83
- const rightTouchesRight = (scrollData.local.offsetX + scrollData.local.width) - scrollData.global.win.width;
84
- const innerYRange = [bottomTouchesBottom, topTouchesTop];
85
- const outerYRange = [topTouchesBottom, bottomTouchesTop];
86
- const innerXRange = [rightTouchesRight, leftTouchesLeft];
87
- const outerXRange = [leftTouchesRight, rightTouchesLeft];
88
- const innerScrollYRatio = (scrollData.global.scroll.y - innerYRange[0]) / Math.max((innerYRange[1] - innerYRange[0]), 1);
89
- const outerScrollYRatio = (scrollData.global.scroll.y - outerYRange[0]) / Math.max((outerYRange[1] - outerYRange[0]), 1);
90
- const innerScrollXRatio = (scrollData.global.scroll.x - innerXRange[0]) / Math.max((innerXRange[1] - innerXRange[0]), 1);
91
- const outerScrollXRatio = (scrollData.global.scroll.x - outerXRange[0]) / Math.max((outerXRange[1] - outerXRange[0]), 1);
92
- customProps[`--${publicClassName}-self-inner-scrolled-y-ratio`] = innerScrollYRatio;
93
- customProps[`--${publicClassName}-self-outer-scrolled-y-ratio`] = outerScrollYRatio;
94
- customProps[`--${publicClassName}-self-inner-scrolled-x-ratio`] = innerScrollXRatio;
95
- customProps[`--${publicClassName}-self-outer-scrolled-x-ratio`] = outerScrollXRatio;
96
- }
70
+ if (!isVisible && stopOnHidden === true)
71
+ unsubscribe(subscriberId);
72
+ }, [startOnVisible, stopOnHidden, onVisibilityChanged]);
73
+ // Rendering
97
74
  const c = clss(publicClassName, { cssModule });
98
75
  const rootClss = mergeClassNames(c(), className);
76
+ const customProps = scrollState === undefined
77
+ ? {}
78
+ : toScrollCssProps(scrollState);
99
79
  return _jsx("div", { className: rootClss, ref: rootRef, style: { ...customProps }, children: _jsx(IntersectionObserverComponent, { onIntersected: handleIntersection, children: children }) });
100
80
  };
@@ -1,4 +1,4 @@
1
- import type { RefObject, Dispatch, SetStateAction } from 'react';
1
+ import type { RefObject } from 'react';
2
2
  /**
3
3
  * Scroll-related measurements for the document and viewport.
4
4
  *
@@ -42,29 +42,50 @@ export type LocalScrollData = {
42
42
  height: number;
43
43
  };
44
44
  /**
45
- * Combined scroll state passed to each registered {@link ScrollListener}.
45
+ * Combined scroll state handed to each subscribed {@link ScrollListener}.
46
46
  *
47
- * @property global - Document-level scroll data.
48
- * @property local - Element-level position data.
47
+ * @property global - Document-level scroll data, measured once per frame and
48
+ * shared by every subscriber.
49
+ * @property local - Position and size of this subscriber's own element.
49
50
  */
50
51
  export type ScrollState = {
51
52
  global: GlobalScrollData;
52
53
  local: LocalScrollData;
53
54
  };
54
- export declare const globalDocumentScrollListener: () => GlobalScrollData;
55
- export declare const localElementScrollListener: (div: HTMLDivElement) => LocalScrollData;
56
55
  /**
57
- * An entry registered with the global scroll listener.
56
+ * A component subscribed to the shared scroll listener.
58
57
  *
59
- * @property id - Unique identifier for this registration.
60
- * @property rootRef - Ref to the tracked DOM element.
61
- * @property setData - State setter to update the component's scroll state.
58
+ * @property rootRef - Ref to the tracked element. A subscriber whose ref is not
59
+ * attached yet is skipped for that frame.
60
+ * @property onScrollStateChange - Receives the freshly measured state.
62
61
  */
63
- export type RegisterEntry = {
64
- id: string;
62
+ export type Subscriber = {
65
63
  rootRef: RefObject<HTMLDivElement | null>;
66
- setData: Dispatch<SetStateAction<ScrollState | undefined>>;
64
+ onScrollStateChange: (scrollState: ScrollState) => void;
67
65
  };
68
- export declare const registeredIds: Map<string, RegisterEntry>;
69
- export declare const register: (props: RegisterEntry) => void;
70
- export declare const unregister: (id: string) => void;
66
+ /**
67
+ * Starts tracking one element, attaching the shared listeners if this is the
68
+ * first subscriber. A measurement is scheduled right away, so a subscriber gets
69
+ * its initial state without waiting for a first scroll.
70
+ *
71
+ * @param id - Identifies the subscriber for {@link unsubscribe}.
72
+ * @param subscriber - The element to track and where to report.
73
+ */
74
+ export declare function subscribe(id: string, subscriber: Subscriber): void;
75
+ /**
76
+ * Stops tracking one element, detaching the shared listeners once no subscriber
77
+ * is left. Unsubscribing an unknown id is a no-op.
78
+ *
79
+ * @param id - The id passed to {@link subscribe}.
80
+ */
81
+ export declare function unsubscribe(id: string): void;
82
+ /**
83
+ * Builds the CSS custom properties exposed on a {@link ScrollListener} root.
84
+ *
85
+ * Measurements are lengths and come with a `-raw` twin carrying the plain number
86
+ * for `calc()`; ratios are unitless and have no twin.
87
+ *
88
+ * @param scrollState - The state to expose.
89
+ * @returns The custom properties, keyed by their full name.
90
+ */
91
+ export declare function toScrollCssProps(scrollState: ScrollState): Record<string, string>;