preact-missing-hooks 1.0.1 → 1.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/.github/workflows/sync-npm.yml +38 -0
- package/.github/workflows/test-hooks.yml +30 -0
- package/LICENSE +21 -0
- package/Readme.md +208 -39
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.module.js +1 -1
- package/dist/index.module.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/useClipboard.d.ts +36 -0
- package/dist/useEventBus.d.ts +10 -0
- package/dist/useMutationObserver.d.ts +9 -0
- package/dist/useNetworkState.d.ts +40 -0
- package/dist/usePreferredTheme.d.ts +21 -0
- package/dist/useWrappedChildren.d.ts +10 -0
- package/package.json +64 -48
- package/src/index.ts +7 -1
- package/src/useClipboard.ts +97 -0
- package/src/useEventBus.ts +36 -0
- package/src/useMutationObserver.ts +26 -0
- package/src/useNetworkState.ts +122 -0
- package/src/usePreferredTheme.ts +68 -0
- package/src/useWrappedChildren.ts +58 -0
- package/tests/useClipboard.test.tsx +159 -0
- package/tests/useEventBus.test.tsx +41 -0
- package/tests/useMutationObserver.test.tsx +35 -0
- package/tests/useNetworkState.test.tsx +140 -0
- package/tests/usePreferredTheme.test.tsx +119 -0
- package/tests/useTransition.test.tsx +25 -0
- package/tests/useWrappedChildren.test.tsx +156 -0
- package/vite.config.ts +9 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface UseClipboardOptions {
|
|
2
|
+
/** Duration in ms to keep `copied` true before resetting. Default: 2000 */
|
|
3
|
+
resetDelay?: number;
|
|
4
|
+
}
|
|
5
|
+
export interface UseClipboardReturn {
|
|
6
|
+
/** Copy text to the clipboard. Returns true on success. */
|
|
7
|
+
copy: (text: string) => Promise<boolean>;
|
|
8
|
+
/** Read text from the clipboard. Returns empty string if denied or unavailable. */
|
|
9
|
+
paste: () => Promise<string>;
|
|
10
|
+
/** Whether the last copy operation succeeded (resets after resetDelay) */
|
|
11
|
+
copied: boolean;
|
|
12
|
+
/** Error from the last failed operation, or null */
|
|
13
|
+
error: Error | null;
|
|
14
|
+
/** Manually reset copied and error state */
|
|
15
|
+
reset: () => void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A Preact hook for reading and writing the clipboard. Uses the async
|
|
19
|
+
* Clipboard API when available (requires secure context and user gesture).
|
|
20
|
+
*
|
|
21
|
+
* @param options - Optional configuration (e.g., resetDelay for copied state)
|
|
22
|
+
* @returns Object with copy, paste, copied, error, and reset
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* function CopyButton() {
|
|
27
|
+
* const { copy, copied, error } = useClipboard();
|
|
28
|
+
* return (
|
|
29
|
+
* <button onClick={() => copy('Hello!')}>
|
|
30
|
+
* {copied ? 'Copied!' : 'Copy'}
|
|
31
|
+
* </button>
|
|
32
|
+
* );
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare function useClipboard(options?: UseClipboardOptions): UseClipboardReturn;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type EventMap = Record<string, (...args: any[]) => void>;
|
|
2
|
+
/**
|
|
3
|
+
* A Preact hook to publish and subscribe to custom events across components.
|
|
4
|
+
* @returns An object with `emit` and `on` methods.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useEventBus<T extends EventMap>(): {
|
|
7
|
+
emit: <K extends keyof T>(event: K, ...args: Parameters<T[K]>) => void;
|
|
8
|
+
on: <K extends keyof T>(event: K, handler: T[K]) => () => void;
|
|
9
|
+
};
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { RefObject } from 'preact';
|
|
2
|
+
export type UseMutationObserverOptions = MutationObserverInit;
|
|
3
|
+
/**
|
|
4
|
+
* A Preact hook to observe DOM mutations using MutationObserver.
|
|
5
|
+
* @param target - The element to observe.
|
|
6
|
+
* @param callback - Function to call on mutation.
|
|
7
|
+
* @param options - MutationObserver options.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useMutationObserver(targetRef: RefObject<HTMLElement | null>, callback: MutationCallback, options: MutationObserverInit): void;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Effective connection type from Network Information API */
|
|
2
|
+
export type EffectiveConnectionType = 'slow-2g' | '2g' | '3g' | '4g';
|
|
3
|
+
/** Network connection type (e.g., wifi, cellular) */
|
|
4
|
+
export type ConnectionType = 'bluetooth' | 'cellular' | 'ethernet' | 'mixed' | 'none' | 'other' | 'unknown' | 'wifi';
|
|
5
|
+
export interface NetworkState {
|
|
6
|
+
/** Whether the browser is online */
|
|
7
|
+
online: boolean;
|
|
8
|
+
/** Effective connection type (when supported) */
|
|
9
|
+
effectiveType?: EffectiveConnectionType;
|
|
10
|
+
/** Estimated downlink speed in Mbps (when supported) */
|
|
11
|
+
downlink?: number;
|
|
12
|
+
/** Estimated round-trip time in ms (when supported) */
|
|
13
|
+
rtt?: number;
|
|
14
|
+
/** Whether the user has requested reduced data usage (when supported) */
|
|
15
|
+
saveData?: boolean;
|
|
16
|
+
/** Connection type (when supported) */
|
|
17
|
+
connectionType?: ConnectionType;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A Preact hook that returns the current network state, including online/offline
|
|
21
|
+
* status and (when supported) connection type, downlink, RTT, and save-data preference.
|
|
22
|
+
* Updates reactively when the network state changes.
|
|
23
|
+
*
|
|
24
|
+
* @returns The current network state object
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* function NetworkStatus() {
|
|
29
|
+
* const { online, effectiveType, saveData } = useNetworkState();
|
|
30
|
+
* return (
|
|
31
|
+
* <div>
|
|
32
|
+
* Status: {online ? 'Online' : 'Offline'}
|
|
33
|
+
* {effectiveType && ` (${effectiveType})`}
|
|
34
|
+
* {saveData && ' - Reduced data mode'}
|
|
35
|
+
* </div>
|
|
36
|
+
* );
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function useNetworkState(): NetworkState;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type PreferredTheme = 'light' | 'dark' | 'no-preference';
|
|
2
|
+
/**
|
|
3
|
+
* A Preact hook that returns the user's preferred color scheme based on the
|
|
4
|
+
* `prefers-color-scheme` media query. Updates reactively when the user changes
|
|
5
|
+
* their system or browser theme preference.
|
|
6
|
+
*
|
|
7
|
+
* @returns The preferred theme: 'light', 'dark', or 'no-preference'
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* function ThemeAwareComponent() {
|
|
12
|
+
* const theme = usePreferredTheme();
|
|
13
|
+
* return (
|
|
14
|
+
* <div data-theme={theme}>
|
|
15
|
+
* Current preference: {theme}
|
|
16
|
+
* </div>
|
|
17
|
+
* );
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function usePreferredTheme(): PreferredTheme;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ComponentChildren } from 'preact';
|
|
2
|
+
export type InjectableProps = Record<string, any>;
|
|
3
|
+
/**
|
|
4
|
+
* A Preact hook to wrap children components and inject additional props into them.
|
|
5
|
+
* @param children - The children to wrap and enhance with props.
|
|
6
|
+
* @param injectProps - The props to inject into each child component.
|
|
7
|
+
* @param mergeStrategy - How to handle prop conflicts ('override' | 'preserve'). Defaults to 'preserve'.
|
|
8
|
+
* @returns Enhanced children with injected props.
|
|
9
|
+
*/
|
|
10
|
+
export declare function useWrappedChildren(children: ComponentChildren, injectProps: InjectableProps, mergeStrategy?: 'override' | 'preserve'): ComponentChildren;
|
package/package.json
CHANGED
|
@@ -1,48 +1,64 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "preact-missing-hooks",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "A lightweight, extendable collection of missing React-like hooks for Preact — plus fresh, powerful new ones designed specifically for modern Preact apps.",
|
|
5
|
-
"author": "Prakhar Dubey",
|
|
6
|
-
"license": "
|
|
7
|
-
"main": "dist/index.js",
|
|
8
|
-
"module": "dist/index.module.js",
|
|
9
|
-
"types": "dist/index.d.ts",
|
|
10
|
-
"source": "src/index.ts",
|
|
11
|
-
"exports": {
|
|
12
|
-
".": {
|
|
13
|
-
"import": "./dist/index.module.js",
|
|
14
|
-
"require": "./dist/index.js",
|
|
15
|
-
"types": "./dist/index.d.ts"
|
|
16
|
-
},
|
|
17
|
-
"./useTransition": {
|
|
18
|
-
"import": "./dist/useTransition.module.js",
|
|
19
|
-
"require": "./dist/useTransition.js",
|
|
20
|
-
"types": "./dist/useTransition.d.ts"
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
"scripts": {
|
|
24
|
-
"build": "microbundle",
|
|
25
|
-
"dev": "microbundle watch",
|
|
26
|
-
"prepublishOnly": "npm run build",
|
|
27
|
-
"test": "
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "preact-missing-hooks",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "A lightweight, extendable collection of missing React-like hooks for Preact — plus fresh, powerful new ones designed specifically for modern Preact apps.",
|
|
5
|
+
"author": "Prakhar Dubey",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"module": "dist/index.module.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"source": "src/index.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.module.js",
|
|
14
|
+
"require": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts"
|
|
16
|
+
},
|
|
17
|
+
"./useTransition": {
|
|
18
|
+
"import": "./dist/useTransition.module.js",
|
|
19
|
+
"require": "./dist/useTransition.js",
|
|
20
|
+
"types": "./dist/useTransition.d.ts"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "microbundle",
|
|
25
|
+
"dev": "microbundle watch",
|
|
26
|
+
"prepublishOnly": "npm run build",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"type-check": "tsc --noEmit"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"preact",
|
|
32
|
+
"hooks",
|
|
33
|
+
"usetransition",
|
|
34
|
+
"useMutationObserver",
|
|
35
|
+
"useEventBus",
|
|
36
|
+
"useWrappedChildren",
|
|
37
|
+
"usePreferredTheme",
|
|
38
|
+
"useNetworkState",
|
|
39
|
+
"useClipboard",
|
|
40
|
+
"react-hooks",
|
|
41
|
+
"microbundle",
|
|
42
|
+
"typescript",
|
|
43
|
+
"preact-hooks"
|
|
44
|
+
],
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/prakhardubey2002/Preact-Missing-Hooks"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"preact": ">=10.0.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
54
|
+
"@testing-library/preact": "^3.2.4",
|
|
55
|
+
"@types/jest": "^29.5.14",
|
|
56
|
+
"jsdom": "^26.1.0",
|
|
57
|
+
"microbundle": "^0.15.1",
|
|
58
|
+
"typescript": "^5.8.3",
|
|
59
|
+
"vitest": "^3.1.4"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"preact": ">=10.0.0"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
-
export * from './useTransition'
|
|
1
|
+
export * from './useTransition'
|
|
2
|
+
export * from './useMutationObserver'
|
|
3
|
+
export * from './useEventBus'
|
|
4
|
+
export * from './useWrappedChildren'
|
|
5
|
+
export * from './usePreferredTheme'
|
|
6
|
+
export * from './useNetworkState'
|
|
7
|
+
export * from './useClipboard'
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useCallback, useState } from 'preact/hooks';
|
|
2
|
+
|
|
3
|
+
export interface UseClipboardOptions {
|
|
4
|
+
/** Duration in ms to keep `copied` true before resetting. Default: 2000 */
|
|
5
|
+
resetDelay?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface UseClipboardReturn {
|
|
9
|
+
/** Copy text to the clipboard. Returns true on success. */
|
|
10
|
+
copy: (text: string) => Promise<boolean>;
|
|
11
|
+
/** Read text from the clipboard. Returns empty string if denied or unavailable. */
|
|
12
|
+
paste: () => Promise<string>;
|
|
13
|
+
/** Whether the last copy operation succeeded (resets after resetDelay) */
|
|
14
|
+
copied: boolean;
|
|
15
|
+
/** Error from the last failed operation, or null */
|
|
16
|
+
error: Error | null;
|
|
17
|
+
/** Manually reset copied and error state */
|
|
18
|
+
reset: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A Preact hook for reading and writing the clipboard. Uses the async
|
|
23
|
+
* Clipboard API when available (requires secure context and user gesture).
|
|
24
|
+
*
|
|
25
|
+
* @param options - Optional configuration (e.g., resetDelay for copied state)
|
|
26
|
+
* @returns Object with copy, paste, copied, error, and reset
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* function CopyButton() {
|
|
31
|
+
* const { copy, copied, error } = useClipboard();
|
|
32
|
+
* return (
|
|
33
|
+
* <button onClick={() => copy('Hello!')}>
|
|
34
|
+
* {copied ? 'Copied!' : 'Copy'}
|
|
35
|
+
* </button>
|
|
36
|
+
* );
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function useClipboard(options: UseClipboardOptions = {}): UseClipboardReturn {
|
|
41
|
+
const { resetDelay = 2000 } = options;
|
|
42
|
+
|
|
43
|
+
const [copied, setCopied] = useState(false);
|
|
44
|
+
const [error, setError] = useState<Error | null>(null);
|
|
45
|
+
|
|
46
|
+
const reset = useCallback(() => {
|
|
47
|
+
setCopied(false);
|
|
48
|
+
setError(null);
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const copy = useCallback(
|
|
52
|
+
async (text: string): Promise<boolean> => {
|
|
53
|
+
setError(null);
|
|
54
|
+
|
|
55
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard) {
|
|
56
|
+
const err = new Error('Clipboard API is not available');
|
|
57
|
+
setError(err);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await navigator.clipboard.writeText(text);
|
|
63
|
+
setCopied(true);
|
|
64
|
+
if (resetDelay > 0) {
|
|
65
|
+
setTimeout(() => setCopied(false), resetDelay);
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
70
|
+
setError(err);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
[resetDelay]
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const paste = useCallback(async (): Promise<string> => {
|
|
78
|
+
setError(null);
|
|
79
|
+
|
|
80
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard) {
|
|
81
|
+
const err = new Error('Clipboard API is not available');
|
|
82
|
+
setError(err);
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const text = await navigator.clipboard.readText();
|
|
88
|
+
return text;
|
|
89
|
+
} catch (e) {
|
|
90
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
91
|
+
setError(err);
|
|
92
|
+
return '';
|
|
93
|
+
}
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
return { copy, paste, copied, error, reset };
|
|
97
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useCallback, useEffect } from 'preact/hooks';
|
|
2
|
+
|
|
3
|
+
type EventMap = Record<string, (...args: any[]) => void>;
|
|
4
|
+
|
|
5
|
+
const listeners = new Map<string, Set<(...args: any[]) => void>>();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A Preact hook to publish and subscribe to custom events across components.
|
|
9
|
+
* @returns An object with `emit` and `on` methods.
|
|
10
|
+
*/
|
|
11
|
+
export function useEventBus<T extends EventMap>() {
|
|
12
|
+
const emit = useCallback(<K extends keyof T>(event: K, ...args: Parameters<T[K]>) => {
|
|
13
|
+
const handlers = listeners.get(event as string);
|
|
14
|
+
if (handlers) {
|
|
15
|
+
handlers.forEach((handler) => handler(...args));
|
|
16
|
+
}
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
const on = useCallback(<K extends keyof T>(event: K, handler: T[K]) => {
|
|
20
|
+
let handlers = listeners.get(event as string);
|
|
21
|
+
if (!handlers) {
|
|
22
|
+
handlers = new Set();
|
|
23
|
+
listeners.set(event as string, handlers);
|
|
24
|
+
}
|
|
25
|
+
handlers.add(handler);
|
|
26
|
+
|
|
27
|
+
return () => {
|
|
28
|
+
handlers!.delete(handler);
|
|
29
|
+
if (handlers!.size === 0) {
|
|
30
|
+
listeners.delete(event as string);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
return { emit, on };
|
|
36
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { RefObject } from 'preact'
|
|
2
|
+
import { useEffect } from 'preact/hooks'
|
|
3
|
+
|
|
4
|
+
export type UseMutationObserverOptions = MutationObserverInit
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A Preact hook to observe DOM mutations using MutationObserver.
|
|
8
|
+
* @param target - The element to observe.
|
|
9
|
+
* @param callback - Function to call on mutation.
|
|
10
|
+
* @param options - MutationObserver options.
|
|
11
|
+
*/
|
|
12
|
+
export function useMutationObserver(
|
|
13
|
+
targetRef: RefObject<HTMLElement | null>,
|
|
14
|
+
callback: MutationCallback,
|
|
15
|
+
options: MutationObserverInit
|
|
16
|
+
) {
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const node = targetRef.current
|
|
19
|
+
if (!node) return
|
|
20
|
+
|
|
21
|
+
const observer = new MutationObserver(callback)
|
|
22
|
+
observer.observe(node, options)
|
|
23
|
+
|
|
24
|
+
return () => observer.disconnect()
|
|
25
|
+
}, [targetRef, callback, options])
|
|
26
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { useEffect, useState } from 'preact/hooks';
|
|
2
|
+
|
|
3
|
+
/** Network Information API (not in all browsers) */
|
|
4
|
+
interface NetworkInformation extends EventTarget {
|
|
5
|
+
effectiveType?: string;
|
|
6
|
+
downlink?: number;
|
|
7
|
+
rtt?: number;
|
|
8
|
+
saveData?: boolean;
|
|
9
|
+
type?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Effective connection type from Network Information API */
|
|
13
|
+
export type EffectiveConnectionType = 'slow-2g' | '2g' | '3g' | '4g';
|
|
14
|
+
|
|
15
|
+
/** Network connection type (e.g., wifi, cellular) */
|
|
16
|
+
export type ConnectionType =
|
|
17
|
+
| 'bluetooth'
|
|
18
|
+
| 'cellular'
|
|
19
|
+
| 'ethernet'
|
|
20
|
+
| 'mixed'
|
|
21
|
+
| 'none'
|
|
22
|
+
| 'other'
|
|
23
|
+
| 'unknown'
|
|
24
|
+
| 'wifi';
|
|
25
|
+
|
|
26
|
+
export interface NetworkState {
|
|
27
|
+
/** Whether the browser is online */
|
|
28
|
+
online: boolean;
|
|
29
|
+
/** Effective connection type (when supported) */
|
|
30
|
+
effectiveType?: EffectiveConnectionType;
|
|
31
|
+
/** Estimated downlink speed in Mbps (when supported) */
|
|
32
|
+
downlink?: number;
|
|
33
|
+
/** Estimated round-trip time in ms (when supported) */
|
|
34
|
+
rtt?: number;
|
|
35
|
+
/** Whether the user has requested reduced data usage (when supported) */
|
|
36
|
+
saveData?: boolean;
|
|
37
|
+
/** Connection type (when supported) */
|
|
38
|
+
connectionType?: ConnectionType;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getNetworkState(): NetworkState {
|
|
42
|
+
if (typeof navigator === 'undefined') {
|
|
43
|
+
return { online: true };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const state: NetworkState = {
|
|
47
|
+
online: navigator.onLine,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const connection =
|
|
51
|
+
(navigator as Navigator & { connection?: NetworkInformation }).connection;
|
|
52
|
+
|
|
53
|
+
if (connection) {
|
|
54
|
+
if (connection.effectiveType !== undefined) {
|
|
55
|
+
state.effectiveType = connection.effectiveType as EffectiveConnectionType;
|
|
56
|
+
}
|
|
57
|
+
if (connection.downlink !== undefined) {
|
|
58
|
+
state.downlink = connection.downlink;
|
|
59
|
+
}
|
|
60
|
+
if (connection.rtt !== undefined) {
|
|
61
|
+
state.rtt = connection.rtt;
|
|
62
|
+
}
|
|
63
|
+
if (connection.saveData !== undefined) {
|
|
64
|
+
state.saveData = connection.saveData;
|
|
65
|
+
}
|
|
66
|
+
if (connection.type !== undefined) {
|
|
67
|
+
state.connectionType = connection.type as ConnectionType;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return state;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A Preact hook that returns the current network state, including online/offline
|
|
76
|
+
* status and (when supported) connection type, downlink, RTT, and save-data preference.
|
|
77
|
+
* Updates reactively when the network state changes.
|
|
78
|
+
*
|
|
79
|
+
* @returns The current network state object
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```tsx
|
|
83
|
+
* function NetworkStatus() {
|
|
84
|
+
* const { online, effectiveType, saveData } = useNetworkState();
|
|
85
|
+
* return (
|
|
86
|
+
* <div>
|
|
87
|
+
* Status: {online ? 'Online' : 'Offline'}
|
|
88
|
+
* {effectiveType && ` (${effectiveType})`}
|
|
89
|
+
* {saveData && ' - Reduced data mode'}
|
|
90
|
+
* </div>
|
|
91
|
+
* );
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function useNetworkState(): NetworkState {
|
|
96
|
+
const [state, setState] = useState<NetworkState>(getNetworkState);
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (typeof window === 'undefined') return;
|
|
100
|
+
|
|
101
|
+
const updateState = () => setState(getNetworkState());
|
|
102
|
+
|
|
103
|
+
window.addEventListener('online', updateState);
|
|
104
|
+
window.addEventListener('offline', updateState);
|
|
105
|
+
|
|
106
|
+
const connection = (navigator as Navigator & { connection?: NetworkInformation })
|
|
107
|
+
.connection;
|
|
108
|
+
if (connection?.addEventListener) {
|
|
109
|
+
connection.addEventListener('change', updateState);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return () => {
|
|
113
|
+
window.removeEventListener('online', updateState);
|
|
114
|
+
window.removeEventListener('offline', updateState);
|
|
115
|
+
if (connection?.removeEventListener) {
|
|
116
|
+
connection.removeEventListener('change', updateState);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
return state;
|
|
122
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useEffect, useState } from 'preact/hooks';
|
|
2
|
+
|
|
3
|
+
export type PreferredTheme = 'light' | 'dark' | 'no-preference';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A Preact hook that returns the user's preferred color scheme based on the
|
|
7
|
+
* `prefers-color-scheme` media query. Updates reactively when the user changes
|
|
8
|
+
* their system or browser theme preference.
|
|
9
|
+
*
|
|
10
|
+
* @returns The preferred theme: 'light', 'dark', or 'no-preference'
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* function ThemeAwareComponent() {
|
|
15
|
+
* const theme = usePreferredTheme();
|
|
16
|
+
* return (
|
|
17
|
+
* <div data-theme={theme}>
|
|
18
|
+
* Current preference: {theme}
|
|
19
|
+
* </div>
|
|
20
|
+
* );
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function usePreferredTheme(): PreferredTheme {
|
|
25
|
+
const [theme, setTheme] = useState<PreferredTheme>(() => {
|
|
26
|
+
if (typeof window === 'undefined') return 'no-preference';
|
|
27
|
+
|
|
28
|
+
const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
29
|
+
const lightQuery = window.matchMedia('(prefers-color-scheme: light)');
|
|
30
|
+
|
|
31
|
+
if (darkQuery.matches) return 'dark';
|
|
32
|
+
if (lightQuery.matches) return 'light';
|
|
33
|
+
return 'no-preference';
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (typeof window === 'undefined') return;
|
|
38
|
+
|
|
39
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
40
|
+
|
|
41
|
+
const handleChange = (e: MediaQueryListEvent) => {
|
|
42
|
+
setTheme(e.matches ? 'dark' : 'light');
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Re-check in case of no-preference (some browsers don't support light query)
|
|
46
|
+
const updateTheme = () => {
|
|
47
|
+
const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
48
|
+
const lightQuery = window.matchMedia('(prefers-color-scheme: light)');
|
|
49
|
+
|
|
50
|
+
if (darkQuery.matches) setTheme('dark');
|
|
51
|
+
else if (lightQuery.matches) setTheme('light');
|
|
52
|
+
else setTheme('no-preference');
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
mediaQuery.addEventListener('change', handleChange);
|
|
56
|
+
|
|
57
|
+
// Fallback: some environments may not fire change, so we also listen for light
|
|
58
|
+
const lightQuery = window.matchMedia('(prefers-color-scheme: light)');
|
|
59
|
+
lightQuery.addEventListener('change', updateTheme);
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
mediaQuery.removeEventListener('change', handleChange);
|
|
63
|
+
lightQuery.removeEventListener('change', updateTheme);
|
|
64
|
+
};
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
return theme;
|
|
68
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ComponentChildren, cloneElement, isValidElement } from 'preact'
|
|
2
|
+
import { useMemo } from 'preact/hooks'
|
|
3
|
+
|
|
4
|
+
export type InjectableProps = Record<string, any>
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A Preact hook to wrap children components and inject additional props into them.
|
|
8
|
+
* @param children - The children to wrap and enhance with props.
|
|
9
|
+
* @param injectProps - The props to inject into each child component.
|
|
10
|
+
* @param mergeStrategy - How to handle prop conflicts ('override' | 'preserve'). Defaults to 'preserve'.
|
|
11
|
+
* @returns Enhanced children with injected props.
|
|
12
|
+
*/
|
|
13
|
+
export function useWrappedChildren(
|
|
14
|
+
children: ComponentChildren,
|
|
15
|
+
injectProps: InjectableProps,
|
|
16
|
+
mergeStrategy: 'override' | 'preserve' = 'preserve'
|
|
17
|
+
): ComponentChildren {
|
|
18
|
+
return useMemo(() => {
|
|
19
|
+
if (!children) return children
|
|
20
|
+
|
|
21
|
+
const enhanceChild = (child: any): any => {
|
|
22
|
+
if (!isValidElement(child)) return child
|
|
23
|
+
|
|
24
|
+
const existingProps = child.props || {}
|
|
25
|
+
|
|
26
|
+
let mergedProps: InjectableProps
|
|
27
|
+
|
|
28
|
+
if (mergeStrategy === 'override') {
|
|
29
|
+
// Injected props override existing ones
|
|
30
|
+
mergedProps = { ...existingProps, ...injectProps }
|
|
31
|
+
} else {
|
|
32
|
+
// Existing props are preserved, injected props are added only if not present
|
|
33
|
+
mergedProps = { ...injectProps, ...existingProps }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Special handling for style prop to merge style objects properly
|
|
37
|
+
const existingStyle = (existingProps as any)?.style
|
|
38
|
+
const injectStyle = (injectProps as any)?.style
|
|
39
|
+
|
|
40
|
+
if (existingStyle && injectStyle &&
|
|
41
|
+
typeof existingStyle === 'object' && typeof injectStyle === 'object') {
|
|
42
|
+
if (mergeStrategy === 'override') {
|
|
43
|
+
(mergedProps as any).style = { ...existingStyle, ...injectStyle }
|
|
44
|
+
} else {
|
|
45
|
+
(mergedProps as any).style = { ...injectStyle, ...existingStyle }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return cloneElement(child, mergedProps)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (Array.isArray(children)) {
|
|
53
|
+
return children.map(enhanceChild)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return enhanceChild(children)
|
|
57
|
+
}, [children, injectProps, mergeStrategy])
|
|
58
|
+
}
|