@design-edito/tools 0.5.37 → 0.5.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/agnostic/@react/effects/index.d.ts +1 -0
  2. package/agnostic/@react/effects/index.js +1 -0
  3. package/agnostic/@react/effects/use-state-change-dispatch/index.d.ts +22 -0
  4. package/agnostic/@react/effects/use-state-change-dispatch/index.js +31 -0
  5. package/agnostic/@react/index.d.ts +1 -0
  6. package/agnostic/@react/index.js +1 -0
  7. package/agnostic/arrays/index.d.ts +3 -2
  8. package/agnostic/arrays/index.js +3 -2
  9. package/agnostic/arrays/integers-between/index.d.ts +8 -0
  10. package/agnostic/arrays/integers-between/index.js +11 -0
  11. package/agnostic/arrays/integers-between/index.test.d.ts +1 -0
  12. package/agnostic/arrays/integers-between/index.test.js +16 -0
  13. package/agnostic/colors/index.d.ts +2 -2
  14. package/agnostic/colors/index.js +2 -2
  15. package/agnostic/css/index.d.ts +2 -1
  16. package/agnostic/css/index.js +2 -1
  17. package/agnostic/css/merge-class-names/index.d.ts +8 -0
  18. package/agnostic/css/merge-class-names/index.js +23 -0
  19. package/agnostic/css/merge-class-names/index.test.d.ts +1 -0
  20. package/agnostic/css/merge-class-names/index.test.js +52 -0
  21. package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +11 -11
  22. package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +11 -11
  23. package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +2 -2
  24. package/agnostic/html/hyper-json/smart-tags/isolated/index.js +2 -2
  25. package/agnostic/html/index.d.ts +2 -2
  26. package/agnostic/html/index.js +2 -2
  27. package/agnostic/index.d.ts +2 -1
  28. package/agnostic/index.js +2 -1
  29. package/agnostic/misc/index.d.ts +1 -1
  30. package/agnostic/misc/index.js +1 -1
  31. package/agnostic/numbers/index.d.ts +2 -2
  32. package/agnostic/numbers/index.js +2 -2
  33. package/agnostic/objects/index.d.ts +2 -2
  34. package/agnostic/objects/index.js +2 -2
  35. package/agnostic/random/index.d.ts +1 -1
  36. package/agnostic/random/index.js +1 -1
  37. package/agnostic/time/dates/index.d.ts +1 -1
  38. package/agnostic/time/dates/index.js +1 -1
  39. package/components/BeforeAfter/index.controlled.js +1 -1
  40. package/components/BeforeAfter/index.js +2 -2
  41. package/components/Button/index.js +1 -1
  42. package/components/Clippable/index.js +1 -1
  43. package/components/Disclaimer/index.d.ts +2 -2
  44. package/components/Disclaimer/index.js +4 -3
  45. package/components/Drawer/index.js +4 -3
  46. package/components/EventListener/index.js +1 -1
  47. package/components/Gallery/index.js +5 -4
  48. package/components/Iframe/index.js +1 -1
  49. package/components/Image/index.d.ts +1 -1
  50. package/components/Image/index.js +3 -2
  51. package/components/Input/index.js +1 -1
  52. package/components/IntersectionObserver/index.js +1 -1
  53. package/components/JsonEditor/index.js +1 -1
  54. package/components/JsonEditor/types.d.ts +0 -6
  55. package/components/JsonEditor/utils.d.ts +0 -5
  56. package/components/JsonEditor/utils.js +0 -5
  57. package/components/Lightbox/index.d.ts +12 -12
  58. package/components/Lightbox/index.js +37 -35
  59. package/components/Lightbox/store.d.ts +9 -43
  60. package/components/Lightbox/store.js +51 -73
  61. package/components/Lightbox/types.d.ts +16 -0
  62. package/components/Lightbox/types.js +1 -0
  63. package/components/ListLoader/index.controlled.js +19 -13
  64. package/components/ListLoader/index.js +19 -3
  65. package/components/Overlayer/index.js +1 -1
  66. package/components/Paginator/index.js +4 -3
  67. package/components/ResizeObserver/index.js +1 -1
  68. package/components/Scrllgngn/index.d.ts +4 -4
  69. package/components/Scrllgngn/index.js +4 -3
  70. package/components/Scrllgngn/types.d.ts +55 -0
  71. package/components/Scrllgngn/types.js +24 -0
  72. package/components/Scrllgngn/utils.d.ts +2 -38
  73. package/components/ScrollListener/index.d.ts +1 -6
  74. package/components/ScrollListener/index.js +10 -9
  75. package/components/ScrollListener/types.d.ts +0 -3
  76. package/components/Select/index.js +1 -1
  77. package/components/Sequencer/index.d.ts +2 -9
  78. package/components/Sequencer/index.js +26 -128
  79. package/components/Sequencer/types.d.ts +30 -0
  80. package/components/Sequencer/types.js +30 -0
  81. package/components/Sequencer/utils.d.ts +4 -0
  82. package/components/Sequencer/utils.js +25 -0
  83. package/components/ShadowRoot/index.js +1 -1
  84. package/components/Subtitles/index.js +3 -5
  85. package/components/Subtitles/utils.d.ts +0 -51
  86. package/components/Subtitles/utils.js +0 -41
  87. package/components/Textarea/index.js +1 -1
  88. package/components/UIModule/index.d.ts +8 -6
  89. package/components/UIModule/index.js +11 -14
  90. package/components/Video/index.controlled.d.ts +1 -7
  91. package/components/Video/index.controlled.js +13 -12
  92. package/components/Video/index.d.ts +0 -8
  93. package/components/Video/index.js +11 -15
  94. package/components/Video/types.d.ts +33 -0
  95. package/components/Video/utils.d.ts +1 -67
  96. package/components/Video/utils.js +25 -44
  97. package/components/index.d.ts +3 -3
  98. package/components/index.js +3 -3
  99. package/components/utils/index.d.ts +1 -44
  100. package/components/utils/index.js +1 -83
  101. package/components/utils/viewport-behaviours/index.d.ts +8 -14
  102. package/components/utils/viewport-behaviours/index.js +15 -37
  103. package/components/utils/viewport-behaviours/types.d.ts +3 -9
  104. package/components/utils/viewport-behaviours/types.js +3 -4
  105. package/node/ftps/directory/index.d.ts +1 -1
  106. package/node/ftps/directory/index.js +1 -1
  107. package/node/images/index.d.ts +1 -1
  108. package/node/images/index.js +1 -1
  109. package/node/images/transform/operations/index.d.ts +3 -3
  110. package/node/images/transform/operations/index.js +3 -3
  111. package/node/index.d.ts +2 -2
  112. package/node/index.js +2 -2
  113. package/package.json +44 -1
  114. package/components/ListLoader/utils.d.ts +0 -8
  115. package/components/ListLoader/utils.js +0 -10
@@ -1,94 +1,72 @@
1
- /**
2
- * The registry of lightbox groups, shared by every {@link Lightbox} on the page.
3
- *
4
- * Deliberately outside React. lm-link renders each component in its **own React
5
- * root** — `renderInTarget` calls `createRoot` per component — so two `<lm-image>`
6
- * in the same article never share a tree, and a context could not reach from one to
7
- * the other. A module-level store can.
8
- *
9
- * Members are kept unordered here; the order that matters is the **document's**, and
10
- * it is read from the DOM when a group opens. React's mount order is not it.
11
- */
12
- const members = new Map();
13
- const listeners = new Set();
14
- let state = {
1
+ const storeStateChangeCallbacks = new Set();
2
+ let storeState = {
15
3
  openGroup: null,
16
- openMemberId: null
4
+ openLightboxId: null,
5
+ lightboxesById: new Map()
17
6
  };
18
- function emit() {
19
- for (const listener of listeners)
20
- listener();
21
- }
22
- function setState(next) {
23
- if (next.openGroup === state.openGroup
24
- && next.openMemberId === state.openMemberId)
7
+ export const getLightboxesStoreState = () => storeState;
8
+ function setLightboxesStoreState(nextState) {
9
+ if (nextState.openGroup === storeState.openGroup
10
+ && nextState.openLightboxId === storeState.openLightboxId
11
+ && nextState.lightboxesById === storeState.lightboxesById)
25
12
  return;
26
- state = next;
27
- emit();
28
- }
29
- export function subscribe(listener) {
30
- listeners.add(listener);
31
- return () => { listeners.delete(listener); };
13
+ storeState = nextState;
14
+ for (const onStoreStateChange of storeStateChangeCallbacks)
15
+ onStoreStateChange();
32
16
  }
33
- export function getState() {
34
- return state;
17
+ export function watchLightboxesStoreState(onStoreStateChange) {
18
+ storeStateChangeCallbacks.add(onStoreStateChange);
19
+ const unwatchLightboxesStoreState = () => {
20
+ storeStateChangeCallbacks.delete(onStoreStateChange);
21
+ };
22
+ return unwatchLightboxesStoreState;
35
23
  }
36
- /** A member's own group, when it declared none: it is a group of one. */
37
- export function soloGroupOf(id) {
38
- return `solo:${id}`;
39
- }
40
- export function register(member) {
41
- members.set(member.id, member);
42
- emit();
24
+ export function registerLightbox(lightbox) {
25
+ setLightboxesStoreState({
26
+ ...storeState,
27
+ lightboxesById: new Map(storeState.lightboxesById).set(lightbox.id, lightbox)
28
+ });
43
29
  }
44
- export function unregister(id) {
45
- members.delete(id);
46
- // A member leaving while it is the one shown would strand the group on nothing.
47
- if (state.openMemberId === id)
48
- setState({ openGroup: null, openMemberId: null });
49
- else
50
- emit();
30
+ export function unregisterLightbox(lightboxId) {
31
+ if (!storeState.lightboxesById.has(lightboxId))
32
+ return;
33
+ const lightboxesById = new Map(storeState.lightboxesById);
34
+ lightboxesById.delete(lightboxId);
35
+ const leavesNothingOpen = storeState.openLightboxId === lightboxId;
36
+ setLightboxesStoreState({
37
+ openGroup: leavesNothingOpen ? null : storeState.openGroup,
38
+ openLightboxId: leavesNothingOpen ? null : storeState.openLightboxId,
39
+ lightboxesById
40
+ });
51
41
  }
52
- /**
53
- * The members of a group, in **document order**.
54
- *
55
- * `compareDocumentPosition` rather than mount order: React mounts a tree in its own
56
- * order, and lm-link mounts each component in a root of its own, so nothing
57
- * guarantees the two match. Only runs when a group opens or navigates, over a
58
- * handful of members.
59
- */
60
- export function membersOf(group) {
61
- const found = [...members.values()].filter(member => member.groups.includes(group));
42
+ export function getGroupLightboxes(group) {
43
+ const found = [...storeState.lightboxesById.values()]
44
+ .filter(lightbox => lightbox.groups.includes(group));
62
45
  return found.sort((a, b) => {
63
46
  const aElt = a.getElement();
64
47
  const bElt = b.getElement();
65
48
  if (aElt === null || bElt === null)
66
49
  return 0;
67
50
  const position = aElt.compareDocumentPosition(bElt);
68
- if ((position & Node.DOCUMENT_POSITION_FOLLOWING) !== 0)
69
- return -1;
70
- if ((position & Node.DOCUMENT_POSITION_PRECEDING) !== 0)
71
- return 1;
72
- return 0;
51
+ return (position & Node.DOCUMENT_POSITION_PRECEDING) !== 0 ? 1 : -1;
73
52
  });
74
53
  }
75
- export function open(group, memberId) {
76
- setState({ openGroup: group, openMemberId: memberId });
54
+ export function openLightbox(group, lightboxId) {
55
+ setLightboxesStoreState({ ...storeState, openGroup: group, openLightboxId: lightboxId });
77
56
  }
78
- export function close() {
79
- setState({ openGroup: null, openMemberId: null });
57
+ export function closeLightbox() {
58
+ setLightboxesStoreState({ ...storeState, openGroup: null, openLightboxId: null });
80
59
  }
81
- /** Moves to another member of the open group by `offset`, clamped at both ends. */
82
- export function step(offset) {
83
- const { openGroup, openMemberId } = state;
84
- if (openGroup === null || openMemberId === null)
60
+ export function navigateOpenGroupBy(offset) {
61
+ const { openGroup, openLightboxId } = storeState;
62
+ if (openGroup === null || openLightboxId === null)
85
63
  return;
86
- const ordered = membersOf(openGroup);
87
- const current = ordered.findIndex(member => member.id === openMemberId);
64
+ const ordered = getGroupLightboxes(openGroup);
65
+ const current = ordered.findIndex(lightbox => lightbox.id === openLightboxId);
88
66
  if (current === -1)
89
67
  return;
90
- const target = ordered[Math.min(ordered.length - 1, Math.max(0, current + offset))];
91
- if (target === undefined)
92
- return;
93
- setState({ openGroup, openMemberId: target.id });
68
+ const index = Math.min(ordered.length - 1, Math.max(0, current + offset));
69
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
70
+ const target = ordered[index];
71
+ setLightboxesStoreState({ ...storeState, openLightboxId: target.id });
94
72
  }
@@ -0,0 +1,16 @@
1
+ /** What a lightbox hands the store so its group can find and place it. */
2
+ export type RegisteredLightbox = {
3
+ id: string;
4
+ groups: string[];
5
+ /** The lightbox's anchor in the page, for ordering. `null` before it mounts. */
6
+ getElement: () => HTMLElement | null;
7
+ };
8
+ /** Everything the store holds, and everything a lightbox watches. */
9
+ export type LightboxesStoreState = {
10
+ /** The group currently open, `null` when nothing is. */
11
+ openGroup: string | null;
12
+ /** The lightbox currently shown. */
13
+ openLightboxId: string | null;
14
+ /** Every mounted lightbox of the page, unordered. */
15
+ lightboxesById: ReadonlyMap<string, RegisteredLightbox>;
16
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { integersBetween } from '../../agnostic/arrays/integers-between/index.js';
2
3
  import { clss } from '../../agnostic/css/clss/index.js';
4
+ import { mergeClassNames } from '../../agnostic/css/merge-class-names/index.js';
3
5
  import { IntersectionObserverComponent } from '../IntersectionObserver/index.js';
4
- import { mergeClassNames } from '../utils/index.js';
5
6
  import { listLoader as publicClassName } from '../public-classnames.js';
6
- import { rangeBetween } from './utils.js';
7
7
  import cssModule from './styles.module.css';
8
8
  /**
9
9
  * Presentational layer of the paginated list. Renders already-loaded pages as a
@@ -65,21 +65,27 @@ export const ControlledListLoader = ({ className, pages, firstPagePos, lastPageP
65
65
  const prevPage = loadedPages[pos - 1];
66
66
  if (prevPage === undefined)
67
67
  return [];
68
- return rangeBetween(prevPage + 1, pagePos - 1);
68
+ return integersBetween(prevPage + 1, pagePos - 1);
69
69
  });
70
- const slots = [
71
- ...firstLoadedPage !== undefined && firstLoadedPage > firstPagePos
72
- ? [{ pagePos: firstLoadedPage - 1, kind: 'prev' }]
73
- : [],
74
- ...loadedPages.map(pagePos => ({ pagePos, kind: 'items' })),
75
- ...gapPages.map(pagePos => ({ pagePos, kind: 'gap' })),
76
- ...lastLoadedPage !== undefined && lastLoadedPage < lastPagePos
77
- ? [{ pagePos: lastLoadedPage + 1, kind: 'next' }]
78
- : []
79
- ].sort((a, b) => a.pagePos - b.pagePos);
80
70
  const sortedLoadingPages = [...loadingPages ?? []]
81
71
  .filter(pagePos => inBoundsPages.includes(pagePos))
82
72
  .sort((a, b) => a - b);
73
+ const hasNothing = loadedPages.length === 0 && sortedLoadingPages.length === 0;
74
+ const slots = hasNothing
75
+ ? [{
76
+ pagePos: [...inBoundsPages].sort((a, b) => a - b)[0] ?? firstPagePos,
77
+ kind: 'gap'
78
+ }]
79
+ : [
80
+ ...firstLoadedPage !== undefined && firstLoadedPage > firstPagePos
81
+ ? [{ pagePos: firstLoadedPage - 1, kind: 'prev' }]
82
+ : [],
83
+ ...loadedPages.map(pagePos => ({ pagePos, kind: 'items' })),
84
+ ...gapPages.map(pagePos => ({ pagePos, kind: 'gap' })),
85
+ ...lastLoadedPage !== undefined && lastLoadedPage < lastPagePos
86
+ ? [{ pagePos: lastLoadedPage + 1, kind: 'next' }]
87
+ : []
88
+ ].sort((a, b) => a.pagePos - b.pagePos);
83
89
  const c = clss(publicClassName, { cssModule });
84
90
  const rootClss = mergeClassNames(c(null, { loading: sortedLoadingPages.length > 0 }), className);
85
91
  const renderLoadButton = (pagePos, kind) => {
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { integersBetween } from '../../agnostic/arrays/integers-between/index.js';
3
4
  import { toError } from '../../agnostic/misc/cast/index.js';
4
5
  import { clamp } from '../../agnostic/numbers/clamp/index.js';
5
6
  import { ControlledListLoader } from './index.controlled.js';
6
- import { rangeBetween } from './utils.js';
7
7
  /**
8
8
  * Paginated list abstraction. Fetches the pages it is asked for, keeps them
9
9
  * fresh, and drives a {@link ControlledListLoader} with the result.
@@ -32,6 +32,7 @@ export const ListLoader = ({ className, pages, defaultPage, fillGaps = true, dro
32
32
  clamp(defaultPage ?? firstPagePos, firstPagePos, lastPagePos)
33
33
  ]);
34
34
  const requestedPages = useRef(new Set());
35
+ const retryTimeouts = useRef(new Set());
35
36
  // Single source of truth for what should be loaded. Memoized so the effects
36
37
  // below keep a stable dependency when the page set has not actually changed.
37
38
  const currentPages = useMemo(() => (pages ?? internalPages).filter(page => page >= firstPagePos && page <= lastPagePos), [pages, internalPages, firstPagePos, lastPagePos]);
@@ -45,6 +46,11 @@ export const ListLoader = ({ className, pages, defaultPage, fillGaps = true, dro
45
46
  return kept.length === next.length ? next : kept;
46
47
  });
47
48
  }
49
+ const gaveUp = requestedPages.current.has(pagePos)
50
+ && !itemsPages.has(pagePos)
51
+ && !loadingPages.has(pagePos);
52
+ if (gaveUp)
53
+ loadPage(pagePos);
48
54
  onLoadPageClicked?.(pagePos);
49
55
  };
50
56
  const storePage = (page, items) => setItemsPages(curr => {
@@ -85,9 +91,19 @@ export const ListLoader = ({ className, pages, defaultPage, fillGaps = true, dro
85
91
  reportFetchError(page, err);
86
92
  if (retriesLeft <= 0)
87
93
  return setPageLoading(page, false);
88
- window.setTimeout(() => loadPage(page, retriesLeft - 1), fetchRetriesDelayMs);
94
+ const timeout = window.setTimeout(() => {
95
+ retryTimeouts.current.delete(timeout);
96
+ loadPage(page, retriesLeft - 1);
97
+ }, fetchRetriesDelayMs);
98
+ retryTimeouts.current.add(timeout);
89
99
  });
90
100
  };
101
+ // Fx. dep. none - Emptying `requestedPages` is what stops a fetch already in flight.
102
+ useEffect(() => () => {
103
+ retryTimeouts.current.forEach(timeout => { window.clearTimeout(timeout); });
104
+ retryTimeouts.current.clear();
105
+ requestedPages.current.clear();
106
+ }, []);
91
107
  // Fx. dep. `currentPages` - Drops everything held for pages no longer wanted.
92
108
  // The updaters return `curr` untouched when nothing was removed, so an unstable
93
109
  // `pages` prop cannot spin the render loop.
@@ -112,7 +128,7 @@ export const ListLoader = ({ className, pages, defaultPage, fillGaps = true, dro
112
128
  setInternalPages(curr => {
113
129
  if (curr.length === 0)
114
130
  return curr;
115
- const missing = rangeBetween(Math.min(...curr), Math.max(...curr))
131
+ const missing = integersBetween(Math.min(...curr), Math.max(...curr))
116
132
  .filter(page => !curr.includes(page));
117
133
  if (missing.length === 0)
118
134
  return curr;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { clss } from '../../agnostic/css/clss/index.js';
3
3
  import { isFalsy } from '../../agnostic/booleans/is-falsy/index.js';
4
- import { mergeClassNames } from '../utils/index.js';
4
+ import { mergeClassNames } from '../../agnostic/css/merge-class-names/index.js';
5
5
  import { overlayer as publicClassName } from '../public-classnames.js';
6
6
  import cssModule from './styles.module.css';
7
7
  /**
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, Children, useState } from 'react';
3
+ import { useStateChangeDispatch } from '../../agnostic/@react/effects/use-state-change-dispatch/index.js';
3
4
  import { clss } from '../../agnostic/css/clss/index.js';
4
- import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
5
+ import { mergeClassNames } from '../../agnostic/css/merge-class-names/index.js';
5
6
  import { paginator as publicClassName } from '../public-classnames.js';
6
7
  import cssModule from './styles.module.css';
7
8
  /**
@@ -63,10 +64,10 @@ export const Paginator = ({ thresholdOffsetPercent, onDirectionChanged, onPagesC
63
64
  console.warn('Paginator: thresholdOffsetPercent must be a number between 0 and 100, received', thresholdOffsetPercent, `— clamped to ${thresholdPercent}.`);
64
65
  }, [thresholdOffsetPercent, thresholdPercent]);
65
66
  // State dispatch
66
- useChangeDispatch(pagesState, pages => onPagesChanged?.(Array
67
+ useStateChangeDispatch(pagesState, pages => onPagesChanged?.(Array
67
68
  .from(pages)
68
69
  .map(([, pageState]) => pageState)));
69
- useChangeDispatch(directionState, onDirectionChanged);
70
+ useStateChangeDispatch(directionState, onDirectionChanged);
70
71
  // Catch scroll direction listening on scroll events
71
72
  useEffect(() => {
72
73
  let lastCall = 0;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useRef, useEffect, useState } from 'react';
3
3
  import { clss } from '../../agnostic/css/clss/index.js';
4
- import { mergeClassNames } from '../utils/index.js';
4
+ import { mergeClassNames } from '../../agnostic/css/merge-class-names/index.js';
5
5
  import { resizeObserver as publicClassName } from '../public-classnames.js';
6
6
  import cssModule from './styles.module.css';
7
7
  /**
@@ -1,6 +1,6 @@
1
- import { type PropsWithChildren, type FunctionComponent } from 'react';
1
+ import { type FunctionComponent, type PropsWithChildren } from 'react';
2
2
  import type { WithClassName } from '../utils/types.js';
3
- import { type TrackedBlockContext, type ViewportOffset } from './utils.js';
3
+ import type { ForceStickMode, StickyDepth, TrackedBlockContext, ViewportOffset } from './types.js';
4
4
  /**
5
5
  * Common properties shared by all block types.
6
6
  *
@@ -34,7 +34,7 @@ export type PropsScrollBlock = PropsCommonBlock & {
34
34
  * stacked in the order they appear across pages (ascending).
35
35
  */
36
36
  export type PropsStickyBlock = PropsCommonBlock & {
37
- depth: 'back' | 'front';
37
+ depth: StickyDepth;
38
38
  zIndex?: number;
39
39
  };
40
40
  /**
@@ -98,7 +98,7 @@ export type Props = WithClassName<{
98
98
  viewportOffsetBottom?: ViewportOffset;
99
99
  viewportOffsetLeft?: ViewportOffset;
100
100
  stickyBlocksLazyLoadDistance?: number;
101
- forceStickBlocks?: 'before' | 'after' | 'both' | 'none';
101
+ forceStickBlocks?: ForceStickMode;
102
102
  onPageChanged?: (payload: {
103
103
  currentPagePos: number;
104
104
  pageData?: PropsPage;
@@ -1,12 +1,13 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { useStateChangeDispatch } from '../../agnostic/@react/effects/use-state-change-dispatch/index.js';
3
4
  import { clss } from '../../agnostic/css/clss/index.js';
4
5
  import { randomHash } from '../../agnostic/random/uuid/index.js';
6
+ import { mergeClassNames } from '../../agnostic/css/merge-class-names/index.js';
5
7
  import { IntersectionObserverComponent } from '../IntersectionObserver/index.js';
6
8
  import { Paginator } from '../Paginator/index.js';
7
9
  import { ResizeObserverComponent } from '../ResizeObserver/index.js';
8
10
  import { subscribe, unsubscribe } from '../ScrollListener/utils.js';
9
- import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
10
11
  import { scrllgngn as publicClassName } from '../public-classnames.js';
11
12
  import { blockDisplayZones, consolidateStickyBlocks, contextsAreEqual, lazyLoadedBlocks, measurePages, sameDiscretePart, scrollBlocksOf, scrollKey, stickyKey, toPaginatorThresholdPercent, toScreenCssProps, toThresholdY, toTrackedBlockContext, toTrackedBlockCssProps, toTrackedBlockDataAttributes, toViewportOffsetCssProps, toVisibleZoneRect, toVisibleZoneRootMargin, visibleZonePollInterval, visibleZonesAreEqual } from './utils.js';
12
13
  import cssModule from './styles.module.css';
@@ -254,11 +255,11 @@ export const Scrllgngn = ({ pages, thresholdOffsetPercent, viewportOffsetTop, vi
254
255
  return () => window.clearInterval(interval);
255
256
  }, [isOnScreen, readVisibleZone]);
256
257
  // Handlers
257
- useChangeDispatch(currentPagePos, pagePos => onPageChanged?.({
258
+ useStateChangeDispatch(currentPagePos, pagePos => onPageChanged?.({
258
259
  currentPagePos: pagePos,
259
260
  pageData: pages?.[pagePos]
260
261
  }));
261
- useChangeDispatch(contentVisible, onContentVisibilityChanged);
262
+ useStateChangeDispatch(contentVisible, onContentVisibilityChanged);
262
263
  const handleTopBoundDetect = e => setTopVis(e.ioEntry?.isIntersecting ?? false);
263
264
  const handleCntDetect = e => setCntVis(e.ioEntry?.isIntersecting ?? false);
264
265
  const handleBtmBoundDetect = e => setBtmVis(e.ioEntry?.isIntersecting ?? false);
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The depths that lift a block out of the scroll flow.
3
+ *
4
+ * **Exported because a consumer has to validate what it is handed**, and a type gives a
5
+ * runtime validator nothing to compare against. A block declaring one of these is
6
+ * consolidated across the pages it appears on and rendered in that layer, not inline.
7
+ */
8
+ export declare const SCRLLGNGN_STICKY_DEPTHS: readonly ["back", "front"];
9
+ export type StickyDepth = typeof SCRLLGNGN_STICKY_DEPTHS[number];
10
+ /**
11
+ * Which out-of-range sticky blocks are forced into a stuck state.
12
+ *
13
+ * Exported for the same reason as {@link SCRLLGNGN_STICKY_DEPTHS}: the list is what a
14
+ * validator needs, and the day a mode is added is the day a caller writing it would
15
+ * otherwise silently get nothing.
16
+ */
17
+ export declare const SCRLLGNGN_FORCE_STICK_MODES: readonly ["before", "after", "both", "none"];
18
+ export type ForceStickMode = typeof SCRLLGNGN_FORCE_STICK_MODES[number];
19
+ /** A length as authored: a bare number means pixels, a string is CSS's problem. */
20
+ export type ViewportOffset = number | string;
21
+ /**
22
+ * Where one tracked block stands in the scroll, as handed to its `onScrolled`.
23
+ *
24
+ * Every progression is a `0`–`1` ratio measured against the threshold line, and
25
+ * weighted **by pixel height**: a zone of unequal pages is not the same as
26
+ * `(indexOfCurrentPage + currentPageProgression) / displayZone.length`.
27
+ *
28
+ * @property currentPage - Zero-based position of the page in view.
29
+ * @property currentPageProgression - `0` when the current page's top meets the
30
+ * threshold, `1` when its bottom does.
31
+ * @property displayZone - Every page the block's `id` appears on. A block without
32
+ * an `id` shows on one page, so its zone holds that page alone and its three
33
+ * progressions are equal.
34
+ * @property indexOfCurrentPageInDisplayZone - Where `currentPage` sits in
35
+ * `displayZone`.
36
+ * @property displayZoneProgression - Progression across the whole zone. On a zone
37
+ * like `[2, 3, 5]` it runs 0 → ~0.66 over pages 2–3, pauses while page 4 shows the
38
+ * block nowhere, then resumes to 1 on page 5.
39
+ * @property contiguousDisplayZone - The unbroken run of `displayZone` containing
40
+ * `currentPage` — `[2, 3]` for the zone above while on page 2 or 3.
41
+ * @property indexOfCurrentPageInContiguousDisplayZone - Where `currentPage` sits in
42
+ * `contiguousDisplayZone`.
43
+ * @property contiguousDisplayZoneProgression - Progression across that run alone,
44
+ * so it always spans a full 0 → 1.
45
+ */
46
+ export type TrackedBlockContext = {
47
+ currentPage: number;
48
+ currentPageProgression: number;
49
+ displayZone: number[];
50
+ indexOfCurrentPageInDisplayZone: number;
51
+ displayZoneProgression: number;
52
+ contiguousDisplayZone: number[];
53
+ indexOfCurrentPageInContiguousDisplayZone: number;
54
+ contiguousDisplayZoneProgression: number;
55
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The depths that lift a block out of the scroll flow.
3
+ *
4
+ * **Exported because a consumer has to validate what it is handed**, and a type gives a
5
+ * runtime validator nothing to compare against. A block declaring one of these is
6
+ * consolidated across the pages it appears on and rendered in that layer, not inline.
7
+ */
8
+ export const SCRLLGNGN_STICKY_DEPTHS = [
9
+ 'back',
10
+ 'front'
11
+ ];
12
+ /**
13
+ * Which out-of-range sticky blocks are forced into a stuck state.
14
+ *
15
+ * Exported for the same reason as {@link SCRLLGNGN_STICKY_DEPTHS}: the list is what a
16
+ * validator needs, and the day a mode is added is the day a caller writing it would
17
+ * otherwise silently get nothing.
18
+ */
19
+ export const SCRLLGNGN_FORCE_STICK_MODES = [
20
+ 'before',
21
+ 'after',
22
+ 'both',
23
+ 'none'
24
+ ];
@@ -1,4 +1,5 @@
1
1
  import type { PropsBlock, PropsPage, PropsStickyBlock } from './index.js';
2
+ import type { StickyDepth, TrackedBlockContext, ViewportOffset } from './types.js';
2
3
  /** The subset of a bounding rect a {@link Scrllgngn} tracks and exposes. */
3
4
  export type ScreenRect = {
4
5
  left: number;
@@ -37,7 +38,7 @@ export declare function consolidateStickyBlocks(pages?: PropsPage[]): Map<string
37
38
  * by ascending `zIndex`. The id comes along because it is what identifies the block
38
39
  * across renders — its own `id` is optional, a consolidation key never is.
39
40
  */
40
- export declare function lazyLoadedBlocks(blocks: Map<string, ConsolidatedStickyBlock>, depth: 'back' | 'front', currentPagePos: number, lazyLoadDistance: number): Array<[string, ConsolidatedStickyBlock]>;
41
+ export declare function lazyLoadedBlocks(blocks: Map<string, ConsolidatedStickyBlock>, depth: StickyDepth, currentPagePos: number, lazyLoadDistance: number): Array<[string, ConsolidatedStickyBlock]>;
41
42
  /**
42
43
  * Builds the CSS custom properties exposed on a {@link Scrllgngn} root.
43
44
  *
@@ -58,41 +59,6 @@ export declare function lazyLoadedBlocks(blocks: Map<string, ConsolidatedStickyB
58
59
  * @returns The custom properties, keyed by their full name.
59
60
  */
60
61
  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
62
  /** How far the threshold line has travelled through one page, in pixels. */
97
63
  export type PageScrollMetrics = {
98
64
  scrolled: number;
@@ -200,8 +166,6 @@ export type VisibleZoneRect = {
200
166
  width: number;
201
167
  height: number;
202
168
  };
203
- /** A length as authored: a bare number means pixels, a string is CSS's problem. */
204
- export type ViewportOffset = number | string;
205
169
  /** The four edges, as authored on the component. */
206
170
  export type ViewportOffsets = {
207
171
  top?: ViewportOffset;
@@ -12,9 +12,6 @@ import type { ScrollListenerAction } from './types.js';
12
12
  * which is what this component has always done — a starting point and not a setting,
13
13
  * hence `default…`. `defaultTracking={false}` with `whenVisible='track'` is the old
14
14
  * `startOnVisible`, written in the shared grammar.
15
- * @property behavioursSuspended - Holds back the instructions that **start** something,
16
- * and lets through those that stop it. A gate reaches `'track'`: measuring a subtree
17
- * nobody is being shown is work done for no one.
18
15
  * @property onIsTrackingChanged - Called once the effective tracking state changed.
19
16
  * @property onScrollStateChanged - Called after the measured {@link ScrollState}
20
17
  * changed. Receives `undefined` until the first measurement lands.
@@ -42,9 +39,7 @@ export type Props = PropsWithChildren<WithClassName<{
42
39
  onScrollStateChanged?: (scrollState?: ScrollState) => void;
43
40
  onScrollProgressChanged?: (progress: number) => void;
44
41
  onScrollDirectionChanged?: (direction: 'up' | 'down') => void;
45
- }>> & ViewportBehaviours<ScrollListenerAction> & {
46
- behavioursSuspended?: boolean;
47
- };
42
+ }>> & ViewportBehaviours<ScrollListenerAction>;
48
43
  /**
49
44
  * Exposes scroll metrics — both the document's and its own — as CSS custom
50
45
  * properties on its root element, so scroll-driven styling needs no JavaScript
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
+ import { useStateChangeDispatch } from '../../agnostic/@react/effects/use-state-change-dispatch/index.js';
3
4
  import { clss } from '../../agnostic/css/clss/index.js';
4
5
  import { randomHash } from '../../agnostic/random/uuid/index.js';
5
- import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
6
+ import { mergeClassNames } from '../../agnostic/css/merge-class-names/index.js';
6
7
  import { useViewportBehaviours } from '../utils/viewport-behaviours/index.js';
7
8
  import { scrollListener as publicClassName } from '../public-classnames.js';
8
9
  import { getScrollProgress, subscribe, toScrollCssProps, unsubscribe } from './utils.js';
@@ -64,7 +65,7 @@ import cssModule from './styles.module.css';
64
65
  * for everyone, each element only for itself. The listeners exist only while at
65
66
  * least one instance is tracking.
66
67
  */
67
- export const ScrollListener = ({ tracking, defaultTracking, behavioursSuspended, visibilityThreshold, visibilityRoot, visibilityRootMargin, visibilityOnAfterMs, visibilityOffAfterMs, whenVisible, whenHidden, onIsTrackingChanged, onScrollStateChanged, onVisibilityChanged, onScrollProgressChanged, onScrollDirectionChanged, className, children }) => {
68
+ export const ScrollListener = ({ tracking, defaultTracking, visibilityThreshold, visibilityRoot, visibilityRootMargin, visibilityOnAfterMs, visibilityOffAfterMs, whenVisible, whenHidden, onIsTrackingChanged, onScrollStateChanged, onVisibilityChanged, onScrollProgressChanged, onScrollDirectionChanged, className, children }) => {
68
69
  // State & refs
69
70
  const [subscriberId] = useState(() => randomHash(6));
70
71
  const [internalTracking, setInternalTracking] = useState(defaultTracking ?? true);
@@ -82,8 +83,8 @@ export const ScrollListener = ({ tracking, defaultTracking, behavioursSuspended,
82
83
  // `tracking`: the effective state reads the prop first, so the internal one they write
83
84
  // is simply never consulted.
84
85
  const actions = {
85
- track: { kind: 'start', domain: 'tracking', run: () => setInternalTracking(true) },
86
- untrack: { kind: 'stop', domain: 'tracking', run: () => setInternalTracking(false) }
86
+ track: { domain: 'tracking', run: () => setInternalTracking(true) },
87
+ untrack: { domain: 'tracking', run: () => setInternalTracking(false) }
87
88
  };
88
89
  // The surrender half of the result is dropped: this component has no control a reader
89
90
  // could take over. The observer runs on the root itself — there is no inner box any
@@ -98,13 +99,13 @@ export const ScrollListener = ({ tracking, defaultTracking, behavioursSuspended,
98
99
  whenVisible,
99
100
  whenHidden,
100
101
  onVisibilityChanged
101
- }, actions, behavioursSuspended === true);
102
+ }, actions);
102
103
  // State dispatch
103
- useChangeDispatch(isTracking, onIsTrackingChanged);
104
- useChangeDispatch(scrollState, onScrollStateChanged);
105
- useChangeDispatch(scrollProgress, progress => { if (progress !== undefined)
104
+ useStateChangeDispatch(isTracking, onIsTrackingChanged);
105
+ useStateChangeDispatch(scrollState, onScrollStateChanged);
106
+ useStateChangeDispatch(scrollProgress, progress => { if (progress !== undefined)
106
107
  onScrollProgressChanged?.(progress); });
107
- useChangeDispatch(scrollDirection, direction => { if (direction !== null)
108
+ useStateChangeDispatch(scrollDirection, direction => { if (direction !== null)
108
109
  onScrollDirectionChanged?.(direction); });
109
110
  // Fx. dep. `scrollState` - derive the document scroll direction
110
111
  useEffect(() => {
@@ -24,8 +24,5 @@ export type ScrollListenerAction = ScrollListenerVerb;
24
24
  * and nothing that ever surrenders. The field is part of `ActionSpec` and is declared
25
25
  * honestly rather than split into domains no hand could ever tell apart. The consequence
26
26
  * is that `':force'` has no trigger here.
27
- *
28
- * A gate is the other brake, and it does reach `track`: measuring a subtree nobody is
29
- * being shown is work done for no one.
30
27
  */
31
28
  export type ScrollListenerDomain = 'tracking';
@@ -3,7 +3,7 @@ import { useState } from 'react';
3
3
  import { clss } from '../../agnostic/css/clss/index.js';
4
4
  import { isNotFalsy } from '../../agnostic/booleans/is-falsy/index.js';
5
5
  import { randomHash } from '../../agnostic/random/uuid/index.js';
6
- import { mergeClassNames } from '../utils/index.js';
6
+ import { mergeClassNames } from '../../agnostic/css/merge-class-names/index.js';
7
7
  import { select as publicClassName } from '../public-classnames.js';
8
8
  import cssModule from './styles.module.css';
9
9
  /**