@-reddacted-/react-hooks 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,12 @@
1
- export { useFullscreen } from "./useFullscreen";
2
- export { useInstallPrompt } from "./useInstallPrompt";
3
- export type { UseInstallPromptOptions } from "./useInstallPrompt";
4
- export { useValueChange } from "./useValueChange";
1
+ export { useFullscreen } from "./useFullscreen.js";
2
+ export { useInstallPrompt, type UseInstallPromptOptions } from "./useInstallPrompt.js";
3
+ export { useValueChange } from "./useValueChange.js";
4
+ export { useAsync, type AsyncStatus, type AsyncResult } from "./useAsync.js";
5
+ export { useElementSize, type ElementSize } from "./useElementSize.js";
6
+ export { useClickOutside } from "./useClickOutside.js";
7
+ export { useEscapeKey } from "./useEscapeKey.js";
8
+ export { useDocumentTitle } from "./useDocumentTitle.js";
9
+ export { useFocusTrap } from "./useFocusTrap.js";
10
+ export { useScrollLock } from "./useScrollLock.js";
11
+ export { useOverlayStack } from "./useOverlayStack.js";
5
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,YAAY,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AACtF,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,QAAQ,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAA;AAC5E,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA"}
package/dist/index.js CHANGED
@@ -1,3 +1,11 @@
1
- export { useFullscreen } from "./useFullscreen";
2
- export { useInstallPrompt } from "./useInstallPrompt";
3
- export { useValueChange } from "./useValueChange";
1
+ export { useFullscreen } from "./useFullscreen.js";
2
+ export { useInstallPrompt } from "./useInstallPrompt.js";
3
+ export { useValueChange } from "./useValueChange.js";
4
+ export { useAsync } from "./useAsync.js";
5
+ export { useElementSize } from "./useElementSize.js";
6
+ export { useClickOutside } from "./useClickOutside.js";
7
+ export { useEscapeKey } from "./useEscapeKey.js";
8
+ export { useDocumentTitle } from "./useDocumentTitle.js";
9
+ export { useFocusTrap } from "./useFocusTrap.js";
10
+ export { useScrollLock } from "./useScrollLock.js";
11
+ export { useOverlayStack } from "./useOverlayStack.js";
@@ -0,0 +1,18 @@
1
+ import type { DependencyList } from "react";
2
+ export type AsyncStatus = "loading" | "ready" | "error";
3
+ export type AsyncResult<T> = {
4
+ data: T | null;
5
+ status: AsyncStatus;
6
+ error: unknown;
7
+ /** Runs the task again. Wire it to a retry button. */
8
+ retry: () => void;
9
+ };
10
+ /**
11
+ * Runs an abortable async task and tracks its three states.
12
+ *
13
+ * The task receives an AbortSignal that fires when the dependencies change or the
14
+ * component unmounts, so a stale response can never overwrite a fresh one. An aborted
15
+ * task is not an error and leaves the state untouched.
16
+ */
17
+ export declare function useAsync<T>(task: (signal: AbortSignal) => Promise<T>, deps?: DependencyList): AsyncResult<T>;
18
+ //# sourceMappingURL=useAsync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAsync.d.ts","sourceRoot":"","sources":["../src/useAsync.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAE3C,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;AAEvD,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC3B,IAAI,EAAE,CAAC,GAAG,IAAI,CAAA;IACd,MAAM,EAAE,WAAW,CAAA;IACnB,KAAK,EAAE,OAAO,CAAA;IACd,sDAAsD;IACtD,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB,CAAA;AAQD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,GAAE,cAAmB,GAAG,WAAW,CAAC,CAAC,CAAC,CAmChH"}
@@ -0,0 +1,39 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ function isAbort(cause) {
3
+ return cause instanceof Error && cause.name === "AbortError";
4
+ }
5
+ /**
6
+ * Runs an abortable async task and tracks its three states.
7
+ *
8
+ * The task receives an AbortSignal that fires when the dependencies change or the
9
+ * component unmounts, so a stale response can never overwrite a fresh one. An aborted
10
+ * task is not an error and leaves the state untouched.
11
+ */
12
+ export function useAsync(task, deps = []) {
13
+ const [state, setState] = useState({ data: null, status: "loading", error: null });
14
+ const [attempt, setAttempt] = useState(0);
15
+ // La tâche est le plus souvent une closure recréée à chaque rendu : la garder dans un
16
+ // ref laisse les dépendances déclarées seules maîtresses du redéclenchement.
17
+ const taskRef = useRef(task);
18
+ taskRef.current = task;
19
+ useEffect(() => {
20
+ const controller = new AbortController();
21
+ let active = true;
22
+ setState(current => ({ ...current, status: "loading" }));
23
+ taskRef.current(controller.signal).then(data => {
24
+ if (active)
25
+ setState({ data, status: "ready", error: null });
26
+ }, error => {
27
+ if (!active || controller.signal.aborted || isAbort(error))
28
+ return;
29
+ setState({ data: null, status: "error", error });
30
+ });
31
+ return () => {
32
+ active = false;
33
+ controller.abort();
34
+ };
35
+ // eslint-disable-next-line react-hooks/exhaustive-deps
36
+ }, [...deps, attempt]);
37
+ const retry = useCallback(() => setAttempt(n => n + 1), []);
38
+ return { ...state, retry };
39
+ }
@@ -0,0 +1,9 @@
1
+ import type { RefObject } from "react";
2
+ /**
3
+ * Calls the handler on a pointer press outside every given element.
4
+ *
5
+ * Listens on mousedown rather than click: a press that starts outside and ends inside
6
+ * should still dismiss, and the handler runs before the target rerenders.
7
+ */
8
+ export declare function useClickOutside(refs: Array<RefObject<HTMLElement | null>>, handler: () => void, enabled?: boolean): void;
9
+ //# sourceMappingURL=useClickOutside.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useClickOutside.d.ts","sourceRoot":"","sources":["../src/useClickOutside.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC,EAC1C,OAAO,EAAE,MAAM,IAAI,EACnB,OAAO,UAAO,GACb,IAAI,CAcN"}
@@ -0,0 +1,22 @@
1
+ import { useEffect } from "react";
2
+ /**
3
+ * Calls the handler on a pointer press outside every given element.
4
+ *
5
+ * Listens on mousedown rather than click: a press that starts outside and ends inside
6
+ * should still dismiss, and the handler runs before the target rerenders.
7
+ */
8
+ export function useClickOutside(refs, handler, enabled = true) {
9
+ useEffect(() => {
10
+ if (!enabled)
11
+ return;
12
+ const onMouseDown = (event) => {
13
+ const target = event.target;
14
+ if (refs.some(ref => ref.current?.contains(target)))
15
+ return;
16
+ handler();
17
+ };
18
+ document.addEventListener("mousedown", onMouseDown);
19
+ return () => document.removeEventListener("mousedown", onMouseDown);
20
+ // eslint-disable-next-line react-hooks/exhaustive-deps
21
+ }, [enabled, handler, ...refs]);
22
+ }
@@ -0,0 +1,2 @@
1
+ export declare function useDocumentTitle(title: string | undefined, siteName: string, fallback?: string): void;
2
+ //# sourceMappingURL=useDocumentTitle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDocumentTitle.d.ts","sourceRoot":"","sources":["../src/useDocumentTitle.ts"],"names":[],"mappings":"AAIA,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,QAI9F"}
@@ -0,0 +1,8 @@
1
+ import { useEffect } from "react";
2
+ // Without this every page shares the title of index.html in the tab, the history
3
+ // and the search results.
4
+ export function useDocumentTitle(title, siteName, fallback) {
5
+ useEffect(() => {
6
+ document.title = title ? `${title} — ${siteName}` : (fallback ?? siteName);
7
+ }, [title, siteName, fallback]);
8
+ }
@@ -0,0 +1,13 @@
1
+ import type { RefObject } from "react";
2
+ export type ElementSize = {
3
+ width: number;
4
+ height: number;
5
+ };
6
+ /**
7
+ * Observed size of an element. Null until the first measure.
8
+ *
9
+ * useLayoutEffect rather than useEffect: the first measure lands before paint, so a
10
+ * layout that depends on it never flashes at a default size before settling.
11
+ */
12
+ export declare function useElementSize(ref: RefObject<HTMLElement | null>): ElementSize | null;
13
+ //# sourceMappingURL=useElementSize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useElementSize.d.ts","sourceRoot":"","sources":["../src/useElementSize.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,MAAM,MAAM,WAAW,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAE3D;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,WAAW,GAAG,IAAI,CAsBrF"}
@@ -0,0 +1,26 @@
1
+ import { useLayoutEffect, useState } from "react";
2
+ /**
3
+ * Observed size of an element. Null until the first measure.
4
+ *
5
+ * useLayoutEffect rather than useEffect: the first measure lands before paint, so a
6
+ * layout that depends on it never flashes at a default size before settling.
7
+ */
8
+ export function useElementSize(ref) {
9
+ const [size, setSize] = useState(null);
10
+ useLayoutEffect(() => {
11
+ const element = ref.current;
12
+ if (!element)
13
+ return;
14
+ const measure = ({ width, height }) => setSize({ width, height });
15
+ const rect = element.getBoundingClientRect();
16
+ measure({ width: rect.width, height: rect.height });
17
+ const observer = new ResizeObserver(entries => {
18
+ const rect = entries[0]?.contentRect;
19
+ if (rect)
20
+ measure({ width: rect.width, height: rect.height });
21
+ });
22
+ observer.observe(element);
23
+ return () => observer.disconnect();
24
+ }, [ref]);
25
+ return size;
26
+ }
@@ -0,0 +1,2 @@
1
+ export declare function useEscapeKey(handler: () => void, enabled?: boolean): void;
2
+ //# sourceMappingURL=useEscapeKey.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useEscapeKey.d.ts","sourceRoot":"","sources":["../src/useEscapeKey.ts"],"names":[],"mappings":"AAEA,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,IAAI,EAAE,OAAO,UAAO,GAAG,IAAI,CAWtE"}
@@ -0,0 +1,13 @@
1
+ import { useEffect } from "react";
2
+ export function useEscapeKey(handler, enabled = true) {
3
+ useEffect(() => {
4
+ if (!enabled)
5
+ return;
6
+ const onKeyDown = (event) => {
7
+ if (event.key === "Escape")
8
+ handler();
9
+ };
10
+ document.addEventListener("keydown", onKeyDown);
11
+ return () => document.removeEventListener("keydown", onKeyDown);
12
+ }, [enabled, handler]);
13
+ }
@@ -0,0 +1,11 @@
1
+ import type { RefObject } from "react";
2
+ /**
3
+ * Keeps the focus inside `ref` while `enabled`: focuses its first focusable element
4
+ * (or the container itself) on activation, loops Tab / Shift+Tab, pulls back a focus
5
+ * that escaped, and restores the previously focused element on deactivation.
6
+ * The container should have `tabIndex={-1}` so it can receive the focus.
7
+ * `paused` suspends Tab looping and focus rescue without releasing the trap, e.g. while
8
+ * another overlay (menu, drawer) is open above.
9
+ */
10
+ export declare function useFocusTrap(ref: RefObject<HTMLElement | null>, enabled?: boolean, paused?: boolean): void;
11
+ //# sourceMappingURL=useFocusTrap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFocusTrap.d.ts","sourceRoot":"","sources":["../src/useFocusTrap.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AActC;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,EAAE,OAAO,UAAO,EAAE,MAAM,UAAQ,GAAG,IAAI,CA0DrG"}
@@ -0,0 +1,74 @@
1
+ import { useEffect, useRef } from "react";
2
+ const FOCUSABLE = `a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])`;
3
+ // Active traps, last activated last: only that one handles Tab and focus rescue,
4
+ // so nested overlays (a Modal over a Drawer) don't fight over the focus.
5
+ const traps = [];
6
+ function getFocusables(container) {
7
+ return Array.from(container.querySelectorAll(FOCUSABLE)).filter(el => !el.hasAttribute("disabled") && !el.getAttribute("aria-hidden"));
8
+ }
9
+ /**
10
+ * Keeps the focus inside `ref` while `enabled`: focuses its first focusable element
11
+ * (or the container itself) on activation, loops Tab / Shift+Tab, pulls back a focus
12
+ * that escaped, and restores the previously focused element on deactivation.
13
+ * The container should have `tabIndex={-1}` so it can receive the focus.
14
+ * `paused` suspends Tab looping and focus rescue without releasing the trap, e.g. while
15
+ * another overlay (menu, drawer) is open above.
16
+ */
17
+ export function useFocusTrap(ref, enabled = true, paused = false) {
18
+ const pausedRef = useRef(paused);
19
+ pausedRef.current = paused;
20
+ useEffect(() => {
21
+ if (!enabled)
22
+ return;
23
+ const key = Symbol("focus-trap");
24
+ traps.push(key);
25
+ const isActive = () => !pausedRef.current && traps[traps.length - 1] === key;
26
+ const previouslyFocused = document.activeElement;
27
+ // Deferred: the container may not be focusable yet in the commit that enables the trap.
28
+ const timeout = setTimeout(() => {
29
+ const container = ref.current;
30
+ if (!container)
31
+ return;
32
+ (getFocusables(container)[0] ?? container).focus();
33
+ }, 0);
34
+ const onKeyDown = (event) => {
35
+ if (event.key !== "Tab" || !isActive())
36
+ return;
37
+ const container = ref.current;
38
+ if (!container)
39
+ return;
40
+ const focusables = getFocusables(container);
41
+ if (focusables.length === 0)
42
+ return;
43
+ const first = focusables[0];
44
+ const last = focusables[focusables.length - 1];
45
+ const active = document.activeElement;
46
+ if (event.shiftKey) {
47
+ if (active === first || !container.contains(active)) {
48
+ last.focus();
49
+ event.preventDefault();
50
+ }
51
+ }
52
+ else if (active === last || !container.contains(active)) {
53
+ first.focus();
54
+ event.preventDefault();
55
+ }
56
+ };
57
+ const onFocusIn = (event) => {
58
+ if (!isActive())
59
+ return;
60
+ const container = ref.current;
61
+ if (container && !container.contains(event.target))
62
+ container.focus();
63
+ };
64
+ document.addEventListener("keydown", onKeyDown);
65
+ document.addEventListener("focusin", onFocusIn);
66
+ return () => {
67
+ clearTimeout(timeout);
68
+ document.removeEventListener("keydown", onKeyDown);
69
+ document.removeEventListener("focusin", onFocusIn);
70
+ traps.splice(traps.indexOf(key), 1);
71
+ previouslyFocused?.focus?.();
72
+ };
73
+ }, [enabled, ref]);
74
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Registers an overlay in a page-wide stack while `enabled`.
3
+ * `isTop` is true for the last opened overlay only (the one that should react to Escape);
4
+ * `zIndex` puts every overlay above the ones opened before it.
5
+ */
6
+ export declare function useOverlayStack(enabled?: boolean): {
7
+ isTop: boolean;
8
+ zIndex: number;
9
+ };
10
+ //# sourceMappingURL=useOverlayStack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useOverlayStack.d.ts","sourceRoot":"","sources":["../src/useOverlayStack.ts"],"names":[],"mappings":"AAyBA;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,UAAO,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAclF"}
@@ -0,0 +1,39 @@
1
+ import { useLayoutEffect, useState, useSyncExternalStore } from "react";
2
+ const BASE_Z_INDEX = 1000;
3
+ const Z_INDEX_STEP = 10;
4
+ // Global stack of open overlays, last opened on top.
5
+ let stack = [];
6
+ const listeners = new Set();
7
+ function subscribe(listener) {
8
+ listeners.add(listener);
9
+ return () => {
10
+ listeners.delete(listener);
11
+ };
12
+ }
13
+ function getSnapshot() {
14
+ return stack;
15
+ }
16
+ function setStack(next) {
17
+ stack = next;
18
+ listeners.forEach(listener => listener());
19
+ }
20
+ /**
21
+ * Registers an overlay in a page-wide stack while `enabled`.
22
+ * `isTop` is true for the last opened overlay only (the one that should react to Escape);
23
+ * `zIndex` puts every overlay above the ones opened before it.
24
+ */
25
+ export function useOverlayStack(enabled = true) {
26
+ const [key] = useState(() => Symbol("overlay"));
27
+ // Layout effect: the z-index is known before the overlay is painted.
28
+ useLayoutEffect(() => {
29
+ if (!enabled)
30
+ return;
31
+ setStack([...stack, key]);
32
+ return () => setStack(stack.filter(k => k !== key));
33
+ }, [enabled, key]);
34
+ const current = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
35
+ const index = current.indexOf(key);
36
+ if (index === -1)
37
+ return { isTop: false, zIndex: BASE_Z_INDEX };
38
+ return { isTop: index === current.length - 1, zIndex: BASE_Z_INDEX + index * Z_INDEX_STEP };
39
+ }
@@ -0,0 +1,3 @@
1
+ /** Blocks page scrolling while `enabled`, compensating the scrollbar width to avoid a layout shift. */
2
+ export declare function useScrollLock(enabled?: boolean): void;
3
+ //# sourceMappingURL=useScrollLock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useScrollLock.d.ts","sourceRoot":"","sources":["../src/useScrollLock.ts"],"names":[],"mappings":"AAOA,uGAAuG;AACvG,wBAAgB,aAAa,CAAC,OAAO,UAAO,GAAG,IAAI,CAsBlD"}
@@ -0,0 +1,29 @@
1
+ import { useEffect } from "react";
2
+ // Reference count: nested locks only release the page when the last one goes away,
3
+ // whatever the order they are released in.
4
+ let lockCount = 0;
5
+ let saved = null;
6
+ /** Blocks page scrolling while `enabled`, compensating the scrollbar width to avoid a layout shift. */
7
+ export function useScrollLock(enabled = true) {
8
+ useEffect(() => {
9
+ if (!enabled)
10
+ return;
11
+ const body = document.body;
12
+ if (lockCount === 0) {
13
+ saved = { overflow: body.style.overflow, paddingRight: body.style.paddingRight };
14
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
15
+ if (scrollbarWidth > 0)
16
+ body.style.paddingRight = `${scrollbarWidth}px`;
17
+ body.style.overflow = "hidden";
18
+ }
19
+ lockCount++;
20
+ return () => {
21
+ lockCount--;
22
+ if (lockCount === 0 && saved) {
23
+ body.style.overflow = saved.overflow;
24
+ body.style.paddingRight = saved.paddingRight;
25
+ saved = null;
26
+ }
27
+ };
28
+ }, [enabled]);
29
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@-reddacted-/react-hooks",
3
- "version": "0.1.0",
4
- "description": "useFullscreen, useInstallPrompt, useValueChange React hooks",
3
+ "version": "0.3.0",
4
+ "description": "React hooks: useAsync, useElementSize, useClickOutside, useEscapeKey, useDocumentTitle, useFullscreen, useInstallPrompt, useValueChange, useFocusTrap, useScrollLock, useOverlayStack",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -11,15 +11,18 @@
11
11
  "types": "./dist/index.d.ts"
12
12
  }
13
13
  },
14
- "files": ["dist"],
14
+ "files": [
15
+ "dist"
16
+ ],
15
17
  "scripts": {
16
- "build": "tsc"
18
+ "build": "tsc",
19
+ "typecheck": "tsc --noEmit"
17
20
  },
18
21
  "peerDependencies": {
19
22
  "react": ">=18"
20
23
  },
21
24
  "devDependencies": {
22
- "@types/react": "^19.0.0",
25
+ "@types/react": "^18.3.1",
23
26
  "typescript": "~5.8.3"
24
27
  }
25
28
  }