@-reddacted-/react-hooks 0.1.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/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/useFullscreen.d.ts +7 -0
- package/dist/useFullscreen.d.ts.map +1 -0
- package/dist/useFullscreen.js +42 -0
- package/dist/useInstallPrompt.d.ts +13 -0
- package/dist/useInstallPrompt.d.ts.map +1 -0
- package/dist/useInstallPrompt.js +59 -0
- package/dist/useValueChange.d.ts +6 -0
- package/dist/useValueChange.d.ts.map +1 -0
- package/dist/useValueChange.js +28 -0
- package/package.json +25 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,YAAY,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFullscreen.d.ts","sourceRoot":"","sources":["../src/useFullscreen.ts"],"names":[],"mappings":"AAEA,wBAAgB,aAAa;;iBAIW,WAAW,GAAG,IAAI;;kBAoBhD,WAAW,GAAG,IAAI;EAkB3B"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from "react";
|
|
2
|
+
export function useFullscreen() {
|
|
3
|
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
4
|
+
const targetRef = useRef(null);
|
|
5
|
+
const enter = useCallback(async (el) => {
|
|
6
|
+
const target = el ?? document.documentElement;
|
|
7
|
+
targetRef.current = target;
|
|
8
|
+
try {
|
|
9
|
+
if (target.requestFullscreen) {
|
|
10
|
+
await target.requestFullscreen();
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
const webkitEl = target;
|
|
14
|
+
await webkitEl.webkitRequestFullscreen?.();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch { /* browser may deny fullscreen */ }
|
|
18
|
+
}, []);
|
|
19
|
+
const exit = useCallback(async () => {
|
|
20
|
+
try {
|
|
21
|
+
if (document.fullscreenElement)
|
|
22
|
+
await document.exitFullscreen();
|
|
23
|
+
}
|
|
24
|
+
catch { /* browser may deny exit */ }
|
|
25
|
+
}, []);
|
|
26
|
+
const toggle = useCallback((el) => {
|
|
27
|
+
if (isFullscreen)
|
|
28
|
+
exit();
|
|
29
|
+
else
|
|
30
|
+
enter(el);
|
|
31
|
+
}, [isFullscreen, enter, exit]);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const handler = () => setIsFullscreen(!!document.fullscreenElement);
|
|
34
|
+
document.addEventListener("fullscreenchange", handler);
|
|
35
|
+
document.addEventListener("webkitfullscreenchange", handler);
|
|
36
|
+
return () => {
|
|
37
|
+
document.removeEventListener("fullscreenchange", handler);
|
|
38
|
+
document.removeEventListener("webkitfullscreenchange", handler);
|
|
39
|
+
};
|
|
40
|
+
}, []);
|
|
41
|
+
return { isFullscreen, enter, exit, toggle };
|
|
42
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface UseInstallPromptOptions {
|
|
2
|
+
/** localStorage key used to track dismissal (default: "pwa-install-dismissed") */
|
|
3
|
+
storageKey?: string;
|
|
4
|
+
/** How long a dismissal is remembered in ms (default: 30 days) */
|
|
5
|
+
dismissTtlMs?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function useInstallPrompt(options?: UseInstallPromptOptions): {
|
|
8
|
+
showBanner: boolean;
|
|
9
|
+
isIOS: boolean;
|
|
10
|
+
install: () => Promise<void>;
|
|
11
|
+
dismiss: () => void;
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=useInstallPrompt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useInstallPrompt.d.ts","sourceRoot":"","sources":["../src/useInstallPrompt.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,uBAAuB;IACtC,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,kEAAkE;IAClE,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,uBAAuB;;;;;EAiDjE"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
const DEFAULT_STORAGE_KEY = "pwa-install-dismissed";
|
|
3
|
+
const DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
4
|
+
function isStandalone() {
|
|
5
|
+
if (navigator.standalone === true)
|
|
6
|
+
return true;
|
|
7
|
+
if (window.matchMedia("(display-mode: standalone)").matches)
|
|
8
|
+
return true;
|
|
9
|
+
if (window.matchMedia("(display-mode: fullscreen)").matches)
|
|
10
|
+
return true;
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
export function useInstallPrompt(options) {
|
|
14
|
+
const storageKey = options?.storageKey ?? DEFAULT_STORAGE_KEY;
|
|
15
|
+
const dismissTtlMs = options?.dismissTtlMs ?? DEFAULT_TTL_MS;
|
|
16
|
+
const [deferredPrompt, setDeferredPrompt] = useState(null);
|
|
17
|
+
const [showBanner, setShowBanner] = useState(false);
|
|
18
|
+
const [isIOS, setIsIOS] = useState(false);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const isDismissed = () => {
|
|
21
|
+
const raw = localStorage.getItem(storageKey);
|
|
22
|
+
if (!raw)
|
|
23
|
+
return false;
|
|
24
|
+
const ts = parseInt(raw, 10);
|
|
25
|
+
if (isNaN(ts))
|
|
26
|
+
return false;
|
|
27
|
+
return Date.now() - ts < dismissTtlMs;
|
|
28
|
+
};
|
|
29
|
+
if (isStandalone() || isDismissed())
|
|
30
|
+
return;
|
|
31
|
+
const ios = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
32
|
+
if (ios) {
|
|
33
|
+
setIsIOS(true);
|
|
34
|
+
setShowBanner(true);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const handler = (e) => {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
setDeferredPrompt(e);
|
|
40
|
+
setShowBanner(true);
|
|
41
|
+
};
|
|
42
|
+
window.addEventListener("beforeinstallprompt", handler);
|
|
43
|
+
return () => window.removeEventListener("beforeinstallprompt", handler);
|
|
44
|
+
}, [storageKey, dismissTtlMs]);
|
|
45
|
+
const install = async () => {
|
|
46
|
+
if (!deferredPrompt)
|
|
47
|
+
return;
|
|
48
|
+
await deferredPrompt.prompt();
|
|
49
|
+
const { outcome } = await deferredPrompt.userChoice;
|
|
50
|
+
if (outcome === "accepted")
|
|
51
|
+
setShowBanner(false);
|
|
52
|
+
setDeferredPrompt(null);
|
|
53
|
+
};
|
|
54
|
+
const dismiss = () => {
|
|
55
|
+
localStorage.setItem(storageKey, String(Date.now()));
|
|
56
|
+
setShowBanner(false);
|
|
57
|
+
};
|
|
58
|
+
return { showBanner, isIOS, install, dismiss };
|
|
59
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true for ~600ms whenever `value` changes between fetches.
|
|
3
|
+
* @param animationsEnabled Pass false to disable (e.g. from user preferences or prefers-reduced-motion)
|
|
4
|
+
*/
|
|
5
|
+
export declare function useValueChange(value: string, fetchedAt: number | null, animationsEnabled?: boolean): boolean;
|
|
6
|
+
//# sourceMappingURL=useValueChange.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useValueChange.d.ts","sourceRoot":"","sources":["../src/useValueChange.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,iBAAiB,UAAO,GACvB,OAAO,CAwBT"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useRef, useState, useEffect } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Returns true for ~600ms whenever `value` changes between fetches.
|
|
4
|
+
* @param animationsEnabled Pass false to disable (e.g. from user preferences or prefers-reduced-motion)
|
|
5
|
+
*/
|
|
6
|
+
export function useValueChange(value, fetchedAt, animationsEnabled = true) {
|
|
7
|
+
const prevValueRef = useRef(undefined);
|
|
8
|
+
const [changed, setChanged] = useState(false);
|
|
9
|
+
const timerRef = useRef(null);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!animationsEnabled || fetchedAt === null)
|
|
12
|
+
return;
|
|
13
|
+
if (prevValueRef.current === undefined) {
|
|
14
|
+
prevValueRef.current = value;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (value !== prevValueRef.current) {
|
|
18
|
+
prevValueRef.current = value;
|
|
19
|
+
setChanged(true);
|
|
20
|
+
if (timerRef.current)
|
|
21
|
+
clearTimeout(timerRef.current);
|
|
22
|
+
timerRef.current = setTimeout(() => setChanged(false), 600);
|
|
23
|
+
}
|
|
24
|
+
}, [fetchedAt]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
25
|
+
useEffect(() => () => { if (timerRef.current)
|
|
26
|
+
clearTimeout(timerRef.current); }, []);
|
|
27
|
+
return changed && animationsEnabled;
|
|
28
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@-reddacted-/react-hooks",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "useFullscreen, useInstallPrompt, useValueChange React hooks",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": ["dist"],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"react": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/react": "^19.0.0",
|
|
23
|
+
"typescript": "~5.8.3"
|
|
24
|
+
}
|
|
25
|
+
}
|