@hh.ru/magritte-ui-nav-bar 1.0.1

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 (79) hide show
  1. package/NavBar-CrD8CEWb.js +118 -0
  2. package/NavBar-CrD8CEWb.js.map +1 -0
  3. package/index.css +289 -0
  4. package/index.d.ts +9 -0
  5. package/index.js +35 -0
  6. package/index.js.map +1 -0
  7. package/index.mock.d.ts +17 -0
  8. package/index.mock.js +47 -0
  9. package/index.mock.js.map +1 -0
  10. package/internal/KeyedSubscriptions.d.ts +41 -0
  11. package/internal/KeyedSubscriptions.js +79 -0
  12. package/internal/KeyedSubscriptions.js.map +1 -0
  13. package/internal/MetricsProvider.d.ts +77 -0
  14. package/internal/MetricsProvider.js +275 -0
  15. package/internal/MetricsProvider.js.map +1 -0
  16. package/internal/MorphStore.d.ts +10 -0
  17. package/internal/MorphStore.js +36 -0
  18. package/internal/MorphStore.js.map +1 -0
  19. package/internal/PaneStore.d.ts +60 -0
  20. package/internal/PaneStore.js +102 -0
  21. package/internal/PaneStore.js.map +1 -0
  22. package/internal/ProgressiveBlur.d.ts +7 -0
  23. package/internal/ProgressiveBlur.js +43 -0
  24. package/internal/ProgressiveBlur.js.map +1 -0
  25. package/internal/useAnimationRanges.d.ts +38 -0
  26. package/internal/useAnimationRanges.js +52 -0
  27. package/internal/useAnimationRanges.js.map +1 -0
  28. package/internal/useBindScrollToAnimationProgress.d.ts +9 -0
  29. package/internal/useBindScrollToAnimationProgress.js +82 -0
  30. package/internal/useBindScrollToAnimationProgress.js.map +1 -0
  31. package/internal/useDivider.d.ts +4 -0
  32. package/internal/useDivider.js +38 -0
  33. package/internal/useDivider.js.map +1 -0
  34. package/internal/useNavBarMetrics.d.ts +9 -0
  35. package/internal/useNavBarMetrics.js +34 -0
  36. package/internal/useNavBarMetrics.js.map +1 -0
  37. package/internal/useResetFocus.d.ts +3 -0
  38. package/internal/useResetFocus.js +31 -0
  39. package/internal/useResetFocus.js.map +1 -0
  40. package/internal/useScrollAdapter.d.ts +15 -0
  41. package/internal/useScrollAdapter.js +116 -0
  42. package/internal/useScrollAdapter.js.map +1 -0
  43. package/internal/useSnapScroll.d.ts +6 -0
  44. package/internal/useSnapScroll.js +148 -0
  45. package/internal/useSnapScroll.js.map +1 -0
  46. package/internal/useSyncMotionValue.d.ts +2 -0
  47. package/internal/useSyncMotionValue.js +9 -0
  48. package/internal/useSyncMotionValue.js.map +1 -0
  49. package/internal/utils.d.ts +207 -0
  50. package/internal/utils.js +359 -0
  51. package/internal/utils.js.map +1 -0
  52. package/package.json +38 -0
  53. package/public/Actions.d.ts +26 -0
  54. package/public/Actions.js +47 -0
  55. package/public/Actions.js.map +1 -0
  56. package/public/EnvironmentFingerprintNode.d.ts +7 -0
  57. package/public/EnvironmentFingerprintNode.js +70 -0
  58. package/public/EnvironmentFingerprintNode.js.map +1 -0
  59. package/public/LayoutMorph.d.ts +32 -0
  60. package/public/LayoutMorph.js +132 -0
  61. package/public/LayoutMorph.js.map +1 -0
  62. package/public/LayoutStage.d.ts +7 -0
  63. package/public/LayoutStage.js +87 -0
  64. package/public/LayoutStage.js.map +1 -0
  65. package/public/Morph.d.ts +28 -0
  66. package/public/Morph.js +66 -0
  67. package/public/Morph.js.map +1 -0
  68. package/public/NavBar.d.ts +57 -0
  69. package/public/NavBar.js +21 -0
  70. package/public/NavBar.js.map +1 -0
  71. package/public/Pane.d.ts +22 -0
  72. package/public/Pane.js +79 -0
  73. package/public/Pane.js.map +1 -0
  74. package/public/Stage.d.ts +10 -0
  75. package/public/Stage.js +43 -0
  76. package/public/Stage.js.map +1 -0
  77. package/public/TitleContainer.d.ts +24 -0
  78. package/public/TitleContainer.js +34 -0
  79. package/public/TitleContainer.js.map +1 -0
@@ -0,0 +1,15 @@
1
+ import { type RefObject } from 'react';
2
+ import { type MotionValue } from 'motion/react';
3
+ export interface ScrollAdapter {
4
+ getMaxScrollTop: () => number;
5
+ getScrollTop: () => number;
6
+ setScrollTop: (pos: number) => void;
7
+ onScroll: (cb: VoidFunction) => VoidFunction;
8
+ onTouchStart: (cb: VoidFunction) => VoidFunction;
9
+ onTouchEnd: (cb: VoidFunction) => VoidFunction;
10
+ onResize: (cb: VoidFunction) => VoidFunction;
11
+ scrollContainer: RefObject<Element>;
12
+ hasTouchSupport: () => boolean;
13
+ breakScrollSession: VoidFunction;
14
+ }
15
+ export declare const useScrollAdapter: (elementRef: RefObject<HTMLElement>) => [ScrollAdapter, MotionValue<number>];
@@ -0,0 +1,116 @@
1
+ import './../index.css';
2
+ import { useRef, useEffect, useLayoutEffect } from 'react';
3
+ import { useMotionValue } from 'motion/react';
4
+ import { useScrollContext } from '@hh.ru/magritte-internal-custom-scroll';
5
+ import { useInitOnce, findScrollContainer } from './utils.js';
6
+
7
+ const createSubFn = (store) => (handler) => {
8
+ store.add(handler);
9
+ return () => store.delete(handler);
10
+ };
11
+ const useScrollAdapter = (elementRef) => {
12
+ const scrollContext = useScrollContext();
13
+ const scrollContainerRef = useRef();
14
+ const scrollPosition = useMotionValue(0);
15
+ const mediaQueryRef = useRef(null);
16
+ const scrollContainerElRef = useRef(null);
17
+ const [subscriptionsManager, notifier] = useInitOnce(() => {
18
+ const touchStartHandlers = new Set();
19
+ const touchEndHandlers = new Set();
20
+ const scrollHandlers = new Set();
21
+ const resizeHandlers = new Set();
22
+ const subscriptionsManager = {
23
+ onTouchStart: createSubFn(touchStartHandlers),
24
+ onTouchEnd: createSubFn(touchEndHandlers),
25
+ onScroll: createSubFn(scrollHandlers),
26
+ onResize: createSubFn(resizeHandlers),
27
+ };
28
+ const notifier = {
29
+ notifyTouchStart: () => touchStartHandlers.forEach((fn) => fn()),
30
+ notifyTouchEnd: () => touchEndHandlers.forEach((fn) => fn()),
31
+ notifyScroll: () => scrollHandlers.forEach((fn) => fn()),
32
+ notifyResize: () => resizeHandlers.forEach((fn) => fn()),
33
+ };
34
+ return [subscriptionsManager, notifier];
35
+ });
36
+ useEffect(() => {
37
+ if (window && 'matchMedia' in window) {
38
+ mediaQueryRef.current = window.matchMedia('(pointer:coarse)');
39
+ }
40
+ }, []);
41
+ const scrollAdapter = useInitOnce(() => {
42
+ const hasTouchSupport = () => mediaQueryRef.current?.matches ?? false;
43
+ if (scrollContext) {
44
+ const subscriptions = [
45
+ scrollContext.onTouchStart(notifier.notifyTouchStart),
46
+ scrollContext.onTouchEnd(notifier.notifyTouchEnd),
47
+ scrollContext.onScroll(notifier.notifyScroll),
48
+ ];
49
+ return {
50
+ ...scrollContext,
51
+ scrollContainer: scrollContext.wrapperRef,
52
+ ...subscriptionsManager,
53
+ unsubscribe: () => subscriptions.forEach((fn) => fn()),
54
+ hasTouchSupport,
55
+ // не нужно для ненативного скролла
56
+ breakScrollSession: () => void 0,
57
+ };
58
+ }
59
+ return {
60
+ getMaxScrollTop: () => {
61
+ if (!scrollContainerRef.current) {
62
+ return 0;
63
+ }
64
+ const height = scrollContainerRef.current.mode === 'window'
65
+ ? scrollContainerRef.current.infoProvider.clientHeight
66
+ : scrollContainerRef.current.infoProvider.offsetHeight;
67
+ return scrollContainerRef.current.infoProvider.scrollHeight - height;
68
+ },
69
+ getScrollTop: () => scrollContainerRef.current ? Math.max(scrollContainerRef.current.infoProvider.scrollTop, 0) : 0,
70
+ setScrollTop: (pos) => {
71
+ if (scrollContainerRef.current) {
72
+ scrollContainerRef.current.infoProvider.scrollTop = pos;
73
+ }
74
+ },
75
+ scrollContainer: scrollContainerElRef,
76
+ ...subscriptionsManager,
77
+ unsubscribe: () => void 0,
78
+ breakScrollSession: () => {
79
+ if (scrollContainerRef.current?.mode !== 'element' || !scrollContainerElRef.current) {
80
+ return;
81
+ }
82
+ const element = scrollContainerElRef.current;
83
+ const overflowY = element.style.overflowY;
84
+ element.style.overflowY = 'hidden';
85
+ // force layout
86
+ void element.offsetHeight;
87
+ element.style.overflowY = overflowY;
88
+ },
89
+ hasTouchSupport,
90
+ };
91
+ });
92
+ useLayoutEffect(() => {
93
+ const subscriptions = [];
94
+ if (!scrollContext && elementRef.current) {
95
+ scrollContainerRef.current = findScrollContainer(elementRef.current);
96
+ const subscriptionsController = new AbortController();
97
+ const signal = subscriptionsController.signal;
98
+ const eventsProvider = scrollContainerRef.current.eventsProvider;
99
+ eventsProvider.addEventListener('scroll', notifier.notifyScroll, { passive: true, signal });
100
+ eventsProvider.addEventListener('touchstart', notifier.notifyTouchStart, { passive: true, signal });
101
+ eventsProvider.addEventListener('touchend', notifier.notifyTouchEnd, { passive: true, signal });
102
+ scrollContainerElRef.current = scrollContainerRef.current.infoProvider;
103
+ subscriptions.push(() => subscriptionsController.abort());
104
+ }
105
+ if (scrollAdapter.scrollContainer.current) {
106
+ const observer = new ResizeObserver(notifier.notifyResize);
107
+ observer.observe(scrollAdapter.scrollContainer.current);
108
+ subscriptions.push(() => observer.disconnect());
109
+ }
110
+ return () => subscriptions.forEach((fn) => fn());
111
+ }, [notifier, elementRef, scrollContext, scrollAdapter, scrollPosition]);
112
+ return [scrollAdapter, scrollPosition];
113
+ };
114
+
115
+ export { useScrollAdapter };
116
+ //# sourceMappingURL=useScrollAdapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useScrollAdapter.js","sources":["../../src/internal/useScrollAdapter.ts"],"sourcesContent":["import { MutableRefObject, useEffect, useLayoutEffect, useRef, type RefObject } from 'react';\nimport { type MotionValue, useMotionValue } from 'motion/react';\n\nimport { useScrollContext } from '@hh.ru/magritte-internal-custom-scroll';\nimport { findScrollContainer, useInitOnce } from '@hh.ru/magritte-ui-nav-bar/internal/utils';\n\nconst createSubFn = (store: Set<VoidFunction>) => (handler: VoidFunction) => {\n store.add(handler);\n return () => store.delete(handler);\n};\n\nexport interface ScrollAdapter {\n getMaxScrollTop: () => number;\n getScrollTop: () => number;\n setScrollTop: (pos: number) => void;\n onScroll: (cb: VoidFunction) => VoidFunction;\n onTouchStart: (cb: VoidFunction) => VoidFunction;\n onTouchEnd: (cb: VoidFunction) => VoidFunction;\n onResize: (cb: VoidFunction) => VoidFunction;\n scrollContainer: RefObject<Element>;\n hasTouchSupport: () => boolean;\n breakScrollSession: VoidFunction;\n}\n\nexport const useScrollAdapter = (elementRef: RefObject<HTMLElement>): [ScrollAdapter, MotionValue<number>] => {\n const scrollContext = useScrollContext();\n const scrollContainerRef = useRef<ReturnType<typeof findScrollContainer>>();\n const scrollPosition = useMotionValue(0);\n const mediaQueryRef: MutableRefObject<MediaQueryList | null> = useRef(null);\n const scrollContainerElRef: MutableRefObject<null | Element> = useRef(null);\n\n const [subscriptionsManager, notifier] = useInitOnce(() => {\n const touchStartHandlers = new Set<VoidFunction>();\n const touchEndHandlers = new Set<VoidFunction>();\n const scrollHandlers = new Set<VoidFunction>();\n const resizeHandlers = new Set<VoidFunction>();\n const subscriptionsManager = {\n onTouchStart: createSubFn(touchStartHandlers),\n onTouchEnd: createSubFn(touchEndHandlers),\n onScroll: createSubFn(scrollHandlers),\n onResize: createSubFn(resizeHandlers),\n };\n\n const notifier = {\n notifyTouchStart: () => touchStartHandlers.forEach((fn) => fn()),\n notifyTouchEnd: () => touchEndHandlers.forEach((fn) => fn()),\n notifyScroll: () => scrollHandlers.forEach((fn) => fn()),\n notifyResize: () => resizeHandlers.forEach((fn) => fn()),\n };\n\n return [subscriptionsManager, notifier];\n });\n\n useEffect(() => {\n if (window && 'matchMedia' in window) {\n mediaQueryRef.current = window.matchMedia('(pointer:coarse)');\n }\n }, []);\n\n const scrollAdapter = useInitOnce(() => {\n const hasTouchSupport = () => mediaQueryRef.current?.matches ?? false;\n if (scrollContext) {\n const subscriptions = [\n scrollContext.onTouchStart(notifier.notifyTouchStart),\n scrollContext.onTouchEnd(notifier.notifyTouchEnd),\n scrollContext.onScroll(notifier.notifyScroll),\n ];\n return {\n ...scrollContext,\n scrollContainer: scrollContext.wrapperRef,\n ...subscriptionsManager,\n unsubscribe: () => subscriptions.forEach((fn) => fn()),\n hasTouchSupport,\n // не нужно для ненативного скролла\n breakScrollSession: (): void => void 0,\n };\n }\n return {\n getMaxScrollTop: () => {\n if (!scrollContainerRef.current) {\n return 0;\n }\n\n const height =\n scrollContainerRef.current.mode === 'window'\n ? scrollContainerRef.current.infoProvider.clientHeight\n : scrollContainerRef.current.infoProvider.offsetHeight;\n return scrollContainerRef.current.infoProvider.scrollHeight - height;\n },\n getScrollTop: () =>\n scrollContainerRef.current ? Math.max(scrollContainerRef.current.infoProvider.scrollTop, 0) : 0,\n setScrollTop: (pos: number) => {\n if (scrollContainerRef.current) {\n scrollContainerRef.current.infoProvider.scrollTop = pos;\n }\n },\n scrollContainer: scrollContainerElRef,\n ...subscriptionsManager,\n unsubscribe: (): void => void 0,\n breakScrollSession: () => {\n if (scrollContainerRef.current?.mode !== 'element' || !scrollContainerElRef.current) {\n return;\n }\n\n const element = scrollContainerElRef.current as HTMLElement;\n const overflowY = element.style.overflowY;\n element.style.overflowY = 'hidden';\n // force layout\n void element.offsetHeight;\n element.style.overflowY = overflowY;\n },\n hasTouchSupport,\n };\n }) satisfies ScrollAdapter;\n\n useLayoutEffect(() => {\n const subscriptions: VoidFunction[] = [];\n if (!scrollContext && elementRef.current) {\n scrollContainerRef.current = findScrollContainer(elementRef.current);\n const subscriptionsController = new AbortController();\n const signal = subscriptionsController.signal;\n const eventsProvider = scrollContainerRef.current.eventsProvider;\n eventsProvider.addEventListener('scroll', notifier.notifyScroll, { passive: true, signal });\n eventsProvider.addEventListener('touchstart', notifier.notifyTouchStart, { passive: true, signal });\n eventsProvider.addEventListener('touchend', notifier.notifyTouchEnd, { passive: true, signal });\n scrollContainerElRef.current = scrollContainerRef.current.infoProvider;\n subscriptions.push(() => subscriptionsController.abort());\n }\n\n if (scrollAdapter.scrollContainer.current) {\n const observer = new ResizeObserver(notifier.notifyResize);\n observer.observe(scrollAdapter.scrollContainer.current);\n subscriptions.push(() => observer.disconnect());\n }\n\n return () => subscriptions.forEach((fn) => fn());\n }, [notifier, elementRef, scrollContext, scrollAdapter, scrollPosition]);\n\n return [scrollAdapter, scrollPosition];\n};\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import { type MotionValue } from 'motion/react';
2
+ import { type ScrollAdapter } from '@hh.ru/magritte-ui-nav-bar/internal/useScrollAdapter';
3
+ export declare const useSnapScroll: (scrollPosition: MotionValue<number>, totalAnimationProgress: MotionValue<number>, scrollAdapter: ScrollAdapter, getAnimationStops: () => {
4
+ top: number;
5
+ bottom: number;
6
+ }, enableScrollSnap: boolean) => VoidFunction;
@@ -0,0 +1,148 @@
1
+ import './../index.css';
2
+ import { useRef, useEffect } from 'react';
3
+ import { useMotionValue, animate } from 'motion/react';
4
+ import { useActualRef, useInitOnce } from './utils.js';
5
+
6
+ const AUTOSCROLL_SPEED = 1600; // px/sec
7
+ const SETTLE_FRAMES_COUNT = 10;
8
+ const useSnapScroll = (scrollPosition, totalAnimationProgress, scrollAdapter, getAnimationStops, enableScrollSnap) => {
9
+ const scrollAnimationController = useMotionValue(scrollPosition.get());
10
+ const prevStopRef = useRef(scrollAdapter.getScrollTop() === getAnimationStops().top ? 'top' : 'bottom');
11
+ const touchedRef = useRef(false);
12
+ const lastDirectionRef = useRef(null);
13
+ const animationControlsRef = useRef(null);
14
+ const enableScrollSnapRef = useActualRef(enableScrollSnap);
15
+ const prevScrollPositionRef = useRef(0);
16
+ const stopAnimation = useInitOnce(() => () => {
17
+ if (animationControlsRef.current) {
18
+ animationControlsRef.current.stop();
19
+ animationControlsRef.current = null;
20
+ }
21
+ });
22
+ const snapScroll = useInitOnce(() => () => {
23
+ if (touchedRef.current || animationControlsRef.current || !enableScrollSnapRef.current) {
24
+ return;
25
+ }
26
+ const stops = getAnimationStops();
27
+ const target = lastDirectionRef.current === null ? prevStopRef.current : lastDirectionRef.current;
28
+ const targetPos = stops[target];
29
+ const currentPos = scrollAdapter.getScrollTop();
30
+ // за границами анимации навбара, т.е. он или полностью раскрыт или схлопнут
31
+ if (currentPos >= stops.top || currentPos <= stops.bottom) {
32
+ return;
33
+ }
34
+ // ios safari не предоставляет достаточно данных чтобы точно определить что пользователь перестал
35
+ // взаимодействовать со страницей, поэтому мы принимаем решение о запуске анимации скролла по косвенному
36
+ // признаку - отсутствие события скролл в течение SETTLE_FRAMES_COUNT.
37
+ // Однако в случае если пользователь поскроллил страницу и задержал палец на экране анимация тоже будет
38
+ // запущена. Если после этого положение пальца на экране изменится скролл моментально вернется к положению
39
+ // которое было до анимации, после чего снова будет запущена анимация и так по кругу.
40
+ // Единственное что тут можно сделать - после запуска анимации прекратить "скролл сессию", чтобы при движении
41
+ // пальца позиция скролла не менялась до начала слежующей скролл сессии. К сожалению это работает только
42
+ // если скролл происходит в каком то контейнере. Если же скроллится вся страница, то скролл сессию нельзя
43
+ // разорвать без видимых для пользователя изменений в лейауте.
44
+ scrollAdapter.breakScrollSession();
45
+ scrollAnimationController.jump(currentPos);
46
+ prevScrollPositionRef.current = currentPos;
47
+ const controls = animate(scrollAnimationController, targetPos, {
48
+ duration: Math.abs(scrollAnimationController.get() - targetPos) / AUTOSCROLL_SPEED,
49
+ ease: 'easeOut',
50
+ });
51
+ animationControlsRef.current = controls;
52
+ controls
53
+ .then(() => {
54
+ lastDirectionRef.current = null;
55
+ prevStopRef.current = target;
56
+ animationControlsRef.current = null;
57
+ })
58
+ .catch(() => {
59
+ prevStopRef.current = target;
60
+ animationControlsRef.current = null;
61
+ });
62
+ });
63
+ const checkSettled = useInitOnce(() => {
64
+ let frames = 0;
65
+ let run = false;
66
+ return () => {
67
+ frames = SETTLE_FRAMES_COUNT;
68
+ if (run) {
69
+ return;
70
+ }
71
+ run = true;
72
+ const step = () => {
73
+ frames -= 1;
74
+ if (frames === 0) {
75
+ run = false;
76
+ snapScroll();
77
+ return;
78
+ }
79
+ requestAnimationFrame(step);
80
+ };
81
+ requestAnimationFrame(step);
82
+ };
83
+ });
84
+ const onTouchEnd = useInitOnce(() => () => {
85
+ touchedRef.current = false;
86
+ stopAnimation();
87
+ checkSettled();
88
+ });
89
+ useEffect(() => {
90
+ const subscriptions = [
91
+ scrollAdapter.onTouchStart(() => {
92
+ stopAnimation();
93
+ touchedRef.current = true;
94
+ }),
95
+ scrollAdapter.onTouchEnd(onTouchEnd),
96
+ scrollAdapter.onScroll(() => {
97
+ const scrollTop = scrollAdapter.getScrollTop();
98
+ const dir = Math.sign(scrollTop - prevScrollPositionRef.current);
99
+ if (animationControlsRef.current) {
100
+ // ios safari шлет только первый touchstart/touchend в случае скролла который происходит
101
+ // в несколько жестов. Т.е. на ios мы не можем положиться на touchedRef для прерывания анимации.
102
+ // Вместо этого мы проверяем не изменилось ли направление скролла и не "перескочил" ли скролл
103
+ // значение анимируемой переменной в направлении скролла. Оба этих факта говорят о вмешательстве
104
+ // пользователя в процесс скролла, а значит надо прервать анимацию. Можно было бы сделать
105
+ // проще и проверять расстояние между значением анимируемой переменной и фактическим положением
106
+ // скролла, но сафари после установки scrollTop может прислать событие со старым значением
107
+ // scrollTop, и только потом с новым, что делает такой подход неприменимым.
108
+ const isDirChanged = dir !== 0 &&
109
+ lastDirectionRef.current &&
110
+ (lastDirectionRef.current === 'top' ? dir < 0 : dir > 0);
111
+ const isOutOfRange = lastDirectionRef.current &&
112
+ (lastDirectionRef.current === 'top'
113
+ ? scrollTop > scrollAnimationController.get() + 1
114
+ : scrollTop < scrollAnimationController.get() - 1);
115
+ if (isDirChanged || isOutOfRange) {
116
+ stopAnimation();
117
+ }
118
+ }
119
+ prevScrollPositionRef.current = scrollTop;
120
+ // Не обновляем MotionValue вместе со скроллом если происходит анимация скролла
121
+ // потому что ios safari шлет события скролла с низкой частотой, из-за чего вся анимация может
122
+ // выглядеть дерганно. Вместо этого ниже есть прямая связь scrollAnimationController -> scrollPosition
123
+ !animationControlsRef.current && scrollPosition.set(scrollTop);
124
+ if (dir !== 0) {
125
+ lastDirectionRef.current = dir > 0 ? 'top' : 'bottom';
126
+ }
127
+ }),
128
+ totalAnimationProgress.on('change', checkSettled),
129
+ scrollAnimationController.on('change', (value) => {
130
+ scrollAdapter.setScrollTop(value);
131
+ scrollPosition.set(value);
132
+ }),
133
+ ];
134
+ return () => subscriptions.forEach((unsubscribe) => unsubscribe());
135
+ }, [
136
+ checkSettled,
137
+ scrollAdapter,
138
+ scrollPosition,
139
+ onTouchEnd,
140
+ scrollAnimationController,
141
+ stopAnimation,
142
+ totalAnimationProgress,
143
+ ]);
144
+ return snapScroll;
145
+ };
146
+
147
+ export { useSnapScroll };
148
+ //# sourceMappingURL=useSnapScroll.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSnapScroll.js","sources":["../../src/internal/useSnapScroll.ts"],"sourcesContent":["import { type MutableRefObject, useEffect, useRef } from 'react';\nimport { useMotionValue, animate, type MotionValue, AnimationPlaybackControlsWithThen } from 'motion/react';\n\nimport { type ScrollAdapter } from '@hh.ru/magritte-ui-nav-bar/internal/useScrollAdapter';\nimport { useActualRef, useInitOnce } from '@hh.ru/magritte-ui-nav-bar/internal/utils';\n\nconst AUTOSCROLL_SPEED = 1600; // px/sec\nconst SETTLE_FRAMES_COUNT = 10;\n\nexport const useSnapScroll = (\n scrollPosition: MotionValue<number>,\n totalAnimationProgress: MotionValue<number>,\n scrollAdapter: ScrollAdapter,\n getAnimationStops: () => { top: number; bottom: number },\n enableScrollSnap: boolean\n): VoidFunction => {\n const scrollAnimationController = useMotionValue(scrollPosition.get());\n const prevStopRef = useRef<'top' | 'bottom'>(\n scrollAdapter.getScrollTop() === getAnimationStops().top ? 'top' : 'bottom'\n );\n const touchedRef = useRef(false);\n const lastDirectionRef: MutableRefObject<'top' | 'bottom' | null> = useRef(null);\n const animationControlsRef: MutableRefObject<AnimationPlaybackControlsWithThen | null> = useRef(null);\n const enableScrollSnapRef = useActualRef(enableScrollSnap);\n const prevScrollPositionRef = useRef(0);\n\n const stopAnimation = useInitOnce(() => () => {\n if (animationControlsRef.current) {\n animationControlsRef.current.stop();\n animationControlsRef.current = null;\n }\n });\n\n const snapScroll = useInitOnce(() => () => {\n if (touchedRef.current || animationControlsRef.current || !enableScrollSnapRef.current) {\n return;\n }\n\n const stops = getAnimationStops();\n const target = lastDirectionRef.current === null ? prevStopRef.current : lastDirectionRef.current;\n const targetPos = stops[target];\n const currentPos = scrollAdapter.getScrollTop();\n\n // за границами анимации навбара, т.е. он или полностью раскрыт или схлопнут\n if (currentPos >= stops.top || currentPos <= stops.bottom) {\n return;\n }\n\n // ios safari не предоставляет достаточно данных чтобы точно определить что пользователь перестал\n // взаимодействовать со страницей, поэтому мы принимаем решение о запуске анимации скролла по косвенному\n // признаку - отсутствие события скролл в течение SETTLE_FRAMES_COUNT.\n // Однако в случае если пользователь поскроллил страницу и задержал палец на экране анимация тоже будет\n // запущена. Если после этого положение пальца на экране изменится скролл моментально вернется к положению\n // которое было до анимации, после чего снова будет запущена анимация и так по кругу.\n // Единственное что тут можно сделать - после запуска анимации прекратить \"скролл сессию\", чтобы при движении\n // пальца позиция скролла не менялась до начала слежующей скролл сессии. К сожалению это работает только\n // если скролл происходит в каком то контейнере. Если же скроллится вся страница, то скролл сессию нельзя\n // разорвать без видимых для пользователя изменений в лейауте.\n scrollAdapter.breakScrollSession();\n scrollAnimationController.jump(currentPos);\n prevScrollPositionRef.current = currentPos;\n const controls = animate(scrollAnimationController, targetPos, {\n duration: Math.abs(scrollAnimationController.get() - targetPos) / AUTOSCROLL_SPEED,\n ease: 'easeOut',\n });\n animationControlsRef.current = controls;\n controls\n .then(() => {\n lastDirectionRef.current = null;\n prevStopRef.current = target;\n animationControlsRef.current = null;\n })\n .catch(() => {\n prevStopRef.current = target;\n animationControlsRef.current = null;\n });\n });\n\n const checkSettled = useInitOnce(() => {\n let frames = 0;\n let run = false;\n return () => {\n frames = SETTLE_FRAMES_COUNT;\n if (run) {\n return;\n }\n\n run = true;\n const step = () => {\n frames -= 1;\n if (frames === 0) {\n run = false;\n snapScroll();\n return;\n }\n\n requestAnimationFrame(step);\n };\n\n requestAnimationFrame(step);\n };\n });\n\n const onTouchEnd = useInitOnce(() => () => {\n touchedRef.current = false;\n stopAnimation();\n checkSettled();\n });\n\n useEffect(() => {\n const subscriptions = [\n scrollAdapter.onTouchStart(() => {\n stopAnimation();\n touchedRef.current = true;\n }),\n scrollAdapter.onTouchEnd(onTouchEnd),\n scrollAdapter.onScroll(() => {\n const scrollTop = scrollAdapter.getScrollTop();\n const dir = Math.sign(scrollTop - prevScrollPositionRef.current);\n if (animationControlsRef.current) {\n // ios safari шлет только первый touchstart/touchend в случае скролла который происходит\n // в несколько жестов. Т.е. на ios мы не можем положиться на touchedRef для прерывания анимации.\n // Вместо этого мы проверяем не изменилось ли направление скролла и не \"перескочил\" ли скролл\n // значение анимируемой переменной в направлении скролла. Оба этих факта говорят о вмешательстве\n // пользователя в процесс скролла, а значит надо прервать анимацию. Можно было бы сделать\n // проще и проверять расстояние между значением анимируемой переменной и фактическим положением\n // скролла, но сафари после установки scrollTop может прислать событие со старым значением\n // scrollTop, и только потом с новым, что делает такой подход неприменимым.\n const isDirChanged =\n dir !== 0 &&\n lastDirectionRef.current &&\n (lastDirectionRef.current === 'top' ? dir < 0 : dir > 0);\n const isOutOfRange =\n lastDirectionRef.current &&\n (lastDirectionRef.current === 'top'\n ? scrollTop > scrollAnimationController.get() + 1\n : scrollTop < scrollAnimationController.get() - 1);\n if (isDirChanged || isOutOfRange) {\n stopAnimation();\n }\n }\n prevScrollPositionRef.current = scrollTop;\n // Не обновляем MotionValue вместе со скроллом если происходит анимация скролла\n // потому что ios safari шлет события скролла с низкой частотой, из-за чего вся анимация может\n // выглядеть дерганно. Вместо этого ниже есть прямая связь scrollAnimationController -> scrollPosition\n !animationControlsRef.current && scrollPosition.set(scrollTop);\n if (dir !== 0) {\n lastDirectionRef.current = dir > 0 ? 'top' : 'bottom';\n }\n }),\n totalAnimationProgress.on('change', checkSettled),\n scrollAnimationController.on('change', (value) => {\n scrollAdapter.setScrollTop(value);\n scrollPosition.set(value);\n }),\n ];\n\n return () => subscriptions.forEach((unsubscribe) => unsubscribe());\n }, [\n checkSettled,\n scrollAdapter,\n scrollPosition,\n onTouchEnd,\n scrollAnimationController,\n stopAnimation,\n totalAnimationProgress,\n ]);\n\n return snapScroll;\n};\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { type MotionValue } from 'motion/react';
2
+ export declare const useSyncMotionValue: (source: MotionValue<number>, destination?: MotionValue<number>) => void;
@@ -0,0 +1,9 @@
1
+ import './../index.css';
2
+ import { useEffect } from 'react';
3
+
4
+ const useSyncMotionValue = (source, destination) => {
5
+ useEffect(() => (destination ? source.on('change', (value) => destination.set(value)) : void 0), [source, destination]);
6
+ };
7
+
8
+ export { useSyncMotionValue };
9
+ //# sourceMappingURL=useSyncMotionValue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSyncMotionValue.js","sources":["../../src/internal/useSyncMotionValue.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { type MotionValue } from 'motion/react';\n\nexport const useSyncMotionValue = (source: MotionValue<number>, destination?: MotionValue<number>): void => {\n useEffect(\n () => (destination ? source.on('change', (value) => destination.set(value)) : void 0),\n [source, destination]\n );\n};\n"],"names":[],"mappings":";;MAGa,kBAAkB,GAAG,CAAC,MAA2B,EAAE,WAAiC,KAAU;AACvG,IAAA,SAAS,CACL,OAAO,WAAW,GAAG,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,KAAK,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,EACrF,CAAC,MAAM,EAAE,WAAW,CAAC,CACxB,CAAC;AACN;;;;"}
@@ -0,0 +1,207 @@
1
+ import { type MutableRefObject } from 'react';
2
+ import type { MotionValue } from 'motion';
3
+ import { KeyedSubscriptions } from '@hh.ru/magritte-ui-nav-bar/internal/KeyedSubscriptions';
4
+ import { ScrollAdapter } from '@hh.ru/magritte-ui-nav-bar/internal/useScrollAdapter';
5
+ export declare const lerp: (from: number, to: number, progress: number) => number;
6
+ export declare const clamp: (value: number, min: number, max: number) => number;
7
+ /**
8
+ * Создаёт функцию преобразования значений из одного числового интервала (домен) в другой (образ).
9
+ *
10
+ * @param domain Интервал входных значений [min, max].
11
+ * @param image Интервал выходных значений [min, max].
12
+ *
13
+ * @returns Функция, принимающая число из диапазона `domain`, преобразующая его в диапазон `image`
14
+ * с линейной интерполяцией.
15
+ */
16
+ export declare const remap: (domain: [number, number], image: [number, number]) => (value: number) => number;
17
+ export declare const useInitOnce: <T>(fn: () => T) => T;
18
+ /**
19
+ * Возвращает «одноразовый» планировщик, который выполнит `fn` в следующем
20
+ * тике **микрозадач** (через `Promise.resolve().then(...)`). Повторные вызовы
21
+ * до момента выполнения схлопываются в один запуск.
22
+ *
23
+ * Семантика:
24
+ * - Первый вызов ставит `fn` в очередь микрозадач; последующие вызовы до
25
+ * выполнения игнорируются.
26
+ * - После выполнения `fn` «засов» снимается — можно планировать снова.
27
+ * - Ошибки из `fn` перебрасываются в следующий макротик (через `setTimeout`),
28
+ * чтобы они были видны в DevTools и не превращались в «unhandled rejection».
29
+ *
30
+ * Когда использовать:
31
+ * - Нужно объединить множество синхронных триггеров в одно действие,
32
+ * выполнив его **раньше таймеров/`requestAnimationFrame`**.
33
+ * - Не требуется ожидать стабилизации DOM/лейаута (микрозадачи идут до кадра).
34
+ *
35
+ * @param fn — колбэк, который будет выполнен в следующем тике микрозадач.
36
+ * @returns Функция-триггер: вызов ставит `fn` в очередь 1 раз на тик микрозадач.
37
+ *
38
+ * @example
39
+ * const trigger = scheduleMicro(recompute);
40
+ * trigger(); // запланирован один запуск
41
+ * trigger(); // проигнорирован (уже запланировано)
42
+ * // `recompute` выполнится один раз на следующем тике микрозадач
43
+ */
44
+ export declare const scheduleMicro: (fn: VoidFunction) => () => void;
45
+ /**
46
+ * Возвращает «одноразовый» планировщик, который выполнит `fn` в следующем
47
+ * тике **макрозадач** (через `setTimeout(0)`). Повторные вызовы до момента
48
+ * выполнения схлопываются в один запуск.
49
+ *
50
+ * Семантика:
51
+ * - Первый вызов ставит `fn` в `setTimeout`; последующие вызовы до выполнения
52
+ * игнорируются.
53
+ * - После выполнения `fn` «засов» снимается — можно планировать снова.
54
+ * - Ошибки из `fn` всплывут как непойманные ошибки из обработчика таймера.
55
+ *
56
+ * Когда использовать:
57
+ * - Нужно отложить выполнение **после** опустошения очереди микрозадач
58
+ * (т.е. позже, чем `scheduleMicro`), часто позволяя браузеру между делом
59
+ * подготовить кадр/перерисовку.
60
+ *
61
+ * @param fn — колбэк, который будет выполнен в следующем тике макрозадач.
62
+ * @returns Функция-триггер: вызов ставит `fn` в очередь 1 раз на тик таймера.
63
+ *
64
+ * @example
65
+ * const trigger = scheduleMacro(flushQueue);
66
+ * trigger(); // запланирован один таймер
67
+ * trigger(); // проигнорирован до выполнения `flushQueue`
68
+ */
69
+ export declare const scheduleMacro: (fn: VoidFunction) => VoidFunction;
70
+ /**
71
+ * Планировщик для «сбора метрик» на основе микрозадач: откладывает выполнение `fn`
72
+ * до момента, когда серия частых вызовов завершится, и запускает `fn` ровно один раз.
73
+ *
74
+ * Идея:
75
+ * - Первый вызов добавляет «запас» из двух тиков микрозадач; каждый следующий
76
+ * в ту же серию добавляет ещё +1.
77
+ * - Единый «дренирующий» цикл (один на серию) уменьшает счётчик на каждом тике
78
+ * микрозадач; когда счётчик достигает нуля — выполняется `fn`.
79
+ * - Повторные вызовы не создают параллельных циклов; `fn` не вызывается несколько раз.
80
+ * - Ошибки из `fn` перебрасываются в макрозадачу (`setTimeout`), чтобы их было видно
81
+ * в DevTools и они не становились unhandled rejection.
82
+ *
83
+ * Когда использовать:
84
+ * - Нужна коалесценция множественных триггеров (Promises, ResizeObserver и т.п.)
85
+ * с запуском «после того, как всё успокоилось», чтобы измерить DOM/пересчитать
86
+ * лейаут/агрегировать состояние один раз.
87
+ *
88
+ * Гарантии и нюансы:
89
+ * - Гарантируется минимум два тика микрозадач после первого вызова (даёт времени
90
+ * промисам/микроочереди завершиться).
91
+ * - Работает как эвристика «дождаться затишья» на уровне микрозадач, без перехода
92
+ * в макрозадачи/таймеры (сам `fn` всё равно выполняется в микрозадаче).
93
+ *
94
+ * @param fn — колбэк, который будет выполнен один раз после завершения серии вызовов.
95
+ * @returns Триггер; его можно вызывать многократно — `fn` запустится один раз,
96
+ * когда внутренний счётчик дойдёт до нуля.
97
+ */
98
+ export declare const scheduleGatherMetrics: (fn: VoidFunction) => VoidFunction;
99
+ /**
100
+ * Хук для получения "живой" ссылки (`ref`) на актуальное значение.
101
+ *
102
+ * В отличие от обычного `useRef(initialValue)`, где `.current` инициализируется
103
+ * только один раз и далее меняется вручную, `useActualRef` автоматически
104
+ * обновляет `.current` при каждом рендере на основе переданного `value`.
105
+ *
106
+ * Зачем это нужно:
107
+ * - Когда в коллбэках или подписках нужно иметь доступ к самому свежему значению
108
+ * пропса или состояния, но при этом не хочется пересоздавать замыкания.
109
+ * - Позволяет избежать проблем со "старыми" значениями внутри `useCallback`,
110
+ * `useEffect` и обработчиков событий.
111
+ *
112
+ * @param value Актуальное значение, которое должно быть доступно через `.current`.
113
+ *
114
+ * @returns `ref`-объект (`MutableRefObject<T>`), чьё свойство `.current` всегда
115
+ * указывает на последнее переданное `value`.
116
+ */
117
+ export declare const useActualRef: <T>(value: T) => MutableRefObject<T>;
118
+ /**
119
+ * Подписывает трансформацию на изменения исходного `MotionValue` **и** внешнего хранилища ключевых подписок.
120
+ *
121
+ * В отличие от `useTransform` из motion, этот хук не создает новый `MotionValue`.
122
+ * Вместо этого он вызывает пользовательскую `transformFn(arg, value)` при любом изменении:
123
+ * - исходного `originalValue`, и/или
124
+ * - значений во внешнем хранилище `store` по указанным `keys`.
125
+ *
126
+ * `transformFn` обычно внутри делает `.set(...)` у одного или нескольких целевых `MotionValue`.
127
+ *
128
+ * @template T Тип значения исходного `MotionValue`.
129
+ * @template K Тип ключей внешнего хранилища (строка | число | символ).
130
+ * @template V Тип агрегированного значения, извлекаемого из хранилища и передаваемого в `transformFn`.
131
+ *
132
+ * @param {MotionValue<T>} originalValue
133
+ * Исходный `MotionValue`, изменения которого инициируют трансформацию.
134
+ *
135
+ * @param {KeyedSubscriptions<K> | (() => KeyedSubscriptions<K>)} store
136
+ * Внешнее хранилище с подписками. Может быть самим объектом или фабрикой.
137
+ * Если передана функция, хук пересоздает подписки при событии `onDestroy`.
138
+ *
139
+ * @param {K[]} keys
140
+ * Набор ключей хранилища, на изменения которых нужно реагировать.
141
+ * Равенство `keys` проверяется по количеству и составу; изменение порядка не важно.
142
+ * Изменение этого набора вызывает пересоздание подписок подписок.
143
+ *
144
+ * @param {() => V} valueExtractor
145
+ * Функция, синхронно извлекающая/агрегирующая данные из хранилища
146
+ * для передачи первым аргументом в `transformFn`.
147
+ *
148
+ * @param {(arg: V, value: T) => void} transformFn
149
+ * Функция-трансформация. Вызывается при каждом триггере с
150
+ * `arg = valueExtractor()` и `value = originalValue.get()`. Обычно внутри вызывает `someMotionValue.set(...)`.
151
+ *
152
+ * @example
153
+ * ```tsx
154
+ * import { motion, useMotionValue } from "motion/react";
155
+ *
156
+ * const store: KeyedSubscriptions<"width" | "height"> = createKeyedStore();
157
+ *
158
+ * export function Example() {
159
+ * const x = useMotionValue(0); // исходный драйвер
160
+ * const y = useMotionValue(0); // целевой MotionValue
161
+ *
162
+ * useStoreSyncedTransform(
163
+ * x,
164
+ * store,
165
+ * ["width", "height"],
166
+ * () => ({
167
+ * w: store.get("width"),
168
+ * h: store.get("height"),
169
+ * }),
170
+ * ({ w, h }, xVal) => {
171
+ * // пример простой зависимости
172
+ * const mapped = (xVal / Math.max(w, 1)) * h;
173
+ * y.set(mapped);
174
+ * }
175
+ * );
176
+ *
177
+ * return <motion.div style={{ x, y }} />;
178
+ * }
179
+ * ```
180
+ */
181
+ export declare const useStoreSyncedTransform: <T, K extends PropertyKey, V>(originalValue: MotionValue<T>, store: KeyedSubscriptions<K>, keys: K[], valueExtractor: () => V, transformFn: (arg: V, value: T) => void) => void;
182
+ export declare const findScrollContainer: (element: HTMLElement) => {
183
+ eventsProvider: HTMLElement;
184
+ infoProvider: HTMLElement;
185
+ mode: "element";
186
+ } | {
187
+ eventsProvider: Window;
188
+ infoProvider: Element;
189
+ mode: "window";
190
+ };
191
+ export declare const isDOMRectsEqual: (a: DOMRectReadOnly | null, b: DOMRectReadOnly | null) => boolean;
192
+ export interface MorphSetup {
193
+ start: DOMRectReadOnly;
194
+ end: DOMRectReadOnly;
195
+ containerStart: DOMRectReadOnly;
196
+ containerEnd: DOMRectReadOnly;
197
+ }
198
+ export type SizeAxis = 'vertical' | 'horizontal' | 'both' | 'auto';
199
+ export type HorizontalAlign = 'left' | 'right' | 'center';
200
+ export type VerticalAlign = 'top' | 'bottom' | 'center';
201
+ export declare const calcMorphParams: (start: DOMRectReadOnly | null, end: DOMRectReadOnly | null, sizeAxis: SizeAxis, horizontalPositionAlign: HorizontalAlign, verticalPositionAlign: VerticalAlign) => {
202
+ deltaX: number;
203
+ deltaY: number;
204
+ scaleX: number;
205
+ scaleY: number;
206
+ };
207
+ export declare const getRelativeOffset: (scrollAdapter: ScrollAdapter, element: HTMLElement | null) => number;