@design-edito/tools 0.5.1 → 0.5.3

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 (156) hide show
  1. package/agnostic/arrays/index.d.ts +1 -1
  2. package/agnostic/arrays/index.js +1 -1
  3. package/agnostic/colors/index.d.ts +1 -1
  4. package/agnostic/colors/index.js +1 -1
  5. package/agnostic/css/index.d.ts +1 -1
  6. package/agnostic/css/index.js +1 -1
  7. package/agnostic/html/deep-select/index.js +5 -3
  8. package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +9 -9
  9. package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +9 -9
  10. package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +1 -1
  11. package/agnostic/html/hyper-json/smart-tags/isolated/index.js +1 -1
  12. package/agnostic/html/index.d.ts +3 -3
  13. package/agnostic/html/index.js +3 -3
  14. package/agnostic/index.d.ts +3 -3
  15. package/agnostic/index.js +3 -3
  16. package/agnostic/misc/index.d.ts +1 -1
  17. package/agnostic/misc/index.js +1 -1
  18. package/agnostic/misc/logs/index.d.ts +1 -1
  19. package/agnostic/misc/logs/index.js +1 -1
  20. package/agnostic/numbers/index.d.ts +3 -3
  21. package/agnostic/numbers/index.js +3 -3
  22. package/agnostic/objects/index.d.ts +3 -3
  23. package/agnostic/objects/index.js +3 -3
  24. package/agnostic/random/index.d.ts +1 -1
  25. package/agnostic/random/index.js +1 -1
  26. package/agnostic/strings/index.d.ts +3 -3
  27. package/agnostic/strings/index.js +3 -3
  28. package/agnostic/time/dates/format-date/index.d.ts +4 -2
  29. package/agnostic/time/dates/format-date/index.js +43 -94
  30. package/agnostic/time/dates/format-date/index.test.js +154 -0
  31. package/agnostic/time/dates/format-date/parts.d.ts +28 -0
  32. package/agnostic/time/dates/format-date/parts.js +136 -0
  33. package/agnostic/time/dates/format-date/types.d.ts +84 -0
  34. package/agnostic/time/dates/format-date/types.js +1 -0
  35. package/agnostic/time/duration/format-duration/index.d.ts +42 -0
  36. package/agnostic/time/duration/format-duration/index.js +94 -0
  37. package/agnostic/time/duration/format-duration/index.test.d.ts +1 -0
  38. package/agnostic/time/duration/format-duration/index.test.js +148 -0
  39. package/agnostic/time/duration/format-duration/parts.d.ts +32 -0
  40. package/agnostic/time/duration/format-duration/parts.js +107 -0
  41. package/agnostic/time/duration/format-duration/types.d.ts +93 -0
  42. package/agnostic/time/duration/format-duration/types.js +1 -0
  43. package/agnostic/time/duration/index.d.ts +6 -1
  44. package/agnostic/time/duration/index.js +2 -2
  45. package/agnostic/time/duration/index.test.d.ts +1 -0
  46. package/agnostic/time/duration/index.test.js +168 -0
  47. package/components/BeforeAfter/index.controlled.d.ts +25 -25
  48. package/components/BeforeAfter/index.controlled.js +47 -53
  49. package/components/BeforeAfter/index.d.ts +20 -23
  50. package/components/BeforeAfter/index.js +48 -40
  51. package/components/Button/index.js +1 -1
  52. package/components/Clippable/index.d.ts +17 -26
  53. package/components/Clippable/index.js +21 -23
  54. package/components/Disclaimer/index.d.ts +29 -22
  55. package/components/Disclaimer/index.js +29 -24
  56. package/components/Drawer/index.d.ts +45 -25
  57. package/components/Drawer/index.js +61 -52
  58. package/components/EventListener/index.d.ts +4 -5
  59. package/components/Gallery/index.d.ts +14 -16
  60. package/components/Gallery/index.js +25 -30
  61. package/components/Iframe/index.js +2 -2
  62. package/components/Image/index.d.ts +5 -12
  63. package/components/Image/index.js +11 -32
  64. package/components/Input/index.d.ts +23 -19
  65. package/components/Input/index.js +30 -20
  66. package/components/JsonEditor/index.d.ts +177 -9
  67. package/components/JsonEditor/index.js +133 -75
  68. package/components/JsonEditor/types.d.ts +11 -0
  69. package/components/JsonEditor/types.js +1 -0
  70. package/components/JsonEditor/utils.d.ts +8 -0
  71. package/components/JsonEditor/utils.js +23 -0
  72. package/components/ListLoader/index.controlled.d.ts +78 -0
  73. package/components/ListLoader/index.controlled.js +99 -0
  74. package/components/ListLoader/index.d.ts +69 -0
  75. package/components/ListLoader/index.js +146 -0
  76. package/components/ListLoader/utils.d.ts +8 -0
  77. package/components/ListLoader/utils.js +10 -0
  78. package/components/Overlayer/index.js +4 -4
  79. package/components/Paginator/index.d.ts +7 -12
  80. package/components/Paginator/index.js +7 -11
  81. package/components/ResizeObserver/index.d.ts +14 -3
  82. package/components/ResizeObserver/index.js +49 -24
  83. package/components/ResizeObserver/styles.module.css +0 -0
  84. package/components/Scrllgngn/index.d.ts +17 -13
  85. package/components/Scrllgngn/index.js +25 -84
  86. package/components/Scrllgngn/utils.d.ts +52 -0
  87. package/components/Scrllgngn/utils.js +102 -0
  88. package/components/ScrollListener/index.d.ts +43 -28
  89. package/components/ScrollListener/index.js +59 -79
  90. package/components/ScrollListener/utils.d.ts +37 -16
  91. package/components/ScrollListener/utils.js +141 -28
  92. package/components/Select/index.d.ts +24 -20
  93. package/components/Select/index.js +30 -20
  94. package/components/Sequencer/index.controlled.d.ts +16 -26
  95. package/components/Sequencer/index.controlled.js +15 -12
  96. package/components/Sequencer/index.d.ts +25 -30
  97. package/components/Sequencer/index.js +20 -34
  98. package/components/Subtitles/index.d.ts +29 -5
  99. package/components/Subtitles/index.js +90 -12
  100. package/components/Textarea/index.d.ts +26 -20
  101. package/components/Textarea/index.js +40 -21
  102. package/components/Theatre/index.d.ts +35 -39
  103. package/components/Theatre/index.js +46 -58
  104. package/components/UIModule/index.d.ts +25 -29
  105. package/components/UIModule/index.js +81 -110
  106. package/components/Video/index.controlled.d.ts +51 -51
  107. package/components/Video/index.controlled.js +75 -72
  108. package/components/Video/index.d.ts +12 -14
  109. package/components/Video/index.js +41 -86
  110. package/components/Video/utils.d.ts +35 -3
  111. package/components/Video/utils.js +37 -14
  112. package/components/index.d.ts +5 -4
  113. package/components/index.js +5 -4
  114. package/components/public-classnames.d.ts +1 -0
  115. package/components/public-classnames.js +1 -0
  116. package/components/utils/index.d.ts +22 -0
  117. package/components/utils/index.js +31 -0
  118. package/node/@google-cloud/storage/directory/index.d.ts +2 -2
  119. package/node/@google-cloud/storage/directory/index.js +2 -2
  120. package/node/@google-cloud/storage/file/index.d.ts +2 -2
  121. package/node/@google-cloud/storage/file/index.js +2 -2
  122. package/node/@google-cloud/storage/index.d.ts +1 -1
  123. package/node/@google-cloud/storage/index.js +1 -1
  124. package/node/cloud-storage/operations/index.d.ts +2 -2
  125. package/node/cloud-storage/operations/index.js +2 -2
  126. package/node/ftps/directory/index.d.ts +1 -1
  127. package/node/ftps/directory/index.js +1 -1
  128. package/node/ftps/file/index.d.ts +2 -2
  129. package/node/ftps/file/index.js +2 -2
  130. package/node/images/index.d.ts +1 -1
  131. package/node/images/index.js +1 -1
  132. package/node/images/transform/operations/index.d.ts +3 -3
  133. package/node/images/transform/operations/index.js +3 -3
  134. package/node/index.d.ts +2 -2
  135. package/node/index.js +2 -2
  136. package/node/process/index.d.ts +1 -1
  137. package/node/process/index.js +1 -1
  138. package/node/sftp/directory/index.d.ts +1 -1
  139. package/node/sftp/directory/index.js +1 -1
  140. package/node/sftp/file/index.d.ts +1 -1
  141. package/node/sftp/file/index.js +1 -1
  142. package/package.json +27 -5
  143. package/TODO.md +0 -269
  144. package/components/BeforeAfter/utils.d.ts +0 -4
  145. package/components/BeforeAfter/utils.js +0 -9
  146. package/components/Input/index.controlled.d.ts +0 -42
  147. package/components/Input/index.controlled.js +0 -54
  148. package/components/Select/index.controlled.d.ts +0 -43
  149. package/components/Select/index.controlled.js +0 -54
  150. package/components/Subtitles/types.d.ts +0 -24
  151. package/components/Subtitles/utils.d.ts +0 -32
  152. package/components/Subtitles/utils.js +0 -107
  153. package/components/Textarea/index.controlled.d.ts +0 -50
  154. package/components/Textarea/index.controlled.js +0 -75
  155. /package/{components/Subtitles/types.js → agnostic/time/dates/format-date/index.test.d.ts} +0 -0
  156. /package/components/{ResizeObserver/style.module.css → ListLoader/styles.module.css} +0 -0
File without changes
@@ -71,10 +71,8 @@ export type PropsPage = {
71
71
  * - `'after'` — forces blocks from pages after the current one.
72
72
  * - `'both'` — forces blocks on both sides.
73
73
  * - `'none'` — no forcing (default behaviour).
74
- * @property stateHandlers - Optional callbacks invoked in response to
75
- * component state changes.
76
- * @property stateHandlers.pageChanged - Called whenever the current page
77
- * changes. Receives the zero-based index of the current page and the
74
+ * @property onPageChanged - Called once the current page has changed, never on
75
+ * mount. Receives the zero-based index of the new current page and the
78
76
  * corresponding page definition, if available.
79
77
  * @property className - Optional additional class name(s) applied to the root
80
78
  * element.
@@ -84,9 +82,7 @@ export type Props = WithClassName<{
84
82
  thresholdOffsetPercent?: number;
85
83
  stickyBlocksLazyLoadDistance?: number;
86
84
  forceStickBlocks?: 'before' | 'after' | 'both' | 'none';
87
- stateHandlers?: {
88
- pageChanged?: (currentPagePos: number, pageData?: PropsPage) => void;
89
- };
85
+ onPageChanged?: (currentPagePos: number, pageData?: PropsPage) => void;
90
86
  }>;
91
87
  /**
92
88
  * Scrollytelling engine component. Orchestrates layered sticky blocks (`back`
@@ -104,16 +100,24 @@ export type Props = WithClassName<{
104
100
  * - `--force-stick-blocks-both` — when `forceStickBlocks === 'both'`.
105
101
  *
106
102
  * ### Data attributes
107
- * - `data-current-page-pos` zero-based index of the page currently in view,
108
- * updated on every page change reported by the internal {@link Paginator}.
103
+ * Both are updated on every page change reported by the internal
104
+ * {@link Paginator}.
105
+ * - `data-current-page-pos` — zero-based index of the page currently in view.
106
+ * - `data-current-page-id` — that page's {@link PropsPage.id}. Absent while the
107
+ * current page carries no id, since the id is optional.
109
108
  *
110
109
  * ### CSS custom properties
111
110
  * Exposed on the root element and updated on resize via the internal
112
111
  * {@link ResizeObserverComponent}:
113
- * - `--scrllgngn-screen-left` / `--PRIVATE-left` — left edge of the bounding rect (px).
114
- * - `--scrllgngn-screen-right` / `--PRIVATE-right` — right edge (px).
115
- * - `--scrllgngn-screen-width` / `--PRIVATE-width` — total width (px).
116
- * - `--scrllgngn-screen-height` / `--PRIVATE-height` — total height (px).
112
+ * - `--lm-scrllgngn-screen-left` / `--lm-scrllgngn-screen-left-raw` — left edge
113
+ * of the bounding rect.
114
+ * - `--lm-scrllgngn-screen-right` / `--lm-scrllgngn-screen-right-raw` — right edge.
115
+ * - `--lm-scrllgngn-screen-width` / `--lm-scrllgngn-screen-width-raw` — total width.
116
+ * - `--lm-scrllgngn-screen-height` / `--lm-scrllgngn-screen-height-raw` — total height.
117
+ *
118
+ * The same four measurements are also exposed as `--PRIVATE-left`, `-right`,
119
+ * `-width` and `-height` for the component's own stylesheet. They are internal:
120
+ * do not read or override them.
117
121
  *
118
122
  * ### Sticky block elements
119
123
  * Each lazy-loaded sticky block receives:
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useEffect } from 'react';
3
3
  import { clss } from '../../agnostic/css/clss/index.js';
4
- import { randomHash } from '../../agnostic/random/uuid/index.js';
5
4
  import { IntersectionObserverComponent } from '../IntersectionObserver/index.js';
6
5
  import { Paginator } from '../Paginator/index.js';
7
6
  import { ResizeObserverComponent } from '../ResizeObserver/index.js';
8
- import { mergeClassNames } from '../utils/index.js';
7
+ import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
9
8
  import { scrllgngn as publicClassName } from '../public-classnames.js';
9
+ import { consolidateStickyBlocks, lazyLoadedBlocks, toScreenCssProps } from './utils.js';
10
10
  import cssModule from './styles.module.css';
11
11
  /**
12
12
  * Scrollytelling engine component. Orchestrates layered sticky blocks (`back`
@@ -24,16 +24,24 @@ import cssModule from './styles.module.css';
24
24
  * - `--force-stick-blocks-both` — when `forceStickBlocks === 'both'`.
25
25
  *
26
26
  * ### Data attributes
27
- * - `data-current-page-pos` zero-based index of the page currently in view,
28
- * updated on every page change reported by the internal {@link Paginator}.
27
+ * Both are updated on every page change reported by the internal
28
+ * {@link Paginator}.
29
+ * - `data-current-page-pos` — zero-based index of the page currently in view.
30
+ * - `data-current-page-id` — that page's {@link PropsPage.id}. Absent while the
31
+ * current page carries no id, since the id is optional.
29
32
  *
30
33
  * ### CSS custom properties
31
34
  * Exposed on the root element and updated on resize via the internal
32
35
  * {@link ResizeObserverComponent}:
33
- * - `--scrllgngn-screen-left` / `--PRIVATE-left` — left edge of the bounding rect (px).
34
- * - `--scrllgngn-screen-right` / `--PRIVATE-right` — right edge (px).
35
- * - `--scrllgngn-screen-width` / `--PRIVATE-width` — total width (px).
36
- * - `--scrllgngn-screen-height` / `--PRIVATE-height` — total height (px).
36
+ * - `--lm-scrllgngn-screen-left` / `--lm-scrllgngn-screen-left-raw` — left edge
37
+ * of the bounding rect.
38
+ * - `--lm-scrllgngn-screen-right` / `--lm-scrllgngn-screen-right-raw` — right edge.
39
+ * - `--lm-scrllgngn-screen-width` / `--lm-scrllgngn-screen-width-raw` — total width.
40
+ * - `--lm-scrllgngn-screen-height` / `--lm-scrllgngn-screen-height-raw` — total height.
41
+ *
42
+ * The same four measurements are also exposed as `--PRIVATE-left`, `-right`,
43
+ * `-width` and `-height` for the component's own stylesheet. They are internal:
44
+ * do not read or override them.
37
45
  *
38
46
  * ### Sticky block elements
39
47
  * Each lazy-loaded sticky block receives:
@@ -48,7 +56,7 @@ import cssModule from './styles.module.css';
48
56
  * back-blocks layer, front-blocks layer, paginated scrolling content, and
49
57
  * bottom-bound sentinel.
50
58
  */
51
- export const Scrllgngn = ({ pages, thresholdOffsetPercent, stickyBlocksLazyLoadDistance = 2, forceStickBlocks, stateHandlers, className }) => {
59
+ export const Scrllgngn = ({ pages, thresholdOffsetPercent, stickyBlocksLazyLoadDistance = 2, forceStickBlocks, onPageChanged, className }) => {
52
60
  // State
53
61
  const [topVisible, setTopVis] = useState(false);
54
62
  const [contentVisible, setCntVis] = useState(false);
@@ -58,69 +66,20 @@ export const Scrllgngn = ({ pages, thresholdOffsetPercent, stickyBlocksLazyLoadD
58
66
  const [partialBoundingRect, setPartialBoundingRect] = useState();
59
67
  // Sticky blocks calculations
60
68
  useEffect(() => {
61
- const consolidatedBlocks = new Map();
62
- for (const page of pages ?? []) {
63
- const pageIndex = pages?.indexOf(page) ?? -1;
64
- for (const block of page.blocks ?? []) {
65
- const blockId = block.id ?? randomHash(12);
66
- const found = consolidatedBlocks.get(blockId);
67
- if (found !== undefined)
68
- consolidatedBlocks.set(blockId, {
69
- ...found,
70
- ...block,
71
- displayOnPages: [
72
- ...found.displayOnPages,
73
- pageIndex
74
- ]
75
- });
76
- else
77
- consolidatedBlocks.set(blockId, {
78
- ...block,
79
- displayOnPages: [pageIndex]
80
- });
81
- }
82
- }
83
- const consolidatedStickyBlocks = new Map(Array
84
- .from(consolidatedBlocks)
85
- .filter((e) => {
86
- const block = e[1];
87
- return block.depth === 'back'
88
- || block.depth === 'front';
89
- }));
90
- setStickyBlocks(consolidatedStickyBlocks);
69
+ setStickyBlocks(consolidateStickyBlocks(pages));
91
70
  }, [pages]);
92
- const lazyLoadedBackBlocks = Array
93
- .from(stickyBlocks)
94
- .filter(([, block]) => block.depth === 'back'
95
- && block.displayOnPages.some(dispPage => {
96
- const absDiff = Math.abs(dispPage - currentPagePos);
97
- return absDiff <= stickyBlocksLazyLoadDistance;
98
- })).map(([, block]) => block)
99
- .sort((a, b) => {
100
- return (a.zIndex ?? -Infinity) - (b.zIndex ?? -Infinity);
101
- });
102
- const lazyLoadedFrontBlocks = Array
103
- .from(stickyBlocks)
104
- .filter(([, block]) => block.depth === 'front'
105
- && block.displayOnPages.some(dispPage => {
106
- const absDiff = Math.abs(dispPage - currentPagePos);
107
- return absDiff <= stickyBlocksLazyLoadDistance;
108
- })).map(([, block]) => block)
109
- .sort((a, b) => {
110
- return (a.zIndex ?? -Infinity) - (b.zIndex ?? -Infinity);
111
- });
71
+ const lazyLoadedBackBlocks = lazyLoadedBlocks(stickyBlocks, 'back', currentPagePos, stickyBlocksLazyLoadDistance);
72
+ const lazyLoadedFrontBlocks = lazyLoadedBlocks(stickyBlocks, 'front', currentPagePos, stickyBlocksLazyLoadDistance);
112
73
  // Handlers
74
+ useChangeDispatch(currentPagePos, pagePos => onPageChanged?.(pagePos, pages?.[pagePos]));
113
75
  const handleTopBoundDetect = e => setTopVis(e.ioEntry?.isIntersecting ?? false);
114
76
  const handleCntDetect = e => setCntVis(e.ioEntry?.isIntersecting ?? false);
115
77
  const handleBtmBoundDetect = e => setBtmVis(e.ioEntry?.isIntersecting ?? false);
116
- const handlePageChange = statePages => {
78
+ const handlePagesChanged = statePages => {
117
79
  const curPagePos = statePages.findIndex(page => page.position === 'curr');
118
80
  if (curPagePos === -1)
119
81
  return;
120
- if (curPagePos === currentPagePos)
121
- return;
122
82
  setCurrentPagePos(curPagePos);
123
- stateHandlers?.pageChanged?.(curPagePos, pages?.[curPagePos]);
124
83
  };
125
84
  const handleResize = ({ boundingClientRect }) => {
126
85
  if (partialBoundingRect === undefined
@@ -140,24 +99,8 @@ export const Scrllgngn = ({ pages, thresholdOffsetPercent, stickyBlocksLazyLoadD
140
99
  'force-stick-blocks-after': forceStickBlocks === 'after',
141
100
  'force-stick-blocks-both': forceStickBlocks === 'both'
142
101
  }), className);
143
- const customCssProps = {};
144
- if (partialBoundingRect?.left !== undefined) {
145
- customCssProps[`--${publicClassName}-screen-left`] = `${partialBoundingRect.left}px`;
146
- customCssProps['--PRIVATE-left'] = `${partialBoundingRect.left}px`;
147
- }
148
- if (partialBoundingRect?.right !== undefined) {
149
- customCssProps[`--${publicClassName}-screen-right`] = `${partialBoundingRect.right}px`;
150
- customCssProps['--PRIVATE-right'] = `${partialBoundingRect.right}px`;
151
- }
152
- if (partialBoundingRect?.width !== undefined) {
153
- customCssProps[`--${publicClassName}-screen-width`] = `${partialBoundingRect.width}px`;
154
- customCssProps['--PRIVATE-width'] = `${partialBoundingRect.width}px`;
155
- }
156
- if (partialBoundingRect?.height !== undefined) {
157
- customCssProps[`--${publicClassName}-screen-height`] = `${partialBoundingRect.height}px`;
158
- customCssProps['--PRIVATE-height'] = `${partialBoundingRect.height}px`;
159
- }
160
- return _jsx("div", { className: rootClss, "data-current-page-pos": currentPagePos, style: { ...customCssProps }, children: _jsxs(ResizeObserverComponent, { onResized: handleResize, children: [_jsx("div", { className: c('top-bound'), children: _jsx(IntersectionObserverComponent, { onIntersected: handleTopBoundDetect }) }), _jsx("div", { className: c('back-blocks'), children: lazyLoadedBackBlocks.map((block, blockPos) => {
102
+ const customCssProps = toScreenCssProps(partialBoundingRect);
103
+ return _jsx("div", { className: rootClss, "data-current-page-pos": currentPagePos, "data-current-page-id": pages?.[currentPagePos]?.id, style: { ...customCssProps }, children: _jsxs(ResizeObserverComponent, { onResized: handleResize, children: [_jsx("div", { className: c('top-bound'), children: _jsx(IntersectionObserverComponent, { onIntersected: handleTopBoundDetect }) }), _jsx("div", { className: c('back-blocks'), children: lazyLoadedBackBlocks.map((block, blockPos) => {
161
104
  const isActive = block.displayOnPages.includes(currentPagePos);
162
105
  const blockClss = c('back-block', {
163
106
  active: isActive,
@@ -171,9 +114,7 @@ export const Scrllgngn = ({ pages, thresholdOffsetPercent, stickyBlocksLazyLoadD
171
114
  'lazy-loaded': !isActive
172
115
  });
173
116
  return _jsx("div", { className: blockClss, style: { zIndex: blockPos }, children: block.children });
174
- }) }), _jsx("div", { className: c('scrolling-content'), children: _jsx(IntersectionObserverComponent, { onIntersected: handleCntDetect, children: _jsx(Paginator, { thresholdOffsetPercent: thresholdOffsetPercent, stateHandlers: {
175
- pageChanged: handlePageChange
176
- }, children: pages?.map(page => {
117
+ }) }), _jsx("div", { className: c('scrolling-content'), children: _jsx(IntersectionObserverComponent, { onIntersected: handleCntDetect, children: _jsx(Paginator, { thresholdOffsetPercent: thresholdOffsetPercent, onPagesChanged: handlePagesChanged, children: pages?.map(page => {
177
118
  const scrollBlocks = page.blocks
178
119
  ?.filter(b => b.depth === 'scroll' || b.depth === undefined) ?? [];
179
120
  // eslint-disable-next-line @typescript-eslint/promise-function-async
@@ -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
  };