@jbpark/use-hooks 2.8.0 → 2.10.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-debounce.d.mts +5 -3
- package/dist/hooks/use-debounce.mjs +9 -9
- package/dist/hooks/use-debounce.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-local-storage.mjs +61 -32
- package/dist/hooks/use-local-storage.mjs.map +1 -1
- package/dist/hooks/use-recursive-timeout.d.mts +4 -1
- package/dist/hooks/use-recursive-timeout.mjs +18 -11
- package/dist/hooks/use-recursive-timeout.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-viewport.d.mts +2 -2
- package/dist/hooks/use-viewport.mjs +6 -3
- package/dist/hooks/use-viewport.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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"}
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
interface Options {
|
|
3
3
|
delay?: number;
|
|
4
4
|
autoInvoke?: boolean;
|
|
5
|
+
leading?: boolean;
|
|
5
6
|
}
|
|
6
|
-
declare const useDebounce:
|
|
7
|
+
declare const useDebounce: (callback: () => unknown, {
|
|
7
8
|
delay,
|
|
8
|
-
autoInvoke
|
|
9
|
-
|
|
9
|
+
autoInvoke,
|
|
10
|
+
leading
|
|
11
|
+
}: Options, deps?: React.DependencyList) => () => void;
|
|
10
12
|
//#endregion
|
|
11
13
|
export { useDebounce };
|
|
12
14
|
//# sourceMappingURL=use-debounce.d.mts.map
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/hooks/use-debounce.ts
|
|
4
|
-
const useDebounce = (callback, { delay = 100, autoInvoke = true }, deps = []) => {
|
|
4
|
+
const useDebounce = (callback, { delay = 100, autoInvoke = true, leading = true }, deps = []) => {
|
|
5
5
|
const timeoutRef = useRef(null);
|
|
6
6
|
const callbackRef = useRef(callback);
|
|
7
|
+
const delayRef = useRef(delay);
|
|
7
8
|
const prevDeps = useRef(void 0);
|
|
8
9
|
useEffect(() => {
|
|
9
10
|
callbackRef.current = callback;
|
|
11
|
+
delayRef.current = delay;
|
|
10
12
|
});
|
|
11
13
|
const stableDebouncedCallback = useRef(null);
|
|
12
|
-
if (!stableDebouncedCallback.current) stableDebouncedCallback.current = (
|
|
14
|
+
if (!stableDebouncedCallback.current) stableDebouncedCallback.current = () => {
|
|
13
15
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
14
16
|
timeoutRef.current = setTimeout(() => {
|
|
15
|
-
callbackRef.current(
|
|
16
|
-
},
|
|
17
|
-
}
|
|
17
|
+
callbackRef.current();
|
|
18
|
+
}, delayRef.current);
|
|
19
|
+
};
|
|
18
20
|
useEffect(() => {
|
|
19
21
|
const depsChanged = prevDeps.current === void 0 || prevDeps.current.length !== deps.length || prevDeps.current.some((dep, i) => dep !== deps[i]);
|
|
20
22
|
if (depsChanged && timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
21
|
-
if (autoInvoke && depsChanged)
|
|
22
|
-
|
|
23
|
-
else if (stableDebouncedCallback.current) stableDebouncedCallback.current();
|
|
24
|
-
}
|
|
23
|
+
if (autoInvoke && depsChanged) if (prevDeps.current === void 0 && leading) callbackRef.current();
|
|
24
|
+
else stableDebouncedCallback.current?.();
|
|
25
25
|
prevDeps.current = deps;
|
|
26
26
|
});
|
|
27
27
|
useEffect(() => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-debounce.mjs","names":[],"sources":["../../src/hooks/use-debounce.ts"],"sourcesContent":["import { useEffect, useRef } from 'react';\n\ninterface Options {\n delay?: number;\n autoInvoke?: boolean;\n}\n\
|
|
1
|
+
{"version":3,"file":"use-debounce.mjs","names":[],"sources":["../../src/hooks/use-debounce.ts"],"sourcesContent":["import { useEffect, useRef } from 'react';\n\ninterface Options {\n delay?: number;\n autoInvoke?: boolean;\n leading?: boolean;\n}\n\n// Auto-invokes `callback` (debounced by `delay`ms) whenever `deps` change,\n// and also returns a stable debounced version of `callback` for manual use.\n// `callback` intentionally takes no arguments — the deps-triggered\n// auto-invoke has no natural argument to supply, so an arg-taking callback\n// here was a latent type hole (it was always invoked with zero args\n// regardless of what the callback's own signature claimed).\nconst useDebounce = (\n callback: () => unknown,\n { delay = 100, autoInvoke = true, leading = true }: Options,\n deps: React.DependencyList = [],\n) => {\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const callbackRef = useRef(callback);\n // Read the latest `delay` when the timeout actually fires, instead of\n // baking in whatever `delay` was on the render that created the stable\n // debounced function (which otherwise never updates again).\n const delayRef = useRef(delay);\n const prevDeps = useRef<React.DependencyList | undefined>(undefined);\n\n useEffect(() => {\n callbackRef.current = callback;\n delayRef.current = delay;\n });\n\n const stableDebouncedCallback = useRef<(() => void) | null>(null);\n\n if (!stableDebouncedCallback.current) {\n stableDebouncedCallback.current = () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n\n timeoutRef.current = setTimeout(() => {\n callbackRef.current();\n }, delayRef.current);\n };\n }\n\n useEffect(() => {\n const depsChanged =\n prevDeps.current === undefined ||\n prevDeps.current.length !== deps.length ||\n prevDeps.current.some((dep, i) => dep !== deps[i]);\n\n if (depsChanged && timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n\n if (autoInvoke && depsChanged) {\n const isFirstRender = prevDeps.current === undefined;\n\n // `leading` makes the \"first invocation fires immediately, without\n // waiting `delay`ms\" behavior explicit and opt-out-able, instead of\n // an unconditional special case baked into the first render.\n if (isFirstRender && leading) {\n callbackRef.current();\n } else {\n stableDebouncedCallback.current?.();\n }\n }\n\n prevDeps.current = deps;\n });\n\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n };\n }, []);\n\n return stableDebouncedCallback.current;\n};\n\nexport default useDebounce;\n"],"mappings":";;;AAcA,MAAM,eACJ,UACA,EAAE,QAAQ,KAAK,aAAa,MAAM,UAAU,QAC5C,OAA6B,EAAE,KAC5B;CACH,MAAM,aAAa,OAA6C,KAAK;CACrE,MAAM,cAAc,OAAO,SAAS;CAIpC,MAAM,WAAW,OAAO,MAAM;CAC9B,MAAM,WAAW,OAAyC,OAAU;AAEpE,iBAAgB;AACd,cAAY,UAAU;AACtB,WAAS,UAAU;GACnB;CAEF,MAAM,0BAA0B,OAA4B,KAAK;AAEjE,KAAI,CAAC,wBAAwB,QAC3B,yBAAwB,gBAAgB;AACtC,MAAI,WAAW,QACb,cAAa,WAAW,QAAQ;AAGlC,aAAW,UAAU,iBAAiB;AACpC,eAAY,SAAS;KACpB,SAAS,QAAQ;;AAIxB,iBAAgB;EACd,MAAM,cACJ,SAAS,YAAY,UACrB,SAAS,QAAQ,WAAW,KAAK,UACjC,SAAS,QAAQ,MAAM,KAAK,MAAM,QAAQ,KAAK,GAAG;AAEpD,MAAI,eAAe,WAAW,QAC5B,cAAa,WAAW,QAAQ;AAGlC,MAAI,cAAc,YAMhB,KALsB,SAAS,YAAY,UAKtB,QACnB,aAAY,SAAS;MAErB,yBAAwB,WAAW;AAIvC,WAAS,UAAU;GACnB;AAEF,iBAAgB;AACd,eAAa;AACX,OAAI,WAAW,QACb,cAAa,WAAW,QAAQ;;IAGnC,EAAE,CAAC;AAEN,QAAO,wBAAwB"}
|
|
@@ -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,50 +1,79 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef,
|
|
1
|
+
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/hooks/use-local-storage.ts
|
|
4
|
+
const cache = /* @__PURE__ */ new Map();
|
|
5
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
6
|
+
function getListeners(key) {
|
|
7
|
+
let set = listeners.get(key);
|
|
8
|
+
if (!set) {
|
|
9
|
+
set = /* @__PURE__ */ new Set();
|
|
10
|
+
listeners.set(key, set);
|
|
11
|
+
}
|
|
12
|
+
return set;
|
|
13
|
+
}
|
|
14
|
+
function notify(key) {
|
|
15
|
+
getListeners(key).forEach((callback) => callback());
|
|
16
|
+
}
|
|
17
|
+
function getSnapshot(key, initialValue) {
|
|
18
|
+
const raw = typeof window === "undefined" ? null : localStorage.getItem(key);
|
|
19
|
+
const cached = cache.get(key);
|
|
20
|
+
if (cached && cached.raw === raw) return cached.value;
|
|
21
|
+
let value;
|
|
22
|
+
try {
|
|
23
|
+
value = raw === null ? initialValue : JSON.parse(raw);
|
|
24
|
+
} catch {
|
|
25
|
+
value = initialValue;
|
|
26
|
+
}
|
|
27
|
+
cache.set(key, {
|
|
28
|
+
raw,
|
|
29
|
+
value
|
|
30
|
+
});
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
function subscribe(key, callback) {
|
|
34
|
+
const set = getListeners(key);
|
|
35
|
+
set.add(callback);
|
|
36
|
+
const onStorage = (event) => {
|
|
37
|
+
if (event.key === key) callback();
|
|
38
|
+
};
|
|
39
|
+
if (typeof window !== "undefined") window.addEventListener("storage", onStorage);
|
|
40
|
+
return () => {
|
|
41
|
+
set.delete(callback);
|
|
42
|
+
if (typeof window !== "undefined") window.removeEventListener("storage", onStorage);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
4
45
|
const useLocalStorage = (key, initialValue) => {
|
|
5
46
|
const initialValueRef = useRef(initialValue);
|
|
6
47
|
useEffect(() => {
|
|
7
48
|
initialValueRef.current = initialValue;
|
|
8
49
|
});
|
|
9
|
-
const [storedValue, setStoredValue] = useState(() => {
|
|
10
|
-
if (typeof window === "undefined") return initialValue;
|
|
11
|
-
try {
|
|
12
|
-
const item = window.localStorage.getItem(key);
|
|
13
|
-
return item ? JSON.parse(item) : initialValue;
|
|
14
|
-
} catch {
|
|
15
|
-
return initialValue;
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
50
|
useEffect(() => {
|
|
19
51
|
if (typeof window === "undefined") return;
|
|
20
52
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
53
|
+
if (localStorage.getItem(key) === null) {
|
|
54
|
+
const raw = JSON.stringify(initialValueRef.current);
|
|
55
|
+
localStorage.setItem(key, raw);
|
|
56
|
+
cache.set(key, {
|
|
57
|
+
raw,
|
|
58
|
+
value: initialValueRef.current
|
|
59
|
+
});
|
|
60
|
+
notify(key);
|
|
61
|
+
}
|
|
24
62
|
} catch (e) {
|
|
25
|
-
console.error(`Error
|
|
63
|
+
console.error(`Error seeding localStorage key "${key}":`, e);
|
|
26
64
|
}
|
|
27
65
|
}, [key]);
|
|
28
|
-
|
|
29
|
-
if (typeof window === "undefined") return;
|
|
30
|
-
const onStorage = (event) => {
|
|
31
|
-
if (event.key !== key) return;
|
|
32
|
-
try {
|
|
33
|
-
setStoredValue(event.newValue ? JSON.parse(event.newValue) : initialValueRef.current);
|
|
34
|
-
} catch (e) {
|
|
35
|
-
console.error(`Error parsing localStorage key "${key}" from storage event:`, e);
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
window.addEventListener("storage", onStorage);
|
|
39
|
-
return () => window.removeEventListener("storage", onStorage);
|
|
40
|
-
}, [key]);
|
|
41
|
-
return [storedValue, useCallback((value) => {
|
|
66
|
+
return [useSyncExternalStore(useCallback((callback) => subscribe(key, callback), [key]), useCallback(() => getSnapshot(key, initialValueRef.current), [key]), useCallback(() => initialValueRef.current, [])), useCallback((value) => {
|
|
42
67
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
68
|
+
const prev = getSnapshot(key, initialValueRef.current);
|
|
69
|
+
const valueToStore = value instanceof Function ? value(prev) : value;
|
|
70
|
+
const raw = JSON.stringify(valueToStore);
|
|
71
|
+
localStorage.setItem(key, raw);
|
|
72
|
+
cache.set(key, {
|
|
73
|
+
raw,
|
|
74
|
+
value: valueToStore
|
|
47
75
|
});
|
|
76
|
+
notify(key);
|
|
48
77
|
} catch (e) {
|
|
49
78
|
console.error(`Error setting localStorage key "${key}":`, e);
|
|
50
79
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-local-storage.mjs","names":[],"sources":["../../src/hooks/use-local-storage.ts"],"sourcesContent":["import { useCallback, useEffect, useRef,
|
|
1
|
+
{"version":3,"file":"use-local-storage.mjs","names":[],"sources":["../../src/hooks/use-local-storage.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useSyncExternalStore } from 'react';\n\n// Module-level so every `useLocalStorage(key)` instance for the same key —\n// even across unrelated components in the same tab — shares one cache and\n// subscriber list. That's what makes same-tab sync possible: the native\n// `storage` event only fires in *other* tabs, so same-tab updates have to\n// be broadcast by us via `notify()`.\ninterface CacheEntry<T> {\n raw: string | null;\n value: T;\n}\n\nconst cache = new Map<string, CacheEntry<unknown>>();\nconst listeners = new Map<string, Set<() => void>>();\n\nfunction getListeners(key: string) {\n let set = listeners.get(key);\n if (!set) {\n set = new Set();\n listeners.set(key, set);\n }\n return set;\n}\n\nfunction notify(key: string) {\n getListeners(key).forEach(callback => callback());\n}\n\n// Returns the same cached value reference as long as the raw localStorage\n// string hasn't changed, so `useSyncExternalStore` doesn't see a \"new\"\n// snapshot (and re-render) on every call — only when the value actually\n// changed.\nfunction getSnapshot<T>(key: string, initialValue: T): T {\n const raw = typeof window === 'undefined' ? null : localStorage.getItem(key);\n const cached = cache.get(key) as CacheEntry<T> | undefined;\n\n if (cached && cached.raw === raw) {\n return cached.value;\n }\n\n let value: T;\n try {\n value = raw === null ? initialValue : (JSON.parse(raw) as T);\n } catch {\n value = initialValue;\n }\n\n cache.set(key, { raw, value });\n return value;\n}\n\nfunction subscribe(key: string, callback: () => void) {\n const set = getListeners(key);\n set.add(callback);\n\n const onStorage = (event: StorageEvent) => {\n if (event.key === key) {\n callback();\n }\n };\n\n if (typeof window !== 'undefined') {\n window.addEventListener('storage', onStorage);\n }\n\n return () => {\n set.delete(callback);\n if (typeof window !== 'undefined') {\n window.removeEventListener('storage', onStorage);\n }\n };\n}\n\nconst useLocalStorage = <T>(key: string, initialValue: T) => {\n const initialValueRef = useRef(initialValue);\n\n useEffect(() => {\n initialValueRef.current = initialValue;\n });\n\n // Seed the key with the initial value if it isn't set yet — done in an\n // effect (not inside the state updater below) so it's a one-shot side\n // effect rather than something React could invoke twice under\n // StrictMode/concurrent rendering.\n useEffect(() => {\n if (typeof window === 'undefined') {\n return;\n }\n\n try {\n if (localStorage.getItem(key) === null) {\n const raw = JSON.stringify(initialValueRef.current);\n localStorage.setItem(key, raw);\n cache.set(key, { raw, value: initialValueRef.current });\n notify(key);\n }\n } catch (e) {\n console.error(`Error seeding localStorage key \"${key}\":`, e);\n }\n }, [key]);\n\n const subscribeForKey = useCallback(\n (callback: () => void) => subscribe(key, callback),\n [key],\n );\n\n const getSnapshotForKey = useCallback(\n () => getSnapshot(key, initialValueRef.current),\n [key],\n );\n\n const getServerSnapshot = useCallback(() => initialValueRef.current, []);\n\n const storedValue = useSyncExternalStore(\n subscribeForKey,\n getSnapshotForKey,\n getServerSnapshot,\n );\n\n const setValue = useCallback(\n (value: T | ((val: T) => T)) => {\n try {\n const prev = getSnapshot(key, initialValueRef.current);\n const valueToStore = value instanceof Function ? value(prev) : value;\n const raw = JSON.stringify(valueToStore);\n\n localStorage.setItem(key, raw);\n cache.set(key, { raw, value: valueToStore });\n notify(key);\n } catch (e) {\n console.error(`Error setting localStorage key \"${key}\":`, e);\n }\n },\n [key],\n );\n\n return [storedValue, setValue] as const;\n};\n\nexport default useLocalStorage;\n"],"mappings":";;;AAYA,MAAM,wBAAQ,IAAI,KAAkC;AACpD,MAAM,4BAAY,IAAI,KAA8B;AAEpD,SAAS,aAAa,KAAa;CACjC,IAAI,MAAM,UAAU,IAAI,IAAI;AAC5B,KAAI,CAAC,KAAK;AACR,wBAAM,IAAI,KAAK;AACf,YAAU,IAAI,KAAK,IAAI;;AAEzB,QAAO;;AAGT,SAAS,OAAO,KAAa;AAC3B,cAAa,IAAI,CAAC,SAAQ,aAAY,UAAU,CAAC;;AAOnD,SAAS,YAAe,KAAa,cAAoB;CACvD,MAAM,MAAM,OAAO,WAAW,cAAc,OAAO,aAAa,QAAQ,IAAI;CAC5E,MAAM,SAAS,MAAM,IAAI,IAAI;AAE7B,KAAI,UAAU,OAAO,QAAQ,IAC3B,QAAO,OAAO;CAGhB,IAAI;AACJ,KAAI;AACF,UAAQ,QAAQ,OAAO,eAAgB,KAAK,MAAM,IAAI;SAChD;AACN,UAAQ;;AAGV,OAAM,IAAI,KAAK;EAAE;EAAK;EAAO,CAAC;AAC9B,QAAO;;AAGT,SAAS,UAAU,KAAa,UAAsB;CACpD,MAAM,MAAM,aAAa,IAAI;AAC7B,KAAI,IAAI,SAAS;CAEjB,MAAM,aAAa,UAAwB;AACzC,MAAI,MAAM,QAAQ,IAChB,WAAU;;AAId,KAAI,OAAO,WAAW,YACpB,QAAO,iBAAiB,WAAW,UAAU;AAG/C,cAAa;AACX,MAAI,OAAO,SAAS;AACpB,MAAI,OAAO,WAAW,YACpB,QAAO,oBAAoB,WAAW,UAAU;;;AAKtD,MAAM,mBAAsB,KAAa,iBAAoB;CAC3D,MAAM,kBAAkB,OAAO,aAAa;AAE5C,iBAAgB;AACd,kBAAgB,UAAU;GAC1B;AAMF,iBAAgB;AACd,MAAI,OAAO,WAAW,YACpB;AAGF,MAAI;AACF,OAAI,aAAa,QAAQ,IAAI,KAAK,MAAM;IACtC,MAAM,MAAM,KAAK,UAAU,gBAAgB,QAAQ;AACnD,iBAAa,QAAQ,KAAK,IAAI;AAC9B,UAAM,IAAI,KAAK;KAAE;KAAK,OAAO,gBAAgB;KAAS,CAAC;AACvD,WAAO,IAAI;;WAEN,GAAG;AACV,WAAQ,MAAM,mCAAmC,IAAI,KAAK,EAAE;;IAE7D,CAAC,IAAI,CAAC;AAqCT,QAAO,CAvBa,qBAZI,aACrB,aAAyB,UAAU,KAAK,SAAS,EAClD,CAAC,IAAI,CACN,EAEyB,kBAClB,YAAY,KAAK,gBAAgB,QAAQ,EAC/C,CAAC,IAAI,CACN,EAEyB,kBAAkB,gBAAgB,SAAS,EAAE,CAAC,CAMvE,EAEgB,aACd,UAA+B;AAC9B,MAAI;GACF,MAAM,OAAO,YAAY,KAAK,gBAAgB,QAAQ;GACtD,MAAM,eAAe,iBAAiB,WAAW,MAAM,KAAK,GAAG;GAC/D,MAAM,MAAM,KAAK,UAAU,aAAa;AAExC,gBAAa,QAAQ,KAAK,IAAI;AAC9B,SAAM,IAAI,KAAK;IAAE;IAAK,OAAO;IAAc,CAAC;AAC5C,UAAO,IAAI;WACJ,GAAG;AACV,WAAQ,MAAM,mCAAmC,IAAI,KAAK,EAAE;;IAGhE,CAAC,IAAI,CACN,CAE6B"}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
//#region src/hooks/use-recursive-timeout.d.ts
|
|
2
|
-
|
|
2
|
+
interface Options {
|
|
3
|
+
stopOnError?: boolean;
|
|
4
|
+
}
|
|
5
|
+
declare const useRecursiveTimeout: <T>(callback: () => Promise<T> | void, delay: number | null, options?: Options) => void;
|
|
3
6
|
//#endregion
|
|
4
7
|
export { useRecursiveTimeout };
|
|
5
8
|
//# sourceMappingURL=use-recursive-timeout.d.mts.map
|
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/hooks/use-recursive-timeout.ts
|
|
4
|
-
const useRecursiveTimeout = (callback, delay) => {
|
|
4
|
+
const useRecursiveTimeout = (callback, delay, options = {}) => {
|
|
5
5
|
const savedCallback = useRef(callback);
|
|
6
|
+
const savedStopOnError = useRef(options.stopOnError ?? false);
|
|
6
7
|
useEffect(() => {
|
|
7
8
|
savedCallback.current = callback;
|
|
8
|
-
|
|
9
|
+
savedStopOnError.current = options.stopOnError ?? false;
|
|
10
|
+
});
|
|
9
11
|
useEffect(() => {
|
|
12
|
+
if (delay === null) return;
|
|
10
13
|
let id;
|
|
14
|
+
let cancelled = false;
|
|
15
|
+
const scheduleNext = () => {
|
|
16
|
+
if (!cancelled) id = setTimeout(tick, delay);
|
|
17
|
+
};
|
|
11
18
|
function tick() {
|
|
12
19
|
const ret = savedCallback.current();
|
|
13
|
-
if (ret instanceof Promise) ret.then(() => {
|
|
14
|
-
|
|
20
|
+
if (ret instanceof Promise) ret.then(scheduleNext, (error) => {
|
|
21
|
+
console.error("useRecursiveTimeout callback rejected:", error);
|
|
22
|
+
if (!savedStopOnError.current) scheduleNext();
|
|
15
23
|
});
|
|
16
|
-
else
|
|
17
|
-
}
|
|
18
|
-
if (delay) {
|
|
19
|
-
id = setTimeout(tick, delay);
|
|
20
|
-
return () => {
|
|
21
|
-
clearTimeout(id);
|
|
22
|
-
};
|
|
24
|
+
else scheduleNext();
|
|
23
25
|
}
|
|
26
|
+
id = setTimeout(tick, delay);
|
|
27
|
+
return () => {
|
|
28
|
+
cancelled = true;
|
|
29
|
+
clearTimeout(id);
|
|
30
|
+
};
|
|
24
31
|
}, [delay]);
|
|
25
32
|
};
|
|
26
33
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-recursive-timeout.mjs","names":[],"sources":["../../src/hooks/use-recursive-timeout.ts"],"sourcesContent":["import { useEffect, useRef } from 'react';\n\nconst useRecursiveTimeout = <T>(\n callback: () => Promise<T> |
|
|
1
|
+
{"version":3,"file":"use-recursive-timeout.mjs","names":[],"sources":["../../src/hooks/use-recursive-timeout.ts"],"sourcesContent":["import { useEffect, useRef } from 'react';\n\ninterface Options {\n // Whether a rejected callback should stop the loop instead of scheduling\n // the next tick. Defaults to false — one failed tick shouldn't kill a\n // polling loop, but the rejection is still logged either way.\n stopOnError?: boolean;\n}\n\nconst useRecursiveTimeout = <T>(\n callback: () => Promise<T> | void,\n delay: number | null,\n options: Options = {},\n) => {\n const savedCallback = useRef(callback);\n const savedStopOnError = useRef(options.stopOnError ?? false);\n\n useEffect(() => {\n savedCallback.current = callback;\n savedStopOnError.current = options.stopOnError ?? false;\n });\n\n useEffect(() => {\n if (delay === null) {\n return;\n }\n\n let id: ReturnType<typeof setTimeout>;\n let cancelled = false;\n\n const scheduleNext = () => {\n if (!cancelled) {\n id = setTimeout(tick, delay);\n }\n };\n\n function tick() {\n const ret = savedCallback.current();\n\n if (ret instanceof Promise) {\n ret.then(scheduleNext, (error: unknown) => {\n console.error('useRecursiveTimeout callback rejected:', error);\n if (!savedStopOnError.current) {\n scheduleNext();\n }\n });\n } else {\n scheduleNext();\n }\n }\n\n id = setTimeout(tick, delay);\n\n return () => {\n cancelled = true;\n clearTimeout(id);\n };\n }, [delay]);\n};\n\nexport default useRecursiveTimeout;\n"],"mappings":";;;AASA,MAAM,uBACJ,UACA,OACA,UAAmB,EAAE,KAClB;CACH,MAAM,gBAAgB,OAAO,SAAS;CACtC,MAAM,mBAAmB,OAAO,QAAQ,eAAe,MAAM;AAE7D,iBAAgB;AACd,gBAAc,UAAU;AACxB,mBAAiB,UAAU,QAAQ,eAAe;GAClD;AAEF,iBAAgB;AACd,MAAI,UAAU,KACZ;EAGF,IAAI;EACJ,IAAI,YAAY;EAEhB,MAAM,qBAAqB;AACzB,OAAI,CAAC,UACH,MAAK,WAAW,MAAM,MAAM;;EAIhC,SAAS,OAAO;GACd,MAAM,MAAM,cAAc,SAAS;AAEnC,OAAI,eAAe,QACjB,KAAI,KAAK,eAAe,UAAmB;AACzC,YAAQ,MAAM,0CAA0C,MAAM;AAC9D,QAAI,CAAC,iBAAiB,QACpB,eAAc;KAEhB;OAEF,eAAc;;AAIlB,OAAK,WAAW,MAAM,MAAM;AAE5B,eAAa;AACX,eAAY;AACZ,gBAAa,GAAG;;IAEjB,CAAC,MAAM,CAAC"}
|
|
@@ -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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/hooks/use-viewport.d.ts
|
|
2
|
-
|
|
2
|
+
interface ViewportInfo {
|
|
3
3
|
width: number;
|
|
4
4
|
height: number;
|
|
5
5
|
offsetLeft: number;
|
|
@@ -7,7 +7,7 @@ type ViewportInfo = VisualViewport | {
|
|
|
7
7
|
pageLeft: number;
|
|
8
8
|
pageTop: number;
|
|
9
9
|
scale: number;
|
|
10
|
-
}
|
|
10
|
+
}
|
|
11
11
|
interface Options {
|
|
12
12
|
isInApp?: boolean;
|
|
13
13
|
debounce?: number;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/hooks/use-viewport.ts
|
|
4
|
+
const viewportEqual = (a, b) => a.width === b.width && a.height === b.height && a.offsetLeft === b.offsetLeft && a.offsetTop === b.offsetTop && a.pageLeft === b.pageLeft && a.pageTop === b.pageTop && a.scale === b.scale;
|
|
4
5
|
const useViewport = (options = {}) => {
|
|
5
6
|
const { isInApp = false, debounce = 100 } = options;
|
|
6
7
|
const [viewport, setViewport] = useState({
|
|
@@ -21,7 +22,6 @@ const useViewport = (options = {}) => {
|
|
|
21
22
|
return Math.max(windowHeight, documentHeight, bodyHeight);
|
|
22
23
|
}, []);
|
|
23
24
|
const readViewport = useCallback(() => {
|
|
24
|
-
if (window.visualViewport && !isInApp) return window.visualViewport;
|
|
25
25
|
return {
|
|
26
26
|
width: window.visualViewport?.width || window.innerWidth,
|
|
27
27
|
height: isInApp ? getAppViewHeight() : window.visualViewport?.height || window.innerHeight,
|
|
@@ -34,13 +34,16 @@ const useViewport = (options = {}) => {
|
|
|
34
34
|
}, [isInApp, getAppViewHeight]);
|
|
35
35
|
useEffect(() => {
|
|
36
36
|
let timeoutId;
|
|
37
|
+
const commitViewport = (next) => {
|
|
38
|
+
setViewport((prev) => viewportEqual(prev, next) ? prev : next);
|
|
39
|
+
};
|
|
37
40
|
const debouncedUpdate = () => {
|
|
38
41
|
clearTimeout(timeoutId);
|
|
39
42
|
timeoutId = setTimeout(() => {
|
|
40
|
-
|
|
43
|
+
commitViewport(readViewport());
|
|
41
44
|
}, debounce);
|
|
42
45
|
};
|
|
43
|
-
const immediateUpdate = () =>
|
|
46
|
+
const immediateUpdate = () => commitViewport(readViewport());
|
|
44
47
|
immediateUpdate();
|
|
45
48
|
const events = ["resize", "orientationchange"];
|
|
46
49
|
if (isInApp) events.push("focus", "blur", "touchstart", "touchend");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-viewport.mjs","names":[],"sources":["../../src/hooks/use-viewport.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\n\
|
|
1
|
+
{"version":3,"file":"use-viewport.mjs","names":[],"sources":["../../src/hooks/use-viewport.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\n\ninterface ViewportInfo {\n width: number;\n height: number;\n offsetLeft: number;\n offsetTop: number;\n pageLeft: number;\n pageTop: number;\n scale: number;\n}\n\nconst viewportEqual = (a: ViewportInfo, b: ViewportInfo) =>\n a.width === b.width &&\n a.height === b.height &&\n a.offsetLeft === b.offsetLeft &&\n a.offsetTop === b.offsetTop &&\n a.pageLeft === b.pageLeft &&\n a.pageTop === b.pageTop &&\n a.scale === b.scale;\n\ninterface Options {\n isInApp?: boolean;\n debounce?: number;\n}\n\nconst useViewport = (options: Options = {}) => {\n const { isInApp = false, debounce = 100 } = options;\n\n const [viewport, setViewport] = useState<ViewportInfo>({\n width: 0,\n height: 0,\n offsetLeft: 0,\n offsetTop: 0,\n pageLeft: 0,\n pageTop: 0,\n scale: 1,\n });\n\n const getAppViewHeight = useCallback(() => {\n const windowHeight = window.innerHeight;\n const visualHeight = window.visualViewport?.height || windowHeight;\n const documentHeight = document.documentElement.clientHeight;\n const bodyHeight = document.body.clientHeight;\n\n if (window.visualViewport && Math.abs(visualHeight - windowHeight) > 100) {\n return visualHeight;\n }\n\n return Math.max(windowHeight, documentHeight, bodyHeight);\n }, []);\n\n // Always builds a fresh plain-object snapshot rather than returning\n // `window.visualViewport` itself — that instance is a single mutable\n // object the browser updates in place, so returning it directly meant\n // every read produced the exact same reference and `setViewport` never\n // saw a change (no re-render, ever).\n const readViewport = useCallback((): ViewportInfo => {\n const width = window.visualViewport?.width || window.innerWidth;\n const height = isInApp\n ? getAppViewHeight()\n : window.visualViewport?.height || window.innerHeight;\n\n return {\n width,\n height,\n offsetLeft: window.visualViewport?.offsetLeft || 0,\n offsetTop: window.visualViewport?.offsetTop || 0,\n pageLeft: window.scrollX ?? window.pageXOffset ?? 0,\n pageTop: window.scrollY ?? window.pageYOffset ?? 0,\n scale: window.visualViewport?.scale || 1,\n };\n }, [isInApp, getAppViewHeight]);\n\n useEffect(() => {\n let timeoutId: ReturnType<typeof setTimeout>;\n\n // Snapshots are freshly-allocated objects on every read, so gate the\n // state update on an actual value change instead of always replacing\n // the reference — otherwise every resize/scroll tick would re-render\n // consumers even when nothing visibly moved.\n const commitViewport = (next: ViewportInfo) => {\n setViewport(prev => (viewportEqual(prev, next) ? prev : next));\n };\n\n const debouncedUpdate = () => {\n clearTimeout(timeoutId);\n timeoutId = setTimeout(() => {\n commitViewport(readViewport());\n }, debounce);\n };\n\n const immediateUpdate = () => commitViewport(readViewport());\n\n immediateUpdate();\n\n const events = ['resize', 'orientationchange'];\n\n if (isInApp) {\n events.push('focus', 'blur', 'touchstart', 'touchend');\n }\n\n events.forEach(event => {\n if (event === 'resize' || event === 'orientationchange') {\n window.addEventListener(event, debouncedUpdate);\n } else {\n window.addEventListener(event, immediateUpdate, { passive: true });\n }\n });\n\n if (window.visualViewport) {\n window.visualViewport.addEventListener('resize', immediateUpdate);\n window.visualViewport.addEventListener('scroll', immediateUpdate);\n }\n\n let intervalId: ReturnType<typeof setInterval>;\n if (isInApp) {\n let lastHeight = readViewport().height;\n intervalId = setInterval(() => {\n const currentHeight = readViewport().height;\n if (Math.abs(currentHeight - lastHeight) > 50) {\n lastHeight = currentHeight;\n immediateUpdate();\n }\n }, 500);\n }\n\n return () => {\n clearTimeout(timeoutId);\n if (intervalId) clearInterval(intervalId);\n\n events.forEach(event => {\n window.removeEventListener(\n event,\n event === 'resize' || event === 'orientationchange'\n ? debouncedUpdate\n : immediateUpdate,\n );\n });\n\n if (window.visualViewport) {\n window.visualViewport.removeEventListener('resize', immediateUpdate);\n window.visualViewport.removeEventListener('scroll', immediateUpdate);\n }\n };\n }, [readViewport, isInApp, debounce]);\n\n return viewport;\n};\n\nexport default useViewport;\n"],"mappings":";;;AAYA,MAAM,iBAAiB,GAAiB,MACtC,EAAE,UAAU,EAAE,SACd,EAAE,WAAW,EAAE,UACf,EAAE,eAAe,EAAE,cACnB,EAAE,cAAc,EAAE,aAClB,EAAE,aAAa,EAAE,YACjB,EAAE,YAAY,EAAE,WAChB,EAAE,UAAU,EAAE;AAOhB,MAAM,eAAe,UAAmB,EAAE,KAAK;CAC7C,MAAM,EAAE,UAAU,OAAO,WAAW,QAAQ;CAE5C,MAAM,CAAC,UAAU,eAAe,SAAuB;EACrD,OAAO;EACP,QAAQ;EACR,YAAY;EACZ,WAAW;EACX,UAAU;EACV,SAAS;EACT,OAAO;EACR,CAAC;CAEF,MAAM,mBAAmB,kBAAkB;EACzC,MAAM,eAAe,OAAO;EAC5B,MAAM,eAAe,OAAO,gBAAgB,UAAU;EACtD,MAAM,iBAAiB,SAAS,gBAAgB;EAChD,MAAM,aAAa,SAAS,KAAK;AAEjC,MAAI,OAAO,kBAAkB,KAAK,IAAI,eAAe,aAAa,GAAG,IACnE,QAAO;AAGT,SAAO,KAAK,IAAI,cAAc,gBAAgB,WAAW;IACxD,EAAE,CAAC;CAON,MAAM,eAAe,kBAAgC;AAMnD,SAAO;GACL,OANY,OAAO,gBAAgB,SAAS,OAAO;GAOnD,QANa,UACX,kBAAkB,GAClB,OAAO,gBAAgB,UAAU,OAAO;GAK1C,YAAY,OAAO,gBAAgB,cAAc;GACjD,WAAW,OAAO,gBAAgB,aAAa;GAC/C,UAAU,OAAO,WAAW,OAAO,eAAe;GAClD,SAAS,OAAO,WAAW,OAAO,eAAe;GACjD,OAAO,OAAO,gBAAgB,SAAS;GACxC;IACA,CAAC,SAAS,iBAAiB,CAAC;AAE/B,iBAAgB;EACd,IAAI;EAMJ,MAAM,kBAAkB,SAAuB;AAC7C,gBAAY,SAAS,cAAc,MAAM,KAAK,GAAG,OAAO,KAAM;;EAGhE,MAAM,wBAAwB;AAC5B,gBAAa,UAAU;AACvB,eAAY,iBAAiB;AAC3B,mBAAe,cAAc,CAAC;MAC7B,SAAS;;EAGd,MAAM,wBAAwB,eAAe,cAAc,CAAC;AAE5D,mBAAiB;EAEjB,MAAM,SAAS,CAAC,UAAU,oBAAoB;AAE9C,MAAI,QACF,QAAO,KAAK,SAAS,QAAQ,cAAc,WAAW;AAGxD,SAAO,SAAQ,UAAS;AACtB,OAAI,UAAU,YAAY,UAAU,oBAClC,QAAO,iBAAiB,OAAO,gBAAgB;OAE/C,QAAO,iBAAiB,OAAO,iBAAiB,EAAE,SAAS,MAAM,CAAC;IAEpE;AAEF,MAAI,OAAO,gBAAgB;AACzB,UAAO,eAAe,iBAAiB,UAAU,gBAAgB;AACjE,UAAO,eAAe,iBAAiB,UAAU,gBAAgB;;EAGnE,IAAI;AACJ,MAAI,SAAS;GACX,IAAI,aAAa,cAAc,CAAC;AAChC,gBAAa,kBAAkB;IAC7B,MAAM,gBAAgB,cAAc,CAAC;AACrC,QAAI,KAAK,IAAI,gBAAgB,WAAW,GAAG,IAAI;AAC7C,kBAAa;AACb,sBAAiB;;MAElB,IAAI;;AAGT,eAAa;AACX,gBAAa,UAAU;AACvB,OAAI,WAAY,eAAc,WAAW;AAEzC,UAAO,SAAQ,UAAS;AACtB,WAAO,oBACL,OACA,UAAU,YAAY,UAAU,sBAC5B,kBACA,gBACL;KACD;AAEF,OAAI,OAAO,gBAAgB;AACzB,WAAO,eAAe,oBAAoB,UAAU,gBAAgB;AACpE,WAAO,eAAe,oBAAoB,UAAU,gBAAgB;;;IAGvE;EAAC;EAAc;EAAS;EAAS,CAAC;AAErC,QAAO"}
|