@publikit/hooks 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +64 -0
- package/dist/index.cjs +421 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +74 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +412 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Pirimera
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @publikit/hooks
|
|
2
|
+
|
|
3
|
+
Generic, reusable React hooks with no UI component dependencies.
|
|
4
|
+
|
|
5
|
+
- **Peer dep**: React 18/19 only — no `react-dom`, Radix, Tailwind, or shadcn
|
|
6
|
+
- **Tree-shakeable** — `sideEffects: false`
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @publikit/hooks
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### Peer dependency
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install react
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Hooks
|
|
21
|
+
|
|
22
|
+
| Hook | Description |
|
|
23
|
+
|------|-------------|
|
|
24
|
+
| `useIsMobile` | Responsive breakpoint detection (768px) |
|
|
25
|
+
| `useInfiniteScroll` | IntersectionObserver-based infinite scroll |
|
|
26
|
+
| `usePullToRefresh` | Touch pull-to-refresh gesture |
|
|
27
|
+
| `useLiveTimestamp` | Adaptive live relative timestamps |
|
|
28
|
+
| `useScrollableContainer` | Scroll position persistence + scroll buttons |
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import {
|
|
34
|
+
useIsMobile,
|
|
35
|
+
useInfiniteScroll,
|
|
36
|
+
usePullToRefresh,
|
|
37
|
+
useLiveTimestamp,
|
|
38
|
+
} from '@publikit/hooks';
|
|
39
|
+
|
|
40
|
+
function FeedList({ loadMore, hasMore, isLoading }) {
|
|
41
|
+
const isMobile = useIsMobile();
|
|
42
|
+
const { Sentinel } = useInfiniteScroll({
|
|
43
|
+
onLoadMore: loadMore,
|
|
44
|
+
hasNextPage: hasMore,
|
|
45
|
+
isFetchingNextPage: isLoading,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div>
|
|
50
|
+
{items.map(renderItem)}
|
|
51
|
+
<Sentinel />
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Related packages
|
|
58
|
+
|
|
59
|
+
- [`@publikit/utils`](../utils) — framework-agnostic utilities
|
|
60
|
+
- [`@publikit/base`](../base) — UI components (re-exports `useIsMobile`)
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
var utils = require('@publikit/utils');
|
|
6
|
+
|
|
7
|
+
// use-mobile.tsx
|
|
8
|
+
var MOBILE_BREAKPOINT = 768;
|
|
9
|
+
var MOBILE_MEDIA_QUERY = `(max-width: ${MOBILE_BREAKPOINT - 1}px)`;
|
|
10
|
+
function getIsMobile() {
|
|
11
|
+
if (typeof window === "undefined") return false;
|
|
12
|
+
return window.matchMedia(MOBILE_MEDIA_QUERY).matches;
|
|
13
|
+
}
|
|
14
|
+
function useIsMobile() {
|
|
15
|
+
const [isMobile, setIsMobile] = react.useState(getIsMobile);
|
|
16
|
+
react.useEffect(() => {
|
|
17
|
+
const mql = window.matchMedia(MOBILE_MEDIA_QUERY);
|
|
18
|
+
const onChange = () => setIsMobile(mql.matches);
|
|
19
|
+
mql.addEventListener("change", onChange);
|
|
20
|
+
return () => mql.removeEventListener("change", onChange);
|
|
21
|
+
}, []);
|
|
22
|
+
return isMobile;
|
|
23
|
+
}
|
|
24
|
+
function useInfiniteScroll({
|
|
25
|
+
onLoadMore,
|
|
26
|
+
hasNextPage,
|
|
27
|
+
isFetchingNextPage,
|
|
28
|
+
rootMargin = "200px",
|
|
29
|
+
threshold = 0,
|
|
30
|
+
enabled = true,
|
|
31
|
+
root
|
|
32
|
+
}) {
|
|
33
|
+
const observerRef = react.useRef(null);
|
|
34
|
+
const [sentinelRef, setSentinelRef] = react.useState(null);
|
|
35
|
+
const handleIntersection = react.useCallback(
|
|
36
|
+
(entries) => {
|
|
37
|
+
const entry = entries[0];
|
|
38
|
+
if (!entry?.isIntersecting) return;
|
|
39
|
+
if (hasNextPage && !isFetchingNextPage && enabled) {
|
|
40
|
+
onLoadMore();
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
[onLoadMore, hasNextPage, isFetchingNextPage, enabled]
|
|
44
|
+
);
|
|
45
|
+
react.useEffect(() => {
|
|
46
|
+
if (!enabled) return;
|
|
47
|
+
if (observerRef.current) {
|
|
48
|
+
observerRef.current.disconnect();
|
|
49
|
+
}
|
|
50
|
+
observerRef.current = new IntersectionObserver(handleIntersection, {
|
|
51
|
+
root: root || null,
|
|
52
|
+
rootMargin,
|
|
53
|
+
threshold
|
|
54
|
+
});
|
|
55
|
+
if (sentinelRef) {
|
|
56
|
+
observerRef.current.observe(sentinelRef);
|
|
57
|
+
}
|
|
58
|
+
return () => {
|
|
59
|
+
if (observerRef.current) {
|
|
60
|
+
observerRef.current.disconnect();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}, [sentinelRef, handleIntersection, rootMargin, threshold, enabled, root]);
|
|
64
|
+
const Sentinel = react.useCallback(
|
|
65
|
+
({ className }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
66
|
+
"div",
|
|
67
|
+
{
|
|
68
|
+
ref: setSentinelRef,
|
|
69
|
+
className,
|
|
70
|
+
"aria-hidden": "true",
|
|
71
|
+
style: { height: "1px" }
|
|
72
|
+
}
|
|
73
|
+
),
|
|
74
|
+
[]
|
|
75
|
+
);
|
|
76
|
+
return {
|
|
77
|
+
Sentinel,
|
|
78
|
+
sentinelRef: setSentinelRef
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function usePullToRefresh({
|
|
82
|
+
onRefresh,
|
|
83
|
+
threshold = 80,
|
|
84
|
+
resistance = 2.5,
|
|
85
|
+
disabled = false
|
|
86
|
+
}) {
|
|
87
|
+
const [pullDistance, setPullDistance] = react.useState(0);
|
|
88
|
+
const [isRefreshing, setIsRefreshing] = react.useState(false);
|
|
89
|
+
const [isPulling, setIsPulling] = react.useState(false);
|
|
90
|
+
const [containerElement, setContainerElement] = react.useState(null);
|
|
91
|
+
const containerRef = react.useRef(null);
|
|
92
|
+
const startYRef = react.useRef(null);
|
|
93
|
+
const currentYRef = react.useRef(null);
|
|
94
|
+
const pullDistanceRef = react.useRef(0);
|
|
95
|
+
pullDistanceRef.current = pullDistance;
|
|
96
|
+
const assignContainerRef = react.useCallback((node) => {
|
|
97
|
+
containerRef.current = node;
|
|
98
|
+
setContainerElement(node);
|
|
99
|
+
}, []);
|
|
100
|
+
const handleTouchStart = react.useCallback(
|
|
101
|
+
(e) => {
|
|
102
|
+
if (disabled || isRefreshing) return;
|
|
103
|
+
const container = containerRef.current;
|
|
104
|
+
if (!container) return;
|
|
105
|
+
if (container.scrollTop <= 0) {
|
|
106
|
+
startYRef.current = e.touches[0].clientY;
|
|
107
|
+
setIsPulling(true);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
[disabled, isRefreshing]
|
|
111
|
+
);
|
|
112
|
+
const handleTouchMove = react.useCallback(
|
|
113
|
+
(e) => {
|
|
114
|
+
if (disabled || isRefreshing || startYRef.current === null) return;
|
|
115
|
+
const container = containerRef.current;
|
|
116
|
+
if (!container) return;
|
|
117
|
+
if (container.scrollTop > 0) {
|
|
118
|
+
startYRef.current = null;
|
|
119
|
+
setPullDistance(0);
|
|
120
|
+
setIsPulling(false);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
currentYRef.current = e.touches[0].clientY;
|
|
124
|
+
const diff = currentYRef.current - startYRef.current;
|
|
125
|
+
if (diff > 0) {
|
|
126
|
+
const distance = Math.min(diff / resistance, threshold * 1.5);
|
|
127
|
+
setPullDistance(distance);
|
|
128
|
+
if (distance > 5) {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
[disabled, isRefreshing, resistance, threshold]
|
|
134
|
+
);
|
|
135
|
+
const handleTouchEnd = react.useCallback(async () => {
|
|
136
|
+
if (disabled || isRefreshing) return;
|
|
137
|
+
startYRef.current = null;
|
|
138
|
+
currentYRef.current = null;
|
|
139
|
+
setIsPulling(false);
|
|
140
|
+
const distance = pullDistanceRef.current;
|
|
141
|
+
if (distance >= threshold) {
|
|
142
|
+
setIsRefreshing(true);
|
|
143
|
+
setPullDistance(threshold * 0.5);
|
|
144
|
+
try {
|
|
145
|
+
await onRefresh();
|
|
146
|
+
} finally {
|
|
147
|
+
setIsRefreshing(false);
|
|
148
|
+
setPullDistance(0);
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
setPullDistance(0);
|
|
152
|
+
}
|
|
153
|
+
}, [disabled, isRefreshing, threshold, onRefresh]);
|
|
154
|
+
react.useEffect(() => {
|
|
155
|
+
const container = containerElement;
|
|
156
|
+
if (!container) return;
|
|
157
|
+
container.addEventListener("touchstart", handleTouchStart, { passive: true });
|
|
158
|
+
container.addEventListener("touchmove", handleTouchMove, { passive: false });
|
|
159
|
+
container.addEventListener("touchend", handleTouchEnd, { passive: true });
|
|
160
|
+
return () => {
|
|
161
|
+
container.removeEventListener("touchstart", handleTouchStart);
|
|
162
|
+
container.removeEventListener("touchmove", handleTouchMove);
|
|
163
|
+
container.removeEventListener("touchend", handleTouchEnd);
|
|
164
|
+
};
|
|
165
|
+
}, [containerElement, handleTouchStart, handleTouchMove, handleTouchEnd]);
|
|
166
|
+
const pullProgress = Math.min(pullDistance / threshold, 1);
|
|
167
|
+
return {
|
|
168
|
+
pullDistance,
|
|
169
|
+
isRefreshing,
|
|
170
|
+
isPulling,
|
|
171
|
+
containerRef: assignContainerRef,
|
|
172
|
+
pullProgress
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
var globalNow = /* @__PURE__ */ new Date();
|
|
176
|
+
var subscribers = /* @__PURE__ */ new Map();
|
|
177
|
+
var visibilityMap = /* @__PURE__ */ new Map();
|
|
178
|
+
var rafId = null;
|
|
179
|
+
var intervalId = null;
|
|
180
|
+
var isTabVisible = true;
|
|
181
|
+
var pendingUpdate = false;
|
|
182
|
+
function getUpdateInterval(date) {
|
|
183
|
+
const ageMs = Date.now() - date.getTime();
|
|
184
|
+
const ageMinutes = ageMs / 6e4;
|
|
185
|
+
if (ageMinutes < 1) return 1e4;
|
|
186
|
+
if (ageMinutes < 60) return 3e4;
|
|
187
|
+
if (ageMinutes < 1440) return 3e5;
|
|
188
|
+
return 36e5;
|
|
189
|
+
}
|
|
190
|
+
function getMinInterval() {
|
|
191
|
+
if (subscribers.size === 0) return 3e4;
|
|
192
|
+
const intervals = Array.from(subscribers.values()).map((sub) => getUpdateInterval(sub.date));
|
|
193
|
+
return Math.max(Math.min(...intervals), 1e4);
|
|
194
|
+
}
|
|
195
|
+
var createObserver = (subscriberId, callback) => {
|
|
196
|
+
return new IntersectionObserver(
|
|
197
|
+
(entries) => {
|
|
198
|
+
entries.forEach((entry) => {
|
|
199
|
+
visibilityMap.set(subscriberId, entry.isIntersecting);
|
|
200
|
+
if (entry.isIntersecting) {
|
|
201
|
+
callback();
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
root: null,
|
|
207
|
+
rootMargin: "50px",
|
|
208
|
+
threshold: 0.01
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
};
|
|
212
|
+
function updateVisibleSubscribers() {
|
|
213
|
+
if (!isTabVisible || subscribers.size === 0) {
|
|
214
|
+
pendingUpdate = false;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
globalNow = /* @__PURE__ */ new Date();
|
|
218
|
+
if (!pendingUpdate) {
|
|
219
|
+
pendingUpdate = true;
|
|
220
|
+
rafId = requestAnimationFrame(() => {
|
|
221
|
+
subscribers.forEach((subscriber, subscriberId) => {
|
|
222
|
+
const isVisible = visibilityMap.get(subscriberId) ?? true;
|
|
223
|
+
if (isVisible) {
|
|
224
|
+
subscriber.callback();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
pendingUpdate = false;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function stopTimer() {
|
|
232
|
+
if (intervalId) {
|
|
233
|
+
clearTimeout(intervalId);
|
|
234
|
+
intervalId = null;
|
|
235
|
+
}
|
|
236
|
+
if (rafId) {
|
|
237
|
+
cancelAnimationFrame(rafId);
|
|
238
|
+
rafId = null;
|
|
239
|
+
}
|
|
240
|
+
pendingUpdate = false;
|
|
241
|
+
}
|
|
242
|
+
function scheduleNextTick() {
|
|
243
|
+
if (intervalId) {
|
|
244
|
+
clearTimeout(intervalId);
|
|
245
|
+
intervalId = null;
|
|
246
|
+
}
|
|
247
|
+
if (subscribers.size === 0 || !isTabVisible) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
intervalId = setTimeout(() => {
|
|
251
|
+
updateVisibleSubscribers();
|
|
252
|
+
scheduleNextTick();
|
|
253
|
+
}, getMinInterval());
|
|
254
|
+
}
|
|
255
|
+
function ensureTimerRunning() {
|
|
256
|
+
if (!intervalId && subscribers.size > 0 && isTabVisible) {
|
|
257
|
+
scheduleNextTick();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function restartTimer() {
|
|
261
|
+
stopTimer();
|
|
262
|
+
ensureTimerRunning();
|
|
263
|
+
}
|
|
264
|
+
if (typeof window !== "undefined") {
|
|
265
|
+
document.addEventListener("visibilitychange", () => {
|
|
266
|
+
isTabVisible = !document.hidden;
|
|
267
|
+
if (isTabVisible && subscribers.size > 0) {
|
|
268
|
+
updateVisibleSubscribers();
|
|
269
|
+
ensureTimerRunning();
|
|
270
|
+
} else if (!isTabVisible) {
|
|
271
|
+
stopTimer();
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
function useLiveTimestamp(date, elementRef) {
|
|
276
|
+
const [, setTick] = react.useState(0);
|
|
277
|
+
const subscriberIdRef = react.useRef(/* @__PURE__ */ Symbol("live-timestamp-subscriber"));
|
|
278
|
+
react.useEffect(() => {
|
|
279
|
+
const subscriberId = subscriberIdRef.current;
|
|
280
|
+
const callback = () => {
|
|
281
|
+
const isVisible = visibilityMap.get(subscriberId) ?? true;
|
|
282
|
+
if (isVisible) {
|
|
283
|
+
setTick((t) => t + 1);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
let observer = null;
|
|
287
|
+
if (elementRef?.current) {
|
|
288
|
+
observer = createObserver(subscriberId, callback);
|
|
289
|
+
observer.observe(elementRef.current);
|
|
290
|
+
visibilityMap.set(subscriberId, false);
|
|
291
|
+
} else {
|
|
292
|
+
visibilityMap.set(subscriberId, true);
|
|
293
|
+
}
|
|
294
|
+
subscribers.set(subscriberId, {
|
|
295
|
+
callback,
|
|
296
|
+
date,
|
|
297
|
+
observer
|
|
298
|
+
});
|
|
299
|
+
ensureTimerRunning();
|
|
300
|
+
return () => {
|
|
301
|
+
const subscriber = subscribers.get(subscriberId);
|
|
302
|
+
if (subscriber?.observer) {
|
|
303
|
+
subscriber.observer.disconnect();
|
|
304
|
+
}
|
|
305
|
+
subscribers.delete(subscriberId);
|
|
306
|
+
visibilityMap.delete(subscriberId);
|
|
307
|
+
if (subscribers.size === 0) {
|
|
308
|
+
stopTimer();
|
|
309
|
+
} else {
|
|
310
|
+
restartTimer();
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}, [date.getTime(), elementRef]);
|
|
314
|
+
return globalNow;
|
|
315
|
+
}
|
|
316
|
+
var SCROLL_AMOUNT = 80;
|
|
317
|
+
var SCROLL_CHECK_DELAY = 100;
|
|
318
|
+
var DEFAULT_SCROLL_POSITION_KEY = "scrollable-container-position";
|
|
319
|
+
var STORAGE_DEBOUNCE_MS = 150;
|
|
320
|
+
function useScrollableContainer(scrollPositionKey = DEFAULT_SCROLL_POSITION_KEY) {
|
|
321
|
+
const containerRef = react.useRef(null);
|
|
322
|
+
const [containerElement, setContainerElement] = react.useState(null);
|
|
323
|
+
const isRestoringRef = react.useRef(false);
|
|
324
|
+
const [canScrollUp, setCanScrollUp] = react.useState(false);
|
|
325
|
+
const [canScrollDown, setCanScrollDown] = react.useState(false);
|
|
326
|
+
const assignContainerRef = react.useCallback((node) => {
|
|
327
|
+
containerRef.current = node;
|
|
328
|
+
setContainerElement(node);
|
|
329
|
+
}, []);
|
|
330
|
+
const saveToStorage = react.useMemo(
|
|
331
|
+
() => utils.debounce((position) => {
|
|
332
|
+
try {
|
|
333
|
+
sessionStorage.setItem(scrollPositionKey, String(position));
|
|
334
|
+
} catch {
|
|
335
|
+
}
|
|
336
|
+
}, STORAGE_DEBOUNCE_MS),
|
|
337
|
+
[scrollPositionKey]
|
|
338
|
+
);
|
|
339
|
+
const checkScrollability = react.useCallback(() => {
|
|
340
|
+
if (!containerRef.current || isRestoringRef.current) return;
|
|
341
|
+
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
|
|
342
|
+
saveToStorage(scrollTop);
|
|
343
|
+
setCanScrollUp(scrollTop > 0);
|
|
344
|
+
setCanScrollDown(scrollTop < scrollHeight - clientHeight - 1);
|
|
345
|
+
}, [saveToStorage]);
|
|
346
|
+
react.useLayoutEffect(() => {
|
|
347
|
+
if (!containerElement || isRestoringRef.current) return;
|
|
348
|
+
try {
|
|
349
|
+
const savedPosition = sessionStorage.getItem(scrollPositionKey);
|
|
350
|
+
if (savedPosition !== null) {
|
|
351
|
+
const position = parseInt(savedPosition, 10);
|
|
352
|
+
const currentPosition = containerElement.scrollTop;
|
|
353
|
+
if (position > 0 && !isNaN(position) && Math.abs(currentPosition - position) > 1) {
|
|
354
|
+
isRestoringRef.current = true;
|
|
355
|
+
containerElement.scrollTop = position;
|
|
356
|
+
requestAnimationFrame(() => {
|
|
357
|
+
isRestoringRef.current = false;
|
|
358
|
+
checkScrollability();
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
} catch {
|
|
363
|
+
}
|
|
364
|
+
}, [containerElement, scrollPositionKey, checkScrollability]);
|
|
365
|
+
const preserveScrollPosition = react.useCallback(() => {
|
|
366
|
+
if (containerRef.current) {
|
|
367
|
+
try {
|
|
368
|
+
sessionStorage.setItem(scrollPositionKey, String(containerRef.current.scrollTop));
|
|
369
|
+
} catch {
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}, [scrollPositionKey]);
|
|
373
|
+
react.useEffect(() => {
|
|
374
|
+
return () => {
|
|
375
|
+
saveToStorage.cancel();
|
|
376
|
+
};
|
|
377
|
+
}, [saveToStorage]);
|
|
378
|
+
react.useEffect(() => {
|
|
379
|
+
if (!containerElement) return;
|
|
380
|
+
const timeoutId = setTimeout(checkScrollability, SCROLL_CHECK_DELAY);
|
|
381
|
+
const handleScroll = () => {
|
|
382
|
+
checkScrollability();
|
|
383
|
+
};
|
|
384
|
+
containerElement.addEventListener("scroll", handleScroll, { passive: true });
|
|
385
|
+
window.addEventListener("resize", checkScrollability);
|
|
386
|
+
const resizeObserver = new ResizeObserver(checkScrollability);
|
|
387
|
+
resizeObserver.observe(containerElement);
|
|
388
|
+
return () => {
|
|
389
|
+
clearTimeout(timeoutId);
|
|
390
|
+
containerElement.removeEventListener("scroll", handleScroll);
|
|
391
|
+
window.removeEventListener("resize", checkScrollability);
|
|
392
|
+
resizeObserver.disconnect();
|
|
393
|
+
};
|
|
394
|
+
}, [containerElement, checkScrollability]);
|
|
395
|
+
const scrollUp = react.useCallback(() => {
|
|
396
|
+
containerRef.current?.scrollBy({ top: -SCROLL_AMOUNT, behavior: "smooth" });
|
|
397
|
+
}, []);
|
|
398
|
+
const scrollDown = react.useCallback(() => {
|
|
399
|
+
containerRef.current?.scrollBy({ top: SCROLL_AMOUNT, behavior: "smooth" });
|
|
400
|
+
}, []);
|
|
401
|
+
return {
|
|
402
|
+
containerRef: assignContainerRef,
|
|
403
|
+
canScrollUp,
|
|
404
|
+
canScrollDown,
|
|
405
|
+
scrollUp,
|
|
406
|
+
scrollDown,
|
|
407
|
+
checkScrollability,
|
|
408
|
+
preserveScrollPosition
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
exports.DEFAULT_SCROLL_POSITION_KEY = DEFAULT_SCROLL_POSITION_KEY;
|
|
413
|
+
exports.SCROLL_AMOUNT = SCROLL_AMOUNT;
|
|
414
|
+
exports.SCROLL_CHECK_DELAY = SCROLL_CHECK_DELAY;
|
|
415
|
+
exports.useInfiniteScroll = useInfiniteScroll;
|
|
416
|
+
exports.useIsMobile = useIsMobile;
|
|
417
|
+
exports.useLiveTimestamp = useLiveTimestamp;
|
|
418
|
+
exports.usePullToRefresh = usePullToRefresh;
|
|
419
|
+
exports.useScrollableContainer = useScrollableContainer;
|
|
420
|
+
//# sourceMappingURL=index.cjs.map
|
|
421
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../use-mobile.tsx","../use-infinite-scroll.tsx","../use-pull-to-refresh.ts","../use-live-timestamp.ts","../use-scrollable-container.ts"],"names":["useState","useEffect","useRef","useCallback","jsx","useMemo","debounce","useLayoutEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["import { useEffect, useState } from 'react';\r\n\r\nconst MOBILE_BREAKPOINT = 768;\r\nconst MOBILE_MEDIA_QUERY = `(max-width: ${MOBILE_BREAKPOINT - 1}px)` as const;\r\n\r\nfunction getIsMobile(): boolean {\r\n if (typeof window === 'undefined') return false;\r\n return window.matchMedia(MOBILE_MEDIA_QUERY).matches;\r\n}\r\n\r\nexport function useIsMobile(): boolean {\r\n const [isMobile, setIsMobile] = useState<boolean>(getIsMobile);\r\n\r\n useEffect(() => {\r\n const mql = window.matchMedia(MOBILE_MEDIA_QUERY);\r\n const onChange = () => setIsMobile(mql.matches);\r\n mql.addEventListener('change', onChange);\r\n return () => mql.removeEventListener('change', onChange);\r\n }, []);\r\n\r\n return isMobile;\r\n}\r\n","import { useEffect, useRef, useCallback, useState } from 'react';\r\n\r\nexport interface UseInfiniteScrollOptions {\r\n /** Callback when intersection occurs */\r\n onLoadMore: () => void;\r\n /** Whether more data can be loaded */\r\n hasNextPage: boolean;\r\n /** Whether currently fetching */\r\n isFetchingNextPage: boolean;\r\n /** Root margin for intersection observer */\r\n rootMargin?: string;\r\n /** Threshold for intersection */\r\n threshold?: number;\r\n /** Enable/disable the hook */\r\n enabled?: boolean;\r\n /** Custom root element for IntersectionObserver (e.g., ScrollArea viewport) */\r\n root?: HTMLElement | null;\r\n}\r\n\r\n/**\r\n * Hook for implementing infinite scroll using IntersectionObserver.\r\n */\r\nexport function useInfiniteScroll({\r\n onLoadMore,\r\n hasNextPage,\r\n isFetchingNextPage,\r\n rootMargin = '200px',\r\n threshold = 0,\r\n enabled = true,\r\n root,\r\n}: UseInfiniteScrollOptions) {\r\n const observerRef = useRef<IntersectionObserver | null>(null);\r\n const [sentinelRef, setSentinelRef] = useState<HTMLDivElement | null>(null);\r\n\r\n const handleIntersection = useCallback(\r\n (entries: IntersectionObserverEntry[]) => {\r\n const entry = entries[0];\r\n if (!entry?.isIntersecting) return;\r\n if (hasNextPage && !isFetchingNextPage && enabled) {\r\n onLoadMore();\r\n }\r\n },\r\n [onLoadMore, hasNextPage, isFetchingNextPage, enabled]\r\n );\r\n\r\n useEffect(() => {\r\n if (!enabled) return;\r\n\r\n if (observerRef.current) {\r\n observerRef.current.disconnect();\r\n }\r\n\r\n observerRef.current = new IntersectionObserver(handleIntersection, {\r\n root: root || null,\r\n rootMargin,\r\n threshold,\r\n });\r\n\r\n if (sentinelRef) {\r\n observerRef.current.observe(sentinelRef);\r\n }\r\n\r\n return () => {\r\n if (observerRef.current) {\r\n observerRef.current.disconnect();\r\n }\r\n };\r\n }, [sentinelRef, handleIntersection, rootMargin, threshold, enabled, root]);\r\n\r\n const Sentinel = useCallback(\r\n ({ className }: { className?: string }) => (\r\n <div\r\n ref={setSentinelRef}\r\n className={className}\r\n aria-hidden=\"true\"\r\n style={{ height: '1px' }}\r\n />\r\n ),\r\n []\r\n );\r\n\r\n return {\r\n Sentinel,\r\n sentinelRef: setSentinelRef,\r\n };\r\n}\r\n","import { useState, useEffect, useRef, useCallback, type Ref } from 'react';\r\n\r\nexport interface UsePullToRefreshOptions {\r\n onRefresh: () => Promise<void>;\r\n threshold?: number;\r\n resistance?: number;\r\n disabled?: boolean;\r\n}\r\n\r\nexport interface UsePullToRefreshReturn {\r\n pullDistance: number;\r\n isRefreshing: boolean;\r\n isPulling: boolean;\r\n /** Callback ref — attach to the scrollable container element */\r\n containerRef: Ref<HTMLDivElement | null>;\r\n pullProgress: number;\r\n}\r\n\r\n/**\r\n * Hook to add pull-to-refresh gesture for mobile devices.\r\n */\r\nexport function usePullToRefresh({\r\n onRefresh,\r\n threshold = 80,\r\n resistance = 2.5,\r\n disabled = false,\r\n}: UsePullToRefreshOptions): UsePullToRefreshReturn {\r\n const [pullDistance, setPullDistance] = useState(0);\r\n const [isRefreshing, setIsRefreshing] = useState(false);\r\n const [isPulling, setIsPulling] = useState(false);\r\n const [containerElement, setContainerElement] = useState<HTMLDivElement | null>(null);\r\n\r\n const containerRef = useRef<HTMLDivElement | null>(null);\r\n const startYRef = useRef<number | null>(null);\r\n const currentYRef = useRef<number | null>(null);\r\n const pullDistanceRef = useRef(0);\r\n\r\n pullDistanceRef.current = pullDistance;\r\n\r\n const assignContainerRef = useCallback((node: HTMLDivElement | null) => {\r\n containerRef.current = node;\r\n setContainerElement(node);\r\n }, []);\r\n\r\n const handleTouchStart = useCallback(\r\n (e: TouchEvent) => {\r\n if (disabled || isRefreshing) return;\r\n\r\n const container = containerRef.current;\r\n if (!container) return;\r\n\r\n if (container.scrollTop <= 0) {\r\n startYRef.current = e.touches[0].clientY;\r\n setIsPulling(true);\r\n }\r\n },\r\n [disabled, isRefreshing]\r\n );\r\n\r\n const handleTouchMove = useCallback(\r\n (e: TouchEvent) => {\r\n if (disabled || isRefreshing || startYRef.current === null) return;\r\n\r\n const container = containerRef.current;\r\n if (!container) return;\r\n\r\n if (container.scrollTop > 0) {\r\n startYRef.current = null;\r\n setPullDistance(0);\r\n setIsPulling(false);\r\n return;\r\n }\r\n\r\n currentYRef.current = e.touches[0].clientY;\r\n const diff = currentYRef.current - startYRef.current;\r\n\r\n if (diff > 0) {\r\n const distance = Math.min(diff / resistance, threshold * 1.5);\r\n setPullDistance(distance);\r\n\r\n if (distance > 5) {\r\n e.preventDefault();\r\n }\r\n }\r\n },\r\n [disabled, isRefreshing, resistance, threshold]\r\n );\r\n\r\n const handleTouchEnd = useCallback(async () => {\r\n if (disabled || isRefreshing) return;\r\n\r\n startYRef.current = null;\r\n currentYRef.current = null;\r\n setIsPulling(false);\r\n\r\n const distance = pullDistanceRef.current;\r\n\r\n if (distance >= threshold) {\r\n setIsRefreshing(true);\r\n setPullDistance(threshold * 0.5);\r\n\r\n try {\r\n await onRefresh();\r\n } finally {\r\n setIsRefreshing(false);\r\n setPullDistance(0);\r\n }\r\n } else {\r\n setPullDistance(0);\r\n }\r\n }, [disabled, isRefreshing, threshold, onRefresh]);\r\n\r\n useEffect(() => {\r\n const container = containerElement;\r\n if (!container) return;\r\n\r\n container.addEventListener('touchstart', handleTouchStart, { passive: true });\r\n container.addEventListener('touchmove', handleTouchMove, { passive: false });\r\n container.addEventListener('touchend', handleTouchEnd, { passive: true });\r\n\r\n return () => {\r\n container.removeEventListener('touchstart', handleTouchStart);\r\n container.removeEventListener('touchmove', handleTouchMove);\r\n container.removeEventListener('touchend', handleTouchEnd);\r\n };\r\n }, [containerElement, handleTouchStart, handleTouchMove, handleTouchEnd]);\r\n\r\n const pullProgress = Math.min(pullDistance / threshold, 1);\r\n\r\n return {\r\n pullDistance,\r\n isRefreshing,\r\n isPulling,\r\n containerRef: assignContainerRef,\r\n pullProgress,\r\n };\r\n}\r\n","import { useState, useEffect, useRef, type RefObject } from 'react';\r\n\r\nlet globalNow = new Date();\r\n\r\ntype SubscriberId = symbol;\r\n\r\ntype Subscriber = {\r\n callback: () => void;\r\n date: Date;\r\n observer: IntersectionObserver | null;\r\n};\r\n\r\nconst subscribers = new Map<SubscriberId, Subscriber>();\r\nconst visibilityMap = new Map<SubscriberId, boolean>();\r\n\r\nlet rafId: number | null = null;\r\nlet intervalId: ReturnType<typeof setTimeout> | null = null;\r\nlet isTabVisible = true;\r\nlet pendingUpdate = false;\r\n\r\nfunction getUpdateInterval(date: Date): number {\r\n const ageMs = Date.now() - date.getTime();\r\n const ageMinutes = ageMs / 60000;\r\n\r\n if (ageMinutes < 1) return 10000;\r\n if (ageMinutes < 60) return 30000;\r\n if (ageMinutes < 1440) return 300000;\r\n return 3600000;\r\n}\r\n\r\nfunction getMinInterval(): number {\r\n if (subscribers.size === 0) return 30000;\r\n const intervals = Array.from(subscribers.values()).map((sub) => getUpdateInterval(sub.date));\r\n return Math.max(Math.min(...intervals), 10000);\r\n}\r\n\r\nconst createObserver = (\r\n subscriberId: SubscriberId,\r\n callback: () => void\r\n): IntersectionObserver => {\r\n return new IntersectionObserver(\r\n (entries) => {\r\n entries.forEach((entry) => {\r\n visibilityMap.set(subscriberId, entry.isIntersecting);\r\n if (entry.isIntersecting) {\r\n callback();\r\n }\r\n });\r\n },\r\n {\r\n root: null,\r\n rootMargin: '50px',\r\n threshold: 0.01,\r\n }\r\n );\r\n};\r\n\r\nfunction updateVisibleSubscribers() {\r\n if (!isTabVisible || subscribers.size === 0) {\r\n pendingUpdate = false;\r\n return;\r\n }\r\n\r\n globalNow = new Date();\r\n\r\n if (!pendingUpdate) {\r\n pendingUpdate = true;\r\n rafId = requestAnimationFrame(() => {\r\n subscribers.forEach((subscriber, subscriberId) => {\r\n const isVisible = visibilityMap.get(subscriberId) ?? true;\r\n if (isVisible) {\r\n subscriber.callback();\r\n }\r\n });\r\n pendingUpdate = false;\r\n });\r\n }\r\n}\r\n\r\nfunction stopTimer() {\r\n if (intervalId) {\r\n clearTimeout(intervalId);\r\n intervalId = null;\r\n }\r\n if (rafId) {\r\n cancelAnimationFrame(rafId);\r\n rafId = null;\r\n }\r\n pendingUpdate = false;\r\n}\r\n\r\nfunction scheduleNextTick() {\r\n if (intervalId) {\r\n clearTimeout(intervalId);\r\n intervalId = null;\r\n }\r\n\r\n if (subscribers.size === 0 || !isTabVisible) {\r\n return;\r\n }\r\n\r\n intervalId = setTimeout(() => {\r\n updateVisibleSubscribers();\r\n scheduleNextTick();\r\n }, getMinInterval());\r\n}\r\n\r\nfunction ensureTimerRunning() {\r\n if (!intervalId && subscribers.size > 0 && isTabVisible) {\r\n scheduleNextTick();\r\n }\r\n}\r\n\r\nfunction restartTimer() {\r\n stopTimer();\r\n ensureTimerRunning();\r\n}\r\n\r\nif (typeof window !== 'undefined') {\r\n document.addEventListener('visibilitychange', () => {\r\n isTabVisible = !document.hidden;\r\n if (isTabVisible && subscribers.size > 0) {\r\n updateVisibleSubscribers();\r\n ensureTimerRunning();\r\n } else if (!isTabVisible) {\r\n stopTimer();\r\n }\r\n });\r\n}\r\n\r\n/**\r\n * Live-updating timestamp hook with visibility-aware adaptive intervals.\r\n */\r\nexport function useLiveTimestamp(date: Date, elementRef?: RefObject<Element>): Date {\r\n const [, setTick] = useState(0);\r\n const subscriberIdRef = useRef<SubscriberId>(Symbol('live-timestamp-subscriber'));\r\n\r\n useEffect(() => {\r\n const subscriberId = subscriberIdRef.current;\r\n\r\n const callback = () => {\r\n const isVisible = visibilityMap.get(subscriberId) ?? true;\r\n if (isVisible) {\r\n setTick((t) => t + 1);\r\n }\r\n };\r\n\r\n let observer: IntersectionObserver | null = null;\r\n if (elementRef?.current) {\r\n observer = createObserver(subscriberId, callback);\r\n observer.observe(elementRef.current);\r\n visibilityMap.set(subscriberId, false);\r\n } else {\r\n visibilityMap.set(subscriberId, true);\r\n }\r\n\r\n subscribers.set(subscriberId, {\r\n callback,\r\n date,\r\n observer,\r\n });\r\n\r\n ensureTimerRunning();\r\n\r\n return () => {\r\n const subscriber = subscribers.get(subscriberId);\r\n if (subscriber?.observer) {\r\n subscriber.observer.disconnect();\r\n }\r\n subscribers.delete(subscriberId);\r\n visibilityMap.delete(subscriberId);\r\n\r\n if (subscribers.size === 0) {\r\n stopTimer();\r\n } else {\r\n restartTimer();\r\n }\r\n };\r\n // Key on the timestamp value, not the Date identity, so callers passing a\r\n // freshly constructed Date each render don't trigger a re-subscribe.\r\n }, [date.getTime(), elementRef]);\r\n\r\n return globalNow;\r\n}\r\n","import { useRef, useEffect, useLayoutEffect, useState, useCallback, useMemo, type Ref } from 'react';\r\nimport { debounce } from '@publikit/utils';\r\n\r\nexport const SCROLL_AMOUNT = 80;\r\nexport const SCROLL_CHECK_DELAY = 100;\r\nexport const DEFAULT_SCROLL_POSITION_KEY = 'scrollable-container-position';\r\nconst STORAGE_DEBOUNCE_MS = 150;\r\n\r\n/**\r\n * Scrollable container with position persistence and scroll button state.\r\n *\r\n * @param scrollPositionKey - sessionStorage key for scroll position\r\n */\r\nexport function useScrollableContainer(\r\n scrollPositionKey: string = DEFAULT_SCROLL_POSITION_KEY\r\n) {\r\n const containerRef = useRef<HTMLElement | null>(null);\r\n const [containerElement, setContainerElement] = useState<HTMLElement | null>(null);\r\n const isRestoringRef = useRef<boolean>(false);\r\n const [canScrollUp, setCanScrollUp] = useState(false);\r\n const [canScrollDown, setCanScrollDown] = useState(false);\r\n\r\n const assignContainerRef = useCallback((node: HTMLElement | null) => {\r\n containerRef.current = node;\r\n setContainerElement(node);\r\n }, []);\r\n\r\n const saveToStorage = useMemo(\r\n () =>\r\n debounce((position: number) => {\r\n try {\r\n sessionStorage.setItem(scrollPositionKey, String(position));\r\n } catch {\r\n // Ignore storage errors\r\n }\r\n }, STORAGE_DEBOUNCE_MS),\r\n [scrollPositionKey]\r\n );\r\n\r\n const checkScrollability = useCallback(() => {\r\n if (!containerRef.current || isRestoringRef.current) return;\r\n const { scrollTop, scrollHeight, clientHeight } = containerRef.current;\r\n\r\n saveToStorage(scrollTop);\r\n\r\n setCanScrollUp(scrollTop > 0);\r\n setCanScrollDown(scrollTop < scrollHeight - clientHeight - 1);\r\n }, [saveToStorage]);\r\n\r\n useLayoutEffect(() => {\r\n if (!containerElement || isRestoringRef.current) return;\r\n\r\n try {\r\n const savedPosition = sessionStorage.getItem(scrollPositionKey);\r\n if (savedPosition !== null) {\r\n const position = parseInt(savedPosition, 10);\r\n const currentPosition = containerElement.scrollTop;\r\n\r\n if (position > 0 && !isNaN(position) && Math.abs(currentPosition - position) > 1) {\r\n isRestoringRef.current = true;\r\n containerElement.scrollTop = position;\r\n requestAnimationFrame(() => {\r\n isRestoringRef.current = false;\r\n checkScrollability();\r\n });\r\n }\r\n }\r\n } catch {\r\n // Ignore storage errors\r\n }\r\n }, [containerElement, scrollPositionKey, checkScrollability]);\r\n\r\n const preserveScrollPosition = useCallback(() => {\r\n if (containerRef.current) {\r\n try {\r\n sessionStorage.setItem(scrollPositionKey, String(containerRef.current.scrollTop));\r\n } catch {\r\n // Ignore storage errors\r\n }\r\n }\r\n }, [scrollPositionKey]);\r\n\r\n useEffect(() => {\r\n return () => {\r\n saveToStorage.cancel();\r\n };\r\n }, [saveToStorage]);\r\n\r\n useEffect(() => {\r\n if (!containerElement) return;\r\n\r\n const timeoutId = setTimeout(checkScrollability, SCROLL_CHECK_DELAY);\r\n\r\n const handleScroll = () => {\r\n checkScrollability();\r\n };\r\n\r\n containerElement.addEventListener('scroll', handleScroll, { passive: true });\r\n window.addEventListener('resize', checkScrollability);\r\n\r\n const resizeObserver = new ResizeObserver(checkScrollability);\r\n resizeObserver.observe(containerElement);\r\n\r\n return () => {\r\n clearTimeout(timeoutId);\r\n containerElement.removeEventListener('scroll', handleScroll);\r\n window.removeEventListener('resize', checkScrollability);\r\n resizeObserver.disconnect();\r\n };\r\n }, [containerElement, checkScrollability]);\r\n\r\n const scrollUp = useCallback(() => {\r\n containerRef.current?.scrollBy({ top: -SCROLL_AMOUNT, behavior: 'smooth' });\r\n }, []);\r\n\r\n const scrollDown = useCallback(() => {\r\n containerRef.current?.scrollBy({ top: SCROLL_AMOUNT, behavior: 'smooth' });\r\n }, []);\r\n\r\n return {\r\n containerRef: assignContainerRef as Ref<HTMLElement | null>,\r\n canScrollUp,\r\n canScrollDown,\r\n scrollUp,\r\n scrollDown,\r\n checkScrollability,\r\n preserveScrollPosition,\r\n };\r\n}\r\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { Ref, RefObject } from 'react';
|
|
3
|
+
|
|
4
|
+
declare function useIsMobile(): boolean;
|
|
5
|
+
|
|
6
|
+
interface UseInfiniteScrollOptions {
|
|
7
|
+
/** Callback when intersection occurs */
|
|
8
|
+
onLoadMore: () => void;
|
|
9
|
+
/** Whether more data can be loaded */
|
|
10
|
+
hasNextPage: boolean;
|
|
11
|
+
/** Whether currently fetching */
|
|
12
|
+
isFetchingNextPage: boolean;
|
|
13
|
+
/** Root margin for intersection observer */
|
|
14
|
+
rootMargin?: string;
|
|
15
|
+
/** Threshold for intersection */
|
|
16
|
+
threshold?: number;
|
|
17
|
+
/** Enable/disable the hook */
|
|
18
|
+
enabled?: boolean;
|
|
19
|
+
/** Custom root element for IntersectionObserver (e.g., ScrollArea viewport) */
|
|
20
|
+
root?: HTMLElement | null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Hook for implementing infinite scroll using IntersectionObserver.
|
|
24
|
+
*/
|
|
25
|
+
declare function useInfiniteScroll({ onLoadMore, hasNextPage, isFetchingNextPage, rootMargin, threshold, enabled, root, }: UseInfiniteScrollOptions): {
|
|
26
|
+
Sentinel: ({ className }: {
|
|
27
|
+
className?: string;
|
|
28
|
+
}) => react.JSX.Element;
|
|
29
|
+
sentinelRef: react.Dispatch<react.SetStateAction<HTMLDivElement | null>>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
interface UsePullToRefreshOptions {
|
|
33
|
+
onRefresh: () => Promise<void>;
|
|
34
|
+
threshold?: number;
|
|
35
|
+
resistance?: number;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
}
|
|
38
|
+
interface UsePullToRefreshReturn {
|
|
39
|
+
pullDistance: number;
|
|
40
|
+
isRefreshing: boolean;
|
|
41
|
+
isPulling: boolean;
|
|
42
|
+
/** Callback ref — attach to the scrollable container element */
|
|
43
|
+
containerRef: Ref<HTMLDivElement | null>;
|
|
44
|
+
pullProgress: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Hook to add pull-to-refresh gesture for mobile devices.
|
|
48
|
+
*/
|
|
49
|
+
declare function usePullToRefresh({ onRefresh, threshold, resistance, disabled, }: UsePullToRefreshOptions): UsePullToRefreshReturn;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Live-updating timestamp hook with visibility-aware adaptive intervals.
|
|
53
|
+
*/
|
|
54
|
+
declare function useLiveTimestamp(date: Date, elementRef?: RefObject<Element>): Date;
|
|
55
|
+
|
|
56
|
+
declare const SCROLL_AMOUNT = 80;
|
|
57
|
+
declare const SCROLL_CHECK_DELAY = 100;
|
|
58
|
+
declare const DEFAULT_SCROLL_POSITION_KEY = "scrollable-container-position";
|
|
59
|
+
/**
|
|
60
|
+
* Scrollable container with position persistence and scroll button state.
|
|
61
|
+
*
|
|
62
|
+
* @param scrollPositionKey - sessionStorage key for scroll position
|
|
63
|
+
*/
|
|
64
|
+
declare function useScrollableContainer(scrollPositionKey?: string): {
|
|
65
|
+
containerRef: Ref<HTMLElement | null>;
|
|
66
|
+
canScrollUp: boolean;
|
|
67
|
+
canScrollDown: boolean;
|
|
68
|
+
scrollUp: () => void;
|
|
69
|
+
scrollDown: () => void;
|
|
70
|
+
checkScrollability: () => void;
|
|
71
|
+
preserveScrollPosition: () => void;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export { DEFAULT_SCROLL_POSITION_KEY, SCROLL_AMOUNT, SCROLL_CHECK_DELAY, type UseInfiniteScrollOptions, type UsePullToRefreshOptions, type UsePullToRefreshReturn, useInfiniteScroll, useIsMobile, useLiveTimestamp, usePullToRefresh, useScrollableContainer };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { Ref, RefObject } from 'react';
|
|
3
|
+
|
|
4
|
+
declare function useIsMobile(): boolean;
|
|
5
|
+
|
|
6
|
+
interface UseInfiniteScrollOptions {
|
|
7
|
+
/** Callback when intersection occurs */
|
|
8
|
+
onLoadMore: () => void;
|
|
9
|
+
/** Whether more data can be loaded */
|
|
10
|
+
hasNextPage: boolean;
|
|
11
|
+
/** Whether currently fetching */
|
|
12
|
+
isFetchingNextPage: boolean;
|
|
13
|
+
/** Root margin for intersection observer */
|
|
14
|
+
rootMargin?: string;
|
|
15
|
+
/** Threshold for intersection */
|
|
16
|
+
threshold?: number;
|
|
17
|
+
/** Enable/disable the hook */
|
|
18
|
+
enabled?: boolean;
|
|
19
|
+
/** Custom root element for IntersectionObserver (e.g., ScrollArea viewport) */
|
|
20
|
+
root?: HTMLElement | null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Hook for implementing infinite scroll using IntersectionObserver.
|
|
24
|
+
*/
|
|
25
|
+
declare function useInfiniteScroll({ onLoadMore, hasNextPage, isFetchingNextPage, rootMargin, threshold, enabled, root, }: UseInfiniteScrollOptions): {
|
|
26
|
+
Sentinel: ({ className }: {
|
|
27
|
+
className?: string;
|
|
28
|
+
}) => react.JSX.Element;
|
|
29
|
+
sentinelRef: react.Dispatch<react.SetStateAction<HTMLDivElement | null>>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
interface UsePullToRefreshOptions {
|
|
33
|
+
onRefresh: () => Promise<void>;
|
|
34
|
+
threshold?: number;
|
|
35
|
+
resistance?: number;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
}
|
|
38
|
+
interface UsePullToRefreshReturn {
|
|
39
|
+
pullDistance: number;
|
|
40
|
+
isRefreshing: boolean;
|
|
41
|
+
isPulling: boolean;
|
|
42
|
+
/** Callback ref — attach to the scrollable container element */
|
|
43
|
+
containerRef: Ref<HTMLDivElement | null>;
|
|
44
|
+
pullProgress: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Hook to add pull-to-refresh gesture for mobile devices.
|
|
48
|
+
*/
|
|
49
|
+
declare function usePullToRefresh({ onRefresh, threshold, resistance, disabled, }: UsePullToRefreshOptions): UsePullToRefreshReturn;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Live-updating timestamp hook with visibility-aware adaptive intervals.
|
|
53
|
+
*/
|
|
54
|
+
declare function useLiveTimestamp(date: Date, elementRef?: RefObject<Element>): Date;
|
|
55
|
+
|
|
56
|
+
declare const SCROLL_AMOUNT = 80;
|
|
57
|
+
declare const SCROLL_CHECK_DELAY = 100;
|
|
58
|
+
declare const DEFAULT_SCROLL_POSITION_KEY = "scrollable-container-position";
|
|
59
|
+
/**
|
|
60
|
+
* Scrollable container with position persistence and scroll button state.
|
|
61
|
+
*
|
|
62
|
+
* @param scrollPositionKey - sessionStorage key for scroll position
|
|
63
|
+
*/
|
|
64
|
+
declare function useScrollableContainer(scrollPositionKey?: string): {
|
|
65
|
+
containerRef: Ref<HTMLElement | null>;
|
|
66
|
+
canScrollUp: boolean;
|
|
67
|
+
canScrollDown: boolean;
|
|
68
|
+
scrollUp: () => void;
|
|
69
|
+
scrollDown: () => void;
|
|
70
|
+
checkScrollability: () => void;
|
|
71
|
+
preserveScrollPosition: () => void;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export { DEFAULT_SCROLL_POSITION_KEY, SCROLL_AMOUNT, SCROLL_CHECK_DELAY, type UseInfiniteScrollOptions, type UsePullToRefreshOptions, type UsePullToRefreshReturn, useInfiniteScroll, useIsMobile, useLiveTimestamp, usePullToRefresh, useScrollableContainer };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useCallback, useMemo, useLayoutEffect } from 'react';
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { debounce } from '@publikit/utils';
|
|
4
|
+
|
|
5
|
+
// use-mobile.tsx
|
|
6
|
+
var MOBILE_BREAKPOINT = 768;
|
|
7
|
+
var MOBILE_MEDIA_QUERY = `(max-width: ${MOBILE_BREAKPOINT - 1}px)`;
|
|
8
|
+
function getIsMobile() {
|
|
9
|
+
if (typeof window === "undefined") return false;
|
|
10
|
+
return window.matchMedia(MOBILE_MEDIA_QUERY).matches;
|
|
11
|
+
}
|
|
12
|
+
function useIsMobile() {
|
|
13
|
+
const [isMobile, setIsMobile] = useState(getIsMobile);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const mql = window.matchMedia(MOBILE_MEDIA_QUERY);
|
|
16
|
+
const onChange = () => setIsMobile(mql.matches);
|
|
17
|
+
mql.addEventListener("change", onChange);
|
|
18
|
+
return () => mql.removeEventListener("change", onChange);
|
|
19
|
+
}, []);
|
|
20
|
+
return isMobile;
|
|
21
|
+
}
|
|
22
|
+
function useInfiniteScroll({
|
|
23
|
+
onLoadMore,
|
|
24
|
+
hasNextPage,
|
|
25
|
+
isFetchingNextPage,
|
|
26
|
+
rootMargin = "200px",
|
|
27
|
+
threshold = 0,
|
|
28
|
+
enabled = true,
|
|
29
|
+
root
|
|
30
|
+
}) {
|
|
31
|
+
const observerRef = useRef(null);
|
|
32
|
+
const [sentinelRef, setSentinelRef] = useState(null);
|
|
33
|
+
const handleIntersection = useCallback(
|
|
34
|
+
(entries) => {
|
|
35
|
+
const entry = entries[0];
|
|
36
|
+
if (!entry?.isIntersecting) return;
|
|
37
|
+
if (hasNextPage && !isFetchingNextPage && enabled) {
|
|
38
|
+
onLoadMore();
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
[onLoadMore, hasNextPage, isFetchingNextPage, enabled]
|
|
42
|
+
);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!enabled) return;
|
|
45
|
+
if (observerRef.current) {
|
|
46
|
+
observerRef.current.disconnect();
|
|
47
|
+
}
|
|
48
|
+
observerRef.current = new IntersectionObserver(handleIntersection, {
|
|
49
|
+
root: root || null,
|
|
50
|
+
rootMargin,
|
|
51
|
+
threshold
|
|
52
|
+
});
|
|
53
|
+
if (sentinelRef) {
|
|
54
|
+
observerRef.current.observe(sentinelRef);
|
|
55
|
+
}
|
|
56
|
+
return () => {
|
|
57
|
+
if (observerRef.current) {
|
|
58
|
+
observerRef.current.disconnect();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}, [sentinelRef, handleIntersection, rootMargin, threshold, enabled, root]);
|
|
62
|
+
const Sentinel = useCallback(
|
|
63
|
+
({ className }) => /* @__PURE__ */ jsx(
|
|
64
|
+
"div",
|
|
65
|
+
{
|
|
66
|
+
ref: setSentinelRef,
|
|
67
|
+
className,
|
|
68
|
+
"aria-hidden": "true",
|
|
69
|
+
style: { height: "1px" }
|
|
70
|
+
}
|
|
71
|
+
),
|
|
72
|
+
[]
|
|
73
|
+
);
|
|
74
|
+
return {
|
|
75
|
+
Sentinel,
|
|
76
|
+
sentinelRef: setSentinelRef
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function usePullToRefresh({
|
|
80
|
+
onRefresh,
|
|
81
|
+
threshold = 80,
|
|
82
|
+
resistance = 2.5,
|
|
83
|
+
disabled = false
|
|
84
|
+
}) {
|
|
85
|
+
const [pullDistance, setPullDistance] = useState(0);
|
|
86
|
+
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
87
|
+
const [isPulling, setIsPulling] = useState(false);
|
|
88
|
+
const [containerElement, setContainerElement] = useState(null);
|
|
89
|
+
const containerRef = useRef(null);
|
|
90
|
+
const startYRef = useRef(null);
|
|
91
|
+
const currentYRef = useRef(null);
|
|
92
|
+
const pullDistanceRef = useRef(0);
|
|
93
|
+
pullDistanceRef.current = pullDistance;
|
|
94
|
+
const assignContainerRef = useCallback((node) => {
|
|
95
|
+
containerRef.current = node;
|
|
96
|
+
setContainerElement(node);
|
|
97
|
+
}, []);
|
|
98
|
+
const handleTouchStart = useCallback(
|
|
99
|
+
(e) => {
|
|
100
|
+
if (disabled || isRefreshing) return;
|
|
101
|
+
const container = containerRef.current;
|
|
102
|
+
if (!container) return;
|
|
103
|
+
if (container.scrollTop <= 0) {
|
|
104
|
+
startYRef.current = e.touches[0].clientY;
|
|
105
|
+
setIsPulling(true);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
[disabled, isRefreshing]
|
|
109
|
+
);
|
|
110
|
+
const handleTouchMove = useCallback(
|
|
111
|
+
(e) => {
|
|
112
|
+
if (disabled || isRefreshing || startYRef.current === null) return;
|
|
113
|
+
const container = containerRef.current;
|
|
114
|
+
if (!container) return;
|
|
115
|
+
if (container.scrollTop > 0) {
|
|
116
|
+
startYRef.current = null;
|
|
117
|
+
setPullDistance(0);
|
|
118
|
+
setIsPulling(false);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
currentYRef.current = e.touches[0].clientY;
|
|
122
|
+
const diff = currentYRef.current - startYRef.current;
|
|
123
|
+
if (diff > 0) {
|
|
124
|
+
const distance = Math.min(diff / resistance, threshold * 1.5);
|
|
125
|
+
setPullDistance(distance);
|
|
126
|
+
if (distance > 5) {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
[disabled, isRefreshing, resistance, threshold]
|
|
132
|
+
);
|
|
133
|
+
const handleTouchEnd = useCallback(async () => {
|
|
134
|
+
if (disabled || isRefreshing) return;
|
|
135
|
+
startYRef.current = null;
|
|
136
|
+
currentYRef.current = null;
|
|
137
|
+
setIsPulling(false);
|
|
138
|
+
const distance = pullDistanceRef.current;
|
|
139
|
+
if (distance >= threshold) {
|
|
140
|
+
setIsRefreshing(true);
|
|
141
|
+
setPullDistance(threshold * 0.5);
|
|
142
|
+
try {
|
|
143
|
+
await onRefresh();
|
|
144
|
+
} finally {
|
|
145
|
+
setIsRefreshing(false);
|
|
146
|
+
setPullDistance(0);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
setPullDistance(0);
|
|
150
|
+
}
|
|
151
|
+
}, [disabled, isRefreshing, threshold, onRefresh]);
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
const container = containerElement;
|
|
154
|
+
if (!container) return;
|
|
155
|
+
container.addEventListener("touchstart", handleTouchStart, { passive: true });
|
|
156
|
+
container.addEventListener("touchmove", handleTouchMove, { passive: false });
|
|
157
|
+
container.addEventListener("touchend", handleTouchEnd, { passive: true });
|
|
158
|
+
return () => {
|
|
159
|
+
container.removeEventListener("touchstart", handleTouchStart);
|
|
160
|
+
container.removeEventListener("touchmove", handleTouchMove);
|
|
161
|
+
container.removeEventListener("touchend", handleTouchEnd);
|
|
162
|
+
};
|
|
163
|
+
}, [containerElement, handleTouchStart, handleTouchMove, handleTouchEnd]);
|
|
164
|
+
const pullProgress = Math.min(pullDistance / threshold, 1);
|
|
165
|
+
return {
|
|
166
|
+
pullDistance,
|
|
167
|
+
isRefreshing,
|
|
168
|
+
isPulling,
|
|
169
|
+
containerRef: assignContainerRef,
|
|
170
|
+
pullProgress
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
var globalNow = /* @__PURE__ */ new Date();
|
|
174
|
+
var subscribers = /* @__PURE__ */ new Map();
|
|
175
|
+
var visibilityMap = /* @__PURE__ */ new Map();
|
|
176
|
+
var rafId = null;
|
|
177
|
+
var intervalId = null;
|
|
178
|
+
var isTabVisible = true;
|
|
179
|
+
var pendingUpdate = false;
|
|
180
|
+
function getUpdateInterval(date) {
|
|
181
|
+
const ageMs = Date.now() - date.getTime();
|
|
182
|
+
const ageMinutes = ageMs / 6e4;
|
|
183
|
+
if (ageMinutes < 1) return 1e4;
|
|
184
|
+
if (ageMinutes < 60) return 3e4;
|
|
185
|
+
if (ageMinutes < 1440) return 3e5;
|
|
186
|
+
return 36e5;
|
|
187
|
+
}
|
|
188
|
+
function getMinInterval() {
|
|
189
|
+
if (subscribers.size === 0) return 3e4;
|
|
190
|
+
const intervals = Array.from(subscribers.values()).map((sub) => getUpdateInterval(sub.date));
|
|
191
|
+
return Math.max(Math.min(...intervals), 1e4);
|
|
192
|
+
}
|
|
193
|
+
var createObserver = (subscriberId, callback) => {
|
|
194
|
+
return new IntersectionObserver(
|
|
195
|
+
(entries) => {
|
|
196
|
+
entries.forEach((entry) => {
|
|
197
|
+
visibilityMap.set(subscriberId, entry.isIntersecting);
|
|
198
|
+
if (entry.isIntersecting) {
|
|
199
|
+
callback();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
root: null,
|
|
205
|
+
rootMargin: "50px",
|
|
206
|
+
threshold: 0.01
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
function updateVisibleSubscribers() {
|
|
211
|
+
if (!isTabVisible || subscribers.size === 0) {
|
|
212
|
+
pendingUpdate = false;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
globalNow = /* @__PURE__ */ new Date();
|
|
216
|
+
if (!pendingUpdate) {
|
|
217
|
+
pendingUpdate = true;
|
|
218
|
+
rafId = requestAnimationFrame(() => {
|
|
219
|
+
subscribers.forEach((subscriber, subscriberId) => {
|
|
220
|
+
const isVisible = visibilityMap.get(subscriberId) ?? true;
|
|
221
|
+
if (isVisible) {
|
|
222
|
+
subscriber.callback();
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
pendingUpdate = false;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function stopTimer() {
|
|
230
|
+
if (intervalId) {
|
|
231
|
+
clearTimeout(intervalId);
|
|
232
|
+
intervalId = null;
|
|
233
|
+
}
|
|
234
|
+
if (rafId) {
|
|
235
|
+
cancelAnimationFrame(rafId);
|
|
236
|
+
rafId = null;
|
|
237
|
+
}
|
|
238
|
+
pendingUpdate = false;
|
|
239
|
+
}
|
|
240
|
+
function scheduleNextTick() {
|
|
241
|
+
if (intervalId) {
|
|
242
|
+
clearTimeout(intervalId);
|
|
243
|
+
intervalId = null;
|
|
244
|
+
}
|
|
245
|
+
if (subscribers.size === 0 || !isTabVisible) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
intervalId = setTimeout(() => {
|
|
249
|
+
updateVisibleSubscribers();
|
|
250
|
+
scheduleNextTick();
|
|
251
|
+
}, getMinInterval());
|
|
252
|
+
}
|
|
253
|
+
function ensureTimerRunning() {
|
|
254
|
+
if (!intervalId && subscribers.size > 0 && isTabVisible) {
|
|
255
|
+
scheduleNextTick();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function restartTimer() {
|
|
259
|
+
stopTimer();
|
|
260
|
+
ensureTimerRunning();
|
|
261
|
+
}
|
|
262
|
+
if (typeof window !== "undefined") {
|
|
263
|
+
document.addEventListener("visibilitychange", () => {
|
|
264
|
+
isTabVisible = !document.hidden;
|
|
265
|
+
if (isTabVisible && subscribers.size > 0) {
|
|
266
|
+
updateVisibleSubscribers();
|
|
267
|
+
ensureTimerRunning();
|
|
268
|
+
} else if (!isTabVisible) {
|
|
269
|
+
stopTimer();
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
function useLiveTimestamp(date, elementRef) {
|
|
274
|
+
const [, setTick] = useState(0);
|
|
275
|
+
const subscriberIdRef = useRef(/* @__PURE__ */ Symbol("live-timestamp-subscriber"));
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
const subscriberId = subscriberIdRef.current;
|
|
278
|
+
const callback = () => {
|
|
279
|
+
const isVisible = visibilityMap.get(subscriberId) ?? true;
|
|
280
|
+
if (isVisible) {
|
|
281
|
+
setTick((t) => t + 1);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
let observer = null;
|
|
285
|
+
if (elementRef?.current) {
|
|
286
|
+
observer = createObserver(subscriberId, callback);
|
|
287
|
+
observer.observe(elementRef.current);
|
|
288
|
+
visibilityMap.set(subscriberId, false);
|
|
289
|
+
} else {
|
|
290
|
+
visibilityMap.set(subscriberId, true);
|
|
291
|
+
}
|
|
292
|
+
subscribers.set(subscriberId, {
|
|
293
|
+
callback,
|
|
294
|
+
date,
|
|
295
|
+
observer
|
|
296
|
+
});
|
|
297
|
+
ensureTimerRunning();
|
|
298
|
+
return () => {
|
|
299
|
+
const subscriber = subscribers.get(subscriberId);
|
|
300
|
+
if (subscriber?.observer) {
|
|
301
|
+
subscriber.observer.disconnect();
|
|
302
|
+
}
|
|
303
|
+
subscribers.delete(subscriberId);
|
|
304
|
+
visibilityMap.delete(subscriberId);
|
|
305
|
+
if (subscribers.size === 0) {
|
|
306
|
+
stopTimer();
|
|
307
|
+
} else {
|
|
308
|
+
restartTimer();
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}, [date.getTime(), elementRef]);
|
|
312
|
+
return globalNow;
|
|
313
|
+
}
|
|
314
|
+
var SCROLL_AMOUNT = 80;
|
|
315
|
+
var SCROLL_CHECK_DELAY = 100;
|
|
316
|
+
var DEFAULT_SCROLL_POSITION_KEY = "scrollable-container-position";
|
|
317
|
+
var STORAGE_DEBOUNCE_MS = 150;
|
|
318
|
+
function useScrollableContainer(scrollPositionKey = DEFAULT_SCROLL_POSITION_KEY) {
|
|
319
|
+
const containerRef = useRef(null);
|
|
320
|
+
const [containerElement, setContainerElement] = useState(null);
|
|
321
|
+
const isRestoringRef = useRef(false);
|
|
322
|
+
const [canScrollUp, setCanScrollUp] = useState(false);
|
|
323
|
+
const [canScrollDown, setCanScrollDown] = useState(false);
|
|
324
|
+
const assignContainerRef = useCallback((node) => {
|
|
325
|
+
containerRef.current = node;
|
|
326
|
+
setContainerElement(node);
|
|
327
|
+
}, []);
|
|
328
|
+
const saveToStorage = useMemo(
|
|
329
|
+
() => debounce((position) => {
|
|
330
|
+
try {
|
|
331
|
+
sessionStorage.setItem(scrollPositionKey, String(position));
|
|
332
|
+
} catch {
|
|
333
|
+
}
|
|
334
|
+
}, STORAGE_DEBOUNCE_MS),
|
|
335
|
+
[scrollPositionKey]
|
|
336
|
+
);
|
|
337
|
+
const checkScrollability = useCallback(() => {
|
|
338
|
+
if (!containerRef.current || isRestoringRef.current) return;
|
|
339
|
+
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
|
|
340
|
+
saveToStorage(scrollTop);
|
|
341
|
+
setCanScrollUp(scrollTop > 0);
|
|
342
|
+
setCanScrollDown(scrollTop < scrollHeight - clientHeight - 1);
|
|
343
|
+
}, [saveToStorage]);
|
|
344
|
+
useLayoutEffect(() => {
|
|
345
|
+
if (!containerElement || isRestoringRef.current) return;
|
|
346
|
+
try {
|
|
347
|
+
const savedPosition = sessionStorage.getItem(scrollPositionKey);
|
|
348
|
+
if (savedPosition !== null) {
|
|
349
|
+
const position = parseInt(savedPosition, 10);
|
|
350
|
+
const currentPosition = containerElement.scrollTop;
|
|
351
|
+
if (position > 0 && !isNaN(position) && Math.abs(currentPosition - position) > 1) {
|
|
352
|
+
isRestoringRef.current = true;
|
|
353
|
+
containerElement.scrollTop = position;
|
|
354
|
+
requestAnimationFrame(() => {
|
|
355
|
+
isRestoringRef.current = false;
|
|
356
|
+
checkScrollability();
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
} catch {
|
|
361
|
+
}
|
|
362
|
+
}, [containerElement, scrollPositionKey, checkScrollability]);
|
|
363
|
+
const preserveScrollPosition = useCallback(() => {
|
|
364
|
+
if (containerRef.current) {
|
|
365
|
+
try {
|
|
366
|
+
sessionStorage.setItem(scrollPositionKey, String(containerRef.current.scrollTop));
|
|
367
|
+
} catch {
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}, [scrollPositionKey]);
|
|
371
|
+
useEffect(() => {
|
|
372
|
+
return () => {
|
|
373
|
+
saveToStorage.cancel();
|
|
374
|
+
};
|
|
375
|
+
}, [saveToStorage]);
|
|
376
|
+
useEffect(() => {
|
|
377
|
+
if (!containerElement) return;
|
|
378
|
+
const timeoutId = setTimeout(checkScrollability, SCROLL_CHECK_DELAY);
|
|
379
|
+
const handleScroll = () => {
|
|
380
|
+
checkScrollability();
|
|
381
|
+
};
|
|
382
|
+
containerElement.addEventListener("scroll", handleScroll, { passive: true });
|
|
383
|
+
window.addEventListener("resize", checkScrollability);
|
|
384
|
+
const resizeObserver = new ResizeObserver(checkScrollability);
|
|
385
|
+
resizeObserver.observe(containerElement);
|
|
386
|
+
return () => {
|
|
387
|
+
clearTimeout(timeoutId);
|
|
388
|
+
containerElement.removeEventListener("scroll", handleScroll);
|
|
389
|
+
window.removeEventListener("resize", checkScrollability);
|
|
390
|
+
resizeObserver.disconnect();
|
|
391
|
+
};
|
|
392
|
+
}, [containerElement, checkScrollability]);
|
|
393
|
+
const scrollUp = useCallback(() => {
|
|
394
|
+
containerRef.current?.scrollBy({ top: -SCROLL_AMOUNT, behavior: "smooth" });
|
|
395
|
+
}, []);
|
|
396
|
+
const scrollDown = useCallback(() => {
|
|
397
|
+
containerRef.current?.scrollBy({ top: SCROLL_AMOUNT, behavior: "smooth" });
|
|
398
|
+
}, []);
|
|
399
|
+
return {
|
|
400
|
+
containerRef: assignContainerRef,
|
|
401
|
+
canScrollUp,
|
|
402
|
+
canScrollDown,
|
|
403
|
+
scrollUp,
|
|
404
|
+
scrollDown,
|
|
405
|
+
checkScrollability,
|
|
406
|
+
preserveScrollPosition
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export { DEFAULT_SCROLL_POSITION_KEY, SCROLL_AMOUNT, SCROLL_CHECK_DELAY, useInfiniteScroll, useIsMobile, useLiveTimestamp, usePullToRefresh, useScrollableContainer };
|
|
411
|
+
//# sourceMappingURL=index.js.map
|
|
412
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../use-mobile.tsx","../use-infinite-scroll.tsx","../use-pull-to-refresh.ts","../use-live-timestamp.ts","../use-scrollable-container.ts"],"names":["useState","useEffect","useRef","useCallback"],"mappings":"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","file":"index.js","sourcesContent":["import { useEffect, useState } from 'react';\r\n\r\nconst MOBILE_BREAKPOINT = 768;\r\nconst MOBILE_MEDIA_QUERY = `(max-width: ${MOBILE_BREAKPOINT - 1}px)` as const;\r\n\r\nfunction getIsMobile(): boolean {\r\n if (typeof window === 'undefined') return false;\r\n return window.matchMedia(MOBILE_MEDIA_QUERY).matches;\r\n}\r\n\r\nexport function useIsMobile(): boolean {\r\n const [isMobile, setIsMobile] = useState<boolean>(getIsMobile);\r\n\r\n useEffect(() => {\r\n const mql = window.matchMedia(MOBILE_MEDIA_QUERY);\r\n const onChange = () => setIsMobile(mql.matches);\r\n mql.addEventListener('change', onChange);\r\n return () => mql.removeEventListener('change', onChange);\r\n }, []);\r\n\r\n return isMobile;\r\n}\r\n","import { useEffect, useRef, useCallback, useState } from 'react';\r\n\r\nexport interface UseInfiniteScrollOptions {\r\n /** Callback when intersection occurs */\r\n onLoadMore: () => void;\r\n /** Whether more data can be loaded */\r\n hasNextPage: boolean;\r\n /** Whether currently fetching */\r\n isFetchingNextPage: boolean;\r\n /** Root margin for intersection observer */\r\n rootMargin?: string;\r\n /** Threshold for intersection */\r\n threshold?: number;\r\n /** Enable/disable the hook */\r\n enabled?: boolean;\r\n /** Custom root element for IntersectionObserver (e.g., ScrollArea viewport) */\r\n root?: HTMLElement | null;\r\n}\r\n\r\n/**\r\n * Hook for implementing infinite scroll using IntersectionObserver.\r\n */\r\nexport function useInfiniteScroll({\r\n onLoadMore,\r\n hasNextPage,\r\n isFetchingNextPage,\r\n rootMargin = '200px',\r\n threshold = 0,\r\n enabled = true,\r\n root,\r\n}: UseInfiniteScrollOptions) {\r\n const observerRef = useRef<IntersectionObserver | null>(null);\r\n const [sentinelRef, setSentinelRef] = useState<HTMLDivElement | null>(null);\r\n\r\n const handleIntersection = useCallback(\r\n (entries: IntersectionObserverEntry[]) => {\r\n const entry = entries[0];\r\n if (!entry?.isIntersecting) return;\r\n if (hasNextPage && !isFetchingNextPage && enabled) {\r\n onLoadMore();\r\n }\r\n },\r\n [onLoadMore, hasNextPage, isFetchingNextPage, enabled]\r\n );\r\n\r\n useEffect(() => {\r\n if (!enabled) return;\r\n\r\n if (observerRef.current) {\r\n observerRef.current.disconnect();\r\n }\r\n\r\n observerRef.current = new IntersectionObserver(handleIntersection, {\r\n root: root || null,\r\n rootMargin,\r\n threshold,\r\n });\r\n\r\n if (sentinelRef) {\r\n observerRef.current.observe(sentinelRef);\r\n }\r\n\r\n return () => {\r\n if (observerRef.current) {\r\n observerRef.current.disconnect();\r\n }\r\n };\r\n }, [sentinelRef, handleIntersection, rootMargin, threshold, enabled, root]);\r\n\r\n const Sentinel = useCallback(\r\n ({ className }: { className?: string }) => (\r\n <div\r\n ref={setSentinelRef}\r\n className={className}\r\n aria-hidden=\"true\"\r\n style={{ height: '1px' }}\r\n />\r\n ),\r\n []\r\n );\r\n\r\n return {\r\n Sentinel,\r\n sentinelRef: setSentinelRef,\r\n };\r\n}\r\n","import { useState, useEffect, useRef, useCallback, type Ref } from 'react';\r\n\r\nexport interface UsePullToRefreshOptions {\r\n onRefresh: () => Promise<void>;\r\n threshold?: number;\r\n resistance?: number;\r\n disabled?: boolean;\r\n}\r\n\r\nexport interface UsePullToRefreshReturn {\r\n pullDistance: number;\r\n isRefreshing: boolean;\r\n isPulling: boolean;\r\n /** Callback ref — attach to the scrollable container element */\r\n containerRef: Ref<HTMLDivElement | null>;\r\n pullProgress: number;\r\n}\r\n\r\n/**\r\n * Hook to add pull-to-refresh gesture for mobile devices.\r\n */\r\nexport function usePullToRefresh({\r\n onRefresh,\r\n threshold = 80,\r\n resistance = 2.5,\r\n disabled = false,\r\n}: UsePullToRefreshOptions): UsePullToRefreshReturn {\r\n const [pullDistance, setPullDistance] = useState(0);\r\n const [isRefreshing, setIsRefreshing] = useState(false);\r\n const [isPulling, setIsPulling] = useState(false);\r\n const [containerElement, setContainerElement] = useState<HTMLDivElement | null>(null);\r\n\r\n const containerRef = useRef<HTMLDivElement | null>(null);\r\n const startYRef = useRef<number | null>(null);\r\n const currentYRef = useRef<number | null>(null);\r\n const pullDistanceRef = useRef(0);\r\n\r\n pullDistanceRef.current = pullDistance;\r\n\r\n const assignContainerRef = useCallback((node: HTMLDivElement | null) => {\r\n containerRef.current = node;\r\n setContainerElement(node);\r\n }, []);\r\n\r\n const handleTouchStart = useCallback(\r\n (e: TouchEvent) => {\r\n if (disabled || isRefreshing) return;\r\n\r\n const container = containerRef.current;\r\n if (!container) return;\r\n\r\n if (container.scrollTop <= 0) {\r\n startYRef.current = e.touches[0].clientY;\r\n setIsPulling(true);\r\n }\r\n },\r\n [disabled, isRefreshing]\r\n );\r\n\r\n const handleTouchMove = useCallback(\r\n (e: TouchEvent) => {\r\n if (disabled || isRefreshing || startYRef.current === null) return;\r\n\r\n const container = containerRef.current;\r\n if (!container) return;\r\n\r\n if (container.scrollTop > 0) {\r\n startYRef.current = null;\r\n setPullDistance(0);\r\n setIsPulling(false);\r\n return;\r\n }\r\n\r\n currentYRef.current = e.touches[0].clientY;\r\n const diff = currentYRef.current - startYRef.current;\r\n\r\n if (diff > 0) {\r\n const distance = Math.min(diff / resistance, threshold * 1.5);\r\n setPullDistance(distance);\r\n\r\n if (distance > 5) {\r\n e.preventDefault();\r\n }\r\n }\r\n },\r\n [disabled, isRefreshing, resistance, threshold]\r\n );\r\n\r\n const handleTouchEnd = useCallback(async () => {\r\n if (disabled || isRefreshing) return;\r\n\r\n startYRef.current = null;\r\n currentYRef.current = null;\r\n setIsPulling(false);\r\n\r\n const distance = pullDistanceRef.current;\r\n\r\n if (distance >= threshold) {\r\n setIsRefreshing(true);\r\n setPullDistance(threshold * 0.5);\r\n\r\n try {\r\n await onRefresh();\r\n } finally {\r\n setIsRefreshing(false);\r\n setPullDistance(0);\r\n }\r\n } else {\r\n setPullDistance(0);\r\n }\r\n }, [disabled, isRefreshing, threshold, onRefresh]);\r\n\r\n useEffect(() => {\r\n const container = containerElement;\r\n if (!container) return;\r\n\r\n container.addEventListener('touchstart', handleTouchStart, { passive: true });\r\n container.addEventListener('touchmove', handleTouchMove, { passive: false });\r\n container.addEventListener('touchend', handleTouchEnd, { passive: true });\r\n\r\n return () => {\r\n container.removeEventListener('touchstart', handleTouchStart);\r\n container.removeEventListener('touchmove', handleTouchMove);\r\n container.removeEventListener('touchend', handleTouchEnd);\r\n };\r\n }, [containerElement, handleTouchStart, handleTouchMove, handleTouchEnd]);\r\n\r\n const pullProgress = Math.min(pullDistance / threshold, 1);\r\n\r\n return {\r\n pullDistance,\r\n isRefreshing,\r\n isPulling,\r\n containerRef: assignContainerRef,\r\n pullProgress,\r\n };\r\n}\r\n","import { useState, useEffect, useRef, type RefObject } from 'react';\r\n\r\nlet globalNow = new Date();\r\n\r\ntype SubscriberId = symbol;\r\n\r\ntype Subscriber = {\r\n callback: () => void;\r\n date: Date;\r\n observer: IntersectionObserver | null;\r\n};\r\n\r\nconst subscribers = new Map<SubscriberId, Subscriber>();\r\nconst visibilityMap = new Map<SubscriberId, boolean>();\r\n\r\nlet rafId: number | null = null;\r\nlet intervalId: ReturnType<typeof setTimeout> | null = null;\r\nlet isTabVisible = true;\r\nlet pendingUpdate = false;\r\n\r\nfunction getUpdateInterval(date: Date): number {\r\n const ageMs = Date.now() - date.getTime();\r\n const ageMinutes = ageMs / 60000;\r\n\r\n if (ageMinutes < 1) return 10000;\r\n if (ageMinutes < 60) return 30000;\r\n if (ageMinutes < 1440) return 300000;\r\n return 3600000;\r\n}\r\n\r\nfunction getMinInterval(): number {\r\n if (subscribers.size === 0) return 30000;\r\n const intervals = Array.from(subscribers.values()).map((sub) => getUpdateInterval(sub.date));\r\n return Math.max(Math.min(...intervals), 10000);\r\n}\r\n\r\nconst createObserver = (\r\n subscriberId: SubscriberId,\r\n callback: () => void\r\n): IntersectionObserver => {\r\n return new IntersectionObserver(\r\n (entries) => {\r\n entries.forEach((entry) => {\r\n visibilityMap.set(subscriberId, entry.isIntersecting);\r\n if (entry.isIntersecting) {\r\n callback();\r\n }\r\n });\r\n },\r\n {\r\n root: null,\r\n rootMargin: '50px',\r\n threshold: 0.01,\r\n }\r\n );\r\n};\r\n\r\nfunction updateVisibleSubscribers() {\r\n if (!isTabVisible || subscribers.size === 0) {\r\n pendingUpdate = false;\r\n return;\r\n }\r\n\r\n globalNow = new Date();\r\n\r\n if (!pendingUpdate) {\r\n pendingUpdate = true;\r\n rafId = requestAnimationFrame(() => {\r\n subscribers.forEach((subscriber, subscriberId) => {\r\n const isVisible = visibilityMap.get(subscriberId) ?? true;\r\n if (isVisible) {\r\n subscriber.callback();\r\n }\r\n });\r\n pendingUpdate = false;\r\n });\r\n }\r\n}\r\n\r\nfunction stopTimer() {\r\n if (intervalId) {\r\n clearTimeout(intervalId);\r\n intervalId = null;\r\n }\r\n if (rafId) {\r\n cancelAnimationFrame(rafId);\r\n rafId = null;\r\n }\r\n pendingUpdate = false;\r\n}\r\n\r\nfunction scheduleNextTick() {\r\n if (intervalId) {\r\n clearTimeout(intervalId);\r\n intervalId = null;\r\n }\r\n\r\n if (subscribers.size === 0 || !isTabVisible) {\r\n return;\r\n }\r\n\r\n intervalId = setTimeout(() => {\r\n updateVisibleSubscribers();\r\n scheduleNextTick();\r\n }, getMinInterval());\r\n}\r\n\r\nfunction ensureTimerRunning() {\r\n if (!intervalId && subscribers.size > 0 && isTabVisible) {\r\n scheduleNextTick();\r\n }\r\n}\r\n\r\nfunction restartTimer() {\r\n stopTimer();\r\n ensureTimerRunning();\r\n}\r\n\r\nif (typeof window !== 'undefined') {\r\n document.addEventListener('visibilitychange', () => {\r\n isTabVisible = !document.hidden;\r\n if (isTabVisible && subscribers.size > 0) {\r\n updateVisibleSubscribers();\r\n ensureTimerRunning();\r\n } else if (!isTabVisible) {\r\n stopTimer();\r\n }\r\n });\r\n}\r\n\r\n/**\r\n * Live-updating timestamp hook with visibility-aware adaptive intervals.\r\n */\r\nexport function useLiveTimestamp(date: Date, elementRef?: RefObject<Element>): Date {\r\n const [, setTick] = useState(0);\r\n const subscriberIdRef = useRef<SubscriberId>(Symbol('live-timestamp-subscriber'));\r\n\r\n useEffect(() => {\r\n const subscriberId = subscriberIdRef.current;\r\n\r\n const callback = () => {\r\n const isVisible = visibilityMap.get(subscriberId) ?? true;\r\n if (isVisible) {\r\n setTick((t) => t + 1);\r\n }\r\n };\r\n\r\n let observer: IntersectionObserver | null = null;\r\n if (elementRef?.current) {\r\n observer = createObserver(subscriberId, callback);\r\n observer.observe(elementRef.current);\r\n visibilityMap.set(subscriberId, false);\r\n } else {\r\n visibilityMap.set(subscriberId, true);\r\n }\r\n\r\n subscribers.set(subscriberId, {\r\n callback,\r\n date,\r\n observer,\r\n });\r\n\r\n ensureTimerRunning();\r\n\r\n return () => {\r\n const subscriber = subscribers.get(subscriberId);\r\n if (subscriber?.observer) {\r\n subscriber.observer.disconnect();\r\n }\r\n subscribers.delete(subscriberId);\r\n visibilityMap.delete(subscriberId);\r\n\r\n if (subscribers.size === 0) {\r\n stopTimer();\r\n } else {\r\n restartTimer();\r\n }\r\n };\r\n // Key on the timestamp value, not the Date identity, so callers passing a\r\n // freshly constructed Date each render don't trigger a re-subscribe.\r\n }, [date.getTime(), elementRef]);\r\n\r\n return globalNow;\r\n}\r\n","import { useRef, useEffect, useLayoutEffect, useState, useCallback, useMemo, type Ref } from 'react';\r\nimport { debounce } from '@publikit/utils';\r\n\r\nexport const SCROLL_AMOUNT = 80;\r\nexport const SCROLL_CHECK_DELAY = 100;\r\nexport const DEFAULT_SCROLL_POSITION_KEY = 'scrollable-container-position';\r\nconst STORAGE_DEBOUNCE_MS = 150;\r\n\r\n/**\r\n * Scrollable container with position persistence and scroll button state.\r\n *\r\n * @param scrollPositionKey - sessionStorage key for scroll position\r\n */\r\nexport function useScrollableContainer(\r\n scrollPositionKey: string = DEFAULT_SCROLL_POSITION_KEY\r\n) {\r\n const containerRef = useRef<HTMLElement | null>(null);\r\n const [containerElement, setContainerElement] = useState<HTMLElement | null>(null);\r\n const isRestoringRef = useRef<boolean>(false);\r\n const [canScrollUp, setCanScrollUp] = useState(false);\r\n const [canScrollDown, setCanScrollDown] = useState(false);\r\n\r\n const assignContainerRef = useCallback((node: HTMLElement | null) => {\r\n containerRef.current = node;\r\n setContainerElement(node);\r\n }, []);\r\n\r\n const saveToStorage = useMemo(\r\n () =>\r\n debounce((position: number) => {\r\n try {\r\n sessionStorage.setItem(scrollPositionKey, String(position));\r\n } catch {\r\n // Ignore storage errors\r\n }\r\n }, STORAGE_DEBOUNCE_MS),\r\n [scrollPositionKey]\r\n );\r\n\r\n const checkScrollability = useCallback(() => {\r\n if (!containerRef.current || isRestoringRef.current) return;\r\n const { scrollTop, scrollHeight, clientHeight } = containerRef.current;\r\n\r\n saveToStorage(scrollTop);\r\n\r\n setCanScrollUp(scrollTop > 0);\r\n setCanScrollDown(scrollTop < scrollHeight - clientHeight - 1);\r\n }, [saveToStorage]);\r\n\r\n useLayoutEffect(() => {\r\n if (!containerElement || isRestoringRef.current) return;\r\n\r\n try {\r\n const savedPosition = sessionStorage.getItem(scrollPositionKey);\r\n if (savedPosition !== null) {\r\n const position = parseInt(savedPosition, 10);\r\n const currentPosition = containerElement.scrollTop;\r\n\r\n if (position > 0 && !isNaN(position) && Math.abs(currentPosition - position) > 1) {\r\n isRestoringRef.current = true;\r\n containerElement.scrollTop = position;\r\n requestAnimationFrame(() => {\r\n isRestoringRef.current = false;\r\n checkScrollability();\r\n });\r\n }\r\n }\r\n } catch {\r\n // Ignore storage errors\r\n }\r\n }, [containerElement, scrollPositionKey, checkScrollability]);\r\n\r\n const preserveScrollPosition = useCallback(() => {\r\n if (containerRef.current) {\r\n try {\r\n sessionStorage.setItem(scrollPositionKey, String(containerRef.current.scrollTop));\r\n } catch {\r\n // Ignore storage errors\r\n }\r\n }\r\n }, [scrollPositionKey]);\r\n\r\n useEffect(() => {\r\n return () => {\r\n saveToStorage.cancel();\r\n };\r\n }, [saveToStorage]);\r\n\r\n useEffect(() => {\r\n if (!containerElement) return;\r\n\r\n const timeoutId = setTimeout(checkScrollability, SCROLL_CHECK_DELAY);\r\n\r\n const handleScroll = () => {\r\n checkScrollability();\r\n };\r\n\r\n containerElement.addEventListener('scroll', handleScroll, { passive: true });\r\n window.addEventListener('resize', checkScrollability);\r\n\r\n const resizeObserver = new ResizeObserver(checkScrollability);\r\n resizeObserver.observe(containerElement);\r\n\r\n return () => {\r\n clearTimeout(timeoutId);\r\n containerElement.removeEventListener('scroll', handleScroll);\r\n window.removeEventListener('resize', checkScrollability);\r\n resizeObserver.disconnect();\r\n };\r\n }, [containerElement, checkScrollability]);\r\n\r\n const scrollUp = useCallback(() => {\r\n containerRef.current?.scrollBy({ top: -SCROLL_AMOUNT, behavior: 'smooth' });\r\n }, []);\r\n\r\n const scrollDown = useCallback(() => {\r\n containerRef.current?.scrollBy({ top: SCROLL_AMOUNT, behavior: 'smooth' });\r\n }, []);\r\n\r\n return {\r\n containerRef: assignContainerRef as Ref<HTMLElement | null>,\r\n canScrollUp,\r\n canScrollDown,\r\n scrollUp,\r\n scrollDown,\r\n checkScrollability,\r\n preserveScrollPosition,\r\n };\r\n}\r\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@publikit/hooks",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Generic, reusable React hooks for Publikit packages and applications",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup",
|
|
24
|
+
"dev": "tsup --watch",
|
|
25
|
+
"clean": "rimraf dist",
|
|
26
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"test": "vitest run"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"react",
|
|
32
|
+
"hooks",
|
|
33
|
+
"infinite-scroll",
|
|
34
|
+
"pull-to-refresh",
|
|
35
|
+
"publikit"
|
|
36
|
+
],
|
|
37
|
+
"author": "Pirimera",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/pirimera/publikit.git",
|
|
42
|
+
"directory": "hooks"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/pirimera/publikit/tree/main/hooks#readme",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/pirimera/publikit/issues"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@publikit/utils": "^0.1.1"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@publikit/utils": "file:../utils",
|
|
62
|
+
"@testing-library/react": "^16.3.0",
|
|
63
|
+
"@types/react": "^18.3.23 || ^19.0.0",
|
|
64
|
+
"@types/react-dom": "^18.3.7 || ^19.0.0",
|
|
65
|
+
"jsdom": "^26.1.0",
|
|
66
|
+
"react": "^19.0.0",
|
|
67
|
+
"react-dom": "^19.0.0",
|
|
68
|
+
"rimraf": "^5.0.0",
|
|
69
|
+
"tsup": "^8.0.0",
|
|
70
|
+
"typescript": "^5.8.3",
|
|
71
|
+
"vitest": "^4.0.18"
|
|
72
|
+
}
|
|
73
|
+
}
|