preact-missing-hooks 1.1.0 → 1.2.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/test-hooks.yml +4 -1
- package/Readme.md +50 -20
- package/dist/index.d.ts +1 -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/useRageClick.d.ts +37 -0
- package/package.json +67 -64
- package/src/index.ts +8 -7
- package/src/useRageClick.ts +103 -0
- package/tests/useRageClick.test.tsx +145 -0
package/Readme.md
CHANGED
|
@@ -4,9 +4,6 @@
|
|
|
4
4
|
<a href="https://www.npmjs.com/package/preact-missing-hooks">
|
|
5
5
|
<img src="https://img.shields.io/npm/v/preact-missing-hooks?color=crimson&label=npm%20version" alt="npm version" />
|
|
6
6
|
</a>
|
|
7
|
-
<a href="https://www.npmjs.com/package/preact-missing-hooks">
|
|
8
|
-
<img src="https://img.shields.io/npm/dm/preact-missing-hooks?label=monthly%20downloads" alt="monthly downloads" />
|
|
9
|
-
</a>
|
|
10
7
|
<a href="https://www.npmjs.com/package/preact-missing-hooks">
|
|
11
8
|
<img src="https://img.shields.io/npm/dt/preact-missing-hooks?label=total%20downloads" alt="total downloads" />
|
|
12
9
|
</a>
|
|
@@ -16,26 +13,29 @@
|
|
|
16
13
|
</a>
|
|
17
14
|
</p>
|
|
18
15
|
|
|
19
|
-
|
|
16
|
+
If this package helps you, please consider dropping a star on the [GitHub repo](https://github.com/prakhardubey2002/Preact-Missing-Hooks).
|
|
17
|
+
|
|
18
|
+
A lightweight, extendable collection of React-like hooks for Preact, including utilities for transitions, DOM mutation observation, global event buses, theme detection, network status, clipboard access, and rage-click detection (e.g. for Sentry).
|
|
20
19
|
|
|
21
20
|
---
|
|
22
21
|
|
|
23
|
-
##
|
|
22
|
+
## Features
|
|
24
23
|
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
24
|
+
- **`useTransition`** — Defers state updates to yield a smoother UI experience.
|
|
25
|
+
- **`useMutationObserver`** — Reactively observes DOM changes with a familiar hook API.
|
|
26
|
+
- **`useEventBus`** — A simple publish/subscribe system, eliminating props drilling or overuse of context.
|
|
27
|
+
- **`useWrappedChildren`** — Injects props into child components with flexible merging strategies.
|
|
28
|
+
- **`usePreferredTheme`** — Detects the user's preferred color scheme (light/dark) from system preferences.
|
|
29
|
+
- **`useNetworkState`** — Tracks online/offline status and connection details (type, downlink, RTT, save-data).
|
|
30
|
+
- **`useClipboard`** — Copy and paste text with the Clipboard API, with copied/error state.
|
|
31
|
+
- **`useRageClick`** — Detects rage clicks (repeated rapid clicks in the same spot). Use with Sentry or similar to detect and fix rage-click issues and lower rage-click-related support.
|
|
32
|
+
- Fully TypeScript compatible
|
|
33
|
+
- Bundled with Microbundle
|
|
34
|
+
- Zero dependencies (except `preact`)
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
38
|
-
##
|
|
38
|
+
## Installation
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
41
|
npm install preact-missing-hooks
|
|
@@ -43,7 +43,7 @@ npm install preact-missing-hooks
|
|
|
43
43
|
|
|
44
44
|
---
|
|
45
45
|
|
|
46
|
-
##
|
|
46
|
+
## Usage Examples
|
|
47
47
|
|
|
48
48
|
### `useTransition`
|
|
49
49
|
|
|
@@ -242,7 +242,37 @@ function PasteInput() {
|
|
|
242
242
|
|
|
243
243
|
---
|
|
244
244
|
|
|
245
|
-
|
|
245
|
+
### `useRageClick`
|
|
246
|
+
|
|
247
|
+
Detects rage clicks (multiple rapid clicks in the same area), e.g. when the UI is unresponsive. Report them to [Sentry](https://docs.sentry.io/product/issues/issue-details/replay-issues/rage-clicks/) or your error tracker to surface rage-click issues and lower rage-click-related support.
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
import { useRef } from 'preact/hooks'
|
|
251
|
+
import { useRageClick } from 'preact-missing-hooks'
|
|
252
|
+
|
|
253
|
+
function SubmitButton() {
|
|
254
|
+
const ref = useRef<HTMLButtonElement>(null)
|
|
255
|
+
|
|
256
|
+
useRageClick(ref, {
|
|
257
|
+
onRageClick: ({ count, event }) => {
|
|
258
|
+
// Report to Sentry (or your error tracker) to create rage-click issues
|
|
259
|
+
Sentry.captureMessage('Rage click detected', {
|
|
260
|
+
level: 'warning',
|
|
261
|
+
extra: { count, target: event.target, tag: 'rage_click' },
|
|
262
|
+
})
|
|
263
|
+
},
|
|
264
|
+
threshold: 5, // min clicks (default 5, Sentry-style)
|
|
265
|
+
timeWindow: 1000, // ms (default 1000)
|
|
266
|
+
distanceThreshold: 30, // px (default 30)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
return <button ref={ref}>Submit</button>
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Built With
|
|
246
276
|
|
|
247
277
|
- [Preact](https://preactjs.com)
|
|
248
278
|
- [Microbundle](https://github.com/developit/microbundle)
|
|
@@ -251,12 +281,12 @@ function PasteInput() {
|
|
|
251
281
|
|
|
252
282
|
---
|
|
253
283
|
|
|
254
|
-
##
|
|
284
|
+
## License
|
|
255
285
|
|
|
256
286
|
MIT © [Prakhar Dubey](https://github.com/prakhardubey2002)
|
|
257
287
|
|
|
258
288
|
---
|
|
259
289
|
|
|
260
|
-
##
|
|
290
|
+
## Contributing
|
|
261
291
|
|
|
262
292
|
Contributions are welcome! Please open issues or submit PRs with new hooks or improvements.
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var e=require("preact/hooks"),
|
|
1
|
+
var e=require("preact/hooks"),n=require("preact"),r=new Map;function t(){return t=Object.assign?Object.assign.bind():function(e){for(var n=1;n<arguments.length;n++){var r=arguments[n];for(var t in r)({}).hasOwnProperty.call(r,t)&&(e[t]=r[t])}return e},t.apply(null,arguments)}function i(){if("undefined"==typeof navigator)return{online:!0};var e={online:navigator.onLine},n=navigator.connection;return n&&(void 0!==n.effectiveType&&(e.effectiveType=n.effectiveType),void 0!==n.downlink&&(e.downlink=n.downlink),void 0!==n.rtt&&(e.rtt=n.rtt),void 0!==n.saveData&&(e.saveData=n.saveData),void 0!==n.type&&(e.connectionType=n.type)),e}function o(e,n){try{var r=e()}catch(e){return n(e)}return r&&r.then?r.then(void 0,n):r}exports.useClipboard=function(n){void 0===n&&(n={});var r=n.resetDelay,t=void 0===r?2e3:r,i=e.useState(!1),a=i[0],u=i[1],c=e.useState(null),s=c[0],f=c[1],v=e.useCallback(function(){u(!1),f(null)},[]);return{copy:e.useCallback(function(e){try{if(f(null),"undefined"==typeof navigator||!navigator.clipboard){var n=new Error("Clipboard API is not available");return f(n),Promise.resolve(!1)}return Promise.resolve(o(function(){return Promise.resolve(navigator.clipboard.writeText(e)).then(function(){return u(!0),t>0&&setTimeout(function(){return u(!1)},t),!0})},function(e){var n=e instanceof Error?e:new Error(String(e));return f(n),!1}))}catch(e){return Promise.reject(e)}},[t]),paste:e.useCallback(function(){try{if(f(null),"undefined"==typeof navigator||!navigator.clipboard){var e=new Error("Clipboard API is not available");return f(e),Promise.resolve("")}return Promise.resolve(o(function(){return Promise.resolve(navigator.clipboard.readText())},function(e){var n=e instanceof Error?e:new Error(String(e));return f(n),""}))}catch(e){return Promise.reject(e)}},[]),copied:a,error:s,reset:v}},exports.useEventBus=function(){return{emit:e.useCallback(function(e){var n=arguments,t=r.get(e);t&&t.forEach(function(e){return e.apply(void 0,[].slice.call(n,1))})},[]),on:e.useCallback(function(e,n){var t=r.get(e);return t||(t=new Set,r.set(e,t)),t.add(n),function(){t.delete(n),0===t.size&&r.delete(e)}},[])}},exports.useMutationObserver=function(n,r,t){e.useEffect(function(){var e=n.current;if(e){var i=new MutationObserver(r);return i.observe(e,t),function(){return i.disconnect()}}},[n,r,t])},exports.useNetworkState=function(){var n=e.useState(i),r=n[0],t=n[1];return e.useEffect(function(){if("undefined"!=typeof window){var e=function(){return t(i())};window.addEventListener("online",e),window.addEventListener("offline",e);var n=navigator.connection;return null!=n&&n.addEventListener&&n.addEventListener("change",e),function(){window.removeEventListener("online",e),window.removeEventListener("offline",e),null!=n&&n.removeEventListener&&n.removeEventListener("change",e)}}},[]),r},exports.usePreferredTheme=function(){var n=e.useState(function(){if("undefined"==typeof window)return"no-preference";var e=window.matchMedia("(prefers-color-scheme: dark)"),n=window.matchMedia("(prefers-color-scheme: light)");return e.matches?"dark":n.matches?"light":"no-preference"}),r=n[0],t=n[1];return e.useEffect(function(){if("undefined"!=typeof window){var e=window.matchMedia("(prefers-color-scheme: dark)"),n=function(e){t(e.matches?"dark":"light")},r=function(){var e=window.matchMedia("(prefers-color-scheme: dark)"),n=window.matchMedia("(prefers-color-scheme: light)");t(e.matches?"dark":n.matches?"light":"no-preference")};e.addEventListener("change",n);var i=window.matchMedia("(prefers-color-scheme: light)");return i.addEventListener("change",r),function(){e.removeEventListener("change",n),i.removeEventListener("change",r)}}},[]),r},exports.useRageClick=function(n,r){var t=r.onRageClick,i=r.threshold,o=void 0===i?5:i,a=r.timeWindow,u=void 0===a?1e3:a,c=r.distanceThreshold,s=void 0===c?30:c,f=e.useRef(t);f.current=t;var v=e.useRef([]);e.useEffect(function(){var e=n.current;if(e){var r=function(e){var n=Date.now(),r={time:n,x:e.clientX,y:e.clientY},t=n-u,i=v.current.filter(function(e){return e.time>=t});if(i.push(r),Infinity!==s){var a=i.filter(function(e){return n=e,t=r,Math.hypot(t.x-n.x,t.y-n.y)<=s;var n,t});if(a.length>=o)return f.current({count:a.length,event:e}),void(v.current=[])}else if(i.length>=o)return f.current({count:i.length,event:e}),void(v.current=[]);v.current=i};return e.addEventListener("click",r),function(){return e.removeEventListener("click",r)}}},[n,o,u,s])},exports.useTransition=function(){var n=e.useState(!1),r=n[0],t=n[1];return[e.useCallback(function(e){t(!0),Promise.resolve().then(function(){e(),t(!1)})},[]),r]},exports.useWrappedChildren=function(r,i,o){return void 0===o&&(o="preserve"),e.useMemo(function(){if(!r)return r;var e=function(e){if(!n.isValidElement(e))return e;var r,a=e.props||{};r="override"===o?t({},a,i):t({},i,a);var u=null==a?void 0:a.style,c=null==i?void 0:i.style;return u&&c&&"object"==typeof u&&"object"==typeof c&&(r.style="override"===o?t({},u,c):t({},c,u)),n.cloneElement(e,r)};return Array.isArray(r)?r.map(e):e(r)},[r,i,o])};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/useEventBus.ts","../src/useNetworkState.ts","../src/useClipboard.ts","../src/useMutationObserver.ts","../src/usePreferredTheme.ts","../src/useTransition.ts","../src/useWrappedChildren.ts"],"sourcesContent":["import { useCallback, useEffect } from 'preact/hooks';\r\n\r\ntype EventMap = Record<string, (...args: any[]) => void>;\r\n\r\nconst listeners = new Map<string, Set<(...args: any[]) => void>>();\r\n\r\n/**\r\n * A Preact hook to publish and subscribe to custom events across components.\r\n * @returns An object with `emit` and `on` methods.\r\n */\r\nexport function useEventBus<T extends EventMap>() {\r\n const emit = useCallback(<K extends keyof T>(event: K, ...args: Parameters<T[K]>) => {\r\n const handlers = listeners.get(event as string);\r\n if (handlers) {\r\n handlers.forEach((handler) => handler(...args));\r\n }\r\n }, []);\r\n\r\n const on = useCallback(<K extends keyof T>(event: K, handler: T[K]) => {\r\n let handlers = listeners.get(event as string);\r\n if (!handlers) {\r\n handlers = new Set();\r\n listeners.set(event as string, handlers);\r\n }\r\n handlers.add(handler);\r\n\r\n return () => {\r\n handlers!.delete(handler);\r\n if (handlers!.size === 0) {\r\n listeners.delete(event as string);\r\n }\r\n };\r\n }, []);\r\n\r\n return { emit, on };\r\n}\r\n","import { useEffect, useState } from 'preact/hooks';\r\n\r\n/** Network Information API (not in all browsers) */\r\ninterface NetworkInformation extends EventTarget {\r\n effectiveType?: string;\r\n downlink?: number;\r\n rtt?: number;\r\n saveData?: boolean;\r\n type?: string;\r\n}\r\n\r\n/** Effective connection type from Network Information API */\r\nexport type EffectiveConnectionType = 'slow-2g' | '2g' | '3g' | '4g';\r\n\r\n/** Network connection type (e.g., wifi, cellular) */\r\nexport type ConnectionType =\r\n | 'bluetooth'\r\n | 'cellular'\r\n | 'ethernet'\r\n | 'mixed'\r\n | 'none'\r\n | 'other'\r\n | 'unknown'\r\n | 'wifi';\r\n\r\nexport interface NetworkState {\r\n /** Whether the browser is online */\r\n online: boolean;\r\n /** Effective connection type (when supported) */\r\n effectiveType?: EffectiveConnectionType;\r\n /** Estimated downlink speed in Mbps (when supported) */\r\n downlink?: number;\r\n /** Estimated round-trip time in ms (when supported) */\r\n rtt?: number;\r\n /** Whether the user has requested reduced data usage (when supported) */\r\n saveData?: boolean;\r\n /** Connection type (when supported) */\r\n connectionType?: ConnectionType;\r\n}\r\n\r\nfunction getNetworkState(): NetworkState {\r\n if (typeof navigator === 'undefined') {\r\n return { online: true };\r\n }\r\n\r\n const state: NetworkState = {\r\n online: navigator.onLine,\r\n };\r\n\r\n const connection =\r\n (navigator as Navigator & { connection?: NetworkInformation }).connection;\r\n\r\n if (connection) {\r\n if (connection.effectiveType !== undefined) {\r\n state.effectiveType = connection.effectiveType as EffectiveConnectionType;\r\n }\r\n if (connection.downlink !== undefined) {\r\n state.downlink = connection.downlink;\r\n }\r\n if (connection.rtt !== undefined) {\r\n state.rtt = connection.rtt;\r\n }\r\n if (connection.saveData !== undefined) {\r\n state.saveData = connection.saveData;\r\n }\r\n if (connection.type !== undefined) {\r\n state.connectionType = connection.type as ConnectionType;\r\n }\r\n }\r\n\r\n return state;\r\n}\r\n\r\n/**\r\n * A Preact hook that returns the current network state, including online/offline\r\n * status and (when supported) connection type, downlink, RTT, and save-data preference.\r\n * Updates reactively when the network state changes.\r\n *\r\n * @returns The current network state object\r\n *\r\n * @example\r\n * ```tsx\r\n * function NetworkStatus() {\r\n * const { online, effectiveType, saveData } = useNetworkState();\r\n * return (\r\n * <div>\r\n * Status: {online ? 'Online' : 'Offline'}\r\n * {effectiveType && ` (${effectiveType})`}\r\n * {saveData && ' - Reduced data mode'}\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function useNetworkState(): NetworkState {\r\n const [state, setState] = useState<NetworkState>(getNetworkState);\r\n\r\n useEffect(() => {\r\n if (typeof window === 'undefined') return;\r\n\r\n const updateState = () => setState(getNetworkState());\r\n\r\n window.addEventListener('online', updateState);\r\n window.addEventListener('offline', updateState);\r\n\r\n const connection = (navigator as Navigator & { connection?: NetworkInformation })\r\n .connection;\r\n if (connection?.addEventListener) {\r\n connection.addEventListener('change', updateState);\r\n }\r\n\r\n return () => {\r\n window.removeEventListener('online', updateState);\r\n window.removeEventListener('offline', updateState);\r\n if (connection?.removeEventListener) {\r\n connection.removeEventListener('change', updateState);\r\n }\r\n };\r\n }, []);\r\n\r\n return state;\r\n}\r\n","import { useCallback, useState } from 'preact/hooks';\r\n\r\nexport interface UseClipboardOptions {\r\n /** Duration in ms to keep `copied` true before resetting. Default: 2000 */\r\n resetDelay?: number;\r\n}\r\n\r\nexport interface UseClipboardReturn {\r\n /** Copy text to the clipboard. Returns true on success. */\r\n copy: (text: string) => Promise<boolean>;\r\n /** Read text from the clipboard. Returns empty string if denied or unavailable. */\r\n paste: () => Promise<string>;\r\n /** Whether the last copy operation succeeded (resets after resetDelay) */\r\n copied: boolean;\r\n /** Error from the last failed operation, or null */\r\n error: Error | null;\r\n /** Manually reset copied and error state */\r\n reset: () => void;\r\n}\r\n\r\n/**\r\n * A Preact hook for reading and writing the clipboard. Uses the async\r\n * Clipboard API when available (requires secure context and user gesture).\r\n *\r\n * @param options - Optional configuration (e.g., resetDelay for copied state)\r\n * @returns Object with copy, paste, copied, error, and reset\r\n *\r\n * @example\r\n * ```tsx\r\n * function CopyButton() {\r\n * const { copy, copied, error } = useClipboard();\r\n * return (\r\n * <button onClick={() => copy('Hello!')}>\r\n * {copied ? 'Copied!' : 'Copy'}\r\n * </button>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function useClipboard(options: UseClipboardOptions = {}): UseClipboardReturn {\r\n const { resetDelay = 2000 } = options;\r\n\r\n const [copied, setCopied] = useState(false);\r\n const [error, setError] = useState<Error | null>(null);\r\n\r\n const reset = useCallback(() => {\r\n setCopied(false);\r\n setError(null);\r\n }, []);\r\n\r\n const copy = useCallback(\r\n async (text: string): Promise<boolean> => {\r\n setError(null);\r\n\r\n if (typeof navigator === 'undefined' || !navigator.clipboard) {\r\n const err = new Error('Clipboard API is not available');\r\n setError(err);\r\n return false;\r\n }\r\n\r\n try {\r\n await navigator.clipboard.writeText(text);\r\n setCopied(true);\r\n if (resetDelay > 0) {\r\n setTimeout(() => setCopied(false), resetDelay);\r\n }\r\n return true;\r\n } catch (e) {\r\n const err = e instanceof Error ? e : new Error(String(e));\r\n setError(err);\r\n return false;\r\n }\r\n },\r\n [resetDelay]\r\n );\r\n\r\n const paste = useCallback(async (): Promise<string> => {\r\n setError(null);\r\n\r\n if (typeof navigator === 'undefined' || !navigator.clipboard) {\r\n const err = new Error('Clipboard API is not available');\r\n setError(err);\r\n return '';\r\n }\r\n\r\n try {\r\n const text = await navigator.clipboard.readText();\r\n return text;\r\n } catch (e) {\r\n const err = e instanceof Error ? e : new Error(String(e));\r\n setError(err);\r\n return '';\r\n }\r\n }, []);\r\n\r\n return { copy, paste, copied, error, reset };\r\n}\r\n","import { RefObject } from 'preact'\r\nimport { useEffect } from 'preact/hooks'\r\n\r\nexport type UseMutationObserverOptions = MutationObserverInit\r\n\r\n/**\r\n * A Preact hook to observe DOM mutations using MutationObserver.\r\n * @param target - The element to observe.\r\n * @param callback - Function to call on mutation.\r\n * @param options - MutationObserver options.\r\n */\r\nexport function useMutationObserver(\r\n targetRef: RefObject<HTMLElement | null>,\r\n callback: MutationCallback,\r\n options: MutationObserverInit\r\n) {\r\n useEffect(() => {\r\n const node = targetRef.current\r\n if (!node) return\r\n\r\n const observer = new MutationObserver(callback)\r\n observer.observe(node, options)\r\n\r\n return () => observer.disconnect()\r\n }, [targetRef, callback, options])\r\n}\r\n","import { useEffect, useState } from 'preact/hooks';\r\n\r\nexport type PreferredTheme = 'light' | 'dark' | 'no-preference';\r\n\r\n/**\r\n * A Preact hook that returns the user's preferred color scheme based on the\r\n * `prefers-color-scheme` media query. Updates reactively when the user changes\r\n * their system or browser theme preference.\r\n *\r\n * @returns The preferred theme: 'light', 'dark', or 'no-preference'\r\n *\r\n * @example\r\n * ```tsx\r\n * function ThemeAwareComponent() {\r\n * const theme = usePreferredTheme();\r\n * return (\r\n * <div data-theme={theme}>\r\n * Current preference: {theme}\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function usePreferredTheme(): PreferredTheme {\r\n const [theme, setTheme] = useState<PreferredTheme>(() => {\r\n if (typeof window === 'undefined') return 'no-preference';\r\n\r\n const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n const lightQuery = window.matchMedia('(prefers-color-scheme: light)');\r\n\r\n if (darkQuery.matches) return 'dark';\r\n if (lightQuery.matches) return 'light';\r\n return 'no-preference';\r\n });\r\n\r\n useEffect(() => {\r\n if (typeof window === 'undefined') return;\r\n\r\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n\r\n const handleChange = (e: MediaQueryListEvent) => {\r\n setTheme(e.matches ? 'dark' : 'light');\r\n };\r\n\r\n // Re-check in case of no-preference (some browsers don't support light query)\r\n const updateTheme = () => {\r\n const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n const lightQuery = window.matchMedia('(prefers-color-scheme: light)');\r\n\r\n if (darkQuery.matches) setTheme('dark');\r\n else if (lightQuery.matches) setTheme('light');\r\n else setTheme('no-preference');\r\n };\r\n\r\n mediaQuery.addEventListener('change', handleChange);\r\n\r\n // Fallback: some environments may not fire change, so we also listen for light\r\n const lightQuery = window.matchMedia('(prefers-color-scheme: light)');\r\n lightQuery.addEventListener('change', updateTheme);\r\n\r\n return () => {\r\n mediaQuery.removeEventListener('change', handleChange);\r\n lightQuery.removeEventListener('change', updateTheme);\r\n };\r\n }, []);\r\n\r\n return theme;\r\n}\r\n","import { useState, useCallback } from 'preact/hooks';\r\n\r\n/**\r\n * Mimics React's useTransition hook in Preact.\r\n * @returns [startTransition, isPending]\r\n */\r\nexport function useTransition(): [startTransition: (callback: () => void) => void, isPending: boolean] {\r\n const [isPending, setIsPending] = useState(false);\r\n\r\n const startTransition = useCallback((callback: () => void) => {\r\n setIsPending(true);\r\n Promise.resolve().then(() => {\r\n callback();\r\n setIsPending(false);\r\n });\r\n }, []);\r\n\r\n return [startTransition, isPending];\r\n}\r\n","import { ComponentChildren, cloneElement, isValidElement } from 'preact'\r\nimport { useMemo } from 'preact/hooks'\r\n\r\nexport type InjectableProps = Record<string, any>\r\n\r\n/**\r\n * A Preact hook to wrap children components and inject additional props into them.\r\n * @param children - The children to wrap and enhance with props.\r\n * @param injectProps - The props to inject into each child component.\r\n * @param mergeStrategy - How to handle prop conflicts ('override' | 'preserve'). Defaults to 'preserve'.\r\n * @returns Enhanced children with injected props.\r\n */\r\nexport function useWrappedChildren(\r\n children: ComponentChildren,\r\n injectProps: InjectableProps,\r\n mergeStrategy: 'override' | 'preserve' = 'preserve'\r\n): ComponentChildren {\r\n return useMemo(() => {\r\n if (!children) return children\r\n\r\n const enhanceChild = (child: any): any => {\r\n if (!isValidElement(child)) return child\r\n\r\n const existingProps = child.props || {}\r\n \r\n let mergedProps: InjectableProps\r\n \r\n if (mergeStrategy === 'override') {\r\n // Injected props override existing ones\r\n mergedProps = { ...existingProps, ...injectProps }\r\n } else {\r\n // Existing props are preserved, injected props are added only if not present\r\n mergedProps = { ...injectProps, ...existingProps }\r\n }\r\n\r\n // Special handling for style prop to merge style objects properly\r\n const existingStyle = (existingProps as any)?.style\r\n const injectStyle = (injectProps as any)?.style\r\n \r\n if (existingStyle && injectStyle && \r\n typeof existingStyle === 'object' && typeof injectStyle === 'object') {\r\n if (mergeStrategy === 'override') {\r\n (mergedProps as any).style = { ...existingStyle, ...injectStyle }\r\n } else {\r\n (mergedProps as any).style = { ...injectStyle, ...existingStyle }\r\n }\r\n }\r\n\r\n return cloneElement(child, mergedProps)\r\n }\r\n\r\n if (Array.isArray(children)) {\r\n return children.map(enhanceChild)\r\n }\r\n\r\n return enhanceChild(children)\r\n }, [children, injectProps, mergeStrategy])\r\n}"],"names":["listeners","Map","getNetworkState","navigator","online","state","onLine","connection","undefined","effectiveType","downlink","rtt","saveData","type","connectionType","options","_options$resetDelay","resetDelay","_useState","useState","copied","setCopied","_useState2","error","setError","reset","useCallback","copy","text","clipboard","err","Error","Promise","resolve","_catch","writeText","then","setTimeout","e","String","reject","paste","readText","emit","event","_arguments","arguments","handlers","get","forEach","handler","apply","slice","call","on","Set","set","add","size","targetRef","callback","useEffect","node","current","observer","MutationObserver","observe","disconnect","setState","window","updateState","addEventListener","removeEventListener","darkQuery","matchMedia","lightQuery","matches","theme","setTheme","mediaQuery","handleChange","updateTheme","isPending","setIsPending","children","injectProps","mergeStrategy","useMemo","enhanceChild","child","isValidElement","mergedProps","existingProps","props","_extends","existingStyle","style","injectStyle","cloneElement","Array","isArray","map"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/useEventBus.ts","../src/useNetworkState.ts","../src/useClipboard.ts","../src/useMutationObserver.ts","../src/usePreferredTheme.ts","../src/useRageClick.ts","../src/useTransition.ts","../src/useWrappedChildren.ts"],"sourcesContent":["import { useCallback, useEffect } from 'preact/hooks';\r\n\r\ntype EventMap = Record<string, (...args: any[]) => void>;\r\n\r\nconst listeners = new Map<string, Set<(...args: any[]) => void>>();\r\n\r\n/**\r\n * A Preact hook to publish and subscribe to custom events across components.\r\n * @returns An object with `emit` and `on` methods.\r\n */\r\nexport function useEventBus<T extends EventMap>() {\r\n const emit = useCallback(<K extends keyof T>(event: K, ...args: Parameters<T[K]>) => {\r\n const handlers = listeners.get(event as string);\r\n if (handlers) {\r\n handlers.forEach((handler) => handler(...args));\r\n }\r\n }, []);\r\n\r\n const on = useCallback(<K extends keyof T>(event: K, handler: T[K]) => {\r\n let handlers = listeners.get(event as string);\r\n if (!handlers) {\r\n handlers = new Set();\r\n listeners.set(event as string, handlers);\r\n }\r\n handlers.add(handler);\r\n\r\n return () => {\r\n handlers!.delete(handler);\r\n if (handlers!.size === 0) {\r\n listeners.delete(event as string);\r\n }\r\n };\r\n }, []);\r\n\r\n return { emit, on };\r\n}\r\n","import { useEffect, useState } from 'preact/hooks';\r\n\r\n/** Network Information API (not in all browsers) */\r\ninterface NetworkInformation extends EventTarget {\r\n effectiveType?: string;\r\n downlink?: number;\r\n rtt?: number;\r\n saveData?: boolean;\r\n type?: string;\r\n}\r\n\r\n/** Effective connection type from Network Information API */\r\nexport type EffectiveConnectionType = 'slow-2g' | '2g' | '3g' | '4g';\r\n\r\n/** Network connection type (e.g., wifi, cellular) */\r\nexport type ConnectionType =\r\n | 'bluetooth'\r\n | 'cellular'\r\n | 'ethernet'\r\n | 'mixed'\r\n | 'none'\r\n | 'other'\r\n | 'unknown'\r\n | 'wifi';\r\n\r\nexport interface NetworkState {\r\n /** Whether the browser is online */\r\n online: boolean;\r\n /** Effective connection type (when supported) */\r\n effectiveType?: EffectiveConnectionType;\r\n /** Estimated downlink speed in Mbps (when supported) */\r\n downlink?: number;\r\n /** Estimated round-trip time in ms (when supported) */\r\n rtt?: number;\r\n /** Whether the user has requested reduced data usage (when supported) */\r\n saveData?: boolean;\r\n /** Connection type (when supported) */\r\n connectionType?: ConnectionType;\r\n}\r\n\r\nfunction getNetworkState(): NetworkState {\r\n if (typeof navigator === 'undefined') {\r\n return { online: true };\r\n }\r\n\r\n const state: NetworkState = {\r\n online: navigator.onLine,\r\n };\r\n\r\n const connection =\r\n (navigator as Navigator & { connection?: NetworkInformation }).connection;\r\n\r\n if (connection) {\r\n if (connection.effectiveType !== undefined) {\r\n state.effectiveType = connection.effectiveType as EffectiveConnectionType;\r\n }\r\n if (connection.downlink !== undefined) {\r\n state.downlink = connection.downlink;\r\n }\r\n if (connection.rtt !== undefined) {\r\n state.rtt = connection.rtt;\r\n }\r\n if (connection.saveData !== undefined) {\r\n state.saveData = connection.saveData;\r\n }\r\n if (connection.type !== undefined) {\r\n state.connectionType = connection.type as ConnectionType;\r\n }\r\n }\r\n\r\n return state;\r\n}\r\n\r\n/**\r\n * A Preact hook that returns the current network state, including online/offline\r\n * status and (when supported) connection type, downlink, RTT, and save-data preference.\r\n * Updates reactively when the network state changes.\r\n *\r\n * @returns The current network state object\r\n *\r\n * @example\r\n * ```tsx\r\n * function NetworkStatus() {\r\n * const { online, effectiveType, saveData } = useNetworkState();\r\n * return (\r\n * <div>\r\n * Status: {online ? 'Online' : 'Offline'}\r\n * {effectiveType && ` (${effectiveType})`}\r\n * {saveData && ' - Reduced data mode'}\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function useNetworkState(): NetworkState {\r\n const [state, setState] = useState<NetworkState>(getNetworkState);\r\n\r\n useEffect(() => {\r\n if (typeof window === 'undefined') return;\r\n\r\n const updateState = () => setState(getNetworkState());\r\n\r\n window.addEventListener('online', updateState);\r\n window.addEventListener('offline', updateState);\r\n\r\n const connection = (navigator as Navigator & { connection?: NetworkInformation })\r\n .connection;\r\n if (connection?.addEventListener) {\r\n connection.addEventListener('change', updateState);\r\n }\r\n\r\n return () => {\r\n window.removeEventListener('online', updateState);\r\n window.removeEventListener('offline', updateState);\r\n if (connection?.removeEventListener) {\r\n connection.removeEventListener('change', updateState);\r\n }\r\n };\r\n }, []);\r\n\r\n return state;\r\n}\r\n","import { useCallback, useState } from 'preact/hooks';\r\n\r\nexport interface UseClipboardOptions {\r\n /** Duration in ms to keep `copied` true before resetting. Default: 2000 */\r\n resetDelay?: number;\r\n}\r\n\r\nexport interface UseClipboardReturn {\r\n /** Copy text to the clipboard. Returns true on success. */\r\n copy: (text: string) => Promise<boolean>;\r\n /** Read text from the clipboard. Returns empty string if denied or unavailable. */\r\n paste: () => Promise<string>;\r\n /** Whether the last copy operation succeeded (resets after resetDelay) */\r\n copied: boolean;\r\n /** Error from the last failed operation, or null */\r\n error: Error | null;\r\n /** Manually reset copied and error state */\r\n reset: () => void;\r\n}\r\n\r\n/**\r\n * A Preact hook for reading and writing the clipboard. Uses the async\r\n * Clipboard API when available (requires secure context and user gesture).\r\n *\r\n * @param options - Optional configuration (e.g., resetDelay for copied state)\r\n * @returns Object with copy, paste, copied, error, and reset\r\n *\r\n * @example\r\n * ```tsx\r\n * function CopyButton() {\r\n * const { copy, copied, error } = useClipboard();\r\n * return (\r\n * <button onClick={() => copy('Hello!')}>\r\n * {copied ? 'Copied!' : 'Copy'}\r\n * </button>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function useClipboard(options: UseClipboardOptions = {}): UseClipboardReturn {\r\n const { resetDelay = 2000 } = options;\r\n\r\n const [copied, setCopied] = useState(false);\r\n const [error, setError] = useState<Error | null>(null);\r\n\r\n const reset = useCallback(() => {\r\n setCopied(false);\r\n setError(null);\r\n }, []);\r\n\r\n const copy = useCallback(\r\n async (text: string): Promise<boolean> => {\r\n setError(null);\r\n\r\n if (typeof navigator === 'undefined' || !navigator.clipboard) {\r\n const err = new Error('Clipboard API is not available');\r\n setError(err);\r\n return false;\r\n }\r\n\r\n try {\r\n await navigator.clipboard.writeText(text);\r\n setCopied(true);\r\n if (resetDelay > 0) {\r\n setTimeout(() => setCopied(false), resetDelay);\r\n }\r\n return true;\r\n } catch (e) {\r\n const err = e instanceof Error ? e : new Error(String(e));\r\n setError(err);\r\n return false;\r\n }\r\n },\r\n [resetDelay]\r\n );\r\n\r\n const paste = useCallback(async (): Promise<string> => {\r\n setError(null);\r\n\r\n if (typeof navigator === 'undefined' || !navigator.clipboard) {\r\n const err = new Error('Clipboard API is not available');\r\n setError(err);\r\n return '';\r\n }\r\n\r\n try {\r\n const text = await navigator.clipboard.readText();\r\n return text;\r\n } catch (e) {\r\n const err = e instanceof Error ? e : new Error(String(e));\r\n setError(err);\r\n return '';\r\n }\r\n }, []);\r\n\r\n return { copy, paste, copied, error, reset };\r\n}\r\n","import { RefObject } from 'preact'\r\nimport { useEffect } from 'preact/hooks'\r\n\r\nexport type UseMutationObserverOptions = MutationObserverInit\r\n\r\n/**\r\n * A Preact hook to observe DOM mutations using MutationObserver.\r\n * @param target - The element to observe.\r\n * @param callback - Function to call on mutation.\r\n * @param options - MutationObserver options.\r\n */\r\nexport function useMutationObserver(\r\n targetRef: RefObject<HTMLElement | null>,\r\n callback: MutationCallback,\r\n options: MutationObserverInit\r\n) {\r\n useEffect(() => {\r\n const node = targetRef.current\r\n if (!node) return\r\n\r\n const observer = new MutationObserver(callback)\r\n observer.observe(node, options)\r\n\r\n return () => observer.disconnect()\r\n }, [targetRef, callback, options])\r\n}\r\n","import { useEffect, useState } from 'preact/hooks';\r\n\r\nexport type PreferredTheme = 'light' | 'dark' | 'no-preference';\r\n\r\n/**\r\n * A Preact hook that returns the user's preferred color scheme based on the\r\n * `prefers-color-scheme` media query. Updates reactively when the user changes\r\n * their system or browser theme preference.\r\n *\r\n * @returns The preferred theme: 'light', 'dark', or 'no-preference'\r\n *\r\n * @example\r\n * ```tsx\r\n * function ThemeAwareComponent() {\r\n * const theme = usePreferredTheme();\r\n * return (\r\n * <div data-theme={theme}>\r\n * Current preference: {theme}\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function usePreferredTheme(): PreferredTheme {\r\n const [theme, setTheme] = useState<PreferredTheme>(() => {\r\n if (typeof window === 'undefined') return 'no-preference';\r\n\r\n const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n const lightQuery = window.matchMedia('(prefers-color-scheme: light)');\r\n\r\n if (darkQuery.matches) return 'dark';\r\n if (lightQuery.matches) return 'light';\r\n return 'no-preference';\r\n });\r\n\r\n useEffect(() => {\r\n if (typeof window === 'undefined') return;\r\n\r\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n\r\n const handleChange = (e: MediaQueryListEvent) => {\r\n setTheme(e.matches ? 'dark' : 'light');\r\n };\r\n\r\n // Re-check in case of no-preference (some browsers don't support light query)\r\n const updateTheme = () => {\r\n const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n const lightQuery = window.matchMedia('(prefers-color-scheme: light)');\r\n\r\n if (darkQuery.matches) setTheme('dark');\r\n else if (lightQuery.matches) setTheme('light');\r\n else setTheme('no-preference');\r\n };\r\n\r\n mediaQuery.addEventListener('change', handleChange);\r\n\r\n // Fallback: some environments may not fire change, so we also listen for light\r\n const lightQuery = window.matchMedia('(prefers-color-scheme: light)');\r\n lightQuery.addEventListener('change', updateTheme);\r\n\r\n return () => {\r\n mediaQuery.removeEventListener('change', handleChange);\r\n lightQuery.removeEventListener('change', updateTheme);\r\n };\r\n }, []);\r\n\r\n return theme;\r\n}\r\n","import type { RefObject } from 'preact'\r\nimport { useEffect, useRef } from 'preact/hooks'\r\n\r\nexport interface RageClickPayload {\r\n /** Number of clicks that triggered the rage click */\r\n count: number\r\n /** Last click event (e.g. for Sentry context) */\r\n event: MouseEvent\r\n}\r\n\r\nexport interface UseRageClickOptions {\r\n /** Called when a rage click is detected. Use this to report to Sentry or your error tracker. */\r\n onRageClick: (payload: RageClickPayload) => void\r\n /** Minimum number of clicks in the time window to count as rage click. Default: 5 (Sentry-style). */\r\n threshold?: number\r\n /** Time window in ms. Default: 1000. */\r\n timeWindow?: number\r\n /** Max distance in px between clicks to count as same spot. Default: 30. Set to Infinity to ignore distance. */\r\n distanceThreshold?: number\r\n}\r\n\r\ninterface ClickRecord {\r\n time: number\r\n x: number\r\n y: number\r\n}\r\n\r\nfunction distance(a: ClickRecord, b: ClickRecord): number {\r\n return Math.hypot(b.x - a.x, b.y - a.y)\r\n}\r\n\r\n/**\r\n * Detects \"rage clicks\" (repeated rapid clicks in the same area), e.g. when the UI\r\n * is unresponsive. Use the callback to report to Sentry or similar tools to surface\r\n * rage click issues and lower rage-click-related support.\r\n *\r\n * @param targetRef - Ref of the element to monitor (e.g. a button or card).\r\n * @param options - onRageClick callback and optional threshold, timeWindow, distanceThreshold.\r\n *\r\n * @example\r\n * ```tsx\r\n * const ref = useRef<HTMLButtonElement>(null)\r\n * useRageClick(ref, {\r\n * onRageClick: ({ count, event }) => {\r\n * Sentry.captureMessage('Rage click detected', { extra: { count, target: event.target } })\r\n * },\r\n * })\r\n * return <button ref={ref}>Submit</button>\r\n * ```\r\n */\r\nexport function useRageClick(\r\n targetRef: RefObject<HTMLElement | null>,\r\n options: UseRageClickOptions\r\n) {\r\n const {\r\n onRageClick,\r\n threshold = 5,\r\n timeWindow = 1000,\r\n distanceThreshold = 30,\r\n } = options\r\n\r\n const onRageClickRef = useRef(onRageClick)\r\n onRageClickRef.current = onRageClick\r\n\r\n const clicksRef = useRef<ClickRecord[]>([])\r\n\r\n useEffect(() => {\r\n const node = targetRef.current\r\n if (!node) return\r\n\r\n const handleClick = (e: MouseEvent) => {\r\n const now = Date.now()\r\n const record: ClickRecord = { time: now, x: e.clientX, y: e.clientY }\r\n\r\n const clicks = clicksRef.current\r\n const cutoff = now - timeWindow\r\n const recent = clicks.filter((c) => c.time >= cutoff)\r\n recent.push(record)\r\n\r\n if (distanceThreshold !== Infinity) {\r\n const inRange = recent.filter(\r\n (c) => distance(c, record) <= distanceThreshold\r\n )\r\n if (inRange.length >= threshold) {\r\n onRageClickRef.current({ count: inRange.length, event: e })\r\n clicksRef.current = []\r\n return\r\n }\r\n } else {\r\n if (recent.length >= threshold) {\r\n onRageClickRef.current({ count: recent.length, event: e })\r\n clicksRef.current = []\r\n return\r\n }\r\n }\r\n\r\n clicksRef.current = recent\r\n }\r\n\r\n node.addEventListener('click', handleClick)\r\n return () => node.removeEventListener('click', handleClick)\r\n }, [targetRef, threshold, timeWindow, distanceThreshold])\r\n}\r\n","import { useState, useCallback } from 'preact/hooks';\r\n\r\n/**\r\n * Mimics React's useTransition hook in Preact.\r\n * @returns [startTransition, isPending]\r\n */\r\nexport function useTransition(): [startTransition: (callback: () => void) => void, isPending: boolean] {\r\n const [isPending, setIsPending] = useState(false);\r\n\r\n const startTransition = useCallback((callback: () => void) => {\r\n setIsPending(true);\r\n Promise.resolve().then(() => {\r\n callback();\r\n setIsPending(false);\r\n });\r\n }, []);\r\n\r\n return [startTransition, isPending];\r\n}\r\n","import { ComponentChildren, cloneElement, isValidElement } from 'preact'\r\nimport { useMemo } from 'preact/hooks'\r\n\r\nexport type InjectableProps = Record<string, any>\r\n\r\n/**\r\n * A Preact hook to wrap children components and inject additional props into them.\r\n * @param children - The children to wrap and enhance with props.\r\n * @param injectProps - The props to inject into each child component.\r\n * @param mergeStrategy - How to handle prop conflicts ('override' | 'preserve'). Defaults to 'preserve'.\r\n * @returns Enhanced children with injected props.\r\n */\r\nexport function useWrappedChildren(\r\n children: ComponentChildren,\r\n injectProps: InjectableProps,\r\n mergeStrategy: 'override' | 'preserve' = 'preserve'\r\n): ComponentChildren {\r\n return useMemo(() => {\r\n if (!children) return children\r\n\r\n const enhanceChild = (child: any): any => {\r\n if (!isValidElement(child)) return child\r\n\r\n const existingProps = child.props || {}\r\n \r\n let mergedProps: InjectableProps\r\n \r\n if (mergeStrategy === 'override') {\r\n // Injected props override existing ones\r\n mergedProps = { ...existingProps, ...injectProps }\r\n } else {\r\n // Existing props are preserved, injected props are added only if not present\r\n mergedProps = { ...injectProps, ...existingProps }\r\n }\r\n\r\n // Special handling for style prop to merge style objects properly\r\n const existingStyle = (existingProps as any)?.style\r\n const injectStyle = (injectProps as any)?.style\r\n \r\n if (existingStyle && injectStyle && \r\n typeof existingStyle === 'object' && typeof injectStyle === 'object') {\r\n if (mergeStrategy === 'override') {\r\n (mergedProps as any).style = { ...existingStyle, ...injectStyle }\r\n } else {\r\n (mergedProps as any).style = { ...injectStyle, ...existingStyle }\r\n }\r\n }\r\n\r\n return cloneElement(child, mergedProps)\r\n }\r\n\r\n if (Array.isArray(children)) {\r\n return children.map(enhanceChild)\r\n }\r\n\r\n return enhanceChild(children)\r\n }, [children, injectProps, mergeStrategy])\r\n}"],"names":["listeners","Map","getNetworkState","navigator","online","state","onLine","connection","undefined","effectiveType","downlink","rtt","saveData","type","connectionType","options","_options$resetDelay","resetDelay","_useState","useState","copied","setCopied","_useState2","error","setError","reset","useCallback","copy","text","clipboard","err","Error","Promise","resolve","_catch","writeText","then","setTimeout","e","String","reject","paste","readText","emit","event","_arguments","arguments","handlers","get","forEach","handler","apply","slice","call","on","Set","set","add","size","targetRef","callback","useEffect","node","current","observer","MutationObserver","observe","disconnect","setState","window","updateState","addEventListener","removeEventListener","darkQuery","matchMedia","lightQuery","matches","theme","setTheme","mediaQuery","handleChange","updateTheme","onRageClick","_options$threshold","threshold","_options$timeWindow","timeWindow","_options$distanceThre","distanceThreshold","onRageClickRef","useRef","clicksRef","handleClick","now","Date","record","time","x","clientX","y","clientY","cutoff","recent","filter","c","push","Infinity","inRange","a","b","Math","hypot","length","count","isPending","setIsPending","children","injectProps","mergeStrategy","useMemo","enhanceChild","child","isValidElement","mergedProps","existingProps","props","_extends","existingStyle","style","injectStyle","cloneElement","Array","isArray","map"],"mappings":"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"}
|
package/dist/index.module.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{useState as e,useCallback as n,useEffect as r,useMemo as t}from"preact/hooks";import{isValidElement as o,cloneElement as
|
|
1
|
+
import{useState as e,useCallback as n,useEffect as r,useMemo as t,useRef as i}from"preact/hooks";import{isValidElement as o,cloneElement as a}from"preact";function c(){var r=e(!1),t=r[0],i=r[1];return[n(function(e){i(!0),Promise.resolve().then(function(){e(),i(!1)})},[]),t]}function u(e,n,t){r(function(){var r=e.current;if(r){var i=new MutationObserver(n);return i.observe(r,t),function(){return i.disconnect()}}},[e,n,t])}var v=new Map;function f(){var e=n(function(e){var n=arguments,r=v.get(e);r&&r.forEach(function(e){return e.apply(void 0,[].slice.call(n,1))})},[]);return{emit:e,on:n(function(e,n){var r=v.get(e);return r||(r=new Set,v.set(e,r)),r.add(n),function(){r.delete(n),0===r.size&&v.delete(e)}},[])}}function d(){return d=Object.assign?Object.assign.bind():function(e){for(var n=1;n<arguments.length;n++){var r=arguments[n];for(var t in r)({}).hasOwnProperty.call(r,t)&&(e[t]=r[t])}return e},d.apply(null,arguments)}function l(e,n,r){return void 0===r&&(r="preserve"),t(function(){if(!e)return e;var t=function(e){if(!o(e))return e;var t,i=e.props||{};t="override"===r?d({},i,n):d({},n,i);var c=null==i?void 0:i.style,u=null==n?void 0:n.style;return c&&u&&"object"==typeof c&&"object"==typeof u&&(t.style="override"===r?d({},c,u):d({},u,c)),a(e,t)};return Array.isArray(e)?e.map(t):t(e)},[e,n,r])}function s(){var n=e(function(){if("undefined"==typeof window)return"no-preference";var e=window.matchMedia("(prefers-color-scheme: dark)"),n=window.matchMedia("(prefers-color-scheme: light)");return e.matches?"dark":n.matches?"light":"no-preference"}),t=n[0],i=n[1];return r(function(){if("undefined"!=typeof window){var e=window.matchMedia("(prefers-color-scheme: dark)"),n=function(e){i(e.matches?"dark":"light")},r=function(){var e=window.matchMedia("(prefers-color-scheme: dark)"),n=window.matchMedia("(prefers-color-scheme: light)");i(e.matches?"dark":n.matches?"light":"no-preference")};e.addEventListener("change",n);var t=window.matchMedia("(prefers-color-scheme: light)");return t.addEventListener("change",r),function(){e.removeEventListener("change",n),t.removeEventListener("change",r)}}},[]),t}function h(){if("undefined"==typeof navigator)return{online:!0};var e={online:navigator.onLine},n=navigator.connection;return n&&(void 0!==n.effectiveType&&(e.effectiveType=n.effectiveType),void 0!==n.downlink&&(e.downlink=n.downlink),void 0!==n.rtt&&(e.rtt=n.rtt),void 0!==n.saveData&&(e.saveData=n.saveData),void 0!==n.type&&(e.connectionType=n.type)),e}function p(){var n=e(h),t=n[0],i=n[1];return r(function(){if("undefined"!=typeof window){var e=function(){return i(h())};window.addEventListener("online",e),window.addEventListener("offline",e);var n=navigator.connection;return null!=n&&n.addEventListener&&n.addEventListener("change",e),function(){window.removeEventListener("online",e),window.removeEventListener("offline",e),null!=n&&n.removeEventListener&&n.removeEventListener("change",e)}}},[]),t}function m(e,n){try{var r=e()}catch(e){return n(e)}return r&&r.then?r.then(void 0,n):r}function w(r){void 0===r&&(r={});var t=r.resetDelay,i=void 0===t?2e3:t,o=e(!1),a=o[0],c=o[1],u=e(null),v=u[0],f=u[1],d=n(function(){c(!1),f(null)},[]);return{copy:n(function(e){try{if(f(null),"undefined"==typeof navigator||!navigator.clipboard){var n=new Error("Clipboard API is not available");return f(n),Promise.resolve(!1)}return Promise.resolve(m(function(){return Promise.resolve(navigator.clipboard.writeText(e)).then(function(){return c(!0),i>0&&setTimeout(function(){return c(!1)},i),!0})},function(e){var n=e instanceof Error?e:new Error(String(e));return f(n),!1}))}catch(e){return Promise.reject(e)}},[i]),paste:n(function(){try{if(f(null),"undefined"==typeof navigator||!navigator.clipboard){var e=new Error("Clipboard API is not available");return f(e),Promise.resolve("")}return Promise.resolve(m(function(){return Promise.resolve(navigator.clipboard.readText())},function(e){var n=e instanceof Error?e:new Error(String(e));return f(n),""}))}catch(e){return Promise.reject(e)}},[]),copied:a,error:v,reset:d}}function g(e,n){var t=n.onRageClick,o=n.threshold,a=void 0===o?5:o,c=n.timeWindow,u=void 0===c?1e3:c,v=n.distanceThreshold,f=void 0===v?30:v,d=i(t);d.current=t;var l=i([]);r(function(){var n=e.current;if(n){var r=function(e){var n=Date.now(),r={time:n,x:e.clientX,y:e.clientY},t=n-u,i=l.current.filter(function(e){return e.time>=t});if(i.push(r),Infinity!==f){var o=i.filter(function(e){return n=e,t=r,Math.hypot(t.x-n.x,t.y-n.y)<=f;var n,t});if(o.length>=a)return d.current({count:o.length,event:e}),void(l.current=[])}else if(i.length>=a)return d.current({count:i.length,event:e}),void(l.current=[]);l.current=i};return n.addEventListener("click",r),function(){return n.removeEventListener("click",r)}}},[e,a,u,f])}export{w as useClipboard,f as useEventBus,u as useMutationObserver,p as useNetworkState,s as usePreferredTheme,g as useRageClick,c as useTransition,l as useWrappedChildren};
|
|
2
2
|
//# sourceMappingURL=index.module.js.map
|
package/dist/index.module.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.module.js","sources":["../src/useTransition.ts","../src/useMutationObserver.ts","../src/useEventBus.ts","../src/useWrappedChildren.ts","../src/usePreferredTheme.ts","../src/useNetworkState.ts","../src/useClipboard.ts"],"sourcesContent":["import { useState, useCallback } from 'preact/hooks';\r\n\r\n/**\r\n * Mimics React's useTransition hook in Preact.\r\n * @returns [startTransition, isPending]\r\n */\r\nexport function useTransition(): [startTransition: (callback: () => void) => void, isPending: boolean] {\r\n const [isPending, setIsPending] = useState(false);\r\n\r\n const startTransition = useCallback((callback: () => void) => {\r\n setIsPending(true);\r\n Promise.resolve().then(() => {\r\n callback();\r\n setIsPending(false);\r\n });\r\n }, []);\r\n\r\n return [startTransition, isPending];\r\n}\r\n","import { RefObject } from 'preact'\r\nimport { useEffect } from 'preact/hooks'\r\n\r\nexport type UseMutationObserverOptions = MutationObserverInit\r\n\r\n/**\r\n * A Preact hook to observe DOM mutations using MutationObserver.\r\n * @param target - The element to observe.\r\n * @param callback - Function to call on mutation.\r\n * @param options - MutationObserver options.\r\n */\r\nexport function useMutationObserver(\r\n targetRef: RefObject<HTMLElement | null>,\r\n callback: MutationCallback,\r\n options: MutationObserverInit\r\n) {\r\n useEffect(() => {\r\n const node = targetRef.current\r\n if (!node) return\r\n\r\n const observer = new MutationObserver(callback)\r\n observer.observe(node, options)\r\n\r\n return () => observer.disconnect()\r\n }, [targetRef, callback, options])\r\n}\r\n","import { useCallback, useEffect } from 'preact/hooks';\r\n\r\ntype EventMap = Record<string, (...args: any[]) => void>;\r\n\r\nconst listeners = new Map<string, Set<(...args: any[]) => void>>();\r\n\r\n/**\r\n * A Preact hook to publish and subscribe to custom events across components.\r\n * @returns An object with `emit` and `on` methods.\r\n */\r\nexport function useEventBus<T extends EventMap>() {\r\n const emit = useCallback(<K extends keyof T>(event: K, ...args: Parameters<T[K]>) => {\r\n const handlers = listeners.get(event as string);\r\n if (handlers) {\r\n handlers.forEach((handler) => handler(...args));\r\n }\r\n }, []);\r\n\r\n const on = useCallback(<K extends keyof T>(event: K, handler: T[K]) => {\r\n let handlers = listeners.get(event as string);\r\n if (!handlers) {\r\n handlers = new Set();\r\n listeners.set(event as string, handlers);\r\n }\r\n handlers.add(handler);\r\n\r\n return () => {\r\n handlers!.delete(handler);\r\n if (handlers!.size === 0) {\r\n listeners.delete(event as string);\r\n }\r\n };\r\n }, []);\r\n\r\n return { emit, on };\r\n}\r\n","import { ComponentChildren, cloneElement, isValidElement } from 'preact'\r\nimport { useMemo } from 'preact/hooks'\r\n\r\nexport type InjectableProps = Record<string, any>\r\n\r\n/**\r\n * A Preact hook to wrap children components and inject additional props into them.\r\n * @param children - The children to wrap and enhance with props.\r\n * @param injectProps - The props to inject into each child component.\r\n * @param mergeStrategy - How to handle prop conflicts ('override' | 'preserve'). Defaults to 'preserve'.\r\n * @returns Enhanced children with injected props.\r\n */\r\nexport function useWrappedChildren(\r\n children: ComponentChildren,\r\n injectProps: InjectableProps,\r\n mergeStrategy: 'override' | 'preserve' = 'preserve'\r\n): ComponentChildren {\r\n return useMemo(() => {\r\n if (!children) return children\r\n\r\n const enhanceChild = (child: any): any => {\r\n if (!isValidElement(child)) return child\r\n\r\n const existingProps = child.props || {}\r\n \r\n let mergedProps: InjectableProps\r\n \r\n if (mergeStrategy === 'override') {\r\n // Injected props override existing ones\r\n mergedProps = { ...existingProps, ...injectProps }\r\n } else {\r\n // Existing props are preserved, injected props are added only if not present\r\n mergedProps = { ...injectProps, ...existingProps }\r\n }\r\n\r\n // Special handling for style prop to merge style objects properly\r\n const existingStyle = (existingProps as any)?.style\r\n const injectStyle = (injectProps as any)?.style\r\n \r\n if (existingStyle && injectStyle && \r\n typeof existingStyle === 'object' && typeof injectStyle === 'object') {\r\n if (mergeStrategy === 'override') {\r\n (mergedProps as any).style = { ...existingStyle, ...injectStyle }\r\n } else {\r\n (mergedProps as any).style = { ...injectStyle, ...existingStyle }\r\n }\r\n }\r\n\r\n return cloneElement(child, mergedProps)\r\n }\r\n\r\n if (Array.isArray(children)) {\r\n return children.map(enhanceChild)\r\n }\r\n\r\n return enhanceChild(children)\r\n }, [children, injectProps, mergeStrategy])\r\n}","import { useEffect, useState } from 'preact/hooks';\r\n\r\nexport type PreferredTheme = 'light' | 'dark' | 'no-preference';\r\n\r\n/**\r\n * A Preact hook that returns the user's preferred color scheme based on the\r\n * `prefers-color-scheme` media query. Updates reactively when the user changes\r\n * their system or browser theme preference.\r\n *\r\n * @returns The preferred theme: 'light', 'dark', or 'no-preference'\r\n *\r\n * @example\r\n * ```tsx\r\n * function ThemeAwareComponent() {\r\n * const theme = usePreferredTheme();\r\n * return (\r\n * <div data-theme={theme}>\r\n * Current preference: {theme}\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function usePreferredTheme(): PreferredTheme {\r\n const [theme, setTheme] = useState<PreferredTheme>(() => {\r\n if (typeof window === 'undefined') return 'no-preference';\r\n\r\n const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n const lightQuery = window.matchMedia('(prefers-color-scheme: light)');\r\n\r\n if (darkQuery.matches) return 'dark';\r\n if (lightQuery.matches) return 'light';\r\n return 'no-preference';\r\n });\r\n\r\n useEffect(() => {\r\n if (typeof window === 'undefined') return;\r\n\r\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n\r\n const handleChange = (e: MediaQueryListEvent) => {\r\n setTheme(e.matches ? 'dark' : 'light');\r\n };\r\n\r\n // Re-check in case of no-preference (some browsers don't support light query)\r\n const updateTheme = () => {\r\n const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n const lightQuery = window.matchMedia('(prefers-color-scheme: light)');\r\n\r\n if (darkQuery.matches) setTheme('dark');\r\n else if (lightQuery.matches) setTheme('light');\r\n else setTheme('no-preference');\r\n };\r\n\r\n mediaQuery.addEventListener('change', handleChange);\r\n\r\n // Fallback: some environments may not fire change, so we also listen for light\r\n const lightQuery = window.matchMedia('(prefers-color-scheme: light)');\r\n lightQuery.addEventListener('change', updateTheme);\r\n\r\n return () => {\r\n mediaQuery.removeEventListener('change', handleChange);\r\n lightQuery.removeEventListener('change', updateTheme);\r\n };\r\n }, []);\r\n\r\n return theme;\r\n}\r\n","import { useEffect, useState } from 'preact/hooks';\r\n\r\n/** Network Information API (not in all browsers) */\r\ninterface NetworkInformation extends EventTarget {\r\n effectiveType?: string;\r\n downlink?: number;\r\n rtt?: number;\r\n saveData?: boolean;\r\n type?: string;\r\n}\r\n\r\n/** Effective connection type from Network Information API */\r\nexport type EffectiveConnectionType = 'slow-2g' | '2g' | '3g' | '4g';\r\n\r\n/** Network connection type (e.g., wifi, cellular) */\r\nexport type ConnectionType =\r\n | 'bluetooth'\r\n | 'cellular'\r\n | 'ethernet'\r\n | 'mixed'\r\n | 'none'\r\n | 'other'\r\n | 'unknown'\r\n | 'wifi';\r\n\r\nexport interface NetworkState {\r\n /** Whether the browser is online */\r\n online: boolean;\r\n /** Effective connection type (when supported) */\r\n effectiveType?: EffectiveConnectionType;\r\n /** Estimated downlink speed in Mbps (when supported) */\r\n downlink?: number;\r\n /** Estimated round-trip time in ms (when supported) */\r\n rtt?: number;\r\n /** Whether the user has requested reduced data usage (when supported) */\r\n saveData?: boolean;\r\n /** Connection type (when supported) */\r\n connectionType?: ConnectionType;\r\n}\r\n\r\nfunction getNetworkState(): NetworkState {\r\n if (typeof navigator === 'undefined') {\r\n return { online: true };\r\n }\r\n\r\n const state: NetworkState = {\r\n online: navigator.onLine,\r\n };\r\n\r\n const connection =\r\n (navigator as Navigator & { connection?: NetworkInformation }).connection;\r\n\r\n if (connection) {\r\n if (connection.effectiveType !== undefined) {\r\n state.effectiveType = connection.effectiveType as EffectiveConnectionType;\r\n }\r\n if (connection.downlink !== undefined) {\r\n state.downlink = connection.downlink;\r\n }\r\n if (connection.rtt !== undefined) {\r\n state.rtt = connection.rtt;\r\n }\r\n if (connection.saveData !== undefined) {\r\n state.saveData = connection.saveData;\r\n }\r\n if (connection.type !== undefined) {\r\n state.connectionType = connection.type as ConnectionType;\r\n }\r\n }\r\n\r\n return state;\r\n}\r\n\r\n/**\r\n * A Preact hook that returns the current network state, including online/offline\r\n * status and (when supported) connection type, downlink, RTT, and save-data preference.\r\n * Updates reactively when the network state changes.\r\n *\r\n * @returns The current network state object\r\n *\r\n * @example\r\n * ```tsx\r\n * function NetworkStatus() {\r\n * const { online, effectiveType, saveData } = useNetworkState();\r\n * return (\r\n * <div>\r\n * Status: {online ? 'Online' : 'Offline'}\r\n * {effectiveType && ` (${effectiveType})`}\r\n * {saveData && ' - Reduced data mode'}\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function useNetworkState(): NetworkState {\r\n const [state, setState] = useState<NetworkState>(getNetworkState);\r\n\r\n useEffect(() => {\r\n if (typeof window === 'undefined') return;\r\n\r\n const updateState = () => setState(getNetworkState());\r\n\r\n window.addEventListener('online', updateState);\r\n window.addEventListener('offline', updateState);\r\n\r\n const connection = (navigator as Navigator & { connection?: NetworkInformation })\r\n .connection;\r\n if (connection?.addEventListener) {\r\n connection.addEventListener('change', updateState);\r\n }\r\n\r\n return () => {\r\n window.removeEventListener('online', updateState);\r\n window.removeEventListener('offline', updateState);\r\n if (connection?.removeEventListener) {\r\n connection.removeEventListener('change', updateState);\r\n }\r\n };\r\n }, []);\r\n\r\n return state;\r\n}\r\n","import { useCallback, useState } from 'preact/hooks';\r\n\r\nexport interface UseClipboardOptions {\r\n /** Duration in ms to keep `copied` true before resetting. Default: 2000 */\r\n resetDelay?: number;\r\n}\r\n\r\nexport interface UseClipboardReturn {\r\n /** Copy text to the clipboard. Returns true on success. */\r\n copy: (text: string) => Promise<boolean>;\r\n /** Read text from the clipboard. Returns empty string if denied or unavailable. */\r\n paste: () => Promise<string>;\r\n /** Whether the last copy operation succeeded (resets after resetDelay) */\r\n copied: boolean;\r\n /** Error from the last failed operation, or null */\r\n error: Error | null;\r\n /** Manually reset copied and error state */\r\n reset: () => void;\r\n}\r\n\r\n/**\r\n * A Preact hook for reading and writing the clipboard. Uses the async\r\n * Clipboard API when available (requires secure context and user gesture).\r\n *\r\n * @param options - Optional configuration (e.g., resetDelay for copied state)\r\n * @returns Object with copy, paste, copied, error, and reset\r\n *\r\n * @example\r\n * ```tsx\r\n * function CopyButton() {\r\n * const { copy, copied, error } = useClipboard();\r\n * return (\r\n * <button onClick={() => copy('Hello!')}>\r\n * {copied ? 'Copied!' : 'Copy'}\r\n * </button>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function useClipboard(options: UseClipboardOptions = {}): UseClipboardReturn {\r\n const { resetDelay = 2000 } = options;\r\n\r\n const [copied, setCopied] = useState(false);\r\n const [error, setError] = useState<Error | null>(null);\r\n\r\n const reset = useCallback(() => {\r\n setCopied(false);\r\n setError(null);\r\n }, []);\r\n\r\n const copy = useCallback(\r\n async (text: string): Promise<boolean> => {\r\n setError(null);\r\n\r\n if (typeof navigator === 'undefined' || !navigator.clipboard) {\r\n const err = new Error('Clipboard API is not available');\r\n setError(err);\r\n return false;\r\n }\r\n\r\n try {\r\n await navigator.clipboard.writeText(text);\r\n setCopied(true);\r\n if (resetDelay > 0) {\r\n setTimeout(() => setCopied(false), resetDelay);\r\n }\r\n return true;\r\n } catch (e) {\r\n const err = e instanceof Error ? e : new Error(String(e));\r\n setError(err);\r\n return false;\r\n }\r\n },\r\n [resetDelay]\r\n );\r\n\r\n const paste = useCallback(async (): Promise<string> => {\r\n setError(null);\r\n\r\n if (typeof navigator === 'undefined' || !navigator.clipboard) {\r\n const err = new Error('Clipboard API is not available');\r\n setError(err);\r\n return '';\r\n }\r\n\r\n try {\r\n const text = await navigator.clipboard.readText();\r\n return text;\r\n } catch (e) {\r\n const err = e instanceof Error ? e : new Error(String(e));\r\n setError(err);\r\n return '';\r\n }\r\n }, []);\r\n\r\n return { copy, paste, copied, error, reset };\r\n}\r\n"],"names":["useTransition","_useState","useState","isPending","setIsPending","useCallback","callback","Promise","resolve","then","useMutationObserver","targetRef","options","useEffect","node","current","observer","MutationObserver","observe","disconnect","listeners","Map","useEventBus","emit","event","_arguments","arguments","handlers","get","forEach","handler","apply","slice","call","on","Set","set","add","size","useWrappedChildren","children","injectProps","mergeStrategy","useMemo","enhanceChild","child","isValidElement","mergedProps","existingProps","props","_extends","existingStyle","style","injectStyle","cloneElement","Array","isArray","map","usePreferredTheme","window","darkQuery","matchMedia","lightQuery","matches","theme","setTheme","mediaQuery","handleChange","e","updateTheme","addEventListener","removeEventListener","getNetworkState","navigator","online","state","onLine","connection","undefined","effectiveType","downlink","rtt","saveData","type","connectionType","useNetworkState","setState","updateState","useClipboard","_options$resetDelay","resetDelay","copied","setCopied","_useState2","error","setError","reset","copy","text","clipboard","err","Error","_catch","writeText","setTimeout","String","reject","paste","readText"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.module.js","sources":["../src/useTransition.ts","../src/useMutationObserver.ts","../src/useEventBus.ts","../src/useWrappedChildren.ts","../src/usePreferredTheme.ts","../src/useNetworkState.ts","../src/useClipboard.ts","../src/useRageClick.ts"],"sourcesContent":["import { useState, useCallback } from 'preact/hooks';\r\n\r\n/**\r\n * Mimics React's useTransition hook in Preact.\r\n * @returns [startTransition, isPending]\r\n */\r\nexport function useTransition(): [startTransition: (callback: () => void) => void, isPending: boolean] {\r\n const [isPending, setIsPending] = useState(false);\r\n\r\n const startTransition = useCallback((callback: () => void) => {\r\n setIsPending(true);\r\n Promise.resolve().then(() => {\r\n callback();\r\n setIsPending(false);\r\n });\r\n }, []);\r\n\r\n return [startTransition, isPending];\r\n}\r\n","import { RefObject } from 'preact'\r\nimport { useEffect } from 'preact/hooks'\r\n\r\nexport type UseMutationObserverOptions = MutationObserverInit\r\n\r\n/**\r\n * A Preact hook to observe DOM mutations using MutationObserver.\r\n * @param target - The element to observe.\r\n * @param callback - Function to call on mutation.\r\n * @param options - MutationObserver options.\r\n */\r\nexport function useMutationObserver(\r\n targetRef: RefObject<HTMLElement | null>,\r\n callback: MutationCallback,\r\n options: MutationObserverInit\r\n) {\r\n useEffect(() => {\r\n const node = targetRef.current\r\n if (!node) return\r\n\r\n const observer = new MutationObserver(callback)\r\n observer.observe(node, options)\r\n\r\n return () => observer.disconnect()\r\n }, [targetRef, callback, options])\r\n}\r\n","import { useCallback, useEffect } from 'preact/hooks';\r\n\r\ntype EventMap = Record<string, (...args: any[]) => void>;\r\n\r\nconst listeners = new Map<string, Set<(...args: any[]) => void>>();\r\n\r\n/**\r\n * A Preact hook to publish and subscribe to custom events across components.\r\n * @returns An object with `emit` and `on` methods.\r\n */\r\nexport function useEventBus<T extends EventMap>() {\r\n const emit = useCallback(<K extends keyof T>(event: K, ...args: Parameters<T[K]>) => {\r\n const handlers = listeners.get(event as string);\r\n if (handlers) {\r\n handlers.forEach((handler) => handler(...args));\r\n }\r\n }, []);\r\n\r\n const on = useCallback(<K extends keyof T>(event: K, handler: T[K]) => {\r\n let handlers = listeners.get(event as string);\r\n if (!handlers) {\r\n handlers = new Set();\r\n listeners.set(event as string, handlers);\r\n }\r\n handlers.add(handler);\r\n\r\n return () => {\r\n handlers!.delete(handler);\r\n if (handlers!.size === 0) {\r\n listeners.delete(event as string);\r\n }\r\n };\r\n }, []);\r\n\r\n return { emit, on };\r\n}\r\n","import { ComponentChildren, cloneElement, isValidElement } from 'preact'\r\nimport { useMemo } from 'preact/hooks'\r\n\r\nexport type InjectableProps = Record<string, any>\r\n\r\n/**\r\n * A Preact hook to wrap children components and inject additional props into them.\r\n * @param children - The children to wrap and enhance with props.\r\n * @param injectProps - The props to inject into each child component.\r\n * @param mergeStrategy - How to handle prop conflicts ('override' | 'preserve'). Defaults to 'preserve'.\r\n * @returns Enhanced children with injected props.\r\n */\r\nexport function useWrappedChildren(\r\n children: ComponentChildren,\r\n injectProps: InjectableProps,\r\n mergeStrategy: 'override' | 'preserve' = 'preserve'\r\n): ComponentChildren {\r\n return useMemo(() => {\r\n if (!children) return children\r\n\r\n const enhanceChild = (child: any): any => {\r\n if (!isValidElement(child)) return child\r\n\r\n const existingProps = child.props || {}\r\n \r\n let mergedProps: InjectableProps\r\n \r\n if (mergeStrategy === 'override') {\r\n // Injected props override existing ones\r\n mergedProps = { ...existingProps, ...injectProps }\r\n } else {\r\n // Existing props are preserved, injected props are added only if not present\r\n mergedProps = { ...injectProps, ...existingProps }\r\n }\r\n\r\n // Special handling for style prop to merge style objects properly\r\n const existingStyle = (existingProps as any)?.style\r\n const injectStyle = (injectProps as any)?.style\r\n \r\n if (existingStyle && injectStyle && \r\n typeof existingStyle === 'object' && typeof injectStyle === 'object') {\r\n if (mergeStrategy === 'override') {\r\n (mergedProps as any).style = { ...existingStyle, ...injectStyle }\r\n } else {\r\n (mergedProps as any).style = { ...injectStyle, ...existingStyle }\r\n }\r\n }\r\n\r\n return cloneElement(child, mergedProps)\r\n }\r\n\r\n if (Array.isArray(children)) {\r\n return children.map(enhanceChild)\r\n }\r\n\r\n return enhanceChild(children)\r\n }, [children, injectProps, mergeStrategy])\r\n}","import { useEffect, useState } from 'preact/hooks';\r\n\r\nexport type PreferredTheme = 'light' | 'dark' | 'no-preference';\r\n\r\n/**\r\n * A Preact hook that returns the user's preferred color scheme based on the\r\n * `prefers-color-scheme` media query. Updates reactively when the user changes\r\n * their system or browser theme preference.\r\n *\r\n * @returns The preferred theme: 'light', 'dark', or 'no-preference'\r\n *\r\n * @example\r\n * ```tsx\r\n * function ThemeAwareComponent() {\r\n * const theme = usePreferredTheme();\r\n * return (\r\n * <div data-theme={theme}>\r\n * Current preference: {theme}\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function usePreferredTheme(): PreferredTheme {\r\n const [theme, setTheme] = useState<PreferredTheme>(() => {\r\n if (typeof window === 'undefined') return 'no-preference';\r\n\r\n const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n const lightQuery = window.matchMedia('(prefers-color-scheme: light)');\r\n\r\n if (darkQuery.matches) return 'dark';\r\n if (lightQuery.matches) return 'light';\r\n return 'no-preference';\r\n });\r\n\r\n useEffect(() => {\r\n if (typeof window === 'undefined') return;\r\n\r\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n\r\n const handleChange = (e: MediaQueryListEvent) => {\r\n setTheme(e.matches ? 'dark' : 'light');\r\n };\r\n\r\n // Re-check in case of no-preference (some browsers don't support light query)\r\n const updateTheme = () => {\r\n const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n const lightQuery = window.matchMedia('(prefers-color-scheme: light)');\r\n\r\n if (darkQuery.matches) setTheme('dark');\r\n else if (lightQuery.matches) setTheme('light');\r\n else setTheme('no-preference');\r\n };\r\n\r\n mediaQuery.addEventListener('change', handleChange);\r\n\r\n // Fallback: some environments may not fire change, so we also listen for light\r\n const lightQuery = window.matchMedia('(prefers-color-scheme: light)');\r\n lightQuery.addEventListener('change', updateTheme);\r\n\r\n return () => {\r\n mediaQuery.removeEventListener('change', handleChange);\r\n lightQuery.removeEventListener('change', updateTheme);\r\n };\r\n }, []);\r\n\r\n return theme;\r\n}\r\n","import { useEffect, useState } from 'preact/hooks';\r\n\r\n/** Network Information API (not in all browsers) */\r\ninterface NetworkInformation extends EventTarget {\r\n effectiveType?: string;\r\n downlink?: number;\r\n rtt?: number;\r\n saveData?: boolean;\r\n type?: string;\r\n}\r\n\r\n/** Effective connection type from Network Information API */\r\nexport type EffectiveConnectionType = 'slow-2g' | '2g' | '3g' | '4g';\r\n\r\n/** Network connection type (e.g., wifi, cellular) */\r\nexport type ConnectionType =\r\n | 'bluetooth'\r\n | 'cellular'\r\n | 'ethernet'\r\n | 'mixed'\r\n | 'none'\r\n | 'other'\r\n | 'unknown'\r\n | 'wifi';\r\n\r\nexport interface NetworkState {\r\n /** Whether the browser is online */\r\n online: boolean;\r\n /** Effective connection type (when supported) */\r\n effectiveType?: EffectiveConnectionType;\r\n /** Estimated downlink speed in Mbps (when supported) */\r\n downlink?: number;\r\n /** Estimated round-trip time in ms (when supported) */\r\n rtt?: number;\r\n /** Whether the user has requested reduced data usage (when supported) */\r\n saveData?: boolean;\r\n /** Connection type (when supported) */\r\n connectionType?: ConnectionType;\r\n}\r\n\r\nfunction getNetworkState(): NetworkState {\r\n if (typeof navigator === 'undefined') {\r\n return { online: true };\r\n }\r\n\r\n const state: NetworkState = {\r\n online: navigator.onLine,\r\n };\r\n\r\n const connection =\r\n (navigator as Navigator & { connection?: NetworkInformation }).connection;\r\n\r\n if (connection) {\r\n if (connection.effectiveType !== undefined) {\r\n state.effectiveType = connection.effectiveType as EffectiveConnectionType;\r\n }\r\n if (connection.downlink !== undefined) {\r\n state.downlink = connection.downlink;\r\n }\r\n if (connection.rtt !== undefined) {\r\n state.rtt = connection.rtt;\r\n }\r\n if (connection.saveData !== undefined) {\r\n state.saveData = connection.saveData;\r\n }\r\n if (connection.type !== undefined) {\r\n state.connectionType = connection.type as ConnectionType;\r\n }\r\n }\r\n\r\n return state;\r\n}\r\n\r\n/**\r\n * A Preact hook that returns the current network state, including online/offline\r\n * status and (when supported) connection type, downlink, RTT, and save-data preference.\r\n * Updates reactively when the network state changes.\r\n *\r\n * @returns The current network state object\r\n *\r\n * @example\r\n * ```tsx\r\n * function NetworkStatus() {\r\n * const { online, effectiveType, saveData } = useNetworkState();\r\n * return (\r\n * <div>\r\n * Status: {online ? 'Online' : 'Offline'}\r\n * {effectiveType && ` (${effectiveType})`}\r\n * {saveData && ' - Reduced data mode'}\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function useNetworkState(): NetworkState {\r\n const [state, setState] = useState<NetworkState>(getNetworkState);\r\n\r\n useEffect(() => {\r\n if (typeof window === 'undefined') return;\r\n\r\n const updateState = () => setState(getNetworkState());\r\n\r\n window.addEventListener('online', updateState);\r\n window.addEventListener('offline', updateState);\r\n\r\n const connection = (navigator as Navigator & { connection?: NetworkInformation })\r\n .connection;\r\n if (connection?.addEventListener) {\r\n connection.addEventListener('change', updateState);\r\n }\r\n\r\n return () => {\r\n window.removeEventListener('online', updateState);\r\n window.removeEventListener('offline', updateState);\r\n if (connection?.removeEventListener) {\r\n connection.removeEventListener('change', updateState);\r\n }\r\n };\r\n }, []);\r\n\r\n return state;\r\n}\r\n","import { useCallback, useState } from 'preact/hooks';\r\n\r\nexport interface UseClipboardOptions {\r\n /** Duration in ms to keep `copied` true before resetting. Default: 2000 */\r\n resetDelay?: number;\r\n}\r\n\r\nexport interface UseClipboardReturn {\r\n /** Copy text to the clipboard. Returns true on success. */\r\n copy: (text: string) => Promise<boolean>;\r\n /** Read text from the clipboard. Returns empty string if denied or unavailable. */\r\n paste: () => Promise<string>;\r\n /** Whether the last copy operation succeeded (resets after resetDelay) */\r\n copied: boolean;\r\n /** Error from the last failed operation, or null */\r\n error: Error | null;\r\n /** Manually reset copied and error state */\r\n reset: () => void;\r\n}\r\n\r\n/**\r\n * A Preact hook for reading and writing the clipboard. Uses the async\r\n * Clipboard API when available (requires secure context and user gesture).\r\n *\r\n * @param options - Optional configuration (e.g., resetDelay for copied state)\r\n * @returns Object with copy, paste, copied, error, and reset\r\n *\r\n * @example\r\n * ```tsx\r\n * function CopyButton() {\r\n * const { copy, copied, error } = useClipboard();\r\n * return (\r\n * <button onClick={() => copy('Hello!')}>\r\n * {copied ? 'Copied!' : 'Copy'}\r\n * </button>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function useClipboard(options: UseClipboardOptions = {}): UseClipboardReturn {\r\n const { resetDelay = 2000 } = options;\r\n\r\n const [copied, setCopied] = useState(false);\r\n const [error, setError] = useState<Error | null>(null);\r\n\r\n const reset = useCallback(() => {\r\n setCopied(false);\r\n setError(null);\r\n }, []);\r\n\r\n const copy = useCallback(\r\n async (text: string): Promise<boolean> => {\r\n setError(null);\r\n\r\n if (typeof navigator === 'undefined' || !navigator.clipboard) {\r\n const err = new Error('Clipboard API is not available');\r\n setError(err);\r\n return false;\r\n }\r\n\r\n try {\r\n await navigator.clipboard.writeText(text);\r\n setCopied(true);\r\n if (resetDelay > 0) {\r\n setTimeout(() => setCopied(false), resetDelay);\r\n }\r\n return true;\r\n } catch (e) {\r\n const err = e instanceof Error ? e : new Error(String(e));\r\n setError(err);\r\n return false;\r\n }\r\n },\r\n [resetDelay]\r\n );\r\n\r\n const paste = useCallback(async (): Promise<string> => {\r\n setError(null);\r\n\r\n if (typeof navigator === 'undefined' || !navigator.clipboard) {\r\n const err = new Error('Clipboard API is not available');\r\n setError(err);\r\n return '';\r\n }\r\n\r\n try {\r\n const text = await navigator.clipboard.readText();\r\n return text;\r\n } catch (e) {\r\n const err = e instanceof Error ? e : new Error(String(e));\r\n setError(err);\r\n return '';\r\n }\r\n }, []);\r\n\r\n return { copy, paste, copied, error, reset };\r\n}\r\n","import type { RefObject } from 'preact'\r\nimport { useEffect, useRef } from 'preact/hooks'\r\n\r\nexport interface RageClickPayload {\r\n /** Number of clicks that triggered the rage click */\r\n count: number\r\n /** Last click event (e.g. for Sentry context) */\r\n event: MouseEvent\r\n}\r\n\r\nexport interface UseRageClickOptions {\r\n /** Called when a rage click is detected. Use this to report to Sentry or your error tracker. */\r\n onRageClick: (payload: RageClickPayload) => void\r\n /** Minimum number of clicks in the time window to count as rage click. Default: 5 (Sentry-style). */\r\n threshold?: number\r\n /** Time window in ms. Default: 1000. */\r\n timeWindow?: number\r\n /** Max distance in px between clicks to count as same spot. Default: 30. Set to Infinity to ignore distance. */\r\n distanceThreshold?: number\r\n}\r\n\r\ninterface ClickRecord {\r\n time: number\r\n x: number\r\n y: number\r\n}\r\n\r\nfunction distance(a: ClickRecord, b: ClickRecord): number {\r\n return Math.hypot(b.x - a.x, b.y - a.y)\r\n}\r\n\r\n/**\r\n * Detects \"rage clicks\" (repeated rapid clicks in the same area), e.g. when the UI\r\n * is unresponsive. Use the callback to report to Sentry or similar tools to surface\r\n * rage click issues and lower rage-click-related support.\r\n *\r\n * @param targetRef - Ref of the element to monitor (e.g. a button or card).\r\n * @param options - onRageClick callback and optional threshold, timeWindow, distanceThreshold.\r\n *\r\n * @example\r\n * ```tsx\r\n * const ref = useRef<HTMLButtonElement>(null)\r\n * useRageClick(ref, {\r\n * onRageClick: ({ count, event }) => {\r\n * Sentry.captureMessage('Rage click detected', { extra: { count, target: event.target } })\r\n * },\r\n * })\r\n * return <button ref={ref}>Submit</button>\r\n * ```\r\n */\r\nexport function useRageClick(\r\n targetRef: RefObject<HTMLElement | null>,\r\n options: UseRageClickOptions\r\n) {\r\n const {\r\n onRageClick,\r\n threshold = 5,\r\n timeWindow = 1000,\r\n distanceThreshold = 30,\r\n } = options\r\n\r\n const onRageClickRef = useRef(onRageClick)\r\n onRageClickRef.current = onRageClick\r\n\r\n const clicksRef = useRef<ClickRecord[]>([])\r\n\r\n useEffect(() => {\r\n const node = targetRef.current\r\n if (!node) return\r\n\r\n const handleClick = (e: MouseEvent) => {\r\n const now = Date.now()\r\n const record: ClickRecord = { time: now, x: e.clientX, y: e.clientY }\r\n\r\n const clicks = clicksRef.current\r\n const cutoff = now - timeWindow\r\n const recent = clicks.filter((c) => c.time >= cutoff)\r\n recent.push(record)\r\n\r\n if (distanceThreshold !== Infinity) {\r\n const inRange = recent.filter(\r\n (c) => distance(c, record) <= distanceThreshold\r\n )\r\n if (inRange.length >= threshold) {\r\n onRageClickRef.current({ count: inRange.length, event: e })\r\n clicksRef.current = []\r\n return\r\n }\r\n } else {\r\n if (recent.length >= threshold) {\r\n onRageClickRef.current({ count: recent.length, event: e })\r\n clicksRef.current = []\r\n return\r\n }\r\n }\r\n\r\n clicksRef.current = recent\r\n }\r\n\r\n node.addEventListener('click', handleClick)\r\n return () => node.removeEventListener('click', handleClick)\r\n }, [targetRef, threshold, timeWindow, distanceThreshold])\r\n}\r\n"],"names":["useTransition","_useState","useState","isPending","setIsPending","useCallback","callback","Promise","resolve","then","useMutationObserver","targetRef","options","useEffect","node","current","observer","MutationObserver","observe","disconnect","listeners","Map","useEventBus","emit","event","_arguments","arguments","handlers","get","forEach","handler","apply","slice","call","on","Set","set","add","size","useWrappedChildren","children","injectProps","mergeStrategy","useMemo","enhanceChild","child","isValidElement","mergedProps","existingProps","props","_extends","existingStyle","style","injectStyle","cloneElement","Array","isArray","map","usePreferredTheme","window","darkQuery","matchMedia","lightQuery","matches","theme","setTheme","mediaQuery","handleChange","e","updateTheme","addEventListener","removeEventListener","getNetworkState","navigator","online","state","onLine","connection","undefined","effectiveType","downlink","rtt","saveData","type","connectionType","useNetworkState","setState","updateState","useClipboard","_options$resetDelay","resetDelay","copied","setCopied","_useState2","error","setError","reset","copy","text","clipboard","err","Error","_catch","writeText","setTimeout","String","reject","paste","readText","useRageClick","onRageClick","_options$threshold","threshold","_options$timeWindow","timeWindow","_options$distanceThre","distanceThreshold","onRageClickRef","useRef","clicksRef","handleClick","now","Date","record","time","x","clientX","y","clientY","cutoff","recent","filter","c","push","Infinity","inRange","a","b","Math","hypot","length","count"],"mappings":"2JAMgB,SAAAA,IACd,IAAAC,EAAkCC,GAAS,GAApCC,EAASF,KAAEG,EAAYH,EAAA,GAU9B,MAAO,CARiBI,EAAY,SAACC,GACnCF,GAAa,GACbG,QAAQC,UAAUC,KAAK,WACrBH,IACAF,GAAa,EACf,EACF,EAAG,IAEsBD,EAC3B,CCPgB,SAAAO,EACdC,EACAL,EACAM,GAEAC,EAAU,WACR,IAAMC,EAAOH,EAAUI,QACvB,GAAKD,EAAL,CAEA,IAAME,EAAW,IAAIC,iBAAiBX,GAGtC,OAFAU,EAASE,QAAQJ,EAAMF,GAEV,WAAA,OAAAI,EAASG,YAAY,CAHlC,CAIF,EAAG,CAACR,EAAWL,EAAUM,GAC3B,CCrBA,IAAMQ,EAAY,IAAIC,IAMN,SAAAC,IACd,IAAMC,EAAOlB,EAAY,SAAoBmB,GAAuCC,IAAAA,EAAAC,UAC5EC,EAAWP,EAAUQ,IAAIJ,GAC3BG,GACFA,EAASE,QAAQ,SAACC,UAAYA,EAAOC,WAAA,EAAA,GAAAC,MAAAC,KAAAR,EAAQ,GAAC,EAElD,EAAG,IAkBH,MAAO,CAAEF,KAAAA,EAAMW,GAhBJ7B,EAAY,SAAoBmB,EAAUM,GACnD,IAAIH,EAAWP,EAAUQ,IAAIJ,GAO7B,OANKG,IACHA,EAAW,IAAIQ,IACff,EAAUgB,IAAIZ,EAAiBG,IAEjCA,EAASU,IAAIP,GAED,WACVH,EAAS,OAAQG,GACM,IAAnBH,EAAUW,MACZlB,EAAS,OAAQI,EAErB,CACF,EAAG,IAGL,yNCvBgB,SAAAe,EACdC,EACAC,EACAC,GAEA,gBAFAA,IAAAA,EAAyC,YAElCC,EAAQ,WACb,IAAKH,EAAU,OAAOA,EAEtB,IAAMI,EAAe,SAACC,GACpB,IAAKC,EAAeD,GAAQ,OAAOA,EAEnC,IAEIE,EAFEC,EAAgBH,EAAMI,OAAS,GAMnCF,EAFoB,aAAlBL,EAESQ,EAAQF,CAAAA,EAAAA,EAAkBP,GAG1BS,EAAQT,CAAAA,EAAAA,EAAgBO,GAIrC,IAAMG,EAAsC,MAArBH,OAAqB,EAArBA,EAAuBI,MACxCC,EAAkC,MAAnBZ,OAAmB,EAAnBA,EAAqBW,MAW1C,OATID,GAAiBE,GACQ,iBAAlBF,GAAqD,iBAAhBE,IAE3CN,EAAoBK,MADD,aAAlBV,EACwBQ,EAAQC,CAAAA,EAAAA,EAAkBE,GAE1BH,EAAA,CAAA,EAAQG,EAAgBF,IAI/CG,EAAaT,EAAOE,EAC7B,EAEA,OAAIQ,MAAMC,QAAQhB,GACTA,EAASiB,IAAIb,GAGfA,EAAaJ,EACtB,EAAG,CAACA,EAAUC,EAAaC,GAC7B,CClCgB,SAAAgB,IACd,IAAAzD,EAA0BC,EAAyB,WACjD,GAAsB,oBAAXyD,OAAwB,MAAO,gBAE1C,IAAMC,EAAYD,OAAOE,WAAW,gCAC9BC,EAAaH,OAAOE,WAAW,iCAErC,OAAID,EAAUG,QAAgB,OAC1BD,EAAWC,QAAgB,QACxB,eACT,GATOC,EAAK/D,EAAA,GAAEgE,EAAQhE,EAAA,GA0CtB,OA/BAY,EAAU,WACR,GAAsB,oBAAX8C,OAAX,CAEA,IAAMO,EAAaP,OAAOE,WAAW,gCAE/BM,EAAe,SAACC,GACpBH,EAASG,EAAEL,QAAU,OAAS,QAChC,EAGMM,EAAc,WAClB,IAAMT,EAAYD,OAAOE,WAAW,gCAC9BC,EAAaH,OAAOE,WAAW,iCAEdI,EAAnBL,EAAUG,QAAkB,OACvBD,EAAWC,QAAkB,QACxB,gBAChB,EAEAG,EAAWI,iBAAiB,SAAUH,GAGtC,IAAML,EAAaH,OAAOE,WAAW,iCAGrC,OAFAC,EAAWQ,iBAAiB,SAAUD,GAE/B,WACLH,EAAWK,oBAAoB,SAAUJ,GACzCL,EAAWS,oBAAoB,SAAUF,EAC3C,EACF,EAAG,IAEIL,CACT,CC3BA,SAASQ,IACP,GAAyB,oBAAdC,UACT,MAAO,CAAEC,QAAQ,GAGnB,IAAMC,EAAsB,CAC1BD,OAAQD,UAAUG,QAGdC,EACHJ,UAA8DI,WAoBjE,OAlBIA,SAC+BC,IAA7BD,EAAWE,gBACbJ,EAAMI,cAAgBF,EAAWE,oBAEPD,IAAxBD,EAAWG,WACbL,EAAMK,SAAWH,EAAWG,eAEPF,IAAnBD,EAAWI,MACbN,EAAMM,IAAMJ,EAAWI,UAEGH,IAAxBD,EAAWK,WACbP,EAAMO,SAAWL,EAAWK,eAENJ,IAApBD,EAAWM,OACbR,EAAMS,eAAiBP,EAAWM,OAI/BR,CACT,UAuBgBU,IACd,IAAApF,EAA0BC,EAAuBsE,GAA1CG,EAAK1E,EAAA,GAAEqF,EAAQrF,EAAA,GAyBtB,OAvBAY,EAAU,WACR,GAAsB,oBAAX8C,OAAX,CAEA,IAAM4B,EAAc,WAAM,OAAAD,EAASd,IAAkB,EAErDb,OAAOW,iBAAiB,SAAUiB,GAClC5B,OAAOW,iBAAiB,UAAWiB,GAEnC,IAAMV,EAAcJ,UACjBI,WAKH,OAJIA,MAAAA,GAAAA,EAAYP,kBACdO,EAAWP,iBAAiB,SAAUiB,GAG5B,WACV5B,OAAOY,oBAAoB,SAAUgB,GACrC5B,OAAOY,oBAAoB,UAAWgB,GAClCV,MAAAA,GAAAA,EAAYN,qBACdM,EAAWN,oBAAoB,SAAUgB,EAE7C,CAjBA,CAkBF,EAAG,IAEIZ,CACT,wFClFgB,SAAAa,EAAa5E,QAAA,IAAAA,IAAAA,EAA+B,IAC1D,IAAqC6E,EAAP7E,EAAtB8E,WAAAA,OAAa,IAAHD,EAAG,IAAIA,EAEzBxF,EAA4BC,GAAS,GAA9ByF,EAAM1F,EAAE2F,GAAAA,EAAS3F,KACxB4F,EAA0B3F,EAAuB,MAA1C4F,EAAKD,EAAA,GAAEE,EAAQF,EAEtB,GAAMG,EAAQ3F,EAAY,WACxBuF,GAAU,GACVG,EAAS,KACX,EAAG,IA+CH,MAAO,CAAEE,KA7CI5F,EACJ6F,SAAAA,GAAkC,IAGvC,GAFAH,EAAS,MAEgB,oBAAdtB,YAA8BA,UAAU0B,UAAW,CAC5D,IAAMC,EAAM,IAAIC,MAAM,kCAEtB,OADAN,EAASK,GACT7F,QAAAC,SAAO,EACT,CAAC,OAAAD,QAAAC,QAAA8F,aAEG/F,OAAAA,QAAAC,QACIiE,UAAU0B,UAAUI,UAAUL,IAAKzF,KAAA,WAKzC,OAJAmF,GAAU,GACNF,EAAa,GACfc,WAAW,WAAM,OAAAZ,GAAU,EAAM,EAAEF,KAEzB,EACd,WAAStB,GACP,IAAMgC,EAAMhC,aAAaiC,MAAQjC,EAAI,IAAIiC,MAAMI,OAAOrC,IAEtD,OADA2B,EAASK,IAEX,CAAA,GACF,CAAC,MAAAhC,UAAA7D,QAAAmG,OAAAtC,EAAA,CAAA,EACD,CAACsB,IAsBYiB,MAnBDtG,EAAwC,WAAA,IAGpD,GAFA0F,EAAS,MAEgB,oBAAdtB,YAA8BA,UAAU0B,UAAW,CAC5D,IAAMC,EAAM,IAAIC,MAAM,kCAEtB,OADAN,EAASK,GACT7F,QAAAC,QAAO,GACT,CAAC,OAAAD,QAAAC,QAAA8F,EAEG,WAAA,OAAA/F,QAAAC,QACiBiE,UAAU0B,UAAUS,WAEzC,EAAC,SAAQxC,GACP,IAAMgC,EAAMhC,aAAaiC,MAAQjC,EAAI,IAAIiC,MAAMI,OAAOrC,IAEtD,OADA2B,EAASK,GACF,EACT,GACF,CAAC,MAAAhC,GAAA,OAAA7D,QAAAmG,OAAAtC,EAAE,CAAA,EAAA,IAEmBuB,OAAAA,EAAQG,MAAAA,EAAOE,MAAAA,EACvC,CC9CgB,SAAAa,EACdlG,EACAC,GAEA,IACEkG,EAIElG,EAJFkG,YAAWC,EAITnG,EAHFoG,UAAAA,OAAS,IAAAD,EAAG,EAACA,EAAAE,EAGXrG,EAFFsG,WAAAA,OAAU,IAAAD,EAAG,IAAIA,EAAAE,EAEfvG,EADFwG,kBAAAA,OAAiB,IAAAD,EAAG,GAAEA,EAGlBE,EAAiBC,EAAOR,GAC9BO,EAAetG,QAAU+F,EAEzB,IAAMS,EAAYD,EAAsB,IAExCzG,EAAU,WACR,IAAMC,EAAOH,EAAUI,QACvB,GAAKD,EAAL,CAEA,IAAM0G,EAAc,SAACpD,GACnB,IAAMqD,EAAMC,KAAKD,MACXE,EAAsB,CAAEC,KAAMH,EAAKI,EAAGzD,EAAE0D,QAASC,EAAG3D,EAAE4D,SAGtDC,EAASR,EAAMP,EACfgB,EAFSX,EAAUxG,QAEHoH,OAAO,SAACC,GAAM,OAAAA,EAAER,MAAQK,CAAM,GAGpD,GAFAC,EAAOG,KAAKV,GAEcW,WAAtBlB,EAAgC,CAClC,IAAMmB,EAAUL,EAAOC,OACrB,SAACC,GAAM,OAtDCI,EAsDQJ,EAtDQK,EAsDLd,EArDpBe,KAAKC,MAAMF,EAAEZ,EAAIW,EAAEX,EAAGY,EAAEV,EAAIS,EAAET,IAqDCX,EAtDxC,IAAkBoB,EAAgBC,CAsDuB,GAEjD,GAAIF,EAAQK,QAAU5B,EAGpB,OAFAK,EAAetG,QAAQ,CAAE8H,MAAON,EAAQK,OAAQpH,MAAO4C,SACvDmD,EAAUxG,QAAU,GAGxB,MACE,GAAImH,EAAOU,QAAU5B,EAGnB,OAFAK,EAAetG,QAAQ,CAAE8H,MAAOX,EAAOU,OAAQpH,MAAO4C,SACtDmD,EAAUxG,QAAU,IAKxBwG,EAAUxG,QAAUmH,CACtB,EAGA,OADApH,EAAKwD,iBAAiB,QAASkD,GACxB,WAAA,OAAM1G,EAAKyD,oBAAoB,QAASiD,EAAY,EAC7D,EAAG,CAAC7G,EAAWqG,EAAWE,EAAYE,GACxC"}
|
package/dist/index.umd.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports,require("preact/hooks"),require("preact")):"function"==typeof define&&define.amd?define(["exports","preact/hooks","preact"],n):n((e||self).preactMissingHooks={},e.hooks,e.preact)}(this,function(e,n,r){var t=new Map;function
|
|
1
|
+
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports,require("preact/hooks"),require("preact")):"function"==typeof define&&define.amd?define(["exports","preact/hooks","preact"],n):n((e||self).preactMissingHooks={},e.hooks,e.preact)}(this,function(e,n,r){var t=new Map;function i(){return i=Object.assign?Object.assign.bind():function(e){for(var n=1;n<arguments.length;n++){var r=arguments[n];for(var t in r)({}).hasOwnProperty.call(r,t)&&(e[t]=r[t])}return e},i.apply(null,arguments)}function o(){if("undefined"==typeof navigator)return{online:!0};var e={online:navigator.onLine},n=navigator.connection;return n&&(void 0!==n.effectiveType&&(e.effectiveType=n.effectiveType),void 0!==n.downlink&&(e.downlink=n.downlink),void 0!==n.rtt&&(e.rtt=n.rtt),void 0!==n.saveData&&(e.saveData=n.saveData),void 0!==n.type&&(e.connectionType=n.type)),e}function a(e,n){try{var r=e()}catch(e){return n(e)}return r&&r.then?r.then(void 0,n):r}e.useClipboard=function(e){void 0===e&&(e={});var r=e.resetDelay,t=void 0===r?2e3:r,i=n.useState(!1),o=i[0],c=i[1],u=n.useState(null),s=u[0],f=u[1],l=n.useCallback(function(){c(!1),f(null)},[]);return{copy:n.useCallback(function(e){try{if(f(null),"undefined"==typeof navigator||!navigator.clipboard){var n=new Error("Clipboard API is not available");return f(n),Promise.resolve(!1)}return Promise.resolve(a(function(){return Promise.resolve(navigator.clipboard.writeText(e)).then(function(){return c(!0),t>0&&setTimeout(function(){return c(!1)},t),!0})},function(e){var n=e instanceof Error?e:new Error(String(e));return f(n),!1}))}catch(e){return Promise.reject(e)}},[t]),paste:n.useCallback(function(){try{if(f(null),"undefined"==typeof navigator||!navigator.clipboard){var e=new Error("Clipboard API is not available");return f(e),Promise.resolve("")}return Promise.resolve(a(function(){return Promise.resolve(navigator.clipboard.readText())},function(e){var n=e instanceof Error?e:new Error(String(e));return f(n),""}))}catch(e){return Promise.reject(e)}},[]),copied:o,error:s,reset:l}},e.useEventBus=function(){var e=n.useCallback(function(e){var n=arguments,r=t.get(e);r&&r.forEach(function(e){return e.apply(void 0,[].slice.call(n,1))})},[]);return{emit:e,on:n.useCallback(function(e,n){var r=t.get(e);return r||(r=new Set,t.set(e,r)),r.add(n),function(){r.delete(n),0===r.size&&t.delete(e)}},[])}},e.useMutationObserver=function(e,r,t){n.useEffect(function(){var n=e.current;if(n){var i=new MutationObserver(r);return i.observe(n,t),function(){return i.disconnect()}}},[e,r,t])},e.useNetworkState=function(){var e=n.useState(o),r=e[0],t=e[1];return n.useEffect(function(){if("undefined"!=typeof window){var e=function(){return t(o())};window.addEventListener("online",e),window.addEventListener("offline",e);var n=navigator.connection;return null!=n&&n.addEventListener&&n.addEventListener("change",e),function(){window.removeEventListener("online",e),window.removeEventListener("offline",e),null!=n&&n.removeEventListener&&n.removeEventListener("change",e)}}},[]),r},e.usePreferredTheme=function(){var e=n.useState(function(){if("undefined"==typeof window)return"no-preference";var e=window.matchMedia("(prefers-color-scheme: dark)"),n=window.matchMedia("(prefers-color-scheme: light)");return e.matches?"dark":n.matches?"light":"no-preference"}),r=e[0],t=e[1];return n.useEffect(function(){if("undefined"!=typeof window){var e=window.matchMedia("(prefers-color-scheme: dark)"),n=function(e){t(e.matches?"dark":"light")},r=function(){var e=window.matchMedia("(prefers-color-scheme: dark)"),n=window.matchMedia("(prefers-color-scheme: light)");t(e.matches?"dark":n.matches?"light":"no-preference")};e.addEventListener("change",n);var i=window.matchMedia("(prefers-color-scheme: light)");return i.addEventListener("change",r),function(){e.removeEventListener("change",n),i.removeEventListener("change",r)}}},[]),r},e.useRageClick=function(e,r){var t=r.onRageClick,i=r.threshold,o=void 0===i?5:i,a=r.timeWindow,c=void 0===a?1e3:a,u=r.distanceThreshold,s=void 0===u?30:u,f=n.useRef(t);f.current=t;var l=n.useRef([]);n.useEffect(function(){var n=e.current;if(n){var r=function(e){var n=Date.now(),r={time:n,x:e.clientX,y:e.clientY},t=n-c,i=l.current.filter(function(e){return e.time>=t});if(i.push(r),Infinity!==s){var a=i.filter(function(e){return n=e,t=r,Math.hypot(t.x-n.x,t.y-n.y)<=s;var n,t});if(a.length>=o)return f.current({count:a.length,event:e}),void(l.current=[])}else if(i.length>=o)return f.current({count:i.length,event:e}),void(l.current=[]);l.current=i};return n.addEventListener("click",r),function(){return n.removeEventListener("click",r)}}},[e,o,c,s])},e.useTransition=function(){var e=n.useState(!1),r=e[0],t=e[1];return[n.useCallback(function(e){t(!0),Promise.resolve().then(function(){e(),t(!1)})},[]),r]},e.useWrappedChildren=function(e,t,o){return void 0===o&&(o="preserve"),n.useMemo(function(){if(!e)return e;var n=function(e){if(!r.isValidElement(e))return e;var n,a=e.props||{};n="override"===o?i({},a,t):i({},t,a);var c=null==a?void 0:a.style,u=null==t?void 0:t.style;return c&&u&&"object"==typeof c&&"object"==typeof u&&(n.style="override"===o?i({},c,u):i({},u,c)),r.cloneElement(e,n)};return Array.isArray(e)?e.map(n):n(e)},[e,t,o])}});
|
|
2
2
|
//# sourceMappingURL=index.umd.js.map
|
package/dist/index.umd.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.umd.js","sources":["../src/useEventBus.ts","../src/useNetworkState.ts","../src/useClipboard.ts","../src/useMutationObserver.ts","../src/usePreferredTheme.ts","../src/useTransition.ts","../src/useWrappedChildren.ts"],"sourcesContent":["import { useCallback, useEffect } from 'preact/hooks';\r\n\r\ntype EventMap = Record<string, (...args: any[]) => void>;\r\n\r\nconst listeners = new Map<string, Set<(...args: any[]) => void>>();\r\n\r\n/**\r\n * A Preact hook to publish and subscribe to custom events across components.\r\n * @returns An object with `emit` and `on` methods.\r\n */\r\nexport function useEventBus<T extends EventMap>() {\r\n const emit = useCallback(<K extends keyof T>(event: K, ...args: Parameters<T[K]>) => {\r\n const handlers = listeners.get(event as string);\r\n if (handlers) {\r\n handlers.forEach((handler) => handler(...args));\r\n }\r\n }, []);\r\n\r\n const on = useCallback(<K extends keyof T>(event: K, handler: T[K]) => {\r\n let handlers = listeners.get(event as string);\r\n if (!handlers) {\r\n handlers = new Set();\r\n listeners.set(event as string, handlers);\r\n }\r\n handlers.add(handler);\r\n\r\n return () => {\r\n handlers!.delete(handler);\r\n if (handlers!.size === 0) {\r\n listeners.delete(event as string);\r\n }\r\n };\r\n }, []);\r\n\r\n return { emit, on };\r\n}\r\n","import { useEffect, useState } from 'preact/hooks';\r\n\r\n/** Network Information API (not in all browsers) */\r\ninterface NetworkInformation extends EventTarget {\r\n effectiveType?: string;\r\n downlink?: number;\r\n rtt?: number;\r\n saveData?: boolean;\r\n type?: string;\r\n}\r\n\r\n/** Effective connection type from Network Information API */\r\nexport type EffectiveConnectionType = 'slow-2g' | '2g' | '3g' | '4g';\r\n\r\n/** Network connection type (e.g., wifi, cellular) */\r\nexport type ConnectionType =\r\n | 'bluetooth'\r\n | 'cellular'\r\n | 'ethernet'\r\n | 'mixed'\r\n | 'none'\r\n | 'other'\r\n | 'unknown'\r\n | 'wifi';\r\n\r\nexport interface NetworkState {\r\n /** Whether the browser is online */\r\n online: boolean;\r\n /** Effective connection type (when supported) */\r\n effectiveType?: EffectiveConnectionType;\r\n /** Estimated downlink speed in Mbps (when supported) */\r\n downlink?: number;\r\n /** Estimated round-trip time in ms (when supported) */\r\n rtt?: number;\r\n /** Whether the user has requested reduced data usage (when supported) */\r\n saveData?: boolean;\r\n /** Connection type (when supported) */\r\n connectionType?: ConnectionType;\r\n}\r\n\r\nfunction getNetworkState(): NetworkState {\r\n if (typeof navigator === 'undefined') {\r\n return { online: true };\r\n }\r\n\r\n const state: NetworkState = {\r\n online: navigator.onLine,\r\n };\r\n\r\n const connection =\r\n (navigator as Navigator & { connection?: NetworkInformation }).connection;\r\n\r\n if (connection) {\r\n if (connection.effectiveType !== undefined) {\r\n state.effectiveType = connection.effectiveType as EffectiveConnectionType;\r\n }\r\n if (connection.downlink !== undefined) {\r\n state.downlink = connection.downlink;\r\n }\r\n if (connection.rtt !== undefined) {\r\n state.rtt = connection.rtt;\r\n }\r\n if (connection.saveData !== undefined) {\r\n state.saveData = connection.saveData;\r\n }\r\n if (connection.type !== undefined) {\r\n state.connectionType = connection.type as ConnectionType;\r\n }\r\n }\r\n\r\n return state;\r\n}\r\n\r\n/**\r\n * A Preact hook that returns the current network state, including online/offline\r\n * status and (when supported) connection type, downlink, RTT, and save-data preference.\r\n * Updates reactively when the network state changes.\r\n *\r\n * @returns The current network state object\r\n *\r\n * @example\r\n * ```tsx\r\n * function NetworkStatus() {\r\n * const { online, effectiveType, saveData } = useNetworkState();\r\n * return (\r\n * <div>\r\n * Status: {online ? 'Online' : 'Offline'}\r\n * {effectiveType && ` (${effectiveType})`}\r\n * {saveData && ' - Reduced data mode'}\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function useNetworkState(): NetworkState {\r\n const [state, setState] = useState<NetworkState>(getNetworkState);\r\n\r\n useEffect(() => {\r\n if (typeof window === 'undefined') return;\r\n\r\n const updateState = () => setState(getNetworkState());\r\n\r\n window.addEventListener('online', updateState);\r\n window.addEventListener('offline', updateState);\r\n\r\n const connection = (navigator as Navigator & { connection?: NetworkInformation })\r\n .connection;\r\n if (connection?.addEventListener) {\r\n connection.addEventListener('change', updateState);\r\n }\r\n\r\n return () => {\r\n window.removeEventListener('online', updateState);\r\n window.removeEventListener('offline', updateState);\r\n if (connection?.removeEventListener) {\r\n connection.removeEventListener('change', updateState);\r\n }\r\n };\r\n }, []);\r\n\r\n return state;\r\n}\r\n","import { useCallback, useState } from 'preact/hooks';\r\n\r\nexport interface UseClipboardOptions {\r\n /** Duration in ms to keep `copied` true before resetting. Default: 2000 */\r\n resetDelay?: number;\r\n}\r\n\r\nexport interface UseClipboardReturn {\r\n /** Copy text to the clipboard. Returns true on success. */\r\n copy: (text: string) => Promise<boolean>;\r\n /** Read text from the clipboard. Returns empty string if denied or unavailable. */\r\n paste: () => Promise<string>;\r\n /** Whether the last copy operation succeeded (resets after resetDelay) */\r\n copied: boolean;\r\n /** Error from the last failed operation, or null */\r\n error: Error | null;\r\n /** Manually reset copied and error state */\r\n reset: () => void;\r\n}\r\n\r\n/**\r\n * A Preact hook for reading and writing the clipboard. Uses the async\r\n * Clipboard API when available (requires secure context and user gesture).\r\n *\r\n * @param options - Optional configuration (e.g., resetDelay for copied state)\r\n * @returns Object with copy, paste, copied, error, and reset\r\n *\r\n * @example\r\n * ```tsx\r\n * function CopyButton() {\r\n * const { copy, copied, error } = useClipboard();\r\n * return (\r\n * <button onClick={() => copy('Hello!')}>\r\n * {copied ? 'Copied!' : 'Copy'}\r\n * </button>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function useClipboard(options: UseClipboardOptions = {}): UseClipboardReturn {\r\n const { resetDelay = 2000 } = options;\r\n\r\n const [copied, setCopied] = useState(false);\r\n const [error, setError] = useState<Error | null>(null);\r\n\r\n const reset = useCallback(() => {\r\n setCopied(false);\r\n setError(null);\r\n }, []);\r\n\r\n const copy = useCallback(\r\n async (text: string): Promise<boolean> => {\r\n setError(null);\r\n\r\n if (typeof navigator === 'undefined' || !navigator.clipboard) {\r\n const err = new Error('Clipboard API is not available');\r\n setError(err);\r\n return false;\r\n }\r\n\r\n try {\r\n await navigator.clipboard.writeText(text);\r\n setCopied(true);\r\n if (resetDelay > 0) {\r\n setTimeout(() => setCopied(false), resetDelay);\r\n }\r\n return true;\r\n } catch (e) {\r\n const err = e instanceof Error ? e : new Error(String(e));\r\n setError(err);\r\n return false;\r\n }\r\n },\r\n [resetDelay]\r\n );\r\n\r\n const paste = useCallback(async (): Promise<string> => {\r\n setError(null);\r\n\r\n if (typeof navigator === 'undefined' || !navigator.clipboard) {\r\n const err = new Error('Clipboard API is not available');\r\n setError(err);\r\n return '';\r\n }\r\n\r\n try {\r\n const text = await navigator.clipboard.readText();\r\n return text;\r\n } catch (e) {\r\n const err = e instanceof Error ? e : new Error(String(e));\r\n setError(err);\r\n return '';\r\n }\r\n }, []);\r\n\r\n return { copy, paste, copied, error, reset };\r\n}\r\n","import { RefObject } from 'preact'\r\nimport { useEffect } from 'preact/hooks'\r\n\r\nexport type UseMutationObserverOptions = MutationObserverInit\r\n\r\n/**\r\n * A Preact hook to observe DOM mutations using MutationObserver.\r\n * @param target - The element to observe.\r\n * @param callback - Function to call on mutation.\r\n * @param options - MutationObserver options.\r\n */\r\nexport function useMutationObserver(\r\n targetRef: RefObject<HTMLElement | null>,\r\n callback: MutationCallback,\r\n options: MutationObserverInit\r\n) {\r\n useEffect(() => {\r\n const node = targetRef.current\r\n if (!node) return\r\n\r\n const observer = new MutationObserver(callback)\r\n observer.observe(node, options)\r\n\r\n return () => observer.disconnect()\r\n }, [targetRef, callback, options])\r\n}\r\n","import { useEffect, useState } from 'preact/hooks';\r\n\r\nexport type PreferredTheme = 'light' | 'dark' | 'no-preference';\r\n\r\n/**\r\n * A Preact hook that returns the user's preferred color scheme based on the\r\n * `prefers-color-scheme` media query. Updates reactively when the user changes\r\n * their system or browser theme preference.\r\n *\r\n * @returns The preferred theme: 'light', 'dark', or 'no-preference'\r\n *\r\n * @example\r\n * ```tsx\r\n * function ThemeAwareComponent() {\r\n * const theme = usePreferredTheme();\r\n * return (\r\n * <div data-theme={theme}>\r\n * Current preference: {theme}\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function usePreferredTheme(): PreferredTheme {\r\n const [theme, setTheme] = useState<PreferredTheme>(() => {\r\n if (typeof window === 'undefined') return 'no-preference';\r\n\r\n const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n const lightQuery = window.matchMedia('(prefers-color-scheme: light)');\r\n\r\n if (darkQuery.matches) return 'dark';\r\n if (lightQuery.matches) return 'light';\r\n return 'no-preference';\r\n });\r\n\r\n useEffect(() => {\r\n if (typeof window === 'undefined') return;\r\n\r\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n\r\n const handleChange = (e: MediaQueryListEvent) => {\r\n setTheme(e.matches ? 'dark' : 'light');\r\n };\r\n\r\n // Re-check in case of no-preference (some browsers don't support light query)\r\n const updateTheme = () => {\r\n const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n const lightQuery = window.matchMedia('(prefers-color-scheme: light)');\r\n\r\n if (darkQuery.matches) setTheme('dark');\r\n else if (lightQuery.matches) setTheme('light');\r\n else setTheme('no-preference');\r\n };\r\n\r\n mediaQuery.addEventListener('change', handleChange);\r\n\r\n // Fallback: some environments may not fire change, so we also listen for light\r\n const lightQuery = window.matchMedia('(prefers-color-scheme: light)');\r\n lightQuery.addEventListener('change', updateTheme);\r\n\r\n return () => {\r\n mediaQuery.removeEventListener('change', handleChange);\r\n lightQuery.removeEventListener('change', updateTheme);\r\n };\r\n }, []);\r\n\r\n return theme;\r\n}\r\n","import { useState, useCallback } from 'preact/hooks';\r\n\r\n/**\r\n * Mimics React's useTransition hook in Preact.\r\n * @returns [startTransition, isPending]\r\n */\r\nexport function useTransition(): [startTransition: (callback: () => void) => void, isPending: boolean] {\r\n const [isPending, setIsPending] = useState(false);\r\n\r\n const startTransition = useCallback((callback: () => void) => {\r\n setIsPending(true);\r\n Promise.resolve().then(() => {\r\n callback();\r\n setIsPending(false);\r\n });\r\n }, []);\r\n\r\n return [startTransition, isPending];\r\n}\r\n","import { ComponentChildren, cloneElement, isValidElement } from 'preact'\r\nimport { useMemo } from 'preact/hooks'\r\n\r\nexport type InjectableProps = Record<string, any>\r\n\r\n/**\r\n * A Preact hook to wrap children components and inject additional props into them.\r\n * @param children - The children to wrap and enhance with props.\r\n * @param injectProps - The props to inject into each child component.\r\n * @param mergeStrategy - How to handle prop conflicts ('override' | 'preserve'). Defaults to 'preserve'.\r\n * @returns Enhanced children with injected props.\r\n */\r\nexport function useWrappedChildren(\r\n children: ComponentChildren,\r\n injectProps: InjectableProps,\r\n mergeStrategy: 'override' | 'preserve' = 'preserve'\r\n): ComponentChildren {\r\n return useMemo(() => {\r\n if (!children) return children\r\n\r\n const enhanceChild = (child: any): any => {\r\n if (!isValidElement(child)) return child\r\n\r\n const existingProps = child.props || {}\r\n \r\n let mergedProps: InjectableProps\r\n \r\n if (mergeStrategy === 'override') {\r\n // Injected props override existing ones\r\n mergedProps = { ...existingProps, ...injectProps }\r\n } else {\r\n // Existing props are preserved, injected props are added only if not present\r\n mergedProps = { ...injectProps, ...existingProps }\r\n }\r\n\r\n // Special handling for style prop to merge style objects properly\r\n const existingStyle = (existingProps as any)?.style\r\n const injectStyle = (injectProps as any)?.style\r\n \r\n if (existingStyle && injectStyle && \r\n typeof existingStyle === 'object' && typeof injectStyle === 'object') {\r\n if (mergeStrategy === 'override') {\r\n (mergedProps as any).style = { ...existingStyle, ...injectStyle }\r\n } else {\r\n (mergedProps as any).style = { ...injectStyle, ...existingStyle }\r\n }\r\n }\r\n\r\n return cloneElement(child, mergedProps)\r\n }\r\n\r\n if (Array.isArray(children)) {\r\n return children.map(enhanceChild)\r\n }\r\n\r\n return enhanceChild(children)\r\n }, [children, injectProps, mergeStrategy])\r\n}"],"names":["listeners","Map","getNetworkState","navigator","online","state","onLine","connection","undefined","effectiveType","downlink","rtt","saveData","type","connectionType","options","_options$resetDelay","resetDelay","_useState","useState","copied","setCopied","_useState2","error","setError","reset","useCallback","copy","text","clipboard","err","Error","Promise","resolve","_catch","writeText","then","setTimeout","e","String","reject","paste","readText","emit","event","_arguments","arguments","handlers","get","forEach","handler","apply","slice","call","on","Set","set","add","size","targetRef","callback","useEffect","node","current","observer","MutationObserver","observe","disconnect","setState","window","updateState","addEventListener","removeEventListener","darkQuery","matchMedia","lightQuery","matches","theme","setTheme","mediaQuery","handleChange","updateTheme","isPending","setIsPending","children","injectProps","mergeStrategy","useMemo","enhanceChild","child","isValidElement","mergedProps","existingProps","props","_extends","existingStyle","style","injectStyle","cloneElement","Array","isArray","map"],"mappings":"mUAIA,IAAMA,EAAY,IAAIC,4NCoCtB,SAASC,IACP,GAAyB,oBAAdC,UACT,MAAO,CAAEC,QAAQ,GAGnB,IAAMC,EAAsB,CAC1BD,OAAQD,UAAUG,QAGdC,EACHJ,UAA8DI,WAoBjE,OAlBIA,SAC+BC,IAA7BD,EAAWE,gBACbJ,EAAMI,cAAgBF,EAAWE,oBAEPD,IAAxBD,EAAWG,WACbL,EAAMK,SAAWH,EAAWG,eAEPF,IAAnBD,EAAWI,MACbN,EAAMM,IAAMJ,EAAWI,UAEGH,IAAxBD,EAAWK,WACbP,EAAMO,SAAWL,EAAWK,eAENJ,IAApBD,EAAWM,OACbR,EAAMS,eAAiBP,EAAWM,OAI/BR,CACT,uGChCgB,SAAaU,QAAA,IAAAA,IAAAA,EAA+B,IAC1D,IAAqCC,EAAPD,EAAtBE,WAAAA,OAAa,IAAHD,EAAG,IAAIA,EAEzBE,EAA4BC,EAAAA,UAAS,GAA9BC,EAAMF,EAAEG,GAAAA,EAASH,KACxBI,EAA0BH,EAAAA,SAAuB,MAA1CI,EAAKD,EAAA,GAAEE,EAAQF,EAEtB,GAAMG,EAAQC,cAAY,WACxBL,GAAU,GACVG,EAAS,KACX,EAAG,IA+CH,MAAO,CAAEG,KA7CID,cACJE,SAAAA,GAAkC,IAGvC,GAFAJ,EAAS,MAEgB,oBAAdrB,YAA8BA,UAAU0B,UAAW,CAC5D,IAAMC,EAAM,IAAIC,MAAM,kCAEtB,OADAP,EAASM,GACTE,QAAAC,SAAO,EACT,CAAC,OAAAD,QAAAC,QAAAC,aAEGF,OAAAA,QAAAC,QACI9B,UAAU0B,UAAUM,UAAUP,IAAKQ,KAAA,WAKzC,OAJAf,GAAU,GACNJ,EAAa,GACfoB,WAAW,WAAM,OAAAhB,GAAU,EAAM,EAAEJ,KAEzB,EACd,WAASqB,GACP,IAAMR,EAAMQ,aAAaP,MAAQO,EAAI,IAAIP,MAAMQ,OAAOD,IAEtD,OADAd,EAASM,IAEX,CAAA,GACF,CAAC,MAAAQ,UAAAN,QAAAQ,OAAAF,EAAA,CAAA,EACD,CAACrB,IAsBYwB,MAnBDf,EAAWA,YAA6B,WAAA,IAGpD,GAFAF,EAAS,MAEgB,oBAAdrB,YAA8BA,UAAU0B,UAAW,CAC5D,IAAMC,EAAM,IAAIC,MAAM,kCAEtB,OADAP,EAASM,GACTE,QAAAC,QAAO,GACT,CAAC,OAAAD,QAAAC,QAAAC,EAEG,WAAA,OAAAF,QAAAC,QACiB9B,UAAU0B,UAAUa,WAEzC,EAAC,SAAQJ,GACP,IAAMR,EAAMQ,aAAaP,MAAQO,EAAI,IAAIP,MAAMQ,OAAOD,IAEtD,OADAd,EAASM,GACF,EACT,GACF,CAAC,MAAAQ,GAAA,OAAAN,QAAAQ,OAAAF,EAAE,CAAA,EAAA,IAEmBlB,OAAAA,EAAQG,MAAAA,EAAOE,MAAAA,EACvC,gBFtFgB,WACd,IAAMkB,EAAOjB,EAAAA,YAAY,SAAoBkB,GAAuCC,IAAAA,EAAAC,UAC5EC,EAAW/C,EAAUgD,IAAIJ,GAC3BG,GACFA,EAASE,QAAQ,SAACC,UAAYA,EAAOC,WAAA,EAAA,GAAAC,MAAAC,KAAAR,EAAQ,GAAC,EAElD,EAAG,IAkBH,MAAO,CAAEF,KAAAA,EAAMW,GAhBJ5B,EAAWA,YAAC,SAAoBkB,EAAUM,GACnD,IAAIH,EAAW/C,EAAUgD,IAAIJ,GAO7B,OANKG,IACHA,EAAW,IAAIQ,IACfvD,EAAUwD,IAAIZ,EAAiBG,IAEjCA,EAASU,IAAIP,GAED,WACVH,EAAS,OAAQG,GACM,IAAnBH,EAAUW,MACZ1D,EAAS,OAAQ4C,EAErB,CACF,EAAG,IAGL,wBGxBgB,SACde,EACAC,EACA7C,GAEA8C,EAAAA,UAAU,WACR,IAAMC,EAAOH,EAAUI,QACvB,GAAKD,EAAL,CAEA,IAAME,EAAW,IAAIC,iBAAiBL,GAGtC,OAFAI,EAASE,QAAQJ,EAAM/C,GAEV,WAAA,OAAAiD,EAASG,YAAY,CAHlC,CAIF,EAAG,CAACR,EAAWC,EAAU7C,GAC3B,+BFsEE,IAAAG,EAA0BC,WAAuBjB,GAA1CG,EAAKa,EAAA,GAAEkD,EAAQlD,EAAA,GAyBtB,OAvBA2C,EAASA,UAAC,WACR,GAAsB,oBAAXQ,OAAX,CAEA,IAAMC,EAAc,WAAM,OAAAF,EAASlE,IAAkB,EAErDmE,OAAOE,iBAAiB,SAAUD,GAClCD,OAAOE,iBAAiB,UAAWD,GAEnC,IAAM/D,EAAcJ,UACjBI,WAKH,OAJIA,MAAAA,GAAAA,EAAYgE,kBACdhE,EAAWgE,iBAAiB,SAAUD,GAG5B,WACVD,OAAOG,oBAAoB,SAAUF,GACrCD,OAAOG,oBAAoB,UAAWF,GAClC/D,MAAAA,GAAAA,EAAYiE,qBACdjE,EAAWiE,oBAAoB,SAAUF,EAE7C,CAjBA,CAkBF,EAAG,IAEIjE,CACT,sBGlGgB,WACd,IAAAa,EAA0BC,EAAQA,SAAiB,WACjD,GAAsB,oBAAXkD,OAAwB,MAAO,gBAE1C,IAAMI,EAAYJ,OAAOK,WAAW,gCAC9BC,EAAaN,OAAOK,WAAW,iCAErC,OAAID,EAAUG,QAAgB,OAC1BD,EAAWC,QAAgB,QACxB,eACT,GATOC,EAAK3D,EAAA,GAAE4D,EAAQ5D,EAAA,GA0CtB,OA/BA2C,EAASA,UAAC,WACR,GAAsB,oBAAXQ,OAAX,CAEA,IAAMU,EAAaV,OAAOK,WAAW,gCAE/BM,EAAe,SAAC1C,GACpBwC,EAASxC,EAAEsC,QAAU,OAAS,QAChC,EAGMK,EAAc,WAClB,IAAMR,EAAYJ,OAAOK,WAAW,gCAC9BC,EAAaN,OAAOK,WAAW,iCAEdI,EAAnBL,EAAUG,QAAkB,OACvBD,EAAWC,QAAkB,QACxB,gBAChB,EAEAG,EAAWR,iBAAiB,SAAUS,GAGtC,IAAML,EAAaN,OAAOK,WAAW,iCAGrC,OAFAC,EAAWJ,iBAAiB,SAAUU,GAE/B,WACLF,EAAWP,oBAAoB,SAAUQ,GACzCL,EAAWH,oBAAoB,SAAUS,EAC3C,EACF,EAAG,IAEIJ,CACT,kBC7DgB,WACd,IAAA3D,EAAkCC,EAAQA,UAAC,GAApC+D,EAAShE,KAAEiE,EAAYjE,EAAA,GAU9B,MAAO,CARiBQ,EAAWA,YAAC,SAACkC,GACnCuB,GAAa,GACbnD,QAAQC,UAAUG,KAAK,WACrBwB,IACAuB,GAAa,EACf,EACF,EAAG,IAEsBD,EAC3B,uBCNgB,SACdE,EACAC,EACAC,GAEA,gBAFAA,IAAAA,EAAyC,YAElCC,EAAOA,QAAC,WACb,IAAKH,EAAU,OAAOA,EAEtB,IAAMI,EAAe,SAACC,GACpB,IAAKC,EAAcA,eAACD,GAAQ,OAAOA,EAEnC,IAEIE,EAFEC,EAAgBH,EAAMI,OAAS,GAMnCF,EAFoB,aAAlBL,EAESQ,EAAQF,CAAAA,EAAAA,EAAkBP,GAG1BS,EAAQT,CAAAA,EAAAA,EAAgBO,GAIrC,IAAMG,EAAsC,MAArBH,OAAqB,EAArBA,EAAuBI,MACxCC,EAAkC,MAAnBZ,OAAmB,EAAnBA,EAAqBW,MAW1C,OATID,GAAiBE,GACQ,iBAAlBF,GAAqD,iBAAhBE,IAE3CN,EAAoBK,MADD,aAAlBV,EACwBQ,EAAQC,CAAAA,EAAAA,EAAkBE,GAE1BH,EAAA,CAAA,EAAQG,EAAgBF,IAI/CG,eAAaT,EAAOE,EAC7B,EAEA,OAAIQ,MAAMC,QAAQhB,GACTA,EAASiB,IAAIb,GAGfA,EAAaJ,EACtB,EAAG,CAACA,EAAUC,EAAaC,GAC7B"}
|
|
1
|
+
{"version":3,"file":"index.umd.js","sources":["../src/useEventBus.ts","../src/useNetworkState.ts","../src/useClipboard.ts","../src/useMutationObserver.ts","../src/usePreferredTheme.ts","../src/useRageClick.ts","../src/useTransition.ts","../src/useWrappedChildren.ts"],"sourcesContent":["import { useCallback, useEffect } from 'preact/hooks';\r\n\r\ntype EventMap = Record<string, (...args: any[]) => void>;\r\n\r\nconst listeners = new Map<string, Set<(...args: any[]) => void>>();\r\n\r\n/**\r\n * A Preact hook to publish and subscribe to custom events across components.\r\n * @returns An object with `emit` and `on` methods.\r\n */\r\nexport function useEventBus<T extends EventMap>() {\r\n const emit = useCallback(<K extends keyof T>(event: K, ...args: Parameters<T[K]>) => {\r\n const handlers = listeners.get(event as string);\r\n if (handlers) {\r\n handlers.forEach((handler) => handler(...args));\r\n }\r\n }, []);\r\n\r\n const on = useCallback(<K extends keyof T>(event: K, handler: T[K]) => {\r\n let handlers = listeners.get(event as string);\r\n if (!handlers) {\r\n handlers = new Set();\r\n listeners.set(event as string, handlers);\r\n }\r\n handlers.add(handler);\r\n\r\n return () => {\r\n handlers!.delete(handler);\r\n if (handlers!.size === 0) {\r\n listeners.delete(event as string);\r\n }\r\n };\r\n }, []);\r\n\r\n return { emit, on };\r\n}\r\n","import { useEffect, useState } from 'preact/hooks';\r\n\r\n/** Network Information API (not in all browsers) */\r\ninterface NetworkInformation extends EventTarget {\r\n effectiveType?: string;\r\n downlink?: number;\r\n rtt?: number;\r\n saveData?: boolean;\r\n type?: string;\r\n}\r\n\r\n/** Effective connection type from Network Information API */\r\nexport type EffectiveConnectionType = 'slow-2g' | '2g' | '3g' | '4g';\r\n\r\n/** Network connection type (e.g., wifi, cellular) */\r\nexport type ConnectionType =\r\n | 'bluetooth'\r\n | 'cellular'\r\n | 'ethernet'\r\n | 'mixed'\r\n | 'none'\r\n | 'other'\r\n | 'unknown'\r\n | 'wifi';\r\n\r\nexport interface NetworkState {\r\n /** Whether the browser is online */\r\n online: boolean;\r\n /** Effective connection type (when supported) */\r\n effectiveType?: EffectiveConnectionType;\r\n /** Estimated downlink speed in Mbps (when supported) */\r\n downlink?: number;\r\n /** Estimated round-trip time in ms (when supported) */\r\n rtt?: number;\r\n /** Whether the user has requested reduced data usage (when supported) */\r\n saveData?: boolean;\r\n /** Connection type (when supported) */\r\n connectionType?: ConnectionType;\r\n}\r\n\r\nfunction getNetworkState(): NetworkState {\r\n if (typeof navigator === 'undefined') {\r\n return { online: true };\r\n }\r\n\r\n const state: NetworkState = {\r\n online: navigator.onLine,\r\n };\r\n\r\n const connection =\r\n (navigator as Navigator & { connection?: NetworkInformation }).connection;\r\n\r\n if (connection) {\r\n if (connection.effectiveType !== undefined) {\r\n state.effectiveType = connection.effectiveType as EffectiveConnectionType;\r\n }\r\n if (connection.downlink !== undefined) {\r\n state.downlink = connection.downlink;\r\n }\r\n if (connection.rtt !== undefined) {\r\n state.rtt = connection.rtt;\r\n }\r\n if (connection.saveData !== undefined) {\r\n state.saveData = connection.saveData;\r\n }\r\n if (connection.type !== undefined) {\r\n state.connectionType = connection.type as ConnectionType;\r\n }\r\n }\r\n\r\n return state;\r\n}\r\n\r\n/**\r\n * A Preact hook that returns the current network state, including online/offline\r\n * status and (when supported) connection type, downlink, RTT, and save-data preference.\r\n * Updates reactively when the network state changes.\r\n *\r\n * @returns The current network state object\r\n *\r\n * @example\r\n * ```tsx\r\n * function NetworkStatus() {\r\n * const { online, effectiveType, saveData } = useNetworkState();\r\n * return (\r\n * <div>\r\n * Status: {online ? 'Online' : 'Offline'}\r\n * {effectiveType && ` (${effectiveType})`}\r\n * {saveData && ' - Reduced data mode'}\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function useNetworkState(): NetworkState {\r\n const [state, setState] = useState<NetworkState>(getNetworkState);\r\n\r\n useEffect(() => {\r\n if (typeof window === 'undefined') return;\r\n\r\n const updateState = () => setState(getNetworkState());\r\n\r\n window.addEventListener('online', updateState);\r\n window.addEventListener('offline', updateState);\r\n\r\n const connection = (navigator as Navigator & { connection?: NetworkInformation })\r\n .connection;\r\n if (connection?.addEventListener) {\r\n connection.addEventListener('change', updateState);\r\n }\r\n\r\n return () => {\r\n window.removeEventListener('online', updateState);\r\n window.removeEventListener('offline', updateState);\r\n if (connection?.removeEventListener) {\r\n connection.removeEventListener('change', updateState);\r\n }\r\n };\r\n }, []);\r\n\r\n return state;\r\n}\r\n","import { useCallback, useState } from 'preact/hooks';\r\n\r\nexport interface UseClipboardOptions {\r\n /** Duration in ms to keep `copied` true before resetting. Default: 2000 */\r\n resetDelay?: number;\r\n}\r\n\r\nexport interface UseClipboardReturn {\r\n /** Copy text to the clipboard. Returns true on success. */\r\n copy: (text: string) => Promise<boolean>;\r\n /** Read text from the clipboard. Returns empty string if denied or unavailable. */\r\n paste: () => Promise<string>;\r\n /** Whether the last copy operation succeeded (resets after resetDelay) */\r\n copied: boolean;\r\n /** Error from the last failed operation, or null */\r\n error: Error | null;\r\n /** Manually reset copied and error state */\r\n reset: () => void;\r\n}\r\n\r\n/**\r\n * A Preact hook for reading and writing the clipboard. Uses the async\r\n * Clipboard API when available (requires secure context and user gesture).\r\n *\r\n * @param options - Optional configuration (e.g., resetDelay for copied state)\r\n * @returns Object with copy, paste, copied, error, and reset\r\n *\r\n * @example\r\n * ```tsx\r\n * function CopyButton() {\r\n * const { copy, copied, error } = useClipboard();\r\n * return (\r\n * <button onClick={() => copy('Hello!')}>\r\n * {copied ? 'Copied!' : 'Copy'}\r\n * </button>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function useClipboard(options: UseClipboardOptions = {}): UseClipboardReturn {\r\n const { resetDelay = 2000 } = options;\r\n\r\n const [copied, setCopied] = useState(false);\r\n const [error, setError] = useState<Error | null>(null);\r\n\r\n const reset = useCallback(() => {\r\n setCopied(false);\r\n setError(null);\r\n }, []);\r\n\r\n const copy = useCallback(\r\n async (text: string): Promise<boolean> => {\r\n setError(null);\r\n\r\n if (typeof navigator === 'undefined' || !navigator.clipboard) {\r\n const err = new Error('Clipboard API is not available');\r\n setError(err);\r\n return false;\r\n }\r\n\r\n try {\r\n await navigator.clipboard.writeText(text);\r\n setCopied(true);\r\n if (resetDelay > 0) {\r\n setTimeout(() => setCopied(false), resetDelay);\r\n }\r\n return true;\r\n } catch (e) {\r\n const err = e instanceof Error ? e : new Error(String(e));\r\n setError(err);\r\n return false;\r\n }\r\n },\r\n [resetDelay]\r\n );\r\n\r\n const paste = useCallback(async (): Promise<string> => {\r\n setError(null);\r\n\r\n if (typeof navigator === 'undefined' || !navigator.clipboard) {\r\n const err = new Error('Clipboard API is not available');\r\n setError(err);\r\n return '';\r\n }\r\n\r\n try {\r\n const text = await navigator.clipboard.readText();\r\n return text;\r\n } catch (e) {\r\n const err = e instanceof Error ? e : new Error(String(e));\r\n setError(err);\r\n return '';\r\n }\r\n }, []);\r\n\r\n return { copy, paste, copied, error, reset };\r\n}\r\n","import { RefObject } from 'preact'\r\nimport { useEffect } from 'preact/hooks'\r\n\r\nexport type UseMutationObserverOptions = MutationObserverInit\r\n\r\n/**\r\n * A Preact hook to observe DOM mutations using MutationObserver.\r\n * @param target - The element to observe.\r\n * @param callback - Function to call on mutation.\r\n * @param options - MutationObserver options.\r\n */\r\nexport function useMutationObserver(\r\n targetRef: RefObject<HTMLElement | null>,\r\n callback: MutationCallback,\r\n options: MutationObserverInit\r\n) {\r\n useEffect(() => {\r\n const node = targetRef.current\r\n if (!node) return\r\n\r\n const observer = new MutationObserver(callback)\r\n observer.observe(node, options)\r\n\r\n return () => observer.disconnect()\r\n }, [targetRef, callback, options])\r\n}\r\n","import { useEffect, useState } from 'preact/hooks';\r\n\r\nexport type PreferredTheme = 'light' | 'dark' | 'no-preference';\r\n\r\n/**\r\n * A Preact hook that returns the user's preferred color scheme based on the\r\n * `prefers-color-scheme` media query. Updates reactively when the user changes\r\n * their system or browser theme preference.\r\n *\r\n * @returns The preferred theme: 'light', 'dark', or 'no-preference'\r\n *\r\n * @example\r\n * ```tsx\r\n * function ThemeAwareComponent() {\r\n * const theme = usePreferredTheme();\r\n * return (\r\n * <div data-theme={theme}>\r\n * Current preference: {theme}\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function usePreferredTheme(): PreferredTheme {\r\n const [theme, setTheme] = useState<PreferredTheme>(() => {\r\n if (typeof window === 'undefined') return 'no-preference';\r\n\r\n const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n const lightQuery = window.matchMedia('(prefers-color-scheme: light)');\r\n\r\n if (darkQuery.matches) return 'dark';\r\n if (lightQuery.matches) return 'light';\r\n return 'no-preference';\r\n });\r\n\r\n useEffect(() => {\r\n if (typeof window === 'undefined') return;\r\n\r\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n\r\n const handleChange = (e: MediaQueryListEvent) => {\r\n setTheme(e.matches ? 'dark' : 'light');\r\n };\r\n\r\n // Re-check in case of no-preference (some browsers don't support light query)\r\n const updateTheme = () => {\r\n const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n const lightQuery = window.matchMedia('(prefers-color-scheme: light)');\r\n\r\n if (darkQuery.matches) setTheme('dark');\r\n else if (lightQuery.matches) setTheme('light');\r\n else setTheme('no-preference');\r\n };\r\n\r\n mediaQuery.addEventListener('change', handleChange);\r\n\r\n // Fallback: some environments may not fire change, so we also listen for light\r\n const lightQuery = window.matchMedia('(prefers-color-scheme: light)');\r\n lightQuery.addEventListener('change', updateTheme);\r\n\r\n return () => {\r\n mediaQuery.removeEventListener('change', handleChange);\r\n lightQuery.removeEventListener('change', updateTheme);\r\n };\r\n }, []);\r\n\r\n return theme;\r\n}\r\n","import type { RefObject } from 'preact'\r\nimport { useEffect, useRef } from 'preact/hooks'\r\n\r\nexport interface RageClickPayload {\r\n /** Number of clicks that triggered the rage click */\r\n count: number\r\n /** Last click event (e.g. for Sentry context) */\r\n event: MouseEvent\r\n}\r\n\r\nexport interface UseRageClickOptions {\r\n /** Called when a rage click is detected. Use this to report to Sentry or your error tracker. */\r\n onRageClick: (payload: RageClickPayload) => void\r\n /** Minimum number of clicks in the time window to count as rage click. Default: 5 (Sentry-style). */\r\n threshold?: number\r\n /** Time window in ms. Default: 1000. */\r\n timeWindow?: number\r\n /** Max distance in px between clicks to count as same spot. Default: 30. Set to Infinity to ignore distance. */\r\n distanceThreshold?: number\r\n}\r\n\r\ninterface ClickRecord {\r\n time: number\r\n x: number\r\n y: number\r\n}\r\n\r\nfunction distance(a: ClickRecord, b: ClickRecord): number {\r\n return Math.hypot(b.x - a.x, b.y - a.y)\r\n}\r\n\r\n/**\r\n * Detects \"rage clicks\" (repeated rapid clicks in the same area), e.g. when the UI\r\n * is unresponsive. Use the callback to report to Sentry or similar tools to surface\r\n * rage click issues and lower rage-click-related support.\r\n *\r\n * @param targetRef - Ref of the element to monitor (e.g. a button or card).\r\n * @param options - onRageClick callback and optional threshold, timeWindow, distanceThreshold.\r\n *\r\n * @example\r\n * ```tsx\r\n * const ref = useRef<HTMLButtonElement>(null)\r\n * useRageClick(ref, {\r\n * onRageClick: ({ count, event }) => {\r\n * Sentry.captureMessage('Rage click detected', { extra: { count, target: event.target } })\r\n * },\r\n * })\r\n * return <button ref={ref}>Submit</button>\r\n * ```\r\n */\r\nexport function useRageClick(\r\n targetRef: RefObject<HTMLElement | null>,\r\n options: UseRageClickOptions\r\n) {\r\n const {\r\n onRageClick,\r\n threshold = 5,\r\n timeWindow = 1000,\r\n distanceThreshold = 30,\r\n } = options\r\n\r\n const onRageClickRef = useRef(onRageClick)\r\n onRageClickRef.current = onRageClick\r\n\r\n const clicksRef = useRef<ClickRecord[]>([])\r\n\r\n useEffect(() => {\r\n const node = targetRef.current\r\n if (!node) return\r\n\r\n const handleClick = (e: MouseEvent) => {\r\n const now = Date.now()\r\n const record: ClickRecord = { time: now, x: e.clientX, y: e.clientY }\r\n\r\n const clicks = clicksRef.current\r\n const cutoff = now - timeWindow\r\n const recent = clicks.filter((c) => c.time >= cutoff)\r\n recent.push(record)\r\n\r\n if (distanceThreshold !== Infinity) {\r\n const inRange = recent.filter(\r\n (c) => distance(c, record) <= distanceThreshold\r\n )\r\n if (inRange.length >= threshold) {\r\n onRageClickRef.current({ count: inRange.length, event: e })\r\n clicksRef.current = []\r\n return\r\n }\r\n } else {\r\n if (recent.length >= threshold) {\r\n onRageClickRef.current({ count: recent.length, event: e })\r\n clicksRef.current = []\r\n return\r\n }\r\n }\r\n\r\n clicksRef.current = recent\r\n }\r\n\r\n node.addEventListener('click', handleClick)\r\n return () => node.removeEventListener('click', handleClick)\r\n }, [targetRef, threshold, timeWindow, distanceThreshold])\r\n}\r\n","import { useState, useCallback } from 'preact/hooks';\r\n\r\n/**\r\n * Mimics React's useTransition hook in Preact.\r\n * @returns [startTransition, isPending]\r\n */\r\nexport function useTransition(): [startTransition: (callback: () => void) => void, isPending: boolean] {\r\n const [isPending, setIsPending] = useState(false);\r\n\r\n const startTransition = useCallback((callback: () => void) => {\r\n setIsPending(true);\r\n Promise.resolve().then(() => {\r\n callback();\r\n setIsPending(false);\r\n });\r\n }, []);\r\n\r\n return [startTransition, isPending];\r\n}\r\n","import { ComponentChildren, cloneElement, isValidElement } from 'preact'\r\nimport { useMemo } from 'preact/hooks'\r\n\r\nexport type InjectableProps = Record<string, any>\r\n\r\n/**\r\n * A Preact hook to wrap children components and inject additional props into them.\r\n * @param children - The children to wrap and enhance with props.\r\n * @param injectProps - The props to inject into each child component.\r\n * @param mergeStrategy - How to handle prop conflicts ('override' | 'preserve'). Defaults to 'preserve'.\r\n * @returns Enhanced children with injected props.\r\n */\r\nexport function useWrappedChildren(\r\n children: ComponentChildren,\r\n injectProps: InjectableProps,\r\n mergeStrategy: 'override' | 'preserve' = 'preserve'\r\n): ComponentChildren {\r\n return useMemo(() => {\r\n if (!children) return children\r\n\r\n const enhanceChild = (child: any): any => {\r\n if (!isValidElement(child)) return child\r\n\r\n const existingProps = child.props || {}\r\n \r\n let mergedProps: InjectableProps\r\n \r\n if (mergeStrategy === 'override') {\r\n // Injected props override existing ones\r\n mergedProps = { ...existingProps, ...injectProps }\r\n } else {\r\n // Existing props are preserved, injected props are added only if not present\r\n mergedProps = { ...injectProps, ...existingProps }\r\n }\r\n\r\n // Special handling for style prop to merge style objects properly\r\n const existingStyle = (existingProps as any)?.style\r\n const injectStyle = (injectProps as any)?.style\r\n \r\n if (existingStyle && injectStyle && \r\n typeof existingStyle === 'object' && typeof injectStyle === 'object') {\r\n if (mergeStrategy === 'override') {\r\n (mergedProps as any).style = { ...existingStyle, ...injectStyle }\r\n } else {\r\n (mergedProps as any).style = { ...injectStyle, ...existingStyle }\r\n }\r\n }\r\n\r\n return cloneElement(child, mergedProps)\r\n }\r\n\r\n if (Array.isArray(children)) {\r\n return children.map(enhanceChild)\r\n }\r\n\r\n return enhanceChild(children)\r\n }, [children, injectProps, mergeStrategy])\r\n}"],"names":["listeners","Map","getNetworkState","navigator","online","state","onLine","connection","undefined","effectiveType","downlink","rtt","saveData","type","connectionType","options","_options$resetDelay","resetDelay","_useState","useState","copied","setCopied","_useState2","error","setError","reset","useCallback","copy","text","clipboard","err","Error","Promise","resolve","_catch","writeText","then","setTimeout","e","String","reject","paste","readText","emit","event","_arguments","arguments","handlers","get","forEach","handler","apply","slice","call","on","Set","set","add","size","targetRef","callback","useEffect","node","current","observer","MutationObserver","observe","disconnect","setState","window","updateState","addEventListener","removeEventListener","darkQuery","matchMedia","lightQuery","matches","theme","setTheme","mediaQuery","handleChange","updateTheme","onRageClick","_options$threshold","threshold","_options$timeWindow","timeWindow","_options$distanceThre","distanceThreshold","onRageClickRef","useRef","clicksRef","handleClick","now","Date","record","time","x","clientX","y","clientY","cutoff","recent","filter","c","push","Infinity","inRange","a","b","Math","hypot","length","count","isPending","setIsPending","children","injectProps","mergeStrategy","useMemo","enhanceChild","child","isValidElement","mergedProps","existingProps","props","_extends","existingStyle","style","injectStyle","cloneElement","Array","isArray","map"],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { RefObject } from 'preact';
|
|
2
|
+
export interface RageClickPayload {
|
|
3
|
+
/** Number of clicks that triggered the rage click */
|
|
4
|
+
count: number;
|
|
5
|
+
/** Last click event (e.g. for Sentry context) */
|
|
6
|
+
event: MouseEvent;
|
|
7
|
+
}
|
|
8
|
+
export interface UseRageClickOptions {
|
|
9
|
+
/** Called when a rage click is detected. Use this to report to Sentry or your error tracker. */
|
|
10
|
+
onRageClick: (payload: RageClickPayload) => void;
|
|
11
|
+
/** Minimum number of clicks in the time window to count as rage click. Default: 5 (Sentry-style). */
|
|
12
|
+
threshold?: number;
|
|
13
|
+
/** Time window in ms. Default: 1000. */
|
|
14
|
+
timeWindow?: number;
|
|
15
|
+
/** Max distance in px between clicks to count as same spot. Default: 30. Set to Infinity to ignore distance. */
|
|
16
|
+
distanceThreshold?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Detects "rage clicks" (repeated rapid clicks in the same area), e.g. when the UI
|
|
20
|
+
* is unresponsive. Use the callback to report to Sentry or similar tools to surface
|
|
21
|
+
* rage click issues and lower rage-click-related support.
|
|
22
|
+
*
|
|
23
|
+
* @param targetRef - Ref of the element to monitor (e.g. a button or card).
|
|
24
|
+
* @param options - onRageClick callback and optional threshold, timeWindow, distanceThreshold.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* const ref = useRef<HTMLButtonElement>(null)
|
|
29
|
+
* useRageClick(ref, {
|
|
30
|
+
* onRageClick: ({ count, event }) => {
|
|
31
|
+
* Sentry.captureMessage('Rage click detected', { extra: { count, target: event.target } })
|
|
32
|
+
* },
|
|
33
|
+
* })
|
|
34
|
+
* return <button ref={ref}>Submit</button>
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function useRageClick(targetRef: RefObject<HTMLElement | null>, options: UseRageClickOptions): void;
|
package/package.json
CHANGED
|
@@ -1,64 +1,67 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "preact-missing-hooks",
|
|
3
|
-
"version": "1.
|
|
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
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
},
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"
|
|
63
|
-
}
|
|
64
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "preact-missing-hooks",
|
|
3
|
+
"version": "1.2.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
|
+
"useRageClick",
|
|
41
|
+
"rage click",
|
|
42
|
+
"sentry",
|
|
43
|
+
"react-hooks",
|
|
44
|
+
"microbundle",
|
|
45
|
+
"typescript",
|
|
46
|
+
"preact-hooks"
|
|
47
|
+
],
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "https://github.com/prakhardubey2002/Preact-Missing-Hooks"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"preact": ">=10.0.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
57
|
+
"@testing-library/preact": "^3.2.4",
|
|
58
|
+
"@types/jest": "^29.5.14",
|
|
59
|
+
"jsdom": "^26.1.0",
|
|
60
|
+
"microbundle": "^0.15.1",
|
|
61
|
+
"typescript": "^5.8.3",
|
|
62
|
+
"vitest": "^3.1.4"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"preact": ">=10.0.0"
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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'
|
|
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'
|
|
8
|
+
export * from './useRageClick'
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { RefObject } from 'preact'
|
|
2
|
+
import { useEffect, useRef } from 'preact/hooks'
|
|
3
|
+
|
|
4
|
+
export interface RageClickPayload {
|
|
5
|
+
/** Number of clicks that triggered the rage click */
|
|
6
|
+
count: number
|
|
7
|
+
/** Last click event (e.g. for Sentry context) */
|
|
8
|
+
event: MouseEvent
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface UseRageClickOptions {
|
|
12
|
+
/** Called when a rage click is detected. Use this to report to Sentry or your error tracker. */
|
|
13
|
+
onRageClick: (payload: RageClickPayload) => void
|
|
14
|
+
/** Minimum number of clicks in the time window to count as rage click. Default: 5 (Sentry-style). */
|
|
15
|
+
threshold?: number
|
|
16
|
+
/** Time window in ms. Default: 1000. */
|
|
17
|
+
timeWindow?: number
|
|
18
|
+
/** Max distance in px between clicks to count as same spot. Default: 30. Set to Infinity to ignore distance. */
|
|
19
|
+
distanceThreshold?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ClickRecord {
|
|
23
|
+
time: number
|
|
24
|
+
x: number
|
|
25
|
+
y: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function distance(a: ClickRecord, b: ClickRecord): number {
|
|
29
|
+
return Math.hypot(b.x - a.x, b.y - a.y)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Detects "rage clicks" (repeated rapid clicks in the same area), e.g. when the UI
|
|
34
|
+
* is unresponsive. Use the callback to report to Sentry or similar tools to surface
|
|
35
|
+
* rage click issues and lower rage-click-related support.
|
|
36
|
+
*
|
|
37
|
+
* @param targetRef - Ref of the element to monitor (e.g. a button or card).
|
|
38
|
+
* @param options - onRageClick callback and optional threshold, timeWindow, distanceThreshold.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* const ref = useRef<HTMLButtonElement>(null)
|
|
43
|
+
* useRageClick(ref, {
|
|
44
|
+
* onRageClick: ({ count, event }) => {
|
|
45
|
+
* Sentry.captureMessage('Rage click detected', { extra: { count, target: event.target } })
|
|
46
|
+
* },
|
|
47
|
+
* })
|
|
48
|
+
* return <button ref={ref}>Submit</button>
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function useRageClick(
|
|
52
|
+
targetRef: RefObject<HTMLElement | null>,
|
|
53
|
+
options: UseRageClickOptions
|
|
54
|
+
) {
|
|
55
|
+
const {
|
|
56
|
+
onRageClick,
|
|
57
|
+
threshold = 5,
|
|
58
|
+
timeWindow = 1000,
|
|
59
|
+
distanceThreshold = 30,
|
|
60
|
+
} = options
|
|
61
|
+
|
|
62
|
+
const onRageClickRef = useRef(onRageClick)
|
|
63
|
+
onRageClickRef.current = onRageClick
|
|
64
|
+
|
|
65
|
+
const clicksRef = useRef<ClickRecord[]>([])
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const node = targetRef.current
|
|
69
|
+
if (!node) return
|
|
70
|
+
|
|
71
|
+
const handleClick = (e: MouseEvent) => {
|
|
72
|
+
const now = Date.now()
|
|
73
|
+
const record: ClickRecord = { time: now, x: e.clientX, y: e.clientY }
|
|
74
|
+
|
|
75
|
+
const clicks = clicksRef.current
|
|
76
|
+
const cutoff = now - timeWindow
|
|
77
|
+
const recent = clicks.filter((c) => c.time >= cutoff)
|
|
78
|
+
recent.push(record)
|
|
79
|
+
|
|
80
|
+
if (distanceThreshold !== Infinity) {
|
|
81
|
+
const inRange = recent.filter(
|
|
82
|
+
(c) => distance(c, record) <= distanceThreshold
|
|
83
|
+
)
|
|
84
|
+
if (inRange.length >= threshold) {
|
|
85
|
+
onRageClickRef.current({ count: inRange.length, event: e })
|
|
86
|
+
clicksRef.current = []
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
if (recent.length >= threshold) {
|
|
91
|
+
onRageClickRef.current({ count: recent.length, event: e })
|
|
92
|
+
clicksRef.current = []
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
clicksRef.current = recent
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
node.addEventListener('click', handleClick)
|
|
101
|
+
return () => node.removeEventListener('click', handleClick)
|
|
102
|
+
}, [targetRef, threshold, timeWindow, distanceThreshold])
|
|
103
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/** @jsx h */
|
|
2
|
+
import { h } from 'preact'
|
|
3
|
+
import { useRef } from 'preact/hooks'
|
|
4
|
+
import { render, fireEvent, waitFor } from '@testing-library/preact'
|
|
5
|
+
import { useRageClick } from '../src/useRageClick'
|
|
6
|
+
import { vi } from 'vitest'
|
|
7
|
+
|
|
8
|
+
describe('useRageClick', () => {
|
|
9
|
+
it('does not call onRageClick when clicks are below threshold', () => {
|
|
10
|
+
const onRageClick = vi.fn()
|
|
11
|
+
|
|
12
|
+
function TestComponent() {
|
|
13
|
+
const ref = useRef<HTMLButtonElement>(null)
|
|
14
|
+
useRageClick(ref, { onRageClick, threshold: 5, timeWindow: 1000 })
|
|
15
|
+
return <button ref={ref}>Click</button>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { getByText } = render(<TestComponent />)
|
|
19
|
+
const btn = getByText('Click')
|
|
20
|
+
|
|
21
|
+
fireEvent.click(btn)
|
|
22
|
+
fireEvent.click(btn)
|
|
23
|
+
fireEvent.click(btn)
|
|
24
|
+
fireEvent.click(btn)
|
|
25
|
+
|
|
26
|
+
expect(onRageClick).not.toHaveBeenCalled()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('calls onRageClick when clicks meet threshold in same area', async () => {
|
|
30
|
+
const onRageClick = vi.fn()
|
|
31
|
+
|
|
32
|
+
function TestComponent() {
|
|
33
|
+
const ref = useRef<HTMLButtonElement>(null)
|
|
34
|
+
useRageClick(ref, { onRageClick, threshold: 5, timeWindow: 1000 })
|
|
35
|
+
return <button ref={ref}>Click</button>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { getByText } = render(<TestComponent />)
|
|
39
|
+
const btn = getByText('Click')
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < 5; i++) {
|
|
42
|
+
fireEvent.click(btn, { clientX: 10, clientY: 10 })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await waitFor(() => {
|
|
46
|
+
expect(onRageClick).toHaveBeenCalledTimes(1)
|
|
47
|
+
expect(onRageClick).toHaveBeenCalledWith(
|
|
48
|
+
expect.objectContaining({ count: 5 })
|
|
49
|
+
)
|
|
50
|
+
expect(onRageClick.mock.calls[0][0].event).toBeDefined()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('respects custom threshold', async () => {
|
|
55
|
+
const onRageClick = vi.fn()
|
|
56
|
+
|
|
57
|
+
function TestComponent() {
|
|
58
|
+
const ref = useRef<HTMLButtonElement>(null)
|
|
59
|
+
useRageClick(ref, { onRageClick, threshold: 3, timeWindow: 1000 })
|
|
60
|
+
return <button ref={ref}>Click</button>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { getByText } = render(<TestComponent />)
|
|
64
|
+
const btn = getByText('Click')
|
|
65
|
+
|
|
66
|
+
fireEvent.click(btn, { clientX: 0, clientY: 0 })
|
|
67
|
+
fireEvent.click(btn, { clientX: 1, clientY: 1 })
|
|
68
|
+
fireEvent.click(btn, { clientX: 2, clientY: 2 })
|
|
69
|
+
|
|
70
|
+
await waitFor(() => {
|
|
71
|
+
expect(onRageClick).toHaveBeenCalledTimes(1)
|
|
72
|
+
expect(onRageClick).toHaveBeenCalledWith(
|
|
73
|
+
expect.objectContaining({ count: 3 })
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('does not fire when clicks are far apart (distanceThreshold)', async () => {
|
|
79
|
+
const onRageClick = vi.fn()
|
|
80
|
+
|
|
81
|
+
function TestComponent() {
|
|
82
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
83
|
+
useRageClick(ref, {
|
|
84
|
+
onRageClick,
|
|
85
|
+
threshold: 5,
|
|
86
|
+
timeWindow: 1000,
|
|
87
|
+
distanceThreshold: 20,
|
|
88
|
+
})
|
|
89
|
+
return <div ref={ref} data-testid="area">Area</div>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const { getByTestId } = render(<TestComponent />)
|
|
93
|
+
const el = getByTestId('area')
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < 5; i++) {
|
|
96
|
+
fireEvent.click(el, { clientX: i * 100, clientY: i * 100 })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
expect(onRageClick).not.toHaveBeenCalled()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('fires when clicks are within distanceThreshold', async () => {
|
|
103
|
+
const onRageClick = vi.fn()
|
|
104
|
+
|
|
105
|
+
function TestComponent() {
|
|
106
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
107
|
+
useRageClick(ref, {
|
|
108
|
+
onRageClick,
|
|
109
|
+
threshold: 5,
|
|
110
|
+
timeWindow: 1000,
|
|
111
|
+
distanceThreshold: 50,
|
|
112
|
+
})
|
|
113
|
+
return <div ref={ref} data-testid="area">Area</div>
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const { getByTestId } = render(<TestComponent />)
|
|
117
|
+
const el = getByTestId('area')
|
|
118
|
+
|
|
119
|
+
for (let i = 0; i < 5; i++) {
|
|
120
|
+
fireEvent.click(el, { clientX: 10 + i * 5, clientY: 10 + i * 5 })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await waitFor(() => {
|
|
124
|
+
expect(onRageClick).toHaveBeenCalledTimes(1)
|
|
125
|
+
expect(onRageClick.mock.calls[0][0].count).toBe(5)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('cleans up listener on unmount', () => {
|
|
130
|
+
const onRageClick = vi.fn()
|
|
131
|
+
|
|
132
|
+
function TestComponent() {
|
|
133
|
+
const ref = useRef<HTMLButtonElement>(null)
|
|
134
|
+
useRageClick(ref, { onRageClick, threshold: 5 })
|
|
135
|
+
return <button ref={ref}>Click</button>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { getByText, unmount } = render(<TestComponent />)
|
|
139
|
+
const btn = getByText('Click')
|
|
140
|
+
unmount()
|
|
141
|
+
fireEvent.click(btn)
|
|
142
|
+
fireEvent.click(btn)
|
|
143
|
+
expect(onRageClick).not.toHaveBeenCalled()
|
|
144
|
+
})
|
|
145
|
+
})
|