@jbpark/use-hooks 2.9.0 → 2.11.0
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.
- package/dist/hooks/use-body-scroll-lock.mjs +19 -51
- package/dist/hooks/use-body-scroll-lock.mjs.map +1 -1
- package/dist/hooks/use-controllable-state.d.mts +4 -5
- package/dist/hooks/use-controllable-state.mjs +10 -4
- package/dist/hooks/use-controllable-state.mjs.map +1 -1
- package/dist/hooks/use-element-position.mjs +37 -8
- package/dist/hooks/use-element-position.mjs.map +1 -1
- package/dist/hooks/use-element-scroll.d.mts +6 -1
- package/dist/hooks/use-element-scroll.mjs +30 -8
- package/dist/hooks/use-element-scroll.mjs.map +1 -1
- package/dist/hooks/use-history-state.mjs +5 -3
- package/dist/hooks/use-history-state.mjs.map +1 -1
- package/dist/hooks/use-multi-select.mjs +14 -5
- package/dist/hooks/use-multi-select.mjs.map +1 -1
- package/dist/hooks/use-responsive-size.d.mts +1 -0
- package/dist/hooks/use-responsive-size.mjs +70 -54
- package/dist/hooks/use-responsive-size.mjs.map +1 -1
- package/dist/hooks/use-scroll-to-elements.mjs +16 -10
- package/dist/hooks/use-scroll-to-elements.mjs.map +1 -1
- package/dist/hooks/use-timeline.mjs +39 -4
- package/dist/hooks/use-timeline.mjs.map +1 -1
- package/dist/hooks/use-window-scroll.mjs +60 -38
- package/dist/hooks/use-window-scroll.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/hooks/use-body-scroll-lock.ts
|
|
4
|
-
|
|
4
|
+
const activeLocks = /* @__PURE__ */ new Set();
|
|
5
5
|
let originalStyles = null;
|
|
6
6
|
let originalScrollY = 0;
|
|
7
|
-
const isIOS = () => /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
8
|
-
const lockScroll = () => {
|
|
9
|
-
if (
|
|
7
|
+
const isIOS = () => typeof navigator !== "undefined" && /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
8
|
+
const lockScroll = (token) => {
|
|
9
|
+
if (activeLocks.size === 0) {
|
|
10
10
|
originalScrollY = window.scrollY;
|
|
11
11
|
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
12
12
|
const currentPaddingRight = parseFloat(getComputedStyle(document.body).paddingRight) || 0;
|
|
@@ -43,11 +43,11 @@ const lockScroll = () => {
|
|
|
43
43
|
if (scrollbarWidth > 0) document.body.style.paddingRight = `${currentPaddingRight + scrollbarWidth}px`;
|
|
44
44
|
if (isIOS()) document.body.style.setProperty("-webkit-overflow-scrolling", "touch");
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
activeLocks.add(token);
|
|
47
47
|
};
|
|
48
|
-
const unlockScroll = () => {
|
|
49
|
-
|
|
50
|
-
if (
|
|
48
|
+
const unlockScroll = (token) => {
|
|
49
|
+
activeLocks.delete(token);
|
|
50
|
+
if (activeLocks.size > 0 || !originalStyles) return;
|
|
51
51
|
document.documentElement.style.overflow = originalStyles.documentElement.overflow;
|
|
52
52
|
document.documentElement.style.height = originalStyles.documentElement.height;
|
|
53
53
|
document.documentElement.style.position = originalStyles.documentElement.position;
|
|
@@ -67,54 +67,22 @@ const unlockScroll = () => {
|
|
|
67
67
|
};
|
|
68
68
|
const useBodyScrollLock = (enabled = true) => {
|
|
69
69
|
useEffect(() => {
|
|
70
|
-
if (!enabled) return;
|
|
71
|
-
|
|
70
|
+
if (typeof window === "undefined" || !enabled) return;
|
|
71
|
+
const token = Symbol("body-scroll-lock");
|
|
72
|
+
lockScroll(token);
|
|
73
|
+
let removeTouchListener;
|
|
72
74
|
if (isIOS()) {
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
75
|
-
e.preventDefault();
|
|
76
|
-
e.stopPropagation();
|
|
77
|
-
e.stopImmediatePropagation();
|
|
78
|
-
}
|
|
75
|
+
const preventTouchMove = (event) => {
|
|
76
|
+
if (event.target === document.body || event.target === document.documentElement) event.preventDefault();
|
|
79
77
|
};
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
document.
|
|
83
|
-
document.documentElement.scrollTop = 0;
|
|
84
|
-
};
|
|
85
|
-
const events = [
|
|
86
|
-
"scroll",
|
|
87
|
-
"touchmove",
|
|
88
|
-
"touchstart",
|
|
89
|
-
"touchend"
|
|
90
|
-
];
|
|
91
|
-
events.forEach((event) => {
|
|
92
|
-
window.addEventListener(event, preventAll, {
|
|
93
|
-
passive: false,
|
|
94
|
-
capture: true
|
|
95
|
-
});
|
|
96
|
-
document.addEventListener(event, preventAll, {
|
|
97
|
-
passive: false,
|
|
98
|
-
capture: true
|
|
99
|
-
});
|
|
100
|
-
document.body.addEventListener(event, preventAll, {
|
|
101
|
-
passive: false,
|
|
102
|
-
capture: true
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
const scrollInterval = setInterval(preventScroll, 16);
|
|
106
|
-
return () => {
|
|
107
|
-
clearInterval(scrollInterval);
|
|
108
|
-
events.forEach((event) => {
|
|
109
|
-
window.removeEventListener(event, preventAll, { capture: true });
|
|
110
|
-
document.removeEventListener(event, preventAll, { capture: true });
|
|
111
|
-
document.body.removeEventListener(event, preventAll, { capture: true });
|
|
112
|
-
});
|
|
113
|
-
unlockScroll();
|
|
78
|
+
document.addEventListener("touchmove", preventTouchMove, { passive: false });
|
|
79
|
+
removeTouchListener = () => {
|
|
80
|
+
document.removeEventListener("touchmove", preventTouchMove);
|
|
114
81
|
};
|
|
115
82
|
}
|
|
116
83
|
return () => {
|
|
117
|
-
|
|
84
|
+
removeTouchListener?.();
|
|
85
|
+
unlockScroll(token);
|
|
118
86
|
};
|
|
119
87
|
}, [enabled]);
|
|
120
88
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-body-scroll-lock.mjs","names":[],"sources":["../../src/hooks/use-body-scroll-lock.ts"],"sourcesContent":["import { useEffect } from 'react';\n\ninterface LockedStyles {\n documentElement: {\n overflow: string;\n height: string;\n position: string;\n width: string;\n };\n body: {\n overflow: string;\n height: string;\n position: string;\n top: string;\n left: string;\n right: string;\n width: string;\n paddingRight: string;\n webkitOverflowScrolling: string;\n };\n}\n\
|
|
1
|
+
{"version":3,"file":"use-body-scroll-lock.mjs","names":[],"sources":["../../src/hooks/use-body-scroll-lock.ts"],"sourcesContent":["import { useEffect } from 'react';\n\ninterface LockedStyles {\n documentElement: {\n overflow: string;\n height: string;\n position: string;\n width: string;\n };\n body: {\n overflow: string;\n height: string;\n position: string;\n top: string;\n left: string;\n right: string;\n width: string;\n paddingRight: string;\n webkitOverflowScrolling: string;\n };\n}\n\n// A Set of per-instance tokens instead of a raw counter — deleting the same\n// token twice (or locking with a token that's already in the set) can't\n// drift the count the way increment/decrement pairs could.\nconst activeLocks = new Set<symbol>();\nlet originalStyles: LockedStyles | null = null;\nlet originalScrollY = 0;\n\nconst isIOS = () =>\n typeof navigator !== 'undefined' &&\n /iPad|iPhone|iPod/.test(navigator.userAgent);\n\nconst lockScroll = (token: symbol) => {\n if (activeLocks.size === 0) {\n originalScrollY = window.scrollY;\n const scrollbarWidth =\n window.innerWidth - document.documentElement.clientWidth;\n const currentPaddingRight =\n parseFloat(getComputedStyle(document.body).paddingRight) || 0;\n\n originalStyles = {\n documentElement: {\n overflow: document.documentElement.style.overflow,\n height: document.documentElement.style.height,\n position: document.documentElement.style.position,\n width: document.documentElement.style.width,\n },\n body: {\n overflow: document.body.style.overflow,\n height: document.body.style.height,\n position: document.body.style.position,\n top: document.body.style.top,\n left: document.body.style.left,\n right: document.body.style.right,\n width: document.body.style.width,\n paddingRight: document.body.style.paddingRight,\n webkitOverflowScrolling: document.body.style.getPropertyValue(\n '-webkit-overflow-scrolling',\n ),\n },\n };\n\n document.documentElement.style.overflow = 'hidden';\n document.documentElement.style.height = '100%';\n document.documentElement.style.position = 'fixed';\n document.documentElement.style.width = '100%';\n\n document.body.style.overflow = 'hidden';\n document.body.style.height = '100%';\n document.body.style.position = 'fixed';\n document.body.style.top = `-${originalScrollY}px`;\n document.body.style.left = '0';\n document.body.style.right = '0';\n document.body.style.width = '100%';\n\n if (scrollbarWidth > 0) {\n document.body.style.paddingRight = `${currentPaddingRight + scrollbarWidth}px`;\n }\n\n if (isIOS()) {\n document.body.style.setProperty('-webkit-overflow-scrolling', 'touch');\n }\n }\n\n activeLocks.add(token);\n};\n\nconst unlockScroll = (token: symbol) => {\n activeLocks.delete(token);\n\n if (activeLocks.size > 0 || !originalStyles) {\n return;\n }\n\n document.documentElement.style.overflow =\n originalStyles.documentElement.overflow;\n document.documentElement.style.height = originalStyles.documentElement.height;\n document.documentElement.style.position =\n originalStyles.documentElement.position;\n document.documentElement.style.width = originalStyles.documentElement.width;\n\n document.body.style.overflow = originalStyles.body.overflow;\n document.body.style.height = originalStyles.body.height;\n document.body.style.position = originalStyles.body.position;\n document.body.style.top = originalStyles.body.top;\n document.body.style.left = originalStyles.body.left;\n document.body.style.right = originalStyles.body.right;\n document.body.style.width = originalStyles.body.width;\n document.body.style.paddingRight = originalStyles.body.paddingRight;\n\n if (originalStyles.body.webkitOverflowScrolling) {\n document.body.style.setProperty(\n '-webkit-overflow-scrolling',\n originalStyles.body.webkitOverflowScrolling,\n );\n } else {\n document.body.style.removeProperty('-webkit-overflow-scrolling');\n }\n\n window.scrollTo(0, originalScrollY);\n originalStyles = null;\n};\n\nconst useBodyScrollLock = (enabled: boolean = true) => {\n useEffect(() => {\n if (typeof window === 'undefined' || !enabled) {\n return;\n }\n\n const token = Symbol('body-scroll-lock');\n lockScroll(token);\n\n // The position:fixed styles above are the real lock — this is only a\n // narrow safety net for iOS Safari, which can still rubber-band the\n // page on a touchmove that starts directly on the body/html background.\n // Only that one event is handled (no capture, no stopPropagation), so\n // a scrollable element inside the locked content (e.g. a modal body)\n // keeps scrolling normally — its touchmove target isn't body/html.\n let removeTouchListener: (() => void) | undefined;\n\n if (isIOS()) {\n const preventTouchMove = (event: TouchEvent) => {\n if (\n event.target === document.body ||\n event.target === document.documentElement\n ) {\n event.preventDefault();\n }\n };\n\n document.addEventListener('touchmove', preventTouchMove, {\n passive: false,\n });\n\n removeTouchListener = () => {\n document.removeEventListener('touchmove', preventTouchMove);\n };\n }\n\n return () => {\n removeTouchListener?.();\n unlockScroll(token);\n };\n }, [enabled]);\n};\n\nexport default useBodyScrollLock;\n"],"mappings":"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"}
|
|
@@ -4,11 +4,10 @@ interface UseControllableStateProps<T> {
|
|
|
4
4
|
defaultValue?: T;
|
|
5
5
|
onChange?: (value: T) => void;
|
|
6
6
|
}
|
|
7
|
-
declare
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}: UseControllableStateProps<T>) => [T, (next: T) => void];
|
|
7
|
+
declare function useControllableState<T>(props: UseControllableStateProps<T> & {
|
|
8
|
+
defaultValue: T;
|
|
9
|
+
}): [T, (next: T) => void];
|
|
10
|
+
declare function useControllableState<T>(props: UseControllableStateProps<T>): [T | undefined, (next: T) => void];
|
|
12
11
|
//#endregion
|
|
13
12
|
export { useControllableState };
|
|
14
13
|
//# sourceMappingURL=use-controllable-state.d.mts.map
|
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
import { useCallback, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/hooks/use-controllable-state.ts
|
|
4
|
-
|
|
4
|
+
function useControllableState({ value, defaultValue, onChange }) {
|
|
5
5
|
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
|
|
6
6
|
const isControlled = value !== void 0;
|
|
7
|
-
|
|
7
|
+
const currentValue = isControlled ? value : uncontrolledValue;
|
|
8
|
+
const wasControlledRef = useRef(isControlled);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!(globalThis.process?.env?.NODE_ENV === "production") && wasControlledRef.current !== isControlled) console.warn(`useControllableState is changing from ${wasControlledRef.current ? "controlled" : "uncontrolled"} to ${isControlled ? "controlled" : "uncontrolled"}. A component should not switch between controlled and uncontrolled — decide on one and stick with it (e.g. by always passing a non-undefined \`value\`, or never passing one).`);
|
|
11
|
+
wasControlledRef.current = isControlled;
|
|
12
|
+
}, [isControlled]);
|
|
13
|
+
return [currentValue, useCallback((next) => {
|
|
8
14
|
if (!isControlled) setUncontrolledValue(next);
|
|
9
15
|
onChange?.(next);
|
|
10
16
|
}, [isControlled, onChange])];
|
|
11
|
-
}
|
|
17
|
+
}
|
|
12
18
|
|
|
13
19
|
//#endregion
|
|
14
20
|
export { useControllableState as default };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-controllable-state.mjs","names":[],"sources":["../../src/hooks/use-controllable-state.ts"],"sourcesContent":["import { useCallback, useState } from 'react';\n\ninterface UseControllableStateProps<T> {\n value?: T;\n defaultValue?: T;\n onChange?: (value: T) => void;\n}\n\n// Backs a controlled/uncontrolled prop pair (`value`/`defaultValue`/\n// `onChange`) with one hook: falls back to internal state whenever `value`\n// is `undefined`, and always calls `onChange` regardless of which mode is\n// active, so callers don't have to branch on it themselves.\
|
|
1
|
+
{"version":3,"file":"use-controllable-state.mjs","names":[],"sources":["../../src/hooks/use-controllable-state.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\ninterface UseControllableStateProps<T> {\n value?: T;\n defaultValue?: T;\n onChange?: (value: T) => void;\n}\n\n// Backs a controlled/uncontrolled prop pair (`value`/`defaultValue`/\n// `onChange`) with one hook: falls back to internal state whenever `value`\n// is `undefined`, and always calls `onChange` regardless of which mode is\n// active, so callers don't have to branch on it themselves.\n//\n// `value === undefined` is what marks a render as uncontrolled, so it can't\n// also be used as a real, controlled value of `undefined` — that's an\n// inherent limitation of this \"sentinel\" style of controlled-state\n// detection (shared by most hooks of this shape, including Radix's own),\n// not something fixable without a different API (e.g. an explicit\n// `controlled` boolean prop instead of inferring it from `value`).\nfunction useControllableState<T>(\n props: UseControllableStateProps<T> & { defaultValue: T },\n): [T, (next: T) => void];\nfunction useControllableState<T>(\n props: UseControllableStateProps<T>,\n): [T | undefined, (next: T) => void];\nfunction useControllableState<T>({\n value,\n defaultValue,\n onChange,\n}: UseControllableStateProps<T>) {\n const [uncontrolledValue, setUncontrolledValue] = useState<T | undefined>(\n defaultValue,\n );\n\n const isControlled = value !== undefined;\n const currentValue = isControlled ? value : uncontrolledValue;\n\n const wasControlledRef = useRef(isControlled);\n\n useEffect(() => {\n // Read `process.env.NODE_ENV` through `globalThis` rather than the\n // bare `process` global — this hook ships as plain ESM and can end up\n // somewhere with no bundler define for it (or no `process` at all, and\n // no @types/node in scope to even name it), where referencing it\n // directly would fail instead of just missing a warning.\n const nodeEnv = (\n globalThis as { process?: { env?: { NODE_ENV?: string } } }\n ).process?.env?.NODE_ENV;\n const isProduction = nodeEnv === 'production';\n\n if (!isProduction && wasControlledRef.current !== isControlled) {\n console.warn(\n `useControllableState is changing from ${\n wasControlledRef.current ? 'controlled' : 'uncontrolled'\n } to ${\n isControlled ? 'controlled' : 'uncontrolled'\n }. A component should not switch between controlled and uncontrolled — decide on one and stick with it (e.g. by always passing a non-undefined \\`value\\`, or never passing one).`,\n );\n }\n wasControlledRef.current = isControlled;\n }, [isControlled]);\n\n const setValue = useCallback(\n (next: T) => {\n if (!isControlled) {\n setUncontrolledValue(next);\n }\n onChange?.(next);\n },\n [isControlled, onChange],\n );\n\n return [currentValue, setValue];\n}\n\nexport default useControllableState;\n"],"mappings":";;;AAyBA,SAAS,qBAAwB,EAC/B,OACA,cACA,YAC+B;CAC/B,MAAM,CAAC,mBAAmB,wBAAwB,SAChD,aACD;CAED,MAAM,eAAe,UAAU;CAC/B,MAAM,eAAe,eAAe,QAAQ;CAE5C,MAAM,mBAAmB,OAAO,aAAa;AAE7C,iBAAgB;AAWd,MAAI,EAJF,WACA,SAAS,KAAK,aACiB,iBAEZ,iBAAiB,YAAY,aAChD,SAAQ,KACN,yCACE,iBAAiB,UAAU,eAAe,eAC3C,MACC,eAAe,eAAe,eAC/B,iLACF;AAEH,mBAAiB,UAAU;IAC1B,CAAC,aAAa,CAAC;AAYlB,QAAO,CAAC,cAVS,aACd,SAAY;AACX,MAAI,CAAC,aACH,sBAAqB,KAAK;AAE5B,aAAW,KAAK;IAElB,CAAC,cAAc,SAAS,CACzB,CAE8B"}
|
|
@@ -1,27 +1,56 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/hooks/use-element-position.ts
|
|
4
|
+
const rectsEqual = (a, b) => {
|
|
5
|
+
if (a === b) return true;
|
|
6
|
+
if (!a || !b) return false;
|
|
7
|
+
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height && a.top === b.top && a.left === b.left;
|
|
8
|
+
};
|
|
4
9
|
const useElementPosition = (elementRef) => {
|
|
5
10
|
const [rect, setRect] = useState(null);
|
|
11
|
+
const rectRef = useRef(null);
|
|
6
12
|
useEffect(() => {
|
|
7
13
|
const getElement = (ref) => {
|
|
8
14
|
if (typeof ref === "string") return document.querySelector(ref);
|
|
9
15
|
return ref.current;
|
|
10
16
|
};
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
let rafId;
|
|
18
|
+
let currentElement = null;
|
|
19
|
+
const commitRect = (next) => {
|
|
20
|
+
if (rectsEqual(rectRef.current, next)) return;
|
|
21
|
+
rectRef.current = next;
|
|
22
|
+
setRect(next);
|
|
15
23
|
};
|
|
24
|
+
const resizeObserver = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => updateRect()) : void 0;
|
|
25
|
+
function updateRect() {
|
|
26
|
+
const element = getElement(elementRef);
|
|
27
|
+
if (element !== currentElement) {
|
|
28
|
+
if (currentElement) resizeObserver?.unobserve(currentElement);
|
|
29
|
+
if (element) resizeObserver?.observe(element);
|
|
30
|
+
currentElement = element;
|
|
31
|
+
}
|
|
32
|
+
commitRect(element ? element.getBoundingClientRect() : null);
|
|
33
|
+
}
|
|
16
34
|
const onUpdate = () => {
|
|
17
|
-
requestAnimationFrame(updateRect);
|
|
35
|
+
rafId = requestAnimationFrame(updateRect);
|
|
18
36
|
};
|
|
19
37
|
updateRect();
|
|
20
|
-
window.addEventListener("scroll", onUpdate, {
|
|
38
|
+
window.addEventListener("scroll", onUpdate, {
|
|
39
|
+
passive: true,
|
|
40
|
+
capture: true
|
|
41
|
+
});
|
|
21
42
|
window.addEventListener("resize", onUpdate, { passive: true });
|
|
43
|
+
const mutationObserver = typeof elementRef === "string" && typeof MutationObserver !== "undefined" ? new MutationObserver(onUpdate) : void 0;
|
|
44
|
+
mutationObserver?.observe(document.body, {
|
|
45
|
+
childList: true,
|
|
46
|
+
subtree: true
|
|
47
|
+
});
|
|
22
48
|
return () => {
|
|
23
|
-
|
|
49
|
+
if (rafId !== void 0) cancelAnimationFrame(rafId);
|
|
50
|
+
window.removeEventListener("scroll", onUpdate, { capture: true });
|
|
24
51
|
window.removeEventListener("resize", onUpdate);
|
|
52
|
+
resizeObserver?.disconnect();
|
|
53
|
+
mutationObserver?.disconnect();
|
|
25
54
|
};
|
|
26
55
|
}, [elementRef]);
|
|
27
56
|
return rect;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-element-position.mjs","names":[],"sources":["../../src/hooks/use-element-position.ts"],"sourcesContent":["import { type RefObject, useEffect, useState } from 'react';\n\ntype ElementReference<T> = string | RefObject<T>;\n\nconst useElementPosition = <T>(elementRef: ElementReference<T>) => {\n const [rect, setRect] = useState<DOMRect | null>(null);\n\n useEffect(() => {\n const getElement = (ref: ElementReference<T>): T | null => {\n if (typeof ref === 'string') {\n return document.querySelector(ref) as T;\n }\n return ref.current;\n };\n\n const
|
|
1
|
+
{"version":3,"file":"use-element-position.mjs","names":[],"sources":["../../src/hooks/use-element-position.ts"],"sourcesContent":["import { type RefObject, useEffect, useRef, useState } from 'react';\n\ntype ElementReference<T> = string | RefObject<T>;\n\nconst rectsEqual = (a: DOMRect | null, b: DOMRect | null) => {\n if (a === b) {\n return true;\n }\n if (!a || !b) {\n return false;\n }\n return (\n a.x === b.x &&\n a.y === b.y &&\n a.width === b.width &&\n a.height === b.height &&\n a.top === b.top &&\n a.left === b.left\n );\n};\n\nconst useElementPosition = <T>(elementRef: ElementReference<T>) => {\n const [rect, setRect] = useState<DOMRect | null>(null);\n const rectRef = useRef<DOMRect | null>(null);\n\n useEffect(() => {\n const getElement = (ref: ElementReference<T>): T | null => {\n if (typeof ref === 'string') {\n return document.querySelector(ref) as T | null;\n }\n return ref.current;\n };\n\n let rafId: number | undefined;\n let currentElement: HTMLElement | null = null;\n\n const commitRect = (next: DOMRect | null) => {\n if (rectsEqual(rectRef.current, next)) {\n return;\n }\n rectRef.current = next;\n setRect(next);\n };\n\n // ResizeObserver only fires for the observed element's own size/layout\n // changes, so it's kept pointed at whichever element is actually\n // current — which can change under a string selector, or if the DOM\n // node behind a ref gets swapped out.\n const resizeObserver =\n typeof ResizeObserver !== 'undefined'\n ? new ResizeObserver(() => updateRect())\n : undefined;\n\n function updateRect() {\n const element = getElement(elementRef) as HTMLElement | null;\n\n if (element !== currentElement) {\n if (currentElement) {\n resizeObserver?.unobserve(currentElement);\n }\n if (element) {\n resizeObserver?.observe(element);\n }\n currentElement = element;\n }\n\n commitRect(element ? element.getBoundingClientRect() : null);\n }\n\n const onUpdate = () => {\n rafId = requestAnimationFrame(updateRect);\n };\n\n updateRect();\n\n // `capture: true` also catches scroll events from scrollable ancestor\n // containers, which don't bubble and so wouldn't otherwise reach a\n // listener on `window`.\n window.addEventListener('scroll', onUpdate, {\n passive: true,\n capture: true,\n });\n window.addEventListener('resize', onUpdate, { passive: true });\n\n // A string selector's target may not exist yet when this effect first\n // runs — watch the DOM for it instead of leaving `rect` permanently\n // null once it does mount.\n const mutationObserver =\n typeof elementRef === 'string' && typeof MutationObserver !== 'undefined'\n ? new MutationObserver(onUpdate)\n : undefined;\n mutationObserver?.observe(document.body, {\n childList: true,\n subtree: true,\n });\n\n return () => {\n if (rafId !== undefined) {\n cancelAnimationFrame(rafId);\n }\n window.removeEventListener('scroll', onUpdate, { capture: true });\n window.removeEventListener('resize', onUpdate);\n resizeObserver?.disconnect();\n mutationObserver?.disconnect();\n };\n }, [elementRef]);\n\n return rect;\n};\n\nexport default useElementPosition;\n"],"mappings":";;;AAIA,MAAM,cAAc,GAAmB,MAAsB;AAC3D,KAAI,MAAM,EACR,QAAO;AAET,KAAI,CAAC,KAAK,CAAC,EACT,QAAO;AAET,QACE,EAAE,MAAM,EAAE,KACV,EAAE,MAAM,EAAE,KACV,EAAE,UAAU,EAAE,SACd,EAAE,WAAW,EAAE,UACf,EAAE,QAAQ,EAAE,OACZ,EAAE,SAAS,EAAE;;AAIjB,MAAM,sBAAyB,eAAoC;CACjE,MAAM,CAAC,MAAM,WAAW,SAAyB,KAAK;CACtD,MAAM,UAAU,OAAuB,KAAK;AAE5C,iBAAgB;EACd,MAAM,cAAc,QAAuC;AACzD,OAAI,OAAO,QAAQ,SACjB,QAAO,SAAS,cAAc,IAAI;AAEpC,UAAO,IAAI;;EAGb,IAAI;EACJ,IAAI,iBAAqC;EAEzC,MAAM,cAAc,SAAyB;AAC3C,OAAI,WAAW,QAAQ,SAAS,KAAK,CACnC;AAEF,WAAQ,UAAU;AAClB,WAAQ,KAAK;;EAOf,MAAM,iBACJ,OAAO,mBAAmB,cACtB,IAAI,qBAAqB,YAAY,CAAC,GACtC;EAEN,SAAS,aAAa;GACpB,MAAM,UAAU,WAAW,WAAW;AAEtC,OAAI,YAAY,gBAAgB;AAC9B,QAAI,eACF,iBAAgB,UAAU,eAAe;AAE3C,QAAI,QACF,iBAAgB,QAAQ,QAAQ;AAElC,qBAAiB;;AAGnB,cAAW,UAAU,QAAQ,uBAAuB,GAAG,KAAK;;EAG9D,MAAM,iBAAiB;AACrB,WAAQ,sBAAsB,WAAW;;AAG3C,cAAY;AAKZ,SAAO,iBAAiB,UAAU,UAAU;GAC1C,SAAS;GACT,SAAS;GACV,CAAC;AACF,SAAO,iBAAiB,UAAU,UAAU,EAAE,SAAS,MAAM,CAAC;EAK9D,MAAM,mBACJ,OAAO,eAAe,YAAY,OAAO,qBAAqB,cAC1D,IAAI,iBAAiB,SAAS,GAC9B;AACN,oBAAkB,QAAQ,SAAS,MAAM;GACvC,WAAW;GACX,SAAS;GACV,CAAC;AAEF,eAAa;AACX,OAAI,UAAU,OACZ,sBAAqB,MAAM;AAE7B,UAAO,oBAAoB,UAAU,UAAU,EAAE,SAAS,MAAM,CAAC;AACjE,UAAO,oBAAoB,UAAU,SAAS;AAC9C,mBAAgB,YAAY;AAC5B,qBAAkB,YAAY;;IAE/B,CAAC,WAAW,CAAC;AAEhB,QAAO"}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
//#region src/hooks/use-element-scroll.d.ts
|
|
2
|
-
|
|
2
|
+
interface Options {
|
|
3
|
+
threshold?: number;
|
|
4
|
+
}
|
|
5
|
+
declare const useElementScroll: ({
|
|
6
|
+
threshold
|
|
7
|
+
}?: Options) => {
|
|
3
8
|
element: HTMLElement | null;
|
|
4
9
|
setRef: (el: HTMLElement | null) => void;
|
|
5
10
|
scrollY: number;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/hooks/use-element-scroll.ts
|
|
4
|
-
const
|
|
4
|
+
const scrollPositionEqual = (a, b) => a.scrollY === b.scrollY && a.scrollPercentage === b.scrollPercentage && a.isAtTop === b.isAtTop && a.isAtBottom === b.isAtBottom && a.scrollableHeight === b.scrollableHeight && a.clientHeight === b.clientHeight && a.scrollHeight === b.scrollHeight;
|
|
5
|
+
const useElementScroll = ({ threshold = 1 } = {}) => {
|
|
5
6
|
const [element, setElement] = useState(null);
|
|
6
7
|
const [scrollPosition, setScrollPosition] = useState({
|
|
7
8
|
scrollY: 0,
|
|
@@ -12,16 +13,22 @@ const useElementScroll = () => {
|
|
|
12
13
|
clientHeight: 0,
|
|
13
14
|
scrollHeight: 0
|
|
14
15
|
});
|
|
16
|
+
const scrollPositionRef = useRef(scrollPosition);
|
|
15
17
|
const setRef = useCallback((el) => {
|
|
16
18
|
setElement(el);
|
|
17
19
|
}, []);
|
|
18
20
|
useEffect(() => {
|
|
19
21
|
if (!element) return;
|
|
22
|
+
const commit = (next) => {
|
|
23
|
+
if (scrollPositionEqual(scrollPositionRef.current, next)) return;
|
|
24
|
+
scrollPositionRef.current = next;
|
|
25
|
+
setScrollPosition(next);
|
|
26
|
+
};
|
|
20
27
|
const updateScrollPosition = () => {
|
|
21
28
|
const { scrollTop, scrollHeight, clientHeight } = element;
|
|
22
29
|
const scrollableHeight = scrollHeight - clientHeight;
|
|
23
30
|
if (scrollableHeight <= 0) {
|
|
24
|
-
|
|
31
|
+
commit({
|
|
25
32
|
scrollY: 0,
|
|
26
33
|
scrollPercentage: 0,
|
|
27
34
|
isAtTop: true,
|
|
@@ -32,11 +39,11 @@ const useElementScroll = () => {
|
|
|
32
39
|
});
|
|
33
40
|
return;
|
|
34
41
|
}
|
|
35
|
-
|
|
42
|
+
commit({
|
|
36
43
|
scrollY: scrollTop,
|
|
37
44
|
scrollPercentage: Math.min(100, Math.max(0, scrollTop / scrollableHeight * 100)),
|
|
38
45
|
isAtTop: scrollTop <= 0,
|
|
39
|
-
isAtBottom: scrollTop >= scrollableHeight -
|
|
46
|
+
isAtBottom: scrollTop >= scrollableHeight - threshold,
|
|
40
47
|
scrollableHeight,
|
|
41
48
|
clientHeight,
|
|
42
49
|
scrollHeight
|
|
@@ -50,12 +57,27 @@ const useElementScroll = () => {
|
|
|
50
57
|
const resizeObserver = new ResizeObserver(() => {
|
|
51
58
|
updateScrollPosition();
|
|
52
59
|
});
|
|
53
|
-
|
|
60
|
+
const observeChildren = () => {
|
|
61
|
+
resizeObserver.observe(element);
|
|
62
|
+
Array.from(element.children).forEach((child) => {
|
|
63
|
+
resizeObserver.observe(child);
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
observeChildren();
|
|
67
|
+
const mutationObserver = new MutationObserver(() => {
|
|
68
|
+
observeChildren();
|
|
69
|
+
updateScrollPosition();
|
|
70
|
+
});
|
|
71
|
+
mutationObserver.observe(element, {
|
|
72
|
+
childList: true,
|
|
73
|
+
subtree: true
|
|
74
|
+
});
|
|
54
75
|
return () => {
|
|
55
76
|
element.removeEventListener("scroll", onScroll);
|
|
56
|
-
resizeObserver.
|
|
77
|
+
resizeObserver.disconnect();
|
|
78
|
+
mutationObserver.disconnect();
|
|
57
79
|
};
|
|
58
|
-
}, [element]);
|
|
80
|
+
}, [element, threshold]);
|
|
59
81
|
return {
|
|
60
82
|
...scrollPosition,
|
|
61
83
|
element,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-element-scroll.mjs","names":[],"sources":["../../src/hooks/use-element-scroll.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\n\ninterface ScrollPosition {\n scrollY: number;\n scrollPercentage: number;\n isAtTop: boolean;\n isAtBottom: boolean;\n scrollableHeight: number;\n clientHeight: number;\n scrollHeight: number;\n}\n\nconst useElementScroll = () => {\n const [element, setElement] = useState<HTMLElement | null>(null);\n const [scrollPosition, setScrollPosition] = useState<ScrollPosition>({\n scrollY: 0,\n scrollPercentage: 0,\n isAtTop: true,\n isAtBottom: false,\n scrollableHeight: 0,\n clientHeight: 0,\n scrollHeight: 0,\n });\n\n const setRef = useCallback((el: HTMLElement | null) => {\n setElement(el);\n }, []);\n\n useEffect(() => {\n if (!element) {\n return;\n }\n\n const updateScrollPosition = () => {\n const { scrollTop, scrollHeight, clientHeight } = element;\n const scrollableHeight = scrollHeight - clientHeight;\n\n if (scrollableHeight <= 0) {\n
|
|
1
|
+
{"version":3,"file":"use-element-scroll.mjs","names":[],"sources":["../../src/hooks/use-element-scroll.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\ninterface ScrollPosition {\n scrollY: number;\n scrollPercentage: number;\n isAtTop: boolean;\n isAtBottom: boolean;\n scrollableHeight: number;\n clientHeight: number;\n scrollHeight: number;\n}\n\nconst scrollPositionEqual = (a: ScrollPosition, b: ScrollPosition) =>\n a.scrollY === b.scrollY &&\n a.scrollPercentage === b.scrollPercentage &&\n a.isAtTop === b.isAtTop &&\n a.isAtBottom === b.isAtBottom &&\n a.scrollableHeight === b.scrollableHeight &&\n a.clientHeight === b.clientHeight &&\n a.scrollHeight === b.scrollHeight;\n\ninterface Options {\n // How close to the bottom (in px) still counts as \"at bottom\". Matches\n // the previous hardcoded value by default.\n threshold?: number;\n}\n\nconst useElementScroll = ({ threshold = 1 }: Options = {}) => {\n const [element, setElement] = useState<HTMLElement | null>(null);\n const [scrollPosition, setScrollPosition] = useState<ScrollPosition>({\n scrollY: 0,\n scrollPercentage: 0,\n isAtTop: true,\n isAtBottom: false,\n scrollableHeight: 0,\n clientHeight: 0,\n scrollHeight: 0,\n });\n const scrollPositionRef = useRef(scrollPosition);\n\n const setRef = useCallback((el: HTMLElement | null) => {\n setElement(el);\n }, []);\n\n useEffect(() => {\n if (!element) {\n return;\n }\n\n const commit = (next: ScrollPosition) => {\n if (scrollPositionEqual(scrollPositionRef.current, next)) {\n return;\n }\n scrollPositionRef.current = next;\n setScrollPosition(next);\n };\n\n const updateScrollPosition = () => {\n const { scrollTop, scrollHeight, clientHeight } = element;\n const scrollableHeight = scrollHeight - clientHeight;\n\n if (scrollableHeight <= 0) {\n commit({\n scrollY: 0,\n scrollPercentage: 0,\n isAtTop: true,\n isAtBottom: true,\n scrollableHeight: 0,\n clientHeight,\n scrollHeight,\n });\n return;\n }\n\n const percentage = Math.min(\n 100,\n Math.max(0, (scrollTop / scrollableHeight) * 100),\n );\n\n commit({\n scrollY: scrollTop,\n scrollPercentage: percentage,\n isAtTop: scrollTop <= 0,\n isAtBottom: scrollTop >= scrollableHeight - threshold,\n scrollableHeight,\n clientHeight,\n scrollHeight,\n });\n };\n\n updateScrollPosition();\n\n const onScroll = () => {\n updateScrollPosition();\n };\n\n element.addEventListener('scroll', onScroll, { passive: true });\n\n const resizeObserver = new ResizeObserver(() => {\n updateScrollPosition();\n });\n\n // Observing only the container misses content that grows/shrinks\n // `scrollHeight` without resizing the container's own box (a list\n // gaining items, an image finishing its load) — also watch every\n // child so their own size changes are caught.\n const observeChildren = () => {\n resizeObserver.observe(element);\n Array.from(element.children).forEach(child => {\n resizeObserver.observe(child);\n });\n };\n\n observeChildren();\n\n // Catches children being added/removed (re-syncs which elements the\n // ResizeObserver above watches, and recalculates immediately) —\n // ResizeObserver alone doesn't see brand new children until this runs.\n const mutationObserver = new MutationObserver(() => {\n observeChildren();\n updateScrollPosition();\n });\n mutationObserver.observe(element, { childList: true, subtree: true });\n\n return () => {\n element.removeEventListener('scroll', onScroll);\n resizeObserver.disconnect();\n mutationObserver.disconnect();\n };\n }, [element, threshold]);\n\n return { ...scrollPosition, element, setRef };\n};\n\nexport default useElementScroll;\n"],"mappings":";;;AAYA,MAAM,uBAAuB,GAAmB,MAC9C,EAAE,YAAY,EAAE,WAChB,EAAE,qBAAqB,EAAE,oBACzB,EAAE,YAAY,EAAE,WAChB,EAAE,eAAe,EAAE,cACnB,EAAE,qBAAqB,EAAE,oBACzB,EAAE,iBAAiB,EAAE,gBACrB,EAAE,iBAAiB,EAAE;AAQvB,MAAM,oBAAoB,EAAE,YAAY,MAAe,EAAE,KAAK;CAC5D,MAAM,CAAC,SAAS,cAAc,SAA6B,KAAK;CAChE,MAAM,CAAC,gBAAgB,qBAAqB,SAAyB;EACnE,SAAS;EACT,kBAAkB;EAClB,SAAS;EACT,YAAY;EACZ,kBAAkB;EAClB,cAAc;EACd,cAAc;EACf,CAAC;CACF,MAAM,oBAAoB,OAAO,eAAe;CAEhD,MAAM,SAAS,aAAa,OAA2B;AACrD,aAAW,GAAG;IACb,EAAE,CAAC;AAEN,iBAAgB;AACd,MAAI,CAAC,QACH;EAGF,MAAM,UAAU,SAAyB;AACvC,OAAI,oBAAoB,kBAAkB,SAAS,KAAK,CACtD;AAEF,qBAAkB,UAAU;AAC5B,qBAAkB,KAAK;;EAGzB,MAAM,6BAA6B;GACjC,MAAM,EAAE,WAAW,cAAc,iBAAiB;GAClD,MAAM,mBAAmB,eAAe;AAExC,OAAI,oBAAoB,GAAG;AACzB,WAAO;KACL,SAAS;KACT,kBAAkB;KAClB,SAAS;KACT,YAAY;KACZ,kBAAkB;KAClB;KACA;KACD,CAAC;AACF;;AAQF,UAAO;IACL,SAAS;IACT,kBAPiB,KAAK,IACtB,KACA,KAAK,IAAI,GAAI,YAAY,mBAAoB,IAAI,CAClD;IAKC,SAAS,aAAa;IACtB,YAAY,aAAa,mBAAmB;IAC5C;IACA;IACA;IACD,CAAC;;AAGJ,wBAAsB;EAEtB,MAAM,iBAAiB;AACrB,yBAAsB;;AAGxB,UAAQ,iBAAiB,UAAU,UAAU,EAAE,SAAS,MAAM,CAAC;EAE/D,MAAM,iBAAiB,IAAI,qBAAqB;AAC9C,yBAAsB;IACtB;EAMF,MAAM,wBAAwB;AAC5B,kBAAe,QAAQ,QAAQ;AAC/B,SAAM,KAAK,QAAQ,SAAS,CAAC,SAAQ,UAAS;AAC5C,mBAAe,QAAQ,MAAM;KAC7B;;AAGJ,mBAAiB;EAKjB,MAAM,mBAAmB,IAAI,uBAAuB;AAClD,oBAAiB;AACjB,yBAAsB;IACtB;AACF,mBAAiB,QAAQ,SAAS;GAAE,WAAW;GAAM,SAAS;GAAM,CAAC;AAErE,eAAa;AACX,WAAQ,oBAAoB,UAAU,SAAS;AAC/C,kBAAe,YAAY;AAC3B,oBAAiB,YAAY;;IAE9B,CAAC,SAAS,UAAU,CAAC;AAExB,QAAO;EAAE,GAAG;EAAgB;EAAS;EAAQ"}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { useCallback, useReducer } from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/hooks/use-history-state.ts
|
|
4
|
+
const takeLast = (arr, limit) => limit <= 0 ? [] : arr.slice(-limit);
|
|
5
|
+
const takeFirst = (arr, limit) => limit <= 0 ? [] : arr.slice(0, limit);
|
|
4
6
|
const createReducer = (limit) => (state, action) => {
|
|
5
7
|
switch (action.type) {
|
|
6
8
|
case "SET": {
|
|
7
9
|
const resolved = action.value instanceof Function ? action.value(state.present) : action.value;
|
|
8
10
|
if (resolved === state.present) return state;
|
|
9
11
|
return {
|
|
10
|
-
past: [...state.past, state.present]
|
|
12
|
+
past: takeLast([...state.past, state.present], limit),
|
|
11
13
|
present: resolved,
|
|
12
14
|
future: []
|
|
13
15
|
};
|
|
@@ -18,14 +20,14 @@ const createReducer = (limit) => (state, action) => {
|
|
|
18
20
|
return {
|
|
19
21
|
past: state.past.slice(0, -1),
|
|
20
22
|
present: previous,
|
|
21
|
-
future: [state.present, ...state.future]
|
|
23
|
+
future: takeFirst([state.present, ...state.future], limit)
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
26
|
case "REDO": {
|
|
25
27
|
if (state.future.length === 0) return state;
|
|
26
28
|
const [next, ...rest] = state.future;
|
|
27
29
|
return {
|
|
28
|
-
past: [...state.past, state.present]
|
|
30
|
+
past: takeLast([...state.past, state.present], limit),
|
|
29
31
|
present: next,
|
|
30
32
|
future: rest
|
|
31
33
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-history-state.mjs","names":[],"sources":["../../src/hooks/use-history-state.ts"],"sourcesContent":["import { useCallback, useReducer } from 'react';\n\ninterface HistoryState<T> {\n past: T[];\n present: T;\n future: T[];\n}\n\ntype Action<T> =\n | { type: 'SET'; value: T | ((prev: T) => T) }\n | { type: 'UNDO' }\n | { type: 'REDO' }\n | { type: 'RESET'; value: T };\n\nconst createReducer =\n <T>(limit: number) =>\n (state: HistoryState<T>, action: Action<T>): HistoryState<T> => {\n switch (action.type) {\n case 'SET': {\n const resolved =\n action.value instanceof Function\n ? action.value(state.present)\n : action.value;\n\n if (resolved === state.present) {\n return state;\n }\n\n return {\n past: [...state.past, state.present]
|
|
1
|
+
{"version":3,"file":"use-history-state.mjs","names":[],"sources":["../../src/hooks/use-history-state.ts"],"sourcesContent":["import { useCallback, useReducer } from 'react';\n\ninterface HistoryState<T> {\n past: T[];\n present: T;\n future: T[];\n}\n\n// `arr.slice(-limit)` breaks for `limit <= 0`: `slice(-0)` is `slice(0)`,\n// which keeps the whole array instead of truncating to nothing. Treating\n// any non-positive limit as \"keep none\" makes `limit: 0` (and negative\n// limits, previously undefined behavior) actually mean no history.\nconst takeLast = <T>(arr: T[], limit: number): T[] =>\n limit <= 0 ? [] : arr.slice(-limit);\n\nconst takeFirst = <T>(arr: T[], limit: number): T[] =>\n limit <= 0 ? [] : arr.slice(0, limit);\n\ntype Action<T> =\n | { type: 'SET'; value: T | ((prev: T) => T) }\n | { type: 'UNDO' }\n | { type: 'REDO' }\n | { type: 'RESET'; value: T };\n\nconst createReducer =\n <T>(limit: number) =>\n (state: HistoryState<T>, action: Action<T>): HistoryState<T> => {\n switch (action.type) {\n case 'SET': {\n const resolved =\n action.value instanceof Function\n ? action.value(state.present)\n : action.value;\n\n if (resolved === state.present) {\n return state;\n }\n\n return {\n past: takeLast([...state.past, state.present], limit),\n present: resolved,\n future: [],\n };\n }\n\n case 'UNDO': {\n if (state.past.length === 0) {\n return state;\n }\n\n const previous = state.past[state.past.length - 1] as T;\n\n return {\n past: state.past.slice(0, -1),\n present: previous,\n // Cap `future` at `limit` too — otherwise a long run of undos\n // (more than `limit` of them) grows `future` unbounded, even\n // though `past` is kept within `limit` by REDO/SET.\n future: takeFirst([state.present, ...state.future], limit),\n };\n }\n\n case 'REDO': {\n if (state.future.length === 0) {\n return state;\n }\n\n const [next, ...rest] = state.future;\n\n return {\n past: takeLast([...state.past, state.present], limit),\n present: next as T,\n future: rest,\n };\n }\n\n case 'RESET': {\n return { past: [], present: action.value, future: [] };\n }\n\n default: {\n return state;\n }\n }\n };\n\ninterface UseHistoryStateOptions {\n limit?: number;\n}\n\nconst DEFAULT_LIMIT = 50;\n\nconst useHistoryState = <T>(\n initialValue: T,\n options?: UseHistoryStateOptions,\n) => {\n const limit = options?.limit ?? DEFAULT_LIMIT;\n\n const [state, dispatch] = useReducer(createReducer<T>(limit), {\n past: [],\n present: initialValue,\n future: [],\n });\n\n const setValue = useCallback((value: T | ((prev: T) => T)) => {\n dispatch({ type: 'SET', value });\n }, []);\n\n const undo = useCallback(() => {\n dispatch({ type: 'UNDO' });\n }, []);\n\n const redo = useCallback(() => {\n dispatch({ type: 'REDO' });\n }, []);\n\n const reset = useCallback((value: T) => {\n dispatch({ type: 'RESET', value });\n }, []);\n\n return {\n value: state.present,\n setValue,\n undo,\n redo,\n reset,\n canUndo: state.past.length > 0,\n canRedo: state.future.length > 0,\n } as const;\n};\n\nexport default useHistoryState;\n"],"mappings":";;;AAYA,MAAM,YAAe,KAAU,UAC7B,SAAS,IAAI,EAAE,GAAG,IAAI,MAAM,CAAC,MAAM;AAErC,MAAM,aAAgB,KAAU,UAC9B,SAAS,IAAI,EAAE,GAAG,IAAI,MAAM,GAAG,MAAM;AAQvC,MAAM,iBACA,WACH,OAAwB,WAAuC;AAC9D,SAAQ,OAAO,MAAf;EACE,KAAK,OAAO;GACV,MAAM,WACJ,OAAO,iBAAiB,WACpB,OAAO,MAAM,MAAM,QAAQ,GAC3B,OAAO;AAEb,OAAI,aAAa,MAAM,QACrB,QAAO;AAGT,UAAO;IACL,MAAM,SAAS,CAAC,GAAG,MAAM,MAAM,MAAM,QAAQ,EAAE,MAAM;IACrD,SAAS;IACT,QAAQ,EAAE;IACX;;EAGH,KAAK,QAAQ;AACX,OAAI,MAAM,KAAK,WAAW,EACxB,QAAO;GAGT,MAAM,WAAW,MAAM,KAAK,MAAM,KAAK,SAAS;AAEhD,UAAO;IACL,MAAM,MAAM,KAAK,MAAM,GAAG,GAAG;IAC7B,SAAS;IAIT,QAAQ,UAAU,CAAC,MAAM,SAAS,GAAG,MAAM,OAAO,EAAE,MAAM;IAC3D;;EAGH,KAAK,QAAQ;AACX,OAAI,MAAM,OAAO,WAAW,EAC1B,QAAO;GAGT,MAAM,CAAC,MAAM,GAAG,QAAQ,MAAM;AAE9B,UAAO;IACL,MAAM,SAAS,CAAC,GAAG,MAAM,MAAM,MAAM,QAAQ,EAAE,MAAM;IACrD,SAAS;IACT,QAAQ;IACT;;EAGH,KAAK,QACH,QAAO;GAAE,MAAM,EAAE;GAAE,SAAS,OAAO;GAAO,QAAQ,EAAE;GAAE;EAGxD,QACE,QAAO;;;AASf,MAAM,gBAAgB;AAEtB,MAAM,mBACJ,cACA,YACG;CAGH,MAAM,CAAC,OAAO,YAAY,WAAW,cAFvB,SAAS,SAAS,cAE4B,EAAE;EAC5D,MAAM,EAAE;EACR,SAAS;EACT,QAAQ,EAAE;EACX,CAAC;CAEF,MAAM,WAAW,aAAa,UAAgC;AAC5D,WAAS;GAAE,MAAM;GAAO;GAAO,CAAC;IAC/B,EAAE,CAAC;CAEN,MAAM,OAAO,kBAAkB;AAC7B,WAAS,EAAE,MAAM,QAAQ,CAAC;IACzB,EAAE,CAAC;CAEN,MAAM,OAAO,kBAAkB;AAC7B,WAAS,EAAE,MAAM,QAAQ,CAAC;IACzB,EAAE,CAAC;CAEN,MAAM,QAAQ,aAAa,UAAa;AACtC,WAAS;GAAE,MAAM;GAAS;GAAO,CAAC;IACjC,EAAE,CAAC;AAEN,QAAO;EACL,OAAO,MAAM;EACb;EACA;EACA;EACA;EACA,SAAS,MAAM,KAAK,SAAS;EAC7B,SAAS,MAAM,OAAO,SAAS;EAChC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useMemo, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/hooks/use-multi-select.ts
|
|
4
4
|
const useMultiSelect = (count) => {
|
|
@@ -8,27 +8,36 @@ const useMultiSelect = (count) => {
|
|
|
8
8
|
const next = new Set([...rawSelected].filter((index) => index < count));
|
|
9
9
|
return next.size === rawSelected.size ? rawSelected : next;
|
|
10
10
|
}, [rawSelected, count]);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
setRawSelected((prev) => {
|
|
13
|
+
const next = new Set([...prev].filter((index) => index < count));
|
|
14
|
+
return next.size === prev.size ? prev : next;
|
|
15
|
+
});
|
|
16
|
+
setAnchor((prev) => prev !== null && prev >= count ? null : prev);
|
|
17
|
+
}, [count]);
|
|
11
18
|
const toggle = useCallback((index, shiftKey = false) => {
|
|
19
|
+
if (index < 0 || index >= count) return;
|
|
12
20
|
setRawSelected((prev) => {
|
|
13
|
-
const next = new Set(prev);
|
|
14
21
|
if (shiftKey && anchor !== null) {
|
|
15
22
|
const [start, end] = anchor < index ? [anchor, index] : [index, anchor];
|
|
23
|
+
const next = /* @__PURE__ */ new Set();
|
|
16
24
|
for (let i = start; i <= end; i++) next.add(i);
|
|
17
25
|
return next;
|
|
18
26
|
}
|
|
27
|
+
const next = new Set(prev);
|
|
19
28
|
if (next.has(index)) next.delete(index);
|
|
20
29
|
else next.add(index);
|
|
21
30
|
return next;
|
|
22
31
|
});
|
|
23
32
|
setAnchor(index);
|
|
24
|
-
}, [anchor]);
|
|
33
|
+
}, [anchor, count]);
|
|
25
34
|
const clear = useCallback(() => {
|
|
26
35
|
setRawSelected(/* @__PURE__ */ new Set());
|
|
27
36
|
setAnchor(null);
|
|
28
37
|
}, []);
|
|
29
38
|
const replace = useCallback((indices) => {
|
|
30
|
-
setRawSelected(indices);
|
|
31
|
-
}, []);
|
|
39
|
+
setRawSelected(new Set([...indices].filter((index) => index >= 0 && index < count)));
|
|
40
|
+
}, [count]);
|
|
32
41
|
return {
|
|
33
42
|
selected,
|
|
34
43
|
isSelected: useCallback((index) => selected.has(index), [selected]),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-multi-select.mjs","names":[],"sources":["../../src/hooks/use-multi-select.ts"],"sourcesContent":["import { useCallback, useMemo, useState } from 'react';\n\nconst useMultiSelect = (count: number) => {\n const [rawSelected, setRawSelected] = useState<Set<number>>(new Set());\n const [anchor, setAnchor] = useState<number | null>(null);\n\n
|
|
1
|
+
{"version":3,"file":"use-multi-select.mjs","names":[],"sources":["../../src/hooks/use-multi-select.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react';\n\nconst useMultiSelect = (count: number) => {\n const [rawSelected, setRawSelected] = useState<Set<number>>(new Set());\n const [anchor, setAnchor] = useState<number | null>(null);\n\n const selected = useMemo(() => {\n const next = new Set([...rawSelected].filter(index => index < count));\n return next.size === rawSelected.size ? rawSelected : next;\n }, [rawSelected, count]);\n\n // Clamp `rawSelected`/`anchor` themselves (not just the derived\n // `selected` view above) whenever `count` shrinks — otherwise\n // out-of-range indices and a stale anchor keep accumulating in state\n // indefinitely instead of actually being dropped.\n useEffect(() => {\n setRawSelected(prev => {\n const next = new Set([...prev].filter(index => index < count));\n return next.size === prev.size ? prev : next;\n });\n setAnchor(prev => (prev !== null && prev >= count ? null : prev));\n }, [count]);\n\n const toggle = useCallback(\n (index: number, shiftKey = false) => {\n if (index < 0 || index >= count) {\n return;\n }\n\n setRawSelected(prev => {\n if (shiftKey && anchor !== null) {\n const [start, end] =\n anchor < index ? [anchor, index] : [index, anchor];\n\n // Range-select replaces the previous selection, matching the\n // usual file-explorer/table convention, instead of adding to\n // it — otherwise repeated shift-clicks just accumulate every\n // range ever selected rather than re-anchoring to the new one.\n const next = new Set<number>();\n for (let i = start; i <= end; i++) {\n next.add(i);\n }\n return next;\n }\n\n const next = new Set(prev);\n if (next.has(index)) {\n next.delete(index);\n } else {\n next.add(index);\n }\n return next;\n });\n setAnchor(index);\n },\n [anchor, count],\n );\n\n const clear = useCallback(() => {\n setRawSelected(new Set());\n setAnchor(null);\n }, []);\n\n const replace = useCallback(\n (indices: Set<number>) => {\n setRawSelected(\n new Set([...indices].filter(index => index >= 0 && index < count)),\n );\n },\n [count],\n );\n\n const isSelected = useCallback(\n (index: number) => selected.has(index),\n [selected],\n );\n\n return {\n selected,\n isSelected,\n toggle,\n clear,\n replace,\n } as const;\n};\n\nexport default useMultiSelect;\n"],"mappings":";;;AAEA,MAAM,kBAAkB,UAAkB;CACxC,MAAM,CAAC,aAAa,kBAAkB,yBAAsB,IAAI,KAAK,CAAC;CACtE,MAAM,CAAC,QAAQ,aAAa,SAAwB,KAAK;CAEzD,MAAM,WAAW,cAAc;EAC7B,MAAM,OAAO,IAAI,IAAI,CAAC,GAAG,YAAY,CAAC,QAAO,UAAS,QAAQ,MAAM,CAAC;AACrE,SAAO,KAAK,SAAS,YAAY,OAAO,cAAc;IACrD,CAAC,aAAa,MAAM,CAAC;AAMxB,iBAAgB;AACd,kBAAe,SAAQ;GACrB,MAAM,OAAO,IAAI,IAAI,CAAC,GAAG,KAAK,CAAC,QAAO,UAAS,QAAQ,MAAM,CAAC;AAC9D,UAAO,KAAK,SAAS,KAAK,OAAO,OAAO;IACxC;AACF,aAAU,SAAS,SAAS,QAAQ,QAAQ,QAAQ,OAAO,KAAM;IAChE,CAAC,MAAM,CAAC;CAEX,MAAM,SAAS,aACZ,OAAe,WAAW,UAAU;AACnC,MAAI,QAAQ,KAAK,SAAS,MACxB;AAGF,kBAAe,SAAQ;AACrB,OAAI,YAAY,WAAW,MAAM;IAC/B,MAAM,CAAC,OAAO,OACZ,SAAS,QAAQ,CAAC,QAAQ,MAAM,GAAG,CAAC,OAAO,OAAO;IAMpD,MAAM,uBAAO,IAAI,KAAa;AAC9B,SAAK,IAAI,IAAI,OAAO,KAAK,KAAK,IAC5B,MAAK,IAAI,EAAE;AAEb,WAAO;;GAGT,MAAM,OAAO,IAAI,IAAI,KAAK;AAC1B,OAAI,KAAK,IAAI,MAAM,CACjB,MAAK,OAAO,MAAM;OAElB,MAAK,IAAI,MAAM;AAEjB,UAAO;IACP;AACF,YAAU,MAAM;IAElB,CAAC,QAAQ,MAAM,CAChB;CAED,MAAM,QAAQ,kBAAkB;AAC9B,iCAAe,IAAI,KAAK,CAAC;AACzB,YAAU,KAAK;IACd,EAAE,CAAC;CAEN,MAAM,UAAU,aACb,YAAyB;AACxB,iBACE,IAAI,IAAI,CAAC,GAAG,QAAQ,CAAC,QAAO,UAAS,SAAS,KAAK,QAAQ,MAAM,CAAC,CACnE;IAEH,CAAC,MAAM,CACR;AAOD,QAAO;EACL;EACA,YAPiB,aAChB,UAAkB,SAAS,IAAI,MAAM,EACtC,CAAC,SAAS,CACX;EAKC;EACA;EACA;EACD"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import useDebounce from "./use-debounce.mjs";
|
|
2
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
3
|
|
|
4
4
|
//#region src/hooks/use-responsive-size.ts
|
|
5
5
|
const BREAKPOINTS = {
|
|
@@ -28,76 +28,92 @@ const getBreakpointInfo = (width) => {
|
|
|
28
28
|
"2xl": width >= BREAKPOINTS["2xl"]
|
|
29
29
|
};
|
|
30
30
|
};
|
|
31
|
+
const breakpointEqual = (a, b) => a.current === b.current && a.xs === b.xs && a.sm === b.sm && a.md === b.md && a.lg === b.lg && a.xl === b.xl && a["2xl"] === b["2xl"];
|
|
32
|
+
const measureTarget = (target) => ({
|
|
33
|
+
width: target.offsetWidth,
|
|
34
|
+
height: target.offsetHeight
|
|
35
|
+
});
|
|
36
|
+
const measureViewport = () => ({
|
|
37
|
+
width: window.innerWidth,
|
|
38
|
+
height: window.innerHeight
|
|
39
|
+
});
|
|
31
40
|
const useResponsiveSize = (options) => {
|
|
32
|
-
const { delay = 100, container } = options || {};
|
|
41
|
+
const { delay = 100, container, viewport = false } = options || {};
|
|
42
|
+
const [element, setElement] = useState(null);
|
|
33
43
|
const [size, setSize] = useState({
|
|
34
44
|
width: 0,
|
|
35
45
|
height: 0
|
|
36
46
|
});
|
|
37
|
-
const [breakpoint, setBreakpoint] = useState(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
lg: false,
|
|
43
|
-
xl: false,
|
|
44
|
-
"2xl": false
|
|
47
|
+
const [breakpoint, setBreakpoint] = useState(() => getBreakpointInfo(0));
|
|
48
|
+
const observerRef = useRef(null);
|
|
49
|
+
const latestRef = useRef({
|
|
50
|
+
width: 0,
|
|
51
|
+
height: 0
|
|
45
52
|
});
|
|
46
|
-
const
|
|
53
|
+
const committedRef = useRef({
|
|
47
54
|
width: 0,
|
|
48
55
|
height: 0
|
|
49
56
|
});
|
|
50
|
-
const [element, setElement] = useState(null);
|
|
51
|
-
const observerRef = useRef(null);
|
|
52
57
|
const ref = useCallback((node) => {
|
|
53
58
|
setElement(node);
|
|
54
59
|
}, []);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
});
|
|
70
|
-
setBreakpoint((prev) => {
|
|
71
|
-
const next = getBreakpointInfo(offsetWidth);
|
|
72
|
-
if (prev.current !== next.current) return next;
|
|
73
|
-
return prev;
|
|
74
|
-
});
|
|
75
|
-
};
|
|
76
|
-
const connect = () => {
|
|
60
|
+
const commit = useCallback(() => {
|
|
61
|
+
const next = latestRef.current;
|
|
62
|
+
if (committedRef.current.width === next.width && committedRef.current.height === next.height) return;
|
|
63
|
+
committedRef.current = next;
|
|
64
|
+
setSize(next);
|
|
65
|
+
const nextBreakpoint = getBreakpointInfo(next.width);
|
|
66
|
+
setBreakpoint((prev) => breakpointEqual(prev, nextBreakpoint) ? prev : nextBreakpoint);
|
|
67
|
+
}, []);
|
|
68
|
+
const debouncedCommit = useDebounce(commit, {
|
|
69
|
+
delay,
|
|
70
|
+
autoInvoke: false
|
|
71
|
+
});
|
|
72
|
+
useLayoutEffect(() => {
|
|
73
|
+
const measured = viewport ? measureViewport() : (() => {
|
|
77
74
|
const target = container ?? element ?? document.body;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
75
|
+
return target ? measureTarget(target) : null;
|
|
76
|
+
})();
|
|
77
|
+
if (!measured) return;
|
|
78
|
+
latestRef.current = measured;
|
|
79
|
+
commit();
|
|
80
|
+
}, [
|
|
81
|
+
container,
|
|
82
|
+
element,
|
|
83
|
+
viewport,
|
|
84
|
+
commit
|
|
85
|
+
]);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (viewport) {
|
|
88
|
+
const onResize = () => {
|
|
89
|
+
latestRef.current = measureViewport();
|
|
90
|
+
debouncedCommit();
|
|
91
|
+
};
|
|
92
|
+
window.addEventListener("resize", onResize);
|
|
93
|
+
return () => window.removeEventListener("resize", onResize);
|
|
94
|
+
}
|
|
95
|
+
const target = container ?? element ?? document.body;
|
|
96
|
+
if (!target) return;
|
|
97
|
+
if (observerRef.current) observerRef.current.disconnect();
|
|
98
|
+
observerRef.current = new ResizeObserver(() => {
|
|
99
|
+
requestAnimationFrame(() => {
|
|
100
|
+
latestRef.current = measureTarget(target);
|
|
101
|
+
debouncedCommit();
|
|
85
102
|
});
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const disconnect = () => {
|
|
89
|
-
if (observerRef.current) {
|
|
90
|
-
observerRef.current.disconnect();
|
|
91
|
-
observerRef.current = null;
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
connect();
|
|
103
|
+
});
|
|
104
|
+
observerRef.current.observe(target);
|
|
95
105
|
return () => {
|
|
96
|
-
disconnect();
|
|
106
|
+
observerRef.current?.disconnect();
|
|
107
|
+
observerRef.current = null;
|
|
97
108
|
};
|
|
98
|
-
}, [
|
|
109
|
+
}, [
|
|
110
|
+
container,
|
|
111
|
+
element,
|
|
112
|
+
viewport,
|
|
113
|
+
debouncedCommit
|
|
114
|
+
]);
|
|
99
115
|
return {
|
|
100
|
-
size
|
|
116
|
+
size,
|
|
101
117
|
breakpoint,
|
|
102
118
|
ref
|
|
103
119
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-responsive-size.mjs","names":[],"sources":["../../src/hooks/use-responsive-size.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"use-responsive-size.mjs","names":[],"sources":["../../src/hooks/use-responsive-size.ts"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from 'react';\n\nimport useDebounce from './use-debounce';\n\ntype Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';\n\ninterface BreakpointInfo {\n current: Breakpoint;\n xs: boolean;\n sm: boolean;\n md: boolean;\n lg: boolean;\n xl: boolean;\n '2xl': boolean;\n}\n\ninterface Options {\n delay?: number;\n container?: HTMLElement | null;\n // Measure the viewport (window.innerWidth/innerHeight) instead of an\n // element/document.body. Useful when no `container`/ref is attached and\n // the breakpoint should reflect the viewport rather than document.body's\n // box, which can diverge from it if body has margin/transform.\n viewport?: boolean;\n}\n\nconst BREAKPOINTS = {\n xs: 0, // < 640px\n sm: 640, // >= 640px\n md: 768, // >= 768px\n lg: 1024, // >= 1024px\n xl: 1280, // >= 1280px\n '2xl': 1536, // >= 1536px\n} as const;\n\nconst getBreakpointInfo = (width: number): BreakpointInfo => {\n let current: Breakpoint = 'xs';\n\n if (width >= BREAKPOINTS['2xl']) {\n current = '2xl';\n } else if (width >= BREAKPOINTS.xl) {\n current = 'xl';\n } else if (width >= BREAKPOINTS.lg) {\n current = 'lg';\n } else if (width >= BREAKPOINTS.md) {\n current = 'md';\n } else if (width >= BREAKPOINTS.sm) {\n current = 'sm';\n } else {\n current = 'xs';\n }\n\n return {\n current,\n xs: width < BREAKPOINTS.sm,\n sm: width >= BREAKPOINTS.sm && width < BREAKPOINTS.md,\n md: width >= BREAKPOINTS.md && width < BREAKPOINTS.lg,\n lg: width >= BREAKPOINTS.lg && width < BREAKPOINTS.xl,\n xl: width >= BREAKPOINTS.xl && width < BREAKPOINTS['2xl'],\n '2xl': width >= BREAKPOINTS['2xl'],\n };\n};\n\nconst breakpointEqual = (a: BreakpointInfo, b: BreakpointInfo) =>\n a.current === b.current &&\n a.xs === b.xs &&\n a.sm === b.sm &&\n a.md === b.md &&\n a.lg === b.lg &&\n a.xl === b.xl &&\n a['2xl'] === b['2xl'];\n\nconst measureTarget = (target: HTMLElement) => ({\n width: target.offsetWidth,\n height: target.offsetHeight,\n});\n\nconst measureViewport = () => ({\n width: window.innerWidth,\n height: window.innerHeight,\n});\n\nconst useResponsiveSize = <T extends HTMLElement>(options?: Options) => {\n const { delay = 100, container, viewport = false } = options || {};\n\n const [element, setElement] = useState<T | null>(null);\n const [size, setSize] = useState({ width: 0, height: 0 });\n const [breakpoint, setBreakpoint] = useState<BreakpointInfo>(() =>\n getBreakpointInfo(0),\n );\n\n const observerRef = useRef<ResizeObserver | null>(null);\n const latestRef = useRef({ width: 0, height: 0 });\n const committedRef = useRef({ width: 0, height: 0 });\n\n const ref = useCallback((node: T | null) => {\n setElement(node);\n }, []);\n\n // `size` and `breakpoint` are always committed together here, so\n // consumers never observe one reflecting a newer measurement than the\n // other.\n const commit = useCallback(() => {\n const next = latestRef.current;\n\n if (\n committedRef.current.width === next.width &&\n committedRef.current.height === next.height\n ) {\n return;\n }\n\n committedRef.current = next;\n setSize(next);\n\n const nextBreakpoint = getBreakpointInfo(next.width);\n setBreakpoint(prev =>\n breakpointEqual(prev, nextBreakpoint) ? prev : nextBreakpoint,\n );\n }, []);\n\n // Manual invocation only (`autoInvoke: false`) — reuses useDebounce's\n // debounced-callback machinery instead of duplicating it here.\n const debouncedCommit = useDebounce(commit, { delay, autoInvoke: false });\n\n // Measure synchronously before paint so the very first render reflects\n // the real size/breakpoint instead of the `{0,0}`/`xs` placeholder —\n // only later updates (from the observer/listener below) go through the\n // debounced path.\n useLayoutEffect(() => {\n const measured = viewport\n ? measureViewport()\n : (() => {\n const target = container ?? element ?? document.body;\n return target ? measureTarget(target) : null;\n })();\n\n if (!measured) {\n return;\n }\n\n latestRef.current = measured;\n commit();\n }, [container, element, viewport, commit]);\n\n useEffect(() => {\n if (viewport) {\n const onResize = () => {\n latestRef.current = measureViewport();\n debouncedCommit();\n };\n\n window.addEventListener('resize', onResize);\n return () => window.removeEventListener('resize', onResize);\n }\n\n const target = container ?? element ?? document.body;\n\n if (!target) {\n return;\n }\n\n if (observerRef.current) {\n observerRef.current.disconnect();\n }\n\n observerRef.current = new ResizeObserver(() => {\n requestAnimationFrame(() => {\n latestRef.current = measureTarget(target);\n debouncedCommit();\n });\n });\n\n observerRef.current.observe(target);\n\n return () => {\n observerRef.current?.disconnect();\n observerRef.current = null;\n };\n }, [container, element, viewport, debouncedCommit]);\n\n return {\n size,\n breakpoint,\n ref,\n };\n};\n\nexport default useResponsiveSize;\n"],"mappings":"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"}
|
|
@@ -1,25 +1,31 @@
|
|
|
1
|
-
import { useCallback, useRef } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/hooks/use-scroll-to-elements.ts
|
|
4
4
|
const useScrollToElements = (options) => {
|
|
5
5
|
const elementRefs = useRef([]);
|
|
6
|
+
const optionsRef = useRef(options);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
optionsRef.current = options;
|
|
9
|
+
});
|
|
6
10
|
const scrollToElement = useCallback((index) => {
|
|
7
11
|
const element = elementRefs.current[index];
|
|
8
12
|
if (!element) return;
|
|
13
|
+
const { offset, ...scrollIntoViewOptions } = optionsRef.current ?? {};
|
|
14
|
+
if (offset != null) {
|
|
15
|
+
const top = element.getBoundingClientRect().top + window.scrollY - offset;
|
|
16
|
+
window.scrollTo({
|
|
17
|
+
top,
|
|
18
|
+
behavior: scrollIntoViewOptions.behavior || "smooth"
|
|
19
|
+
});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
9
22
|
element.scrollIntoView({
|
|
10
23
|
behavior: "smooth",
|
|
11
24
|
block: "start",
|
|
12
25
|
inline: "start",
|
|
13
|
-
...
|
|
26
|
+
...scrollIntoViewOptions
|
|
14
27
|
});
|
|
15
|
-
|
|
16
|
-
const top = element.getBoundingClientRect().top + window.scrollY - options.offset;
|
|
17
|
-
window.scrollTo({
|
|
18
|
-
top,
|
|
19
|
-
behavior: options.behavior || "smooth"
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
}, [options]);
|
|
28
|
+
}, []);
|
|
23
29
|
return {
|
|
24
30
|
elementRefs,
|
|
25
31
|
setElementRef: useCallback((element, index) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-scroll-to-elements.mjs","names":[],"sources":["../../src/hooks/use-scroll-to-elements.ts"],"sourcesContent":["import { useCallback, useRef } from 'react';\n\ninterface Options extends ScrollIntoViewOptions {\n offset?: number;\n}\nconst useScrollToElements = (options?: Options) => {\n const elementRefs = useRef<(HTMLElement | null)[]>([]);\n\n const
|
|
1
|
+
{"version":3,"file":"use-scroll-to-elements.mjs","names":[],"sources":["../../src/hooks/use-scroll-to-elements.ts"],"sourcesContent":["import { useCallback, useEffect, useRef } from 'react';\n\ninterface Options extends ScrollIntoViewOptions {\n offset?: number;\n}\n\nconst useScrollToElements = (options?: Options) => {\n const elementRefs = useRef<(HTMLElement | null)[]>([]);\n // `options` is almost always an inline object literal at the call site,\n // so depending on it directly would recreate `scrollToElement` on every\n // render — read the latest value from a ref instead.\n const optionsRef = useRef(options);\n\n useEffect(() => {\n optionsRef.current = options;\n });\n\n const scrollToElement = useCallback((index: number) => {\n const element = elementRefs.current[index];\n\n if (!element) {\n return;\n }\n\n const { offset, ...scrollIntoViewOptions } = optionsRef.current ?? {};\n\n // With an offset, `scrollIntoView` and `scrollTo` would both animate\n // the same scroll, fighting each other and settling in the wrong\n // place — use `scrollTo` alone, computed from the element's position\n // before any scrolling starts (not after `scrollIntoView`, where the\n // rect would reflect a mid-animation position).\n if (offset != null) {\n const top = element.getBoundingClientRect().top + window.scrollY - offset;\n\n window.scrollTo({\n top,\n behavior: scrollIntoViewOptions.behavior || 'smooth',\n });\n return;\n }\n\n element.scrollIntoView({\n behavior: 'smooth',\n block: 'start',\n inline: 'start',\n ...scrollIntoViewOptions,\n });\n }, []);\n\n const setElementRef = useCallback(\n (element: HTMLElement | null, index: number) => {\n elementRefs.current[index] = element;\n },\n [],\n );\n\n return { elementRefs, setElementRef, scrollToElement };\n};\n\nexport default useScrollToElements;\n"],"mappings":";;;AAMA,MAAM,uBAAuB,YAAsB;CACjD,MAAM,cAAc,OAA+B,EAAE,CAAC;CAItD,MAAM,aAAa,OAAO,QAAQ;AAElC,iBAAgB;AACd,aAAW,UAAU;GACrB;CAEF,MAAM,kBAAkB,aAAa,UAAkB;EACrD,MAAM,UAAU,YAAY,QAAQ;AAEpC,MAAI,CAAC,QACH;EAGF,MAAM,EAAE,QAAQ,GAAG,0BAA0B,WAAW,WAAW,EAAE;AAOrE,MAAI,UAAU,MAAM;GAClB,MAAM,MAAM,QAAQ,uBAAuB,CAAC,MAAM,OAAO,UAAU;AAEnE,UAAO,SAAS;IACd;IACA,UAAU,sBAAsB,YAAY;IAC7C,CAAC;AACF;;AAGF,UAAQ,eAAe;GACrB,UAAU;GACV,OAAO;GACP,QAAQ;GACR,GAAG;GACJ,CAAC;IACD,EAAE,CAAC;AASN,QAAO;EAAE;EAAa,eAPA,aACnB,SAA6B,UAAkB;AAC9C,eAAY,QAAQ,SAAS;KAE/B,EAAE,CACH;EAEoC;EAAiB"}
|
|
@@ -16,7 +16,7 @@ const registerCSSProperty = (name, syntax = "<length>", initialValue = "0px") =>
|
|
|
16
16
|
};
|
|
17
17
|
const buildTimeline = (steps) => {
|
|
18
18
|
const groups = [];
|
|
19
|
-
for (const step of steps) if (step.transition.delay === 0 && groups.length > 0) groups[groups.length - 1]?.push(step);
|
|
19
|
+
for (const step of steps) if ((step.transition.delay ?? 0) === 0 && groups.length > 0) groups[groups.length - 1]?.push(step);
|
|
20
20
|
else groups.push([step]);
|
|
21
21
|
return groups;
|
|
22
22
|
};
|
|
@@ -83,12 +83,41 @@ const applyStep = (container, step, skipTransition) => {
|
|
|
83
83
|
});
|
|
84
84
|
return timers;
|
|
85
85
|
};
|
|
86
|
+
const resetAppliedStyles = (container, steps) => {
|
|
87
|
+
new Set(steps.map((step) => step.selector)).forEach((selector) => {
|
|
88
|
+
(container.matches(selector) ? [container] : Array.from(container.querySelectorAll(selector))).forEach((el) => {
|
|
89
|
+
el.style.transition = "";
|
|
90
|
+
el.style.backgroundColor = "";
|
|
91
|
+
el.style.opacity = "";
|
|
92
|
+
el.style.top = "";
|
|
93
|
+
el.style.left = "";
|
|
94
|
+
el.style.width = "";
|
|
95
|
+
el.style.height = "";
|
|
96
|
+
el.style.zIndex = "";
|
|
97
|
+
el.style.filter = "";
|
|
98
|
+
el.style.rotate = "";
|
|
99
|
+
el.style.transform = "";
|
|
100
|
+
el.style.removeProperty("--tx");
|
|
101
|
+
el.style.removeProperty("--ty");
|
|
102
|
+
el.style.removeProperty("--s");
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
};
|
|
86
106
|
const useTimeline = ({ steps = [], loading, immediate, loop }) => {
|
|
87
107
|
const [slotIndex, setSlotIndex] = useState(-1);
|
|
88
108
|
const [completed, setCompleted] = useState(() => !!immediate);
|
|
89
109
|
const ref = useRef(null);
|
|
90
|
-
const
|
|
91
|
-
|
|
110
|
+
const stepsRef = useRef(steps);
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
stepsRef.current = steps;
|
|
113
|
+
});
|
|
114
|
+
const stepsKey = useMemo(() => JSON.stringify(steps), [steps]);
|
|
115
|
+
const timeline = useMemo(() => buildTimeline(steps), [stepsKey]);
|
|
116
|
+
const hasCurveSteps = useMemo(() => steps.some((s) => s.transition.easeX || s.transition.easeY), [stepsKey]);
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
setSlotIndex(-1);
|
|
119
|
+
setCompleted(!!immediate);
|
|
120
|
+
}, [stepsKey, loading]);
|
|
92
121
|
useLayoutEffect(() => {
|
|
93
122
|
if (hasCurveSteps) {
|
|
94
123
|
registerCSSProperty("--tx");
|
|
@@ -100,7 +129,13 @@ const useTimeline = ({ steps = [], loading, immediate, loop }) => {
|
|
|
100
129
|
if (!immediate || !ref.current || !steps.length) return;
|
|
101
130
|
const container = ref.current;
|
|
102
131
|
steps.forEach((step) => applyStep(container, step, true));
|
|
103
|
-
}, [immediate,
|
|
132
|
+
}, [immediate, stepsKey]);
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
const container = ref.current;
|
|
135
|
+
return () => {
|
|
136
|
+
if (container) resetAppliedStyles(container, stepsRef.current);
|
|
137
|
+
};
|
|
138
|
+
}, []);
|
|
104
139
|
useEffect(() => {
|
|
105
140
|
if (immediate || loading || !timeline.length) return;
|
|
106
141
|
const firstDelay = timeline[0]?.[0]?.transition.delay ?? 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-timeline.mjs","names":[],"sources":["../../src/hooks/use-timeline.ts"],"sourcesContent":["import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\n\nconst registeredProperties = new Set<string>();\n\nconst registerCSSProperty = (\n name: string,\n syntax = '<length>',\n initialValue = '0px',\n) => {\n if (\n registeredProperties.has(name) ||\n typeof CSS === 'undefined' ||\n !CSS.registerProperty\n ) {\n return;\n }\n\n try {\n CSS.registerProperty({\n name,\n syntax,\n inherits: false,\n initialValue,\n });\n registeredProperties.add(name);\n } catch {\n // 이미 등록된 경우 무시\n }\n};\n\nexport interface TimelineStep {\n scale?: number;\n rotate?: { x?: number; y?: number; z?: number };\n position?: { x?: number; y?: number };\n top?: number;\n left?: number;\n width?: number;\n height?: number;\n zIndex?: number;\n filter?: string;\n backgroundColor?: string;\n opacity?: number;\n selector: string;\n transition: Transition;\n}\n\ninterface Transition {\n duration: number;\n ease?: string;\n delay?: number;\n easeX?: string;\n easeY?: string;\n}\n\ninterface Options {\n steps?: TimelineStep[];\n loading?: boolean;\n immediate?: boolean;\n loop?: boolean;\n}\n\nconst buildTimeline = (steps: TimelineStep[]): TimelineStep[][] => {\n const groups: TimelineStep[][] = [];\n\n for (const step of steps) {\n if (step.transition.delay === 0 && groups.length > 0) {\n groups[groups.length - 1]?.push(step);\n } else {\n groups.push([step]);\n }\n }\n\n return groups;\n};\n\nconst buildTransform = (step: TimelineStep) => {\n const parts: string[] = [];\n\n if (step.position?.x != null || step.position?.y != null) {\n parts.push(\n `translate(${step.position.x ?? 0}px, ${step.position.y ?? 0}px)`,\n );\n }\n\n if (step.scale != null) {\n parts.push(`scale(${step.scale})`);\n }\n\n if (step.rotate?.x != null) {\n parts.push(`rotateX(${step.rotate.x}deg)`);\n }\n\n if (step.rotate?.y != null) {\n parts.push(`rotateY(${step.rotate.y}deg)`);\n }\n\n if (step.rotate?.z != null) {\n parts.push(`rotateZ(${step.rotate.z}deg)`);\n }\n\n return parts.join(' ') || undefined;\n};\n\nconst applyStep = (\n container: HTMLElement,\n step: TimelineStep,\n skipTransition?: boolean,\n) => {\n const elements = container.matches(step.selector)\n ? [container]\n : Array.from(container.querySelectorAll<HTMLElement>(step.selector));\n\n if (!elements.length) {\n return;\n }\n\n const dur = `${step.transition.duration}ms`;\n const eas = step.transition.ease ?? 'ease-out';\n const easX = step.transition.easeX;\n const easY = step.transition.easeY;\n const hasCurve = !!(easX || easY);\n const timers: ReturnType<typeof setTimeout>[] = [];\n\n elements.forEach(el => {\n if (skipTransition) {\n el.style.transition = 'none';\n } else if (hasCurve) {\n const xEase = easX ?? eas;\n const yEase = easY ?? eas;\n el.style.transition = [\n `--tx ${dur} ${xEase}`,\n `--ty ${dur} ${yEase}`,\n `--s ${dur} ${eas}`,\n `rotate ${dur} ${eas}`,\n `width ${dur} ${eas}`,\n `height ${dur} ${eas}`,\n `filter ${dur} ${eas}`,\n `opacity ${dur} ${eas}`,\n ].join(', ');\n } else {\n el.style.transition = `all ${dur} ${eas}`;\n }\n\n if (step.backgroundColor) {\n el.style.backgroundColor = step.backgroundColor;\n }\n\n if (step.opacity !== undefined) {\n el.style.opacity = String(step.opacity);\n }\n\n if (step.top !== undefined) {\n el.style.top = `${step.top}px`;\n }\n\n if (step.left !== undefined) {\n el.style.left = `${step.left}px`;\n }\n\n if (step.width !== undefined) {\n el.style.width = `${step.width}px`;\n }\n\n if (step.height !== undefined) {\n el.style.height = `${step.height}px`;\n }\n\n if (step.zIndex !== undefined) {\n if (skipTransition) {\n el.style.zIndex = String(step.zIndex);\n } else {\n const timerId = setTimeout(() => {\n el.style.zIndex = String(step.zIndex);\n }, step.transition.duration / 2);\n timers.push(timerId);\n }\n }\n\n if (step.filter !== undefined) {\n el.style.filter = step.filter;\n }\n\n if (step.rotate?.z !== undefined) {\n el.style.rotate = `${step.rotate.z}deg`;\n }\n\n if (hasCurve) {\n if (step.position?.x != null || step.position?.y != null) {\n el.style.setProperty('--tx', `${step.position.x ?? 0}px`);\n el.style.setProperty('--ty', `${step.position.y ?? 0}px`);\n }\n if (step.scale != null) {\n el.style.setProperty('--s', String(step.scale));\n }\n el.style.transform = `translate(var(--tx), var(--ty)) scale(var(--s, 1))`;\n } else {\n const transform = buildTransform(step);\n\n if (transform) {\n el.style.transform = transform;\n }\n }\n });\n\n return timers;\n};\n\nconst useTimeline = ({ steps = [], loading, immediate, loop }: Options) => {\n const [slotIndex, setSlotIndex] = useState(-1);\n const [completed, setCompleted] = useState(() => !!immediate);\n\n const ref = useRef<HTMLDivElement>(null);\n const timeline = useMemo(() => buildTimeline(steps), [steps]);\n const hasCurveSteps = useMemo(\n () => steps.some(s => s.transition.easeX || s.transition.easeY),\n [steps],\n );\n\n useLayoutEffect(() => {\n if (hasCurveSteps) {\n registerCSSProperty('--tx');\n registerCSSProperty('--ty');\n registerCSSProperty('--s', '<number>', '1');\n }\n }, [hasCurveSteps]);\n\n useLayoutEffect(() => {\n if (!immediate || !ref.current || !steps.length) {\n return;\n }\n\n const container = ref.current;\n steps.forEach(step => applyStep(container, step, true));\n }, [immediate, steps]);\n\n useEffect(() => {\n if (immediate || loading || !timeline.length) {\n return;\n }\n\n const firstDelay = timeline[0]?.[0]?.transition.delay ?? 0;\n const timerId = window.setTimeout(() => {\n requestAnimationFrame(() => setSlotIndex(0));\n }, firstDelay);\n\n return () => clearTimeout(timerId);\n }, [loading, timeline, immediate]);\n\n useEffect(() => {\n if (immediate || loading || slotIndex < 0) {\n return;\n }\n\n const isLast = slotIndex >= timeline.length - 1;\n\n if (isLast && !loop) {\n return;\n }\n\n const currentSlot = timeline[slotIndex];\n const maxDuration = Math.max(\n ...(currentSlot?.map(s => s.transition.duration) ?? [0]),\n );\n\n const nextIndex = isLast ? 0 : slotIndex + 1;\n const nextDelay = timeline[nextIndex]?.[0]?.transition.delay ?? 0;\n\n const timerId = window.setTimeout(() => {\n setSlotIndex(nextIndex);\n }, maxDuration + nextDelay);\n\n return () => clearTimeout(timerId);\n }, [loading, slotIndex, timeline, immediate, loop]);\n\n useEffect(() => {\n if (immediate || slotIndex < 0 || !ref.current) {\n return;\n }\n\n const container = ref.current;\n const timers: ReturnType<typeof setTimeout>[] = [];\n\n const rafId = requestAnimationFrame(() => {\n timeline[slotIndex]?.forEach(step => {\n const stepTimers = applyStep(container, step);\n\n if (stepTimers) {\n timers.push(...stepTimers);\n }\n });\n });\n\n return () => {\n cancelAnimationFrame(rafId);\n timers.forEach(clearTimeout);\n };\n }, [immediate, slotIndex, timeline]);\n\n useEffect(() => {\n if (\n immediate ||\n loop ||\n slotIndex < 0 ||\n slotIndex < timeline.length - 1 ||\n !timeline.length\n ) {\n return;\n }\n\n const lastSlot = timeline[slotIndex];\n const maxDuration = Math.max(\n ...(lastSlot?.map(s => s.transition.duration) ?? [0]),\n );\n\n const timerId = window.setTimeout(() => {\n setCompleted(true);\n }, maxDuration);\n\n return () => clearTimeout(timerId);\n }, [slotIndex, timeline, immediate, loop]);\n\n return { ref, completed };\n};\n\nexport default useTimeline;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-timeline.mjs","names":[],"sources":["../../src/hooks/use-timeline.ts"],"sourcesContent":["import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\n\nconst registeredProperties = new Set<string>();\n\n// `CSS.registerProperty` is permanent for the page's lifetime — there is\n// no unregisterProperty API, so once a curved-easing step registers\n// `--tx`/`--ty`/`--s`, they stay registered globally even after every\n// `useTimeline` instance using them has unmounted.\nconst registerCSSProperty = (\n name: string,\n syntax = '<length>',\n initialValue = '0px',\n) => {\n if (\n registeredProperties.has(name) ||\n typeof CSS === 'undefined' ||\n !CSS.registerProperty\n ) {\n return;\n }\n\n try {\n CSS.registerProperty({\n name,\n syntax,\n inherits: false,\n initialValue,\n });\n registeredProperties.add(name);\n } catch {\n // 이미 등록된 경우 무시\n }\n};\n\nexport interface TimelineStep {\n scale?: number;\n rotate?: { x?: number; y?: number; z?: number };\n position?: { x?: number; y?: number };\n top?: number;\n left?: number;\n width?: number;\n height?: number;\n zIndex?: number;\n filter?: string;\n backgroundColor?: string;\n opacity?: number;\n selector: string;\n transition: Transition;\n}\n\ninterface Transition {\n duration: number;\n ease?: string;\n delay?: number;\n easeX?: string;\n easeY?: string;\n}\n\ninterface Options {\n steps?: TimelineStep[];\n loading?: boolean;\n immediate?: boolean;\n loop?: boolean;\n}\n\nconst buildTimeline = (steps: TimelineStep[]): TimelineStep[][] => {\n const groups: TimelineStep[][] = [];\n\n for (const step of steps) {\n // An unspecified delay means \"no delay\", same as an explicit 0 — both\n // merge into the previous group instead of only the explicit case.\n if ((step.transition.delay ?? 0) === 0 && groups.length > 0) {\n groups[groups.length - 1]?.push(step);\n } else {\n groups.push([step]);\n }\n }\n\n return groups;\n};\n\nconst buildTransform = (step: TimelineStep) => {\n const parts: string[] = [];\n\n if (step.position?.x != null || step.position?.y != null) {\n parts.push(\n `translate(${step.position.x ?? 0}px, ${step.position.y ?? 0}px)`,\n );\n }\n\n if (step.scale != null) {\n parts.push(`scale(${step.scale})`);\n }\n\n if (step.rotate?.x != null) {\n parts.push(`rotateX(${step.rotate.x}deg)`);\n }\n\n if (step.rotate?.y != null) {\n parts.push(`rotateY(${step.rotate.y}deg)`);\n }\n\n if (step.rotate?.z != null) {\n parts.push(`rotateZ(${step.rotate.z}deg)`);\n }\n\n return parts.join(' ') || undefined;\n};\n\nconst applyStep = (\n container: HTMLElement,\n step: TimelineStep,\n skipTransition?: boolean,\n) => {\n const elements = container.matches(step.selector)\n ? [container]\n : Array.from(container.querySelectorAll<HTMLElement>(step.selector));\n\n if (!elements.length) {\n return;\n }\n\n const dur = `${step.transition.duration}ms`;\n const eas = step.transition.ease ?? 'ease-out';\n const easX = step.transition.easeX;\n const easY = step.transition.easeY;\n const hasCurve = !!(easX || easY);\n const timers: ReturnType<typeof setTimeout>[] = [];\n\n elements.forEach(el => {\n if (skipTransition) {\n el.style.transition = 'none';\n } else if (hasCurve) {\n const xEase = easX ?? eas;\n const yEase = easY ?? eas;\n el.style.transition = [\n `--tx ${dur} ${xEase}`,\n `--ty ${dur} ${yEase}`,\n `--s ${dur} ${eas}`,\n `rotate ${dur} ${eas}`,\n `width ${dur} ${eas}`,\n `height ${dur} ${eas}`,\n `filter ${dur} ${eas}`,\n `opacity ${dur} ${eas}`,\n ].join(', ');\n } else {\n el.style.transition = `all ${dur} ${eas}`;\n }\n\n if (step.backgroundColor) {\n el.style.backgroundColor = step.backgroundColor;\n }\n\n if (step.opacity !== undefined) {\n el.style.opacity = String(step.opacity);\n }\n\n if (step.top !== undefined) {\n el.style.top = `${step.top}px`;\n }\n\n if (step.left !== undefined) {\n el.style.left = `${step.left}px`;\n }\n\n if (step.width !== undefined) {\n el.style.width = `${step.width}px`;\n }\n\n if (step.height !== undefined) {\n el.style.height = `${step.height}px`;\n }\n\n if (step.zIndex !== undefined) {\n if (skipTransition) {\n el.style.zIndex = String(step.zIndex);\n } else {\n const timerId = setTimeout(() => {\n el.style.zIndex = String(step.zIndex);\n }, step.transition.duration / 2);\n timers.push(timerId);\n }\n }\n\n if (step.filter !== undefined) {\n el.style.filter = step.filter;\n }\n\n if (step.rotate?.z !== undefined) {\n el.style.rotate = `${step.rotate.z}deg`;\n }\n\n if (hasCurve) {\n if (step.position?.x != null || step.position?.y != null) {\n el.style.setProperty('--tx', `${step.position.x ?? 0}px`);\n el.style.setProperty('--ty', `${step.position.y ?? 0}px`);\n }\n if (step.scale != null) {\n el.style.setProperty('--s', String(step.scale));\n }\n el.style.transform = `translate(var(--tx), var(--ty)) scale(var(--s, 1))`;\n } else {\n const transform = buildTransform(step);\n\n if (transform) {\n el.style.transform = transform;\n }\n }\n });\n\n return timers;\n};\n\n// Clears every inline style property `applyStep` can set, on every element\n// matching any of `steps`' selectors, so nothing this hook applied lingers\n// on elements that outlive it (e.g. kept mounted by a parent).\nconst resetAppliedStyles = (container: HTMLElement, steps: TimelineStep[]) => {\n const selectors = new Set(steps.map(step => step.selector));\n\n selectors.forEach(selector => {\n const elements = container.matches(selector)\n ? [container]\n : Array.from(container.querySelectorAll<HTMLElement>(selector));\n\n elements.forEach(el => {\n el.style.transition = '';\n el.style.backgroundColor = '';\n el.style.opacity = '';\n el.style.top = '';\n el.style.left = '';\n el.style.width = '';\n el.style.height = '';\n el.style.zIndex = '';\n el.style.filter = '';\n el.style.rotate = '';\n el.style.transform = '';\n el.style.removeProperty('--tx');\n el.style.removeProperty('--ty');\n el.style.removeProperty('--s');\n });\n });\n};\n\nconst useTimeline = ({ steps = [], loading, immediate, loop }: Options) => {\n const [slotIndex, setSlotIndex] = useState(-1);\n const [completed, setCompleted] = useState(() => !!immediate);\n\n const ref = useRef<HTMLDivElement>(null);\n const stepsRef = useRef(steps);\n\n useEffect(() => {\n stepsRef.current = steps;\n });\n\n // `steps` is naturally passed as an inline array literal at most call\n // sites, so a new reference every render would otherwise rebuild\n // `timeline` — clearing every running timer — on every unrelated\n // re-render. Key derived values off a serialized snapshot instead,\n // which only changes when the content actually does.\n const stepsKey = useMemo(() => JSON.stringify(steps), [steps]);\n const timeline = useMemo(\n () => buildTimeline(steps),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [stepsKey],\n );\n const hasCurveSteps = useMemo(\n () => steps.some(s => s.transition.easeX || s.transition.easeY),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [stepsKey],\n );\n\n // Restart playback from the top whenever the steps' content (not just\n // their reference) or `loading` changes — otherwise slotIndex/completed\n // stay wherever they were left on the previous timeline.\n useEffect(() => {\n setSlotIndex(-1);\n setCompleted(!!immediate);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [stepsKey, loading]);\n\n useLayoutEffect(() => {\n if (hasCurveSteps) {\n registerCSSProperty('--tx');\n registerCSSProperty('--ty');\n registerCSSProperty('--s', '<number>', '1');\n }\n }, [hasCurveSteps]);\n\n useLayoutEffect(() => {\n if (!immediate || !ref.current || !steps.length) {\n return;\n }\n\n const container = ref.current;\n steps.forEach(step => applyStep(container, step, true));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [immediate, stepsKey]);\n\n useEffect(() => {\n const container = ref.current;\n\n return () => {\n if (container) {\n resetAppliedStyles(container, stepsRef.current);\n }\n };\n }, []);\n\n useEffect(() => {\n if (immediate || loading || !timeline.length) {\n return;\n }\n\n const firstDelay = timeline[0]?.[0]?.transition.delay ?? 0;\n const timerId = window.setTimeout(() => {\n requestAnimationFrame(() => setSlotIndex(0));\n }, firstDelay);\n\n return () => clearTimeout(timerId);\n }, [loading, timeline, immediate]);\n\n useEffect(() => {\n if (immediate || loading || slotIndex < 0) {\n return;\n }\n\n const isLast = slotIndex >= timeline.length - 1;\n\n if (isLast && !loop) {\n return;\n }\n\n const currentSlot = timeline[slotIndex];\n const maxDuration = Math.max(\n ...(currentSlot?.map(s => s.transition.duration) ?? [0]),\n );\n\n const nextIndex = isLast ? 0 : slotIndex + 1;\n const nextDelay = timeline[nextIndex]?.[0]?.transition.delay ?? 0;\n\n const timerId = window.setTimeout(() => {\n setSlotIndex(nextIndex);\n }, maxDuration + nextDelay);\n\n return () => clearTimeout(timerId);\n }, [loading, slotIndex, timeline, immediate, loop]);\n\n useEffect(() => {\n if (immediate || slotIndex < 0 || !ref.current) {\n return;\n }\n\n const container = ref.current;\n const timers: ReturnType<typeof setTimeout>[] = [];\n\n const rafId = requestAnimationFrame(() => {\n timeline[slotIndex]?.forEach(step => {\n const stepTimers = applyStep(container, step);\n\n if (stepTimers) {\n timers.push(...stepTimers);\n }\n });\n });\n\n return () => {\n cancelAnimationFrame(rafId);\n timers.forEach(clearTimeout);\n };\n }, [immediate, slotIndex, timeline]);\n\n useEffect(() => {\n if (\n immediate ||\n loop ||\n slotIndex < 0 ||\n slotIndex < timeline.length - 1 ||\n !timeline.length\n ) {\n return;\n }\n\n const lastSlot = timeline[slotIndex];\n const maxDuration = Math.max(\n ...(lastSlot?.map(s => s.transition.duration) ?? [0]),\n );\n\n const timerId = window.setTimeout(() => {\n setCompleted(true);\n }, maxDuration);\n\n return () => clearTimeout(timerId);\n }, [slotIndex, timeline, immediate, loop]);\n\n return { ref, completed };\n};\n\nexport default useTimeline;\n"],"mappings":"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"}
|
|
@@ -11,47 +11,69 @@ const useWindowScroll = (targetRef) => {
|
|
|
11
11
|
}
|
|
12
12
|
});
|
|
13
13
|
useEffect(() => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
14
|
+
let cancelled = false;
|
|
15
|
+
let rafId;
|
|
16
|
+
let cleanup;
|
|
17
|
+
const attach = () => {
|
|
18
|
+
if (cancelled) return;
|
|
19
|
+
if (targetRef && !targetRef.current) {
|
|
20
|
+
rafId = requestAnimationFrame(attach);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const target = targetRef?.current?.ownerDocument.defaultView ?? window;
|
|
24
|
+
const doc = target.document;
|
|
25
|
+
const calculate = () => {
|
|
26
|
+
const x = target.scrollX || 0;
|
|
27
|
+
const y = target.scrollY || 0;
|
|
28
|
+
const isIOS = /iPad|iPhone|iPod/.test(target.navigator.userAgent);
|
|
29
|
+
const visualViewport = target.visualViewport;
|
|
30
|
+
const viewportWidth = isIOS && visualViewport ? visualViewport.width : target.innerWidth;
|
|
31
|
+
const viewportHeight = isIOS && visualViewport ? visualViewport.height : target.innerHeight;
|
|
32
|
+
const maxScrollX = Math.max(0, doc.documentElement.scrollWidth - viewportWidth);
|
|
33
|
+
const maxScrollY = Math.max(0, doc.documentElement.scrollHeight - viewportHeight);
|
|
34
|
+
const percentX = maxScrollX === 0 ? 0 : Math.min(100, x / maxScrollX * 100);
|
|
35
|
+
const percentY = maxScrollY === 0 ? 0 : Math.min(100, y / maxScrollY * 100);
|
|
36
|
+
setState({
|
|
37
|
+
x,
|
|
38
|
+
y,
|
|
39
|
+
percent: {
|
|
40
|
+
x: Math.round(Math.max(0, percentX)),
|
|
41
|
+
y: Math.round(Math.max(0, percentY))
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
};
|
|
38
45
|
calculate();
|
|
46
|
+
const onScroll = () => {
|
|
47
|
+
calculate();
|
|
48
|
+
};
|
|
49
|
+
const onResize = () => {
|
|
50
|
+
setTimeout(calculate, 100);
|
|
51
|
+
};
|
|
52
|
+
const onVisualViewportChange = () => {
|
|
53
|
+
setTimeout(calculate, 50);
|
|
54
|
+
};
|
|
55
|
+
target.addEventListener("scroll", onScroll, { passive: true });
|
|
56
|
+
target.addEventListener("resize", onResize);
|
|
57
|
+
target.addEventListener("orientationchange", onResize);
|
|
58
|
+
if (target.visualViewport) target.visualViewport.addEventListener("resize", onVisualViewportChange);
|
|
59
|
+
let resizeObserver;
|
|
60
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
61
|
+
resizeObserver = new ResizeObserver(() => calculate());
|
|
62
|
+
resizeObserver.observe(doc.documentElement);
|
|
63
|
+
}
|
|
64
|
+
cleanup = () => {
|
|
65
|
+
target.removeEventListener("scroll", onScroll);
|
|
66
|
+
target.removeEventListener("resize", onResize);
|
|
67
|
+
target.removeEventListener("orientationchange", onResize);
|
|
68
|
+
if (target.visualViewport) target.visualViewport.removeEventListener("resize", onVisualViewportChange);
|
|
69
|
+
resizeObserver?.disconnect();
|
|
70
|
+
};
|
|
39
71
|
};
|
|
40
|
-
|
|
41
|
-
setTimeout(calculate, 100);
|
|
42
|
-
};
|
|
43
|
-
const onVisualViewportChange = () => {
|
|
44
|
-
setTimeout(calculate, 50);
|
|
45
|
-
};
|
|
46
|
-
target.addEventListener("scroll", onScroll, { passive: true });
|
|
47
|
-
target.addEventListener("resize", onResize);
|
|
48
|
-
target.addEventListener("orientationchange", onResize);
|
|
49
|
-
if (target.visualViewport) target.visualViewport.addEventListener("resize", onVisualViewportChange);
|
|
72
|
+
attach();
|
|
50
73
|
return () => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (target.visualViewport) target.visualViewport.removeEventListener("resize", onVisualViewportChange);
|
|
74
|
+
cancelled = true;
|
|
75
|
+
if (rafId !== void 0) cancelAnimationFrame(rafId);
|
|
76
|
+
cleanup?.();
|
|
55
77
|
};
|
|
56
78
|
}, [targetRef]);
|
|
57
79
|
return state;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-window-scroll.mjs","names":[],"sources":["../../src/hooks/use-window-scroll.ts"],"sourcesContent":["import { type RefObject, useEffect, useState } from 'react';\n\n// Pass a ref to an element rendered inside the window you actually want to\n// track (e.g. a node portaled into an iframe). Without one, this defaults\n// to the host `window`, which is wrong whenever the caller doesn't live in\n// the top-level document
|
|
1
|
+
{"version":3,"file":"use-window-scroll.mjs","names":[],"sources":["../../src/hooks/use-window-scroll.ts"],"sourcesContent":["import { type RefObject, useEffect, useState } from 'react';\n\n// Pass a ref to an element rendered inside the window you actually want to\n// track (e.g. a node portaled into an iframe). Without one, this defaults\n// to the host `window`, which is wrong whenever the caller doesn't live in\n// the top-level document.\nconst useWindowScroll = (targetRef?: RefObject<Element | null>) => {\n const [state, setState] = useState({\n x: 0,\n y: 0,\n percent: {\n x: 0,\n y: 0,\n },\n });\n\n useEffect(() => {\n let cancelled = false;\n let rafId: number | undefined;\n let cleanup: (() => void) | undefined;\n\n const attach = () => {\n if (cancelled) {\n return;\n }\n\n // `targetRef.current` can still be null right after mount (e.g. a\n // conditionally-rendered or portaled node) — retry every frame\n // until it's populated instead of permanently falling back to the\n // host `window`.\n if (targetRef && !targetRef.current) {\n rafId = requestAnimationFrame(attach);\n return;\n }\n\n const target = targetRef?.current?.ownerDocument.defaultView ?? window;\n const doc = target.document;\n\n const calculate = () => {\n const x = target.scrollX || 0;\n const y = target.scrollY || 0;\n\n const isIOS = /iPad|iPhone|iPod/.test(target.navigator.userAgent);\n const visualViewport = target.visualViewport;\n\n const viewportWidth =\n isIOS && visualViewport ? visualViewport.width : target.innerWidth;\n\n const viewportHeight =\n isIOS && visualViewport ? visualViewport.height : target.innerHeight;\n\n const maxScrollX = Math.max(\n 0,\n doc.documentElement.scrollWidth - viewportWidth,\n );\n const maxScrollY = Math.max(\n 0,\n doc.documentElement.scrollHeight - viewportHeight,\n );\n\n const percentX =\n maxScrollX === 0 ? 0 : Math.min(100, (x / maxScrollX) * 100);\n const percentY =\n maxScrollY === 0 ? 0 : Math.min(100, (y / maxScrollY) * 100);\n\n setState({\n x,\n y,\n percent: {\n // Rounded (not floored) so reaching the true bottom/right edge\n // reads as 100 instead of falling just short of it.\n x: Math.round(Math.max(0, percentX)),\n y: Math.round(Math.max(0, percentY)),\n },\n });\n };\n\n calculate();\n\n const onScroll = () => {\n calculate();\n };\n\n const onResize = () => {\n setTimeout(calculate, 100);\n };\n\n const onVisualViewportChange = () => {\n setTimeout(calculate, 50);\n };\n\n target.addEventListener('scroll', onScroll, { passive: true });\n target.addEventListener('resize', onResize);\n target.addEventListener('orientationchange', onResize);\n\n if (target.visualViewport) {\n target.visualViewport.addEventListener(\n 'resize',\n onVisualViewportChange,\n );\n }\n\n // `resize`/`orientationchange` only fire for the viewport itself —\n // content that grows/shrinks the document height without resizing\n // the window (images loading, dynamic content) needs its own\n // observer to keep maxScrollY accurate.\n let resizeObserver: ResizeObserver | undefined;\n if (typeof ResizeObserver !== 'undefined') {\n resizeObserver = new ResizeObserver(() => calculate());\n resizeObserver.observe(doc.documentElement);\n }\n\n cleanup = () => {\n target.removeEventListener('scroll', onScroll);\n target.removeEventListener('resize', onResize);\n target.removeEventListener('orientationchange', onResize);\n\n if (target.visualViewport) {\n target.visualViewport.removeEventListener(\n 'resize',\n onVisualViewportChange,\n );\n }\n\n resizeObserver?.disconnect();\n };\n };\n\n attach();\n\n return () => {\n cancelled = true;\n if (rafId !== undefined) {\n cancelAnimationFrame(rafId);\n }\n cleanup?.();\n };\n }, [targetRef]);\n\n return state;\n};\n\nexport default useWindowScroll;\n"],"mappings":";;;AAMA,MAAM,mBAAmB,cAA0C;CACjE,MAAM,CAAC,OAAO,YAAY,SAAS;EACjC,GAAG;EACH,GAAG;EACH,SAAS;GACP,GAAG;GACH,GAAG;GACJ;EACF,CAAC;AAEF,iBAAgB;EACd,IAAI,YAAY;EAChB,IAAI;EACJ,IAAI;EAEJ,MAAM,eAAe;AACnB,OAAI,UACF;AAOF,OAAI,aAAa,CAAC,UAAU,SAAS;AACnC,YAAQ,sBAAsB,OAAO;AACrC;;GAGF,MAAM,SAAS,WAAW,SAAS,cAAc,eAAe;GAChE,MAAM,MAAM,OAAO;GAEnB,MAAM,kBAAkB;IACtB,MAAM,IAAI,OAAO,WAAW;IAC5B,MAAM,IAAI,OAAO,WAAW;IAE5B,MAAM,QAAQ,mBAAmB,KAAK,OAAO,UAAU,UAAU;IACjE,MAAM,iBAAiB,OAAO;IAE9B,MAAM,gBACJ,SAAS,iBAAiB,eAAe,QAAQ,OAAO;IAE1D,MAAM,iBACJ,SAAS,iBAAiB,eAAe,SAAS,OAAO;IAE3D,MAAM,aAAa,KAAK,IACtB,GACA,IAAI,gBAAgB,cAAc,cACnC;IACD,MAAM,aAAa,KAAK,IACtB,GACA,IAAI,gBAAgB,eAAe,eACpC;IAED,MAAM,WACJ,eAAe,IAAI,IAAI,KAAK,IAAI,KAAM,IAAI,aAAc,IAAI;IAC9D,MAAM,WACJ,eAAe,IAAI,IAAI,KAAK,IAAI,KAAM,IAAI,aAAc,IAAI;AAE9D,aAAS;KACP;KACA;KACA,SAAS;MAGP,GAAG,KAAK,MAAM,KAAK,IAAI,GAAG,SAAS,CAAC;MACpC,GAAG,KAAK,MAAM,KAAK,IAAI,GAAG,SAAS,CAAC;MACrC;KACF,CAAC;;AAGJ,cAAW;GAEX,MAAM,iBAAiB;AACrB,eAAW;;GAGb,MAAM,iBAAiB;AACrB,eAAW,WAAW,IAAI;;GAG5B,MAAM,+BAA+B;AACnC,eAAW,WAAW,GAAG;;AAG3B,UAAO,iBAAiB,UAAU,UAAU,EAAE,SAAS,MAAM,CAAC;AAC9D,UAAO,iBAAiB,UAAU,SAAS;AAC3C,UAAO,iBAAiB,qBAAqB,SAAS;AAEtD,OAAI,OAAO,eACT,QAAO,eAAe,iBACpB,UACA,uBACD;GAOH,IAAI;AACJ,OAAI,OAAO,mBAAmB,aAAa;AACzC,qBAAiB,IAAI,qBAAqB,WAAW,CAAC;AACtD,mBAAe,QAAQ,IAAI,gBAAgB;;AAG7C,mBAAgB;AACd,WAAO,oBAAoB,UAAU,SAAS;AAC9C,WAAO,oBAAoB,UAAU,SAAS;AAC9C,WAAO,oBAAoB,qBAAqB,SAAS;AAEzD,QAAI,OAAO,eACT,QAAO,eAAe,oBACpB,UACA,uBACD;AAGH,oBAAgB,YAAY;;;AAIhC,UAAQ;AAER,eAAa;AACX,eAAY;AACZ,OAAI,UAAU,OACZ,sBAAqB,MAAM;AAE7B,cAAW;;IAEZ,CAAC,UAAU,CAAC;AAEf,QAAO"}
|