@studiocubics/hooks 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # @studiocubics/hooks
2
+
3
+ ## 0.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - First publish inshallah!
8
+ - Updated dependencies
9
+ - @studiocubics/types@0.0.1
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # React + TypeScript + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is currently not compatible with SWC. See [this issue](https://github.com/vitejs/vite-plugin-react/issues/428) for tracking the progress.
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
17
+
18
+ ```js
19
+ export default defineConfig([
20
+ globalIgnores(['dist']),
21
+ {
22
+ files: ['**/*.{ts,tsx}'],
23
+ extends: [
24
+ // Other configs...
25
+
26
+ // Remove tseslint.configs.recommended and replace with this
27
+ tseslint.configs.recommendedTypeChecked,
28
+ // Alternatively, use this for stricter rules
29
+ tseslint.configs.strictTypeChecked,
30
+ // Optionally, add this for stylistic rules
31
+ tseslint.configs.stylisticTypeChecked,
32
+
33
+ // Other configs...
34
+ ],
35
+ languageOptions: {
36
+ parserOptions: {
37
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
38
+ tsconfigRootDir: import.meta.dirname,
39
+ },
40
+ // other options...
41
+ },
42
+ },
43
+ ])
44
+ ```
45
+
46
+ You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
47
+
48
+ ```js
49
+ // eslint.config.js
50
+ import reactX from 'eslint-plugin-react-x'
51
+ import reactDom from 'eslint-plugin-react-dom'
52
+
53
+ export default defineConfig([
54
+ globalIgnores(['dist']),
55
+ {
56
+ files: ['**/*.{ts,tsx}'],
57
+ extends: [
58
+ // Other configs...
59
+ // Enable lint rules for React
60
+ reactX.configs['recommended-typescript'],
61
+ // Enable lint rules for React DOM
62
+ reactDom.configs.recommended,
63
+ ],
64
+ languageOptions: {
65
+ parserOptions: {
66
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
67
+ tsconfigRootDir: import.meta.dirname,
68
+ },
69
+ // other options...
70
+ },
71
+ },
72
+ ])
73
+ ```
@@ -0,0 +1,21 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import reactHooks from "eslint-plugin-react-hooks";
4
+ import tseslint from "typescript-eslint";
5
+ import { defineConfig, globalIgnores } from "eslint/config";
6
+
7
+ export default defineConfig([
8
+ globalIgnores(["dist"]),
9
+ {
10
+ files: ["**/*.{ts,tsx}"],
11
+ extends: [
12
+ js.configs.recommended,
13
+ tseslint.configs.recommended,
14
+ reactHooks.configs.flat.recommended,
15
+ ],
16
+ languageOptions: {
17
+ ecmaVersion: 2020,
18
+ globals: globals.browser,
19
+ },
20
+ },
21
+ ]);
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@studiocubics/hooks",
3
+ "description": "Package containing important hooks by Studio Cubics",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "private": false,
8
+ "version": "0.0.1",
9
+ "keywords": [
10
+ "@studiocubics",
11
+ "cubics",
12
+ "studio",
13
+ "react",
14
+ "hooks"
15
+ ],
16
+ "author": {
17
+ "name": "Studio Cubics",
18
+ "email": "studiocubics7@gmail.com",
19
+ "url": "https://studio-cubics.vercel.app"
20
+ },
21
+ "license": "MIT",
22
+ "type": "module",
23
+ "main": "./dist/index.js",
24
+ "exports": {
25
+ ".": "./dist/index.js",
26
+ "./styles.css": "./dist/index.css"
27
+ },
28
+ "peerDependencies": {
29
+ "react": "^19.2.0",
30
+ "react-dom": "^19.2.0"
31
+ },
32
+ "dependencies": {
33
+ "@studiocubics/types": "^0.0.1"
34
+ },
35
+ "devDependencies": {
36
+ "@eslint/js": "^9.39.1",
37
+ "@rollup/plugin-terser": "^0.4.4",
38
+ "@rollup/plugin-typescript": "^12.3.0",
39
+ "@types/node": "^24.10.1",
40
+ "@types/react": "^19.2.5",
41
+ "@types/react-dom": "^19.2.3",
42
+ "eslint": "^9.39.1",
43
+ "eslint-plugin-react-hooks": "^7.0.1",
44
+ "globals": "^16.5.0",
45
+ "postcss": "^8.5.6",
46
+ "postcss-modules": "^6.0.1",
47
+ "rollup": "^4.53.3",
48
+ "rollup-plugin-postcss": "^4.0.2",
49
+ "rollup-preserve-directives": "^1.1.3",
50
+ "typescript": "~5.9.3",
51
+ "typescript-eslint": "^8.46.4"
52
+ },
53
+ "scripts": {
54
+ "build": "rollup -c",
55
+ "clean:build": "rimraf dist node_modules && rollup -c",
56
+ "lint": "eslint .",
57
+ "clean": "rimraf dist node_modules"
58
+ }
59
+ }
@@ -0,0 +1,28 @@
1
+ import { defineConfig } from "rollup";
2
+ import typescript from "@rollup/plugin-typescript";
3
+ import postcss from "rollup-plugin-postcss";
4
+ import terser from "@rollup/plugin-terser";
5
+ import preserveDirectives from "rollup-preserve-directives";
6
+
7
+ export default defineConfig({
8
+ input: "src/index.ts",
9
+ output: {
10
+ preserveModules: true,
11
+ preserveModulesRoot: "src",
12
+ dir: "dist",
13
+ format: "esm",
14
+ sourcemap: true,
15
+ },
16
+ plugins: [
17
+ preserveDirectives(),
18
+ postcss({
19
+ include: "**/*.module.css",
20
+ modules: true,
21
+ extract: true,
22
+ minimize: true,
23
+ }),
24
+ typescript({ tsconfig: "./tsconfig.json" }),
25
+ terser({ compress: { directives: false } }),
26
+ ],
27
+ external: ["react/jsx-runtime", "react", "react-dom"],
28
+ });
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export * from "./useAnchorElement/useAnchorElement";
2
+ export * from "./useDelayedAction/useDelayedAction";
3
+ export * from "./useDisclosure/useDisclosure";
4
+ export * from "./useEventCallback/useEventCallback";
5
+ export * from "./useEventListener/useEventListener";
6
+ export * from "./useFormHelpers/useFormHelpers";
7
+ export * from "./useIsomorphicLayoutEffect/useIsomorphicLayoutEffect";
8
+ export * from "./useLocalStorage/useLocalStorage";
9
+ export * from "./useMounted/useMounted";
10
+ export * from "./useMousePosition/useMousePosition";
11
+ export * from "./useScreenSize/useScreenSize";
@@ -0,0 +1,15 @@
1
+ "use client";
2
+
3
+ import { type MouseEvent, useMemo, useState } from "react";
4
+
5
+ export function useAnchorElement<T extends HTMLElement>() {
6
+ const [anchorEl, setAnchorEl] = useState<T | null>(null);
7
+ const open = useMemo(() => Boolean(anchorEl), [anchorEl]);
8
+ const handleClick = (event: MouseEvent<T>) => {
9
+ setAnchorEl(event.currentTarget);
10
+ };
11
+ const handleClose = () => {
12
+ setAnchorEl(null);
13
+ };
14
+ return { open, anchorEl, handleClick, handleClose, setAnchorEl };
15
+ }
@@ -0,0 +1,12 @@
1
+ "use client";
2
+
3
+ import { useCallback } from "react";
4
+
5
+ export function useDelayedAction() {
6
+ const delayedExecute = useCallback(async (fn: Function, ms: number) => {
7
+ await new Promise((resolve) => setTimeout(resolve, ms));
8
+ fn();
9
+ }, []);
10
+
11
+ return delayedExecute;
12
+ }
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+
5
+ export function useDisclosure(initialState: boolean = false) {
6
+ const [open, setOpen] = useState(initialState);
7
+
8
+ function handleOpen() {
9
+ setOpen(true);
10
+ }
11
+ function handleClose() {
12
+ setOpen(false);
13
+ }
14
+
15
+ /**
16
+ * Hijacking the handleClose function to prevent the dialog from closing when the user clicks outside the dialog or presses the escape key.
17
+ * @param _ event not going to be used.
18
+ * @param reason The reason the dialog was closed.
19
+ */
20
+ function handleStrictClose(_: {}, reason: "backdropClick" | "escapeKeyDown") {
21
+ if (reason === "backdropClick" || reason === "escapeKeyDown") return;
22
+ handleClose();
23
+ }
24
+
25
+ function handleToggle() {
26
+ setOpen((prev) => !prev);
27
+ }
28
+
29
+ return {
30
+ open,
31
+ handleClose,
32
+ handleStrictClose,
33
+ handleOpen,
34
+ handleToggle,
35
+ };
36
+ }
@@ -0,0 +1,26 @@
1
+ "use client";
2
+
3
+ import { useCallback, useRef } from "react";
4
+ import { useIsomorphicLayoutEffect } from "../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect";
5
+
6
+ export function useEventCallback<Args extends unknown[], R>(
7
+ fn: (...args: Args) => R
8
+ ): (...args: Args) => R;
9
+ export function useEventCallback<Args extends unknown[], R>(
10
+ fn: ((...args: Args) => R) | undefined
11
+ ): ((...args: Args) => R) | undefined;
12
+ export function useEventCallback<Args extends unknown[], R>(
13
+ fn: ((...args: Args) => R) | undefined
14
+ ): ((...args: Args) => R) | undefined {
15
+ const ref = useRef<typeof fn>(() => {
16
+ throw new Error("Cannot call an event handler while rendering.");
17
+ });
18
+
19
+ useIsomorphicLayoutEffect(() => {
20
+ ref.current = fn;
21
+ }, [fn]);
22
+
23
+ return useCallback((...args: Args) => ref.current?.(...args), [ref]) as (
24
+ ...args: Args
25
+ ) => R;
26
+ }
@@ -0,0 +1,92 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef } from "react";
4
+
5
+ import type { RefObject } from "react";
6
+ import { useIsomorphicLayoutEffect } from "../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect";
7
+
8
+ // MediaQueryList Event based useEventListener interface
9
+ function useEventListener<K extends keyof MediaQueryListEventMap>(
10
+ eventName: K,
11
+ handler: (event: MediaQueryListEventMap[K]) => void,
12
+ element: RefObject<MediaQueryList>,
13
+ options?: boolean | AddEventListenerOptions,
14
+ ): void;
15
+
16
+ // Window Event based useEventListener interface
17
+ function useEventListener<K extends keyof WindowEventMap>(
18
+ eventName: K,
19
+ handler: (event: WindowEventMap[K]) => void,
20
+ element?: undefined,
21
+ options?: boolean | AddEventListenerOptions,
22
+ ): void;
23
+
24
+ // Element Event based useEventListener interface
25
+ function useEventListener<
26
+ K extends keyof HTMLElementEventMap & keyof SVGElementEventMap,
27
+ T extends Element = K extends keyof HTMLElementEventMap
28
+ ? HTMLDivElement
29
+ : SVGElement,
30
+ >(
31
+ eventName: K,
32
+ handler:
33
+ | ((event: HTMLElementEventMap[K]) => void)
34
+ | ((event: SVGElementEventMap[K]) => void),
35
+ element: RefObject<T>,
36
+ options?: boolean | AddEventListenerOptions,
37
+ ): void;
38
+
39
+ // Document Event based useEventListener interface
40
+ function useEventListener<K extends keyof DocumentEventMap>(
41
+ eventName: K,
42
+ handler: (event: DocumentEventMap[K]) => void,
43
+ element: RefObject<Document>,
44
+ options?: boolean | AddEventListenerOptions,
45
+ ): void;
46
+
47
+ function useEventListener<
48
+ KW extends keyof WindowEventMap,
49
+ KH extends keyof HTMLElementEventMap & keyof SVGElementEventMap,
50
+ KM extends keyof MediaQueryListEventMap,
51
+ T extends HTMLElement | SVGAElement | MediaQueryList = HTMLElement,
52
+ >(
53
+ eventName: KW | KH | KM,
54
+ handler: (
55
+ event:
56
+ | WindowEventMap[KW]
57
+ | HTMLElementEventMap[KH]
58
+ | SVGElementEventMap[KH]
59
+ | MediaQueryListEventMap[KM]
60
+ | Event,
61
+ ) => void,
62
+ element?: RefObject<T>,
63
+ options?: boolean | AddEventListenerOptions,
64
+ ) {
65
+ // Create a ref that stores handler
66
+ const savedHandler = useRef(handler);
67
+
68
+ useIsomorphicLayoutEffect(() => {
69
+ savedHandler.current = handler;
70
+ }, [handler]);
71
+
72
+ useEffect(() => {
73
+ // Define the listening target
74
+ const targetElement: T | Window = element?.current ?? window;
75
+
76
+ if (!(targetElement && targetElement.addEventListener)) return;
77
+
78
+ // Create event listener that calls handler function stored in ref
79
+ const listener: typeof handler = (event) => {
80
+ savedHandler.current(event);
81
+ };
82
+
83
+ targetElement.addEventListener(eventName, listener, options);
84
+
85
+ // Remove event listener on cleanup
86
+ return () => {
87
+ targetElement.removeEventListener(eventName, listener, options);
88
+ };
89
+ }, [eventName, element, options]);
90
+ }
91
+
92
+ export { useEventListener };
@@ -0,0 +1,29 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+
5
+ export type FormHelpersProps = {
6
+ initialLoading?: boolean;
7
+ initialError?: string | unknown;
8
+ };
9
+
10
+ export function useFormHelpers(props: FormHelpersProps) {
11
+ const [loading, setLoading] = useState(props?.initialLoading ?? false);
12
+ const [error, setError] = useState(props?.initialError ?? "");
13
+
14
+ function handleLoading(l: boolean) {
15
+ setLoading(l);
16
+ }
17
+ function handleError(e: string | unknown) {
18
+ if (typeof e === "string") setError(e);
19
+ if (e instanceof Error) setError(e.message);
20
+ console.error(e);
21
+ }
22
+
23
+ return {
24
+ loading,
25
+ error,
26
+ handleLoading,
27
+ handleError,
28
+ };
29
+ }
@@ -0,0 +1,6 @@
1
+ "use client";
2
+
3
+ import { useEffect, useLayoutEffect } from "react";
4
+
5
+ export const useIsomorphicLayoutEffect =
6
+ typeof window !== "undefined" ? useLayoutEffect : useEffect;
@@ -0,0 +1,165 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useState } from "react";
4
+ import type { Dispatch, SetStateAction } from "react";
5
+ import { useEventCallback } from "../useEventCallback/useEventCallback";
6
+ import { useEventListener } from "../useEventListener/useEventListener";
7
+
8
+ declare global {
9
+ interface WindowEventMap {
10
+ "local-storage": CustomEvent;
11
+ }
12
+ }
13
+
14
+ type UseLocalStorageOptions<T> = {
15
+ serializer?: (value: T) => string;
16
+ deserializer?: (value: string) => T;
17
+ initializeWithValue?: boolean;
18
+ };
19
+
20
+ const IS_SERVER = typeof window === "undefined";
21
+
22
+ export function useLocalStorage<T>(
23
+ key: string,
24
+ initialValue: T | (() => T),
25
+ options: UseLocalStorageOptions<T> = {},
26
+ ): [T, Dispatch<SetStateAction<T>>, () => void] {
27
+ const { initializeWithValue = true } = options;
28
+
29
+ const serializer = useCallback<(value: T) => string>(
30
+ (value) => {
31
+ if (options.serializer) {
32
+ return options.serializer(value);
33
+ }
34
+
35
+ return JSON.stringify(value);
36
+ },
37
+ [options],
38
+ );
39
+
40
+ const deserializer = useCallback<(value: string) => T>(
41
+ (value) => {
42
+ if (options.deserializer) {
43
+ return options.deserializer(value);
44
+ }
45
+ // Support 'undefined' as a value
46
+ if (value === "undefined") {
47
+ return undefined as unknown as T;
48
+ }
49
+
50
+ const defaultValue =
51
+ initialValue instanceof Function ? initialValue() : initialValue;
52
+
53
+ let parsed: unknown;
54
+ try {
55
+ parsed = JSON.parse(value);
56
+ } catch (error) {
57
+ console.error("Error parsing JSON:", error);
58
+ return defaultValue; // Return initialValue if parsing fails
59
+ }
60
+
61
+ return parsed as T;
62
+ },
63
+ [options, initialValue],
64
+ );
65
+
66
+ // Get from local storage then
67
+ // parse stored json or return initialValue
68
+ const readValue = useCallback((): T => {
69
+ const initialValueToUse =
70
+ initialValue instanceof Function ? initialValue() : initialValue;
71
+
72
+ // Prevent build error "window is undefined" but keep working
73
+ if (IS_SERVER) {
74
+ return initialValueToUse;
75
+ }
76
+
77
+ try {
78
+ const raw = window.localStorage.getItem(key);
79
+ return raw ? deserializer(raw) : initialValueToUse;
80
+ } catch (error) {
81
+ console.warn(`Error reading localStorage key “${key}”:`, error);
82
+ return initialValueToUse;
83
+ }
84
+ }, [initialValue, key, deserializer]);
85
+
86
+ const [storedValue, setStoredValue] = useState(() => {
87
+ if (initializeWithValue) {
88
+ return readValue();
89
+ }
90
+
91
+ return initialValue instanceof Function ? initialValue() : initialValue;
92
+ });
93
+
94
+ // Return a wrapped version of useState's setter function that ...
95
+ // ... persists the new value to localStorage.
96
+ const setValue: Dispatch<SetStateAction<T>> = useEventCallback((value) => {
97
+ // Prevent build error "window is undefined" but keeps working
98
+ if (IS_SERVER) {
99
+ console.warn(
100
+ `Tried setting localStorage key “${key}” even though environment is not a client`,
101
+ );
102
+ }
103
+
104
+ try {
105
+ // Allow value to be a function so we have the same API as useState
106
+ const newValue = value instanceof Function ? value(readValue()) : value;
107
+
108
+ // Save to local storage
109
+ window.localStorage.setItem(key, serializer(newValue));
110
+
111
+ // Save state
112
+ setStoredValue(newValue);
113
+
114
+ // We dispatch a custom event so every similar useLocalStorage hook is notified
115
+ window.dispatchEvent(new StorageEvent("local-storage", { key }));
116
+ } catch (error) {
117
+ console.warn(`Error setting localStorage key “${key}”:`, error);
118
+ }
119
+ });
120
+
121
+ const removeValue = useEventCallback(() => {
122
+ // Prevent build error "window is undefined" but keeps working
123
+ if (IS_SERVER) {
124
+ console.warn(
125
+ `Tried removing localStorage key “${key}” even though environment is not a client`,
126
+ );
127
+ }
128
+
129
+ const defaultValue =
130
+ initialValue instanceof Function ? initialValue() : initialValue;
131
+
132
+ // Remove the key from local storage
133
+ window.localStorage.removeItem(key);
134
+
135
+ // Save state with default value
136
+ setStoredValue(defaultValue);
137
+
138
+ // We dispatch a custom event so every similar useLocalStorage hook is notified
139
+ window.dispatchEvent(new StorageEvent("local-storage", { key }));
140
+ });
141
+
142
+ useEffect(() => {
143
+ setStoredValue(readValue());
144
+ // eslint-disable-next-line react-hooks/exhaustive-deps
145
+ }, [key]);
146
+
147
+ const handleStorageChange = useCallback(
148
+ (event: StorageEvent | CustomEvent) => {
149
+ if ((event as StorageEvent).key && (event as StorageEvent).key !== key) {
150
+ return;
151
+ }
152
+ setStoredValue(readValue());
153
+ },
154
+ [key, readValue],
155
+ );
156
+
157
+ // this only works for other documents, not the current one
158
+ useEventListener("storage", handleStorageChange);
159
+
160
+ // this is a custom event, triggered in writeValueToLocalStorage
161
+ // See: useLocalStorage()
162
+ useEventListener("local-storage", handleStorageChange);
163
+
164
+ return [storedValue, setValue, removeValue];
165
+ }
@@ -0,0 +1,11 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+
5
+ export function useMounted() {
6
+ const [mounted, setMounted] = useState(false);
7
+ useEffect(() => {
8
+ setMounted(true);
9
+ }, []);
10
+ return { mounted };
11
+ }
@@ -0,0 +1,60 @@
1
+ "use client";
2
+ import { useEffect, useState } from "react";
3
+
4
+ interface PositionMatrix {
5
+ x: number | undefined;
6
+ y: number | undefined;
7
+ }
8
+
9
+ type Event = MouseEvent | undefined;
10
+
11
+ export function useMousePosition({ includeTouch }: { includeTouch: Boolean }) {
12
+ const [mousePosition, setMousePosition] = useState<PositionMatrix>({
13
+ x: undefined,
14
+ y: undefined,
15
+ });
16
+ const [touchPosition, setTouchPosition] = useState<PositionMatrix>({
17
+ x: undefined,
18
+ y: undefined,
19
+ });
20
+ const [mouseSpeed, setMouseSpeed] = useState(0);
21
+ const [prevEvent, setPrevEvent] = useState<Event>(undefined);
22
+ useEffect(() => {
23
+ const updateMousePosition = (currentEvent: MouseEvent) => {
24
+ let x, y;
25
+ [x, y] = [currentEvent.clientX, currentEvent.clientY];
26
+ var movementX = Math.abs(
27
+ currentEvent.clientX - (prevEvent?.clientX ? prevEvent?.clientX : 0)
28
+ );
29
+ var movementY = Math.abs(
30
+ currentEvent.clientY - (prevEvent?.clientY ? prevEvent?.clientY : 0)
31
+ );
32
+ var movement = Math.sqrt(movementX * movementX + movementY * movementY);
33
+ var speed = Math.round(10 * movement);
34
+ setMouseSpeed(speed);
35
+ setMousePosition({ x, y });
36
+ setPrevEvent(currentEvent);
37
+ // console.log("prevEvent", prevEvent?.screenX);
38
+ };
39
+ window.addEventListener("mousemove", updateMousePosition);
40
+ return () => {
41
+ window.removeEventListener("mousemove", updateMousePosition);
42
+ };
43
+ }, [prevEvent]);
44
+ useEffect(() => {
45
+ const updateTouchPosition = (currentEvent: TouchEvent) => {
46
+ let x, y;
47
+ if (currentEvent.touches) {
48
+ const touch = currentEvent.touches[0];
49
+ [x, y] = [touch.clientX, touch.clientY];
50
+ }
51
+ setTouchPosition({ x, y });
52
+ };
53
+ return () => {
54
+ if (includeTouch) {
55
+ window.removeEventListener("touchmove", updateTouchPosition);
56
+ }
57
+ };
58
+ }, [includeTouch]);
59
+ return { mousePosition, touchPosition, mouseSpeed };
60
+ }
@@ -0,0 +1,74 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { useLocalStorage } from "../useLocalStorage/useLocalStorage";
5
+
6
+ type ScreenSize = {
7
+ width: number;
8
+ height: number;
9
+ isSmall: boolean;
10
+ isMedium: boolean;
11
+ isLarge: boolean;
12
+ isXLarge: boolean;
13
+ ltSmall: boolean;
14
+ ltMedium: boolean;
15
+ ltLarge: boolean;
16
+ ltXLarge: boolean;
17
+ gtSmall: boolean;
18
+ gtMedium: boolean;
19
+ gtLarge: boolean;
20
+ } | null;
21
+
22
+ // Move breakpoints OUTSIDE. Prevents Rollup/minifiers from collapsing them.
23
+ const BREAKPOINTS = Object.freeze({
24
+ sm: 600,
25
+ md: 900,
26
+ lg: 1281,
27
+ xl: 1536,
28
+ });
29
+
30
+ export function useScreenSize(): Partial<ScreenSize> {
31
+ const [screenSize, setScreenSize] = useLocalStorage("screen", {
32
+ width: 0,
33
+ height: 0,
34
+ });
35
+ const [size, setSize] = useState<Partial<ScreenSize>>({ ...screenSize });
36
+
37
+ useEffect(() => {
38
+ const width = screenSize.width;
39
+ const height = screenSize.height;
40
+ setSize({
41
+ width,
42
+ height,
43
+ isSmall: width < BREAKPOINTS.sm,
44
+ isMedium: width >= BREAKPOINTS.sm && width < BREAKPOINTS.md,
45
+ isLarge: width >= BREAKPOINTS.md && width < BREAKPOINTS.lg,
46
+ isXLarge: width >= BREAKPOINTS.lg,
47
+
48
+ ltSmall: width < BREAKPOINTS.sm,
49
+ ltMedium: width < BREAKPOINTS.md,
50
+ ltLarge: width < BREAKPOINTS.lg,
51
+ ltXLarge: width < BREAKPOINTS.xl,
52
+
53
+ gtSmall: width >= BREAKPOINTS.sm,
54
+ gtMedium: width >= BREAKPOINTS.md,
55
+ gtLarge: width >= BREAKPOINTS.lg,
56
+ });
57
+ }, [screenSize]);
58
+
59
+ useEffect(() => {
60
+ if (typeof window === "undefined") return;
61
+
62
+ const compute = () => {
63
+ const width = window.innerWidth;
64
+ const height = window.innerHeight;
65
+ setScreenSize({ width, height });
66
+ };
67
+
68
+ compute();
69
+ window.addEventListener("resize", compute);
70
+ return () => window.removeEventListener("resize", compute);
71
+ }, []);
72
+
73
+ return size;
74
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "resolveJsonModule": true,
9
+ "allowJs": true,
10
+ "skipLibCheck": true,
11
+
12
+ /* Bundler mode */
13
+ "moduleResolution": "bundler",
14
+ "verbatimModuleSyntax": true,
15
+ "moduleDetection": "force",
16
+ "noEmit": true,
17
+ "jsx": "react-jsx",
18
+ // Output
19
+ "declaration": true,
20
+ "outDir": "./dist",
21
+
22
+ /* Linting */
23
+ "strict": true,
24
+ "noUnusedLocals": true,
25
+ "noUnusedParameters": true,
26
+ "erasableSyntaxOnly": true,
27
+ "noFallthroughCasesInSwitch": true,
28
+ "noUncheckedSideEffectImports": true
29
+ },
30
+ "include": ["src"]
31
+ }