@opensite/hooks 0.1.0 → 2.0.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/README.md CHANGED
@@ -1,102 +1,225 @@
1
1
  ![Opensite AI Utility Hooks](https://octane.cdn.ing/api/v1/images/transform?url=https://cdn.ing/assets/i/r/285728/knsbi168qz1imlat2aq042c10rw8/opensite-react-hooks.png&q=90&f=webp)
2
2
 
3
- ---
3
+ # @opensite/hooks
4
4
 
5
- # @opensite/hooks
5
+ Performance-first React hooks for UI state, storage, events, and responsive behavior.
6
6
 
7
- Performance-first React hooks for UI state, storage, events, and responsive behavior. Designed to be tree-shakable, SSR-safe, and CDN-ready.
8
-
9
- [![npm version](https://img.shields.io/npm/v/@opensite/hooks?style=flat-square)](https://www.npmjs.com/package/@opensite/hooks)
10
- [![npm downloads](https://img.shields.io/npm/dm/@opensite/hooks?style=flat-square)](https://www.npmjs.com/package/@opensite/hooks)
11
- [![License](https://img.shields.io/npm/l/@opensite/hooks?style=flat-square)](./LICENSE)
12
- [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue?style=flat-square)](./tsconfig.json)
13
- [![Tree-Shakeable](https://img.shields.io/badge/Tree%20Shakeable-Yes-brightgreen?style=flat-square)](#tree-shaking)
7
+ [![npm version](https://img.shields.io/npm/v/@opensite/hooks.svg)](https://www.npmjs.com/package/@opensite/hooks)
8
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@opensite/hooks)](https://bundlephobia.com/package/@opensite/hooks)
9
+ [![license](https://img.shields.io/npm/l/@opensite/hooks.svg)](./LICENSE)
14
10
 
15
11
  ## Overview
16
12
 
17
- `@opensite/hooks` provides a focused set of high-performance React hooks that work across OpenSite and any open-source React project. Every hook is built for minimal bundle size, avoids unnecessary renders, and is safe for SSR.
13
+ `@opensite/hooks` provides a suite of zero-dependency, tree-shakable React hooks designed for high-performance marketing sites and web applications. All hooks are SSR-safe and optimized for Core Web Vitals.
14
+
15
+ **Key Features:**
16
+
17
+ - 🚀 **Zero dependencies** – Only React as a peer dependency
18
+ - 🌳 **Tree-shakable** – Import only what you use with flat exports
19
+ - 🔒 **SSR-safe** – All hooks handle server-side rendering correctly
20
+ - ⚡ **Performance-first** – Memoized callbacks, minimal re-renders
21
+ - 📦 **Multiple formats** – ESM, CJS, and UMD builds included
18
22
 
19
23
  ## Installation
20
24
 
21
25
  ```bash
26
+ # npm
27
+ npm install @opensite/hooks
28
+
29
+ # pnpm
22
30
  pnpm add @opensite/hooks
31
+
32
+ # yarn
33
+ yarn add @opensite/hooks
23
34
  ```
24
35
 
25
- Peer deps: `react` and `react-dom` (17+).
36
+ ### Requirements
37
+
38
+ - React 17.0.0 or higher
39
+ - React DOM 17.0.0 or higher
26
40
 
27
41
  ## Quick Start
28
42
 
29
- ```tsx
30
- import { useBoolean, useDebounceValue } from "@opensite/hooks";
43
+ ### Barrel Import
31
44
 
32
- export function SearchBox() {
33
- const [query, setQuery] = React.useState("");
34
- const debouncedQuery = useDebounceValue(query, 300);
35
- const { value: isOpen, setTrue, setFalse } = useBoolean(false);
45
+ Import multiple hooks from the main entry point:
36
46
 
37
- React.useEffect(() => {
38
- if (debouncedQuery) {
39
- // fetch results
40
- }
41
- }, [debouncedQuery]);
47
+ ```typescript
48
+ import { useBoolean, useLocalStorage, useMediaQuery } from '@opensite/hooks';
49
+ ```
50
+
51
+ ### Direct Import (Recommended for Bundle Size)
52
+
53
+ Import individual hooks for optimal tree-shaking:
54
+
55
+ ```typescript
56
+ import { useBoolean } from '@opensite/hooks/useBoolean';
57
+ import { useLocalStorage } from '@opensite/hooks/useLocalStorage';
58
+ import { useMediaQuery } from '@opensite/hooks/useMediaQuery';
59
+ ```
60
+
61
+ ### CDN Usage (UMD)
62
+
63
+ ```html
64
+ <script src="https://cdn.jsdelivr.net/npm/@opensite/hooks/dist/browser/opensite-hooks.umd.js"></script>
65
+ <script>
66
+ const { useBoolean, useDebounceValue } = window.OpensiteHooks;
67
+ </script>
68
+ ```
69
+
70
+ ## Available Hooks
71
+
72
+ | Hook | Description | Docs |
73
+ |------|-------------|------|
74
+ | **State Management** | | |
75
+ | [`useBoolean`](./docs/useBoolean.md) | Boolean state with toggle, setTrue, setFalse helpers | [View](./docs/useBoolean.md) |
76
+ | [`useMap`](./docs/useMap.md) | Map state with set, remove, clear helpers | [View](./docs/useMap.md) |
77
+ | [`usePrevious`](./docs/usePrevious.md) | Access the previous value of a state or prop | [View](./docs/usePrevious.md) |
78
+ | **Storage** | | |
79
+ | [`useLocalStorage`](./docs/useLocalStorage.md) | Synchronized state with localStorage + cross-tab sync | [View](./docs/useLocalStorage.md) |
80
+ | [`useSessionStorage`](./docs/useSessionStorage.md) | Synchronized state with sessionStorage | [View](./docs/useSessionStorage.md) |
81
+ | **Timing** | | |
82
+ | [`useDebounceValue`](./docs/useDebounceValue.md) | Debounce value changes with configurable delay | [View](./docs/useDebounceValue.md) |
83
+ | [`useDebounceCallback`](./docs/useDebounceCallback.md) | Debounce callbacks with cancel/flush controls | [View](./docs/useDebounceCallback.md) |
84
+ | [`useThrottle`](./docs/useThrottle.md) | Throttle value changes with leading/trailing options | [View](./docs/useThrottle.md) |
85
+ | **DOM & Events** | | |
86
+ | [`useEventListener`](./docs/useEventListener.md) | Attach event listeners with automatic cleanup | [View](./docs/useEventListener.md) |
87
+ | [`useOnClickOutside`](./docs/useOnClickOutside.md) | Detect clicks outside specified elements | [View](./docs/useOnClickOutside.md) |
88
+ | [`useHover`](./docs/useHover.md) | Detect hover state using pointer events | [View](./docs/useHover.md) |
89
+ | [`useResizeObserver`](./docs/useResizeObserver.md) | Observe element size changes | [View](./docs/useResizeObserver.md) |
90
+ | **Responsive** | | |
91
+ | [`useMediaQuery`](./docs/useMediaQuery.md) | Reactive CSS media query matching | [View](./docs/useMediaQuery.md) |
92
+ | **Utilities** | | |
93
+ | [`useCopyToClipboard`](./docs/useCopyToClipboard.md) | Copy text to clipboard with feedback state | [View](./docs/useCopyToClipboard.md) |
94
+ | [`useIsClient`](./docs/useIsClient.md) | Detect client-side vs server-side rendering | [View](./docs/useIsClient.md) |
95
+ | [`useIsomorphicLayoutEffect`](./docs/useIsomorphicLayoutEffect.md) | SSR-safe useLayoutEffect | [View](./docs/useIsomorphicLayoutEffect.md) |
96
+
97
+ ## Examples
98
+
99
+ ### useBoolean
100
+
101
+ ```typescript
102
+ import { useBoolean } from '@opensite/hooks/useBoolean';
103
+
104
+ function Modal() {
105
+ const { value: isOpen, setTrue: open, setFalse: close, toggle } = useBoolean(false);
42
106
 
43
107
  return (
44
108
  <>
45
- <button onClick={setTrue}>Open</button>
109
+ <button onClick={open}>Open Modal</button>
46
110
  {isOpen && (
47
- <div>
48
- <input value={query} onChange={(e) => setQuery(e.target.value)} />
49
- <button onClick={setFalse}>Close</button>
50
- </div>
111
+ <dialog open>
112
+ <p>Modal content</p>
113
+ <button onClick={close}>Close</button>
114
+ </dialog>
51
115
  )}
52
116
  </>
53
117
  );
54
118
  }
55
119
  ```
56
120
 
57
- ## Hooks
58
-
59
- - `useBoolean` - Boolean state with stable togglers.
60
- - `useDebounceValue` - Debounced state for inputs and API calls.
61
- - `useDebounceCallback` - Debounced function wrapper with optional max wait.
62
- - `useLocalStorage` - Persistent state with SSR-safe hydration.
63
- - `useSessionStorage` - Session scoped storage state.
64
- - `useOnClickOutside` - Dismiss components on outside clicks.
65
- - `useMediaQuery` - Responsive JavaScript behavior.
66
- - `useEventListener` - Safe event binding with cleanup.
67
- - `useIsClient` - Hydration-safe client checks.
68
- - `useCopyToClipboard` - Clipboard helper with fallback.
69
- - `usePrevious` - Previous value tracking.
70
- - `useThrottle` - Throttled state for scroll/resize events.
71
- - `useResizeObserver` - ResizeObserver-based size tracking.
72
- - `useHover` - Hover state with pointer events.
73
- - `useIsomorphicLayoutEffect` - SSR-safe layout effect.
74
- - `useMap` - Immutable Map state helpers.
75
-
76
- ## Tree Shaking
77
-
78
- Prefer granular imports to minimize bundle size:
79
-
80
- ```tsx
81
- import { useMediaQuery } from "@opensite/hooks/core/useMediaQuery";
82
- import { useBoolean } from "@opensite/hooks/hooks/useBoolean";
121
+ ### useDebounceValue
122
+
123
+ ```typescript
124
+ import { useState } from 'react';
125
+ import { useDebounceValue } from '@opensite/hooks/useDebounceValue';
126
+
127
+ function SearchInput() {
128
+ const [query, setQuery] = useState('');
129
+ const debouncedQuery = useDebounceValue(query, 300);
130
+
131
+ // API call only triggers when debouncedQuery changes
132
+ useEffect(() => {
133
+ if (debouncedQuery) {
134
+ searchAPI(debouncedQuery);
135
+ }
136
+ }, [debouncedQuery]);
137
+
138
+ return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
139
+ }
83
140
  ```
84
141
 
85
- ## CDN / UMD
142
+ ### useMediaQuery
86
143
 
87
- ```html
88
- <script src="https://cdn.jsdelivr.net/npm/@opensite/hooks@0.1.0/dist/browser/opensite-hooks.umd.js"></script>
89
- <script>
90
- const { useBoolean } = window.OpensiteHooks;
91
- </script>
144
+ ```typescript
145
+ import { useMediaQuery } from '@opensite/hooks/useMediaQuery';
146
+
147
+ function ResponsiveComponent() {
148
+ const isMobile = useMediaQuery('(max-width: 768px)');
149
+ const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');
150
+
151
+ return (
152
+ <div className={prefersDark ? 'dark' : 'light'}>
153
+ {isMobile ? <MobileNav /> : <DesktopNav />}
154
+ </div>
155
+ );
156
+ }
157
+ ```
158
+
159
+ ### useLocalStorage
160
+
161
+ ```typescript
162
+ import { useLocalStorage } from '@opensite/hooks/useLocalStorage';
163
+
164
+ function ThemeToggle() {
165
+ const [theme, setTheme] = useLocalStorage('theme', 'light');
166
+
167
+ return (
168
+ <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
169
+ Current: {theme}
170
+ </button>
171
+ );
172
+ }
173
+ ```
174
+
175
+ ## Migration from v1.x
176
+
177
+ Version 2.0.0 simplifies import paths. Update your imports:
178
+
179
+ ```diff
180
+ - import { useBoolean } from '@opensite/hooks/core/useBoolean';
181
+ - import { useBoolean } from '@opensite/hooks/hooks/useBoolean';
182
+ + import { useBoolean } from '@opensite/hooks/useBoolean';
183
+ ```
184
+
185
+ The `/core/*` and `/hooks/*` paths have been removed. Use flat paths (`/useBoolean`) or barrel imports (`@opensite/hooks`) instead.
186
+
187
+ ## TypeScript
188
+
189
+ All hooks are written in TypeScript and include full type definitions. Types are exported alongside hooks:
190
+
191
+ ```typescript
192
+ import { useBoolean, type UseBooleanResult } from '@opensite/hooks/useBoolean';
193
+ import { useLocalStorage, type StorageOptions } from '@opensite/hooks/useLocalStorage';
92
194
  ```
93
195
 
94
- ## Documentation
196
+ ## Contributing
197
+
198
+ We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md) for details.
95
199
 
96
- - [CHANGELOG](./CHANGELOG.md)
97
- - [CONTRIBUTING](./CONTRIBUTING.md)
98
- - [DEVELOPMENT](./DEVELOPMENT.md)
200
+ ```bash
201
+ # Clone the repo
202
+ git clone https://github.com/opensite-ai/opensite-hooks.git
203
+ cd opensite-hooks
204
+
205
+ # Install dependencies
206
+ pnpm install
207
+
208
+ # Run tests
209
+ pnpm test
210
+
211
+ # Build
212
+ pnpm build
213
+ ```
99
214
 
100
215
  ## License
101
216
 
102
- [BSD 3](./LICENSE) © [OpenSite AI](https://opensite.ai)
217
+ [BSD-3-Clause](./LICENSE) © [OpenSite AI](https://opensite.ai)
218
+
219
+ ## Related Projects
220
+
221
+ - [@opensite/ui](https://github.com/opensite-ai/opensite-ui) – React component library for OpenSite
222
+ - [@opensite/blocks](https://github.com/opensite-ai/opensite-blocks) – Semantic page blocks for site builders
223
+ - [@page-speed/forms](https://github.com/opensite-ai/page-speed-forms) – Framework-agnostic form handling
224
+
225
+ Visit [OpenSite AI](https://opensite.ai) for more projects and information.
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Vitest test setup file
3
+ * Runs before each test file
4
+ */
5
+ import { cleanup } from "@testing-library/react";
6
+ import { afterEach, vi } from "vitest";
7
+ // Cleanup after each test case (unmounts React trees that were mounted with render)
8
+ afterEach(() => {
9
+ cleanup();
10
+ });
11
+ // Mock window.matchMedia for useMediaQuery tests
12
+ Object.defineProperty(window, "matchMedia", {
13
+ writable: true,
14
+ value: vi.fn().mockImplementation((query) => ({
15
+ matches: false,
16
+ media: query,
17
+ onchange: null,
18
+ addListener: vi.fn(), // deprecated
19
+ removeListener: vi.fn(), // deprecated
20
+ addEventListener: vi.fn(),
21
+ removeEventListener: vi.fn(),
22
+ dispatchEvent: vi.fn(),
23
+ })),
24
+ });
25
+ // Mock ResizeObserver for useResizeObserver tests
26
+ class MockResizeObserver {
27
+ callback;
28
+ constructor(callback) {
29
+ this.callback = callback;
30
+ }
31
+ observe = vi.fn();
32
+ unobserve = vi.fn();
33
+ disconnect = vi.fn();
34
+ }
35
+ Object.defineProperty(window, "ResizeObserver", {
36
+ writable: true,
37
+ value: MockResizeObserver,
38
+ });
39
+ // Mock clipboard API for useCopyToClipboard tests
40
+ Object.defineProperty(navigator, "clipboard", {
41
+ writable: true,
42
+ value: {
43
+ writeText: vi.fn().mockResolvedValue(undefined),
44
+ readText: vi.fn().mockResolvedValue(""),
45
+ },
46
+ });
47
+ // Mock localStorage
48
+ const localStorageMock = (() => {
49
+ let store = {};
50
+ return {
51
+ getItem: vi.fn((key) => store[key] ?? null),
52
+ setItem: vi.fn((key, value) => {
53
+ store[key] = value;
54
+ }),
55
+ removeItem: vi.fn((key) => {
56
+ delete store[key];
57
+ }),
58
+ clear: vi.fn(() => {
59
+ store = {};
60
+ }),
61
+ get length() {
62
+ return Object.keys(store).length;
63
+ },
64
+ key: vi.fn((index) => Object.keys(store)[index] ?? null),
65
+ };
66
+ })();
67
+ Object.defineProperty(window, "localStorage", {
68
+ value: localStorageMock,
69
+ });
70
+ // Mock sessionStorage (same as localStorage)
71
+ const sessionStorageMock = (() => {
72
+ let store = {};
73
+ return {
74
+ getItem: vi.fn((key) => store[key] ?? null),
75
+ setItem: vi.fn((key, value) => {
76
+ store[key] = value;
77
+ }),
78
+ removeItem: vi.fn((key) => {
79
+ delete store[key];
80
+ }),
81
+ clear: vi.fn(() => {
82
+ store = {};
83
+ }),
84
+ get length() {
85
+ return Object.keys(store).length;
86
+ },
87
+ key: vi.fn((index) => Object.keys(store)[index] ?? null),
88
+ };
89
+ })();
90
+ Object.defineProperty(window, "sessionStorage", {
91
+ value: sessionStorageMock,
92
+ });
93
+ // Clear mocks between tests
94
+ afterEach(() => {
95
+ vi.clearAllMocks();
96
+ localStorageMock.clear();
97
+ sessionStorageMock.clear();
98
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Vitest test setup file
3
+ * Runs before each test file
4
+ */
5
+ export {};
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Vitest test setup file
3
+ * Runs before each test file
4
+ */
5
+ import { cleanup } from "@testing-library/react";
6
+ import { afterEach, vi } from "vitest";
7
+ // Cleanup after each test case (unmounts React trees that were mounted with render)
8
+ afterEach(() => {
9
+ cleanup();
10
+ });
11
+ // Mock window.matchMedia for useMediaQuery tests
12
+ Object.defineProperty(window, "matchMedia", {
13
+ writable: true,
14
+ value: vi.fn().mockImplementation((query) => ({
15
+ matches: false,
16
+ media: query,
17
+ onchange: null,
18
+ addListener: vi.fn(), // deprecated
19
+ removeListener: vi.fn(), // deprecated
20
+ addEventListener: vi.fn(),
21
+ removeEventListener: vi.fn(),
22
+ dispatchEvent: vi.fn(),
23
+ })),
24
+ });
25
+ // Mock ResizeObserver for useResizeObserver tests
26
+ class MockResizeObserver {
27
+ callback;
28
+ constructor(callback) {
29
+ this.callback = callback;
30
+ }
31
+ observe = vi.fn();
32
+ unobserve = vi.fn();
33
+ disconnect = vi.fn();
34
+ }
35
+ Object.defineProperty(window, "ResizeObserver", {
36
+ writable: true,
37
+ value: MockResizeObserver,
38
+ });
39
+ // Mock clipboard API for useCopyToClipboard tests
40
+ Object.defineProperty(navigator, "clipboard", {
41
+ writable: true,
42
+ value: {
43
+ writeText: vi.fn().mockResolvedValue(undefined),
44
+ readText: vi.fn().mockResolvedValue(""),
45
+ },
46
+ });
47
+ // Mock localStorage
48
+ const localStorageMock = (() => {
49
+ let store = {};
50
+ return {
51
+ getItem: vi.fn((key) => store[key] ?? null),
52
+ setItem: vi.fn((key, value) => {
53
+ store[key] = value;
54
+ }),
55
+ removeItem: vi.fn((key) => {
56
+ delete store[key];
57
+ }),
58
+ clear: vi.fn(() => {
59
+ store = {};
60
+ }),
61
+ get length() {
62
+ return Object.keys(store).length;
63
+ },
64
+ key: vi.fn((index) => Object.keys(store)[index] ?? null),
65
+ };
66
+ })();
67
+ Object.defineProperty(window, "localStorage", {
68
+ value: localStorageMock,
69
+ });
70
+ // Mock sessionStorage (same as localStorage)
71
+ const sessionStorageMock = (() => {
72
+ let store = {};
73
+ return {
74
+ getItem: vi.fn((key) => store[key] ?? null),
75
+ setItem: vi.fn((key, value) => {
76
+ store[key] = value;
77
+ }),
78
+ removeItem: vi.fn((key) => {
79
+ delete store[key];
80
+ }),
81
+ clear: vi.fn(() => {
82
+ store = {};
83
+ }),
84
+ get length() {
85
+ return Object.keys(store).length;
86
+ },
87
+ key: vi.fn((index) => Object.keys(store)[index] ?? null),
88
+ };
89
+ })();
90
+ Object.defineProperty(window, "sessionStorage", {
91
+ value: sessionStorageMock,
92
+ });
93
+ // Clear mocks between tests
94
+ afterEach(() => {
95
+ vi.clearAllMocks();
96
+ localStorageMock.clear();
97
+ sessionStorageMock.clear();
98
+ });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Test utilities for React hook testing
3
+ */
4
+ import { render } from "@testing-library/react";
5
+ /**
6
+ * Custom render function that wraps components with providers if needed
7
+ */
8
+ function customRender(ui, options) {
9
+ return render(ui, { ...options });
10
+ }
11
+ // Re-export everything from testing-library
12
+ export * from "@testing-library/react";
13
+ // Override render with our custom version
14
+ export { customRender as render };
15
+ /**
16
+ * Helper to wait for a specific amount of time
17
+ * Useful for testing debounce/throttle hooks
18
+ */
19
+ export function wait(ms) {
20
+ return new Promise((resolve) => setTimeout(resolve, ms));
21
+ }
22
+ /**
23
+ * Helper to mock matchMedia with specific matches value
24
+ */
25
+ export function mockMatchMedia(matches) {
26
+ Object.defineProperty(window, "matchMedia", {
27
+ writable: true,
28
+ value: (query) => ({
29
+ matches,
30
+ media: query,
31
+ onchange: null,
32
+ addListener: () => { },
33
+ removeListener: () => { },
34
+ addEventListener: () => { },
35
+ removeEventListener: () => { },
36
+ dispatchEvent: () => false,
37
+ }),
38
+ });
39
+ }
40
+ /**
41
+ * Helper to create a mock storage event
42
+ */
43
+ export function createStorageEvent(key, newValue, storageArea = localStorage) {
44
+ return new StorageEvent("storage", {
45
+ key,
46
+ newValue,
47
+ oldValue: null,
48
+ storageArea,
49
+ url: window.location.href,
50
+ });
51
+ }
52
+ /**
53
+ * Helper to create a mock resize observer entry
54
+ */
55
+ export function createResizeObserverEntry(target, width, height) {
56
+ return {
57
+ target,
58
+ contentRect: {
59
+ width,
60
+ height,
61
+ top: 0,
62
+ right: width,
63
+ bottom: height,
64
+ left: 0,
65
+ x: 0,
66
+ y: 0,
67
+ toJSON: () => ({}),
68
+ },
69
+ borderBoxSize: [{ blockSize: height, inlineSize: width }],
70
+ contentBoxSize: [{ blockSize: height, inlineSize: width }],
71
+ devicePixelContentBoxSize: [{ blockSize: height, inlineSize: width }],
72
+ };
73
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Test utilities for React hook testing
3
+ */
4
+ import { render } from "@testing-library/react";
5
+ /**
6
+ * Custom render function that wraps components with providers if needed
7
+ */
8
+ function customRender(ui, options) {
9
+ return render(ui, { ...options });
10
+ }
11
+ // Re-export everything from testing-library
12
+ export * from "@testing-library/react";
13
+ // Override render with our custom version
14
+ export { customRender as render };
15
+ /**
16
+ * Helper to wait for a specific amount of time
17
+ * Useful for testing debounce/throttle hooks
18
+ */
19
+ export function wait(ms) {
20
+ return new Promise((resolve) => setTimeout(resolve, ms));
21
+ }
22
+ /**
23
+ * Helper to mock matchMedia with specific matches value
24
+ */
25
+ export function mockMatchMedia(matches) {
26
+ Object.defineProperty(window, "matchMedia", {
27
+ writable: true,
28
+ value: (query) => ({
29
+ matches,
30
+ media: query,
31
+ onchange: null,
32
+ addListener: () => { },
33
+ removeListener: () => { },
34
+ addEventListener: () => { },
35
+ removeEventListener: () => { },
36
+ dispatchEvent: () => false,
37
+ }),
38
+ });
39
+ }
40
+ /**
41
+ * Helper to create a mock storage event
42
+ */
43
+ export function createStorageEvent(key, newValue, storageArea = localStorage) {
44
+ return new StorageEvent("storage", {
45
+ key,
46
+ newValue,
47
+ oldValue: null,
48
+ storageArea,
49
+ url: window.location.href,
50
+ });
51
+ }
52
+ /**
53
+ * Helper to create a mock resize observer entry
54
+ */
55
+ export function createResizeObserverEntry(target, width, height) {
56
+ return {
57
+ target,
58
+ contentRect: {
59
+ width,
60
+ height,
61
+ top: 0,
62
+ right: width,
63
+ bottom: height,
64
+ left: 0,
65
+ x: 0,
66
+ y: 0,
67
+ toJSON: () => ({}),
68
+ },
69
+ borderBoxSize: [{ blockSize: height, inlineSize: width }],
70
+ contentBoxSize: [{ blockSize: height, inlineSize: width }],
71
+ devicePixelContentBoxSize: [{ blockSize: height, inlineSize: width }],
72
+ };
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opensite/hooks",
3
- "version": "0.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "Performance-first React hooks for UI state, storage, events, and responsive behavior with tree-shakable exports.",
5
5
  "keywords": [
6
6
  "react",
@@ -19,7 +19,7 @@
19
19
  "url": "https://github.com/opensite-ai/opensite-hooks/issues"
20
20
  },
21
21
  "author": "OpenSite AI (https://opensite.ai)",
22
- "license": "BSD 3",
22
+ "license": "BSD-3-Clause",
23
23
  "private": false,
24
24
  "type": "module",
25
25
  "main": "dist/index.cjs",
@@ -38,175 +38,85 @@
38
38
  "require": "./dist/index.cjs",
39
39
  "types": "./dist/index.d.ts"
40
40
  },
41
- "./core": {
42
- "import": "./dist/core/index.js",
43
- "require": "./dist/core/index.cjs",
44
- "types": "./dist/core/index.d.ts"
45
- },
46
- "./hooks": {
47
- "import": "./dist/hooks/index.js",
48
- "require": "./dist/hooks/index.cjs",
49
- "types": "./dist/hooks/index.d.ts"
50
- },
51
- "./core/useBoolean": {
41
+ "./useBoolean": {
52
42
  "import": "./dist/core/useBoolean.js",
53
43
  "require": "./dist/core/useBoolean.cjs",
54
44
  "types": "./dist/core/useBoolean.d.ts"
55
45
  },
56
- "./hooks/useBoolean": {
57
- "import": "./dist/hooks/useBoolean.js",
58
- "require": "./dist/hooks/useBoolean.cjs",
59
- "types": "./dist/hooks/useBoolean.d.ts"
60
- },
61
- "./core/useDebounceValue": {
62
- "import": "./dist/core/useDebounceValue.js",
63
- "require": "./dist/core/useDebounceValue.cjs",
64
- "types": "./dist/core/useDebounceValue.d.ts"
65
- },
66
- "./hooks/useDebounceValue": {
67
- "import": "./dist/hooks/useDebounceValue.js",
68
- "require": "./dist/hooks/useDebounceValue.cjs",
69
- "types": "./dist/hooks/useDebounceValue.d.ts"
46
+ "./useCopyToClipboard": {
47
+ "import": "./dist/core/useCopyToClipboard.js",
48
+ "require": "./dist/core/useCopyToClipboard.cjs",
49
+ "types": "./dist/core/useCopyToClipboard.d.ts"
70
50
  },
71
- "./core/useDebounceCallback": {
51
+ "./useDebounceCallback": {
72
52
  "import": "./dist/core/useDebounceCallback.js",
73
53
  "require": "./dist/core/useDebounceCallback.cjs",
74
54
  "types": "./dist/core/useDebounceCallback.d.ts"
75
55
  },
76
- "./hooks/useDebounceCallback": {
77
- "import": "./dist/hooks/useDebounceCallback.js",
78
- "require": "./dist/hooks/useDebounceCallback.cjs",
79
- "types": "./dist/hooks/useDebounceCallback.d.ts"
80
- },
81
- "./core/useLocalStorage": {
82
- "import": "./dist/core/useLocalStorage.js",
83
- "require": "./dist/core/useLocalStorage.cjs",
84
- "types": "./dist/core/useLocalStorage.d.ts"
85
- },
86
- "./hooks/useLocalStorage": {
87
- "import": "./dist/hooks/useLocalStorage.js",
88
- "require": "./dist/hooks/useLocalStorage.cjs",
89
- "types": "./dist/hooks/useLocalStorage.d.ts"
90
- },
91
- "./core/useSessionStorage": {
92
- "import": "./dist/core/useSessionStorage.js",
93
- "require": "./dist/core/useSessionStorage.cjs",
94
- "types": "./dist/core/useSessionStorage.d.ts"
95
- },
96
- "./hooks/useSessionStorage": {
97
- "import": "./dist/hooks/useSessionStorage.js",
98
- "require": "./dist/hooks/useSessionStorage.cjs",
99
- "types": "./dist/hooks/useSessionStorage.d.ts"
100
- },
101
- "./core/useOnClickOutside": {
102
- "import": "./dist/core/useOnClickOutside.js",
103
- "require": "./dist/core/useOnClickOutside.cjs",
104
- "types": "./dist/core/useOnClickOutside.d.ts"
105
- },
106
- "./hooks/useOnClickOutside": {
107
- "import": "./dist/hooks/useOnClickOutside.js",
108
- "require": "./dist/hooks/useOnClickOutside.cjs",
109
- "types": "./dist/hooks/useOnClickOutside.d.ts"
110
- },
111
- "./core/useMediaQuery": {
112
- "import": "./dist/core/useMediaQuery.js",
113
- "require": "./dist/core/useMediaQuery.cjs",
114
- "types": "./dist/core/useMediaQuery.d.ts"
115
- },
116
- "./hooks/useMediaQuery": {
117
- "import": "./dist/hooks/useMediaQuery.js",
118
- "require": "./dist/hooks/useMediaQuery.cjs",
119
- "types": "./dist/hooks/useMediaQuery.d.ts"
56
+ "./useDebounceValue": {
57
+ "import": "./dist/core/useDebounceValue.js",
58
+ "require": "./dist/core/useDebounceValue.cjs",
59
+ "types": "./dist/core/useDebounceValue.d.ts"
120
60
  },
121
- "./core/useEventListener": {
61
+ "./useEventListener": {
122
62
  "import": "./dist/core/useEventListener.js",
123
63
  "require": "./dist/core/useEventListener.cjs",
124
64
  "types": "./dist/core/useEventListener.d.ts"
125
65
  },
126
- "./hooks/useEventListener": {
127
- "import": "./dist/hooks/useEventListener.js",
128
- "require": "./dist/hooks/useEventListener.cjs",
129
- "types": "./dist/hooks/useEventListener.d.ts"
66
+ "./useHover": {
67
+ "import": "./dist/core/useHover.js",
68
+ "require": "./dist/core/useHover.cjs",
69
+ "types": "./dist/core/useHover.d.ts"
130
70
  },
131
- "./core/useIsClient": {
71
+ "./useIsClient": {
132
72
  "import": "./dist/core/useIsClient.js",
133
73
  "require": "./dist/core/useIsClient.cjs",
134
74
  "types": "./dist/core/useIsClient.d.ts"
135
75
  },
136
- "./hooks/useIsClient": {
137
- "import": "./dist/hooks/useIsClient.js",
138
- "require": "./dist/hooks/useIsClient.cjs",
139
- "types": "./dist/hooks/useIsClient.d.ts"
76
+ "./useIsomorphicLayoutEffect": {
77
+ "import": "./dist/core/useIsomorphicLayoutEffect.js",
78
+ "require": "./dist/core/useIsomorphicLayoutEffect.cjs",
79
+ "types": "./dist/core/useIsomorphicLayoutEffect.d.ts"
140
80
  },
141
- "./core/useCopyToClipboard": {
142
- "import": "./dist/core/useCopyToClipboard.js",
143
- "require": "./dist/core/useCopyToClipboard.cjs",
144
- "types": "./dist/core/useCopyToClipboard.d.ts"
81
+ "./useLocalStorage": {
82
+ "import": "./dist/core/useLocalStorage.js",
83
+ "require": "./dist/core/useLocalStorage.cjs",
84
+ "types": "./dist/core/useLocalStorage.d.ts"
145
85
  },
146
- "./hooks/useCopyToClipboard": {
147
- "import": "./dist/hooks/useCopyToClipboard.js",
148
- "require": "./dist/hooks/useCopyToClipboard.cjs",
149
- "types": "./dist/hooks/useCopyToClipboard.d.ts"
86
+ "./useMap": {
87
+ "import": "./dist/core/useMap.js",
88
+ "require": "./dist/core/useMap.cjs",
89
+ "types": "./dist/core/useMap.d.ts"
90
+ },
91
+ "./useMediaQuery": {
92
+ "import": "./dist/core/useMediaQuery.js",
93
+ "require": "./dist/core/useMediaQuery.cjs",
94
+ "types": "./dist/core/useMediaQuery.d.ts"
150
95
  },
151
- "./core/usePrevious": {
96
+ "./useOnClickOutside": {
97
+ "import": "./dist/core/useOnClickOutside.js",
98
+ "require": "./dist/core/useOnClickOutside.cjs",
99
+ "types": "./dist/core/useOnClickOutside.d.ts"
100
+ },
101
+ "./usePrevious": {
152
102
  "import": "./dist/core/usePrevious.js",
153
103
  "require": "./dist/core/usePrevious.cjs",
154
104
  "types": "./dist/core/usePrevious.d.ts"
155
105
  },
156
- "./hooks/usePrevious": {
157
- "import": "./dist/hooks/usePrevious.js",
158
- "require": "./dist/hooks/usePrevious.cjs",
159
- "types": "./dist/hooks/usePrevious.d.ts"
160
- },
161
- "./core/useThrottle": {
162
- "import": "./dist/core/useThrottle.js",
163
- "require": "./dist/core/useThrottle.cjs",
164
- "types": "./dist/core/useThrottle.d.ts"
165
- },
166
- "./hooks/useThrottle": {
167
- "import": "./dist/hooks/useThrottle.js",
168
- "require": "./dist/hooks/useThrottle.cjs",
169
- "types": "./dist/hooks/useThrottle.d.ts"
170
- },
171
- "./core/useResizeObserver": {
106
+ "./useResizeObserver": {
172
107
  "import": "./dist/core/useResizeObserver.js",
173
108
  "require": "./dist/core/useResizeObserver.cjs",
174
109
  "types": "./dist/core/useResizeObserver.d.ts"
175
110
  },
176
- "./hooks/useResizeObserver": {
177
- "import": "./dist/hooks/useResizeObserver.js",
178
- "require": "./dist/hooks/useResizeObserver.cjs",
179
- "types": "./dist/hooks/useResizeObserver.d.ts"
180
- },
181
- "./core/useHover": {
182
- "import": "./dist/core/useHover.js",
183
- "require": "./dist/core/useHover.cjs",
184
- "types": "./dist/core/useHover.d.ts"
185
- },
186
- "./hooks/useHover": {
187
- "import": "./dist/hooks/useHover.js",
188
- "require": "./dist/hooks/useHover.cjs",
189
- "types": "./dist/hooks/useHover.d.ts"
190
- },
191
- "./core/useIsomorphicLayoutEffect": {
192
- "import": "./dist/core/useIsomorphicLayoutEffect.js",
193
- "require": "./dist/core/useIsomorphicLayoutEffect.cjs",
194
- "types": "./dist/core/useIsomorphicLayoutEffect.d.ts"
195
- },
196
- "./hooks/useIsomorphicLayoutEffect": {
197
- "import": "./dist/hooks/useIsomorphicLayoutEffect.js",
198
- "require": "./dist/hooks/useIsomorphicLayoutEffect.cjs",
199
- "types": "./dist/hooks/useIsomorphicLayoutEffect.d.ts"
200
- },
201
- "./core/useMap": {
202
- "import": "./dist/core/useMap.js",
203
- "require": "./dist/core/useMap.cjs",
204
- "types": "./dist/core/useMap.d.ts"
111
+ "./useSessionStorage": {
112
+ "import": "./dist/core/useSessionStorage.js",
113
+ "require": "./dist/core/useSessionStorage.cjs",
114
+ "types": "./dist/core/useSessionStorage.d.ts"
205
115
  },
206
- "./hooks/useMap": {
207
- "import": "./dist/hooks/useMap.js",
208
- "require": "./dist/hooks/useMap.cjs",
209
- "types": "./dist/hooks/useMap.d.ts"
116
+ "./useThrottle": {
117
+ "import": "./dist/core/useThrottle.js",
118
+ "require": "./dist/core/useThrottle.cjs",
119
+ "types": "./dist/core/useThrottle.d.ts"
210
120
  }
211
121
  },
212
122
  "scripts": {
@@ -217,6 +127,9 @@
217
127
  "clean": "rimraf dist",
218
128
  "lint": "eslint src --ext .ts,.tsx",
219
129
  "test": "vitest run",
130
+ "test:watch": "vitest",
131
+ "test:coverage": "vitest run --coverage",
132
+ "test:ui": "vitest --ui",
220
133
  "bundle-analysis": "node scripts/analyze-bundle.js || true",
221
134
  "prepare": "husky",
222
135
  "prepack": "pnpm run build",
@@ -229,21 +142,24 @@
229
142
  "devDependencies": {
230
143
  "@commitlint/cli": "^20.1.0",
231
144
  "@commitlint/config-conventional": "^20.0.0",
145
+ "@testing-library/react": "^16.3.1",
232
146
  "@types/node": "^20.17.6",
233
147
  "@types/react": "^18.3.3",
234
148
  "@types/react-dom": "^18.3.0",
235
149
  "@typescript-eslint/eslint-plugin": "^8.46.0",
236
150
  "@typescript-eslint/parser": "^8.46.0",
237
151
  "@vitejs/plugin-react": "^4.2.1",
152
+ "@vitest/coverage-v8": "^3.2.4",
238
153
  "eslint": "^9.37.0",
239
154
  "happy-dom": "^15.11.7",
240
155
  "husky": "^9.1.7",
156
+ "react": "^19.2.3",
157
+ "react-dom": "^19.2.3",
241
158
  "terser": "^5.44.0",
242
159
  "typescript": "^5.6.2",
243
160
  "vite": "^5.4.20",
244
161
  "vitest": "^3.2.4"
245
162
  },
246
- "dependencies": {},
247
163
  "packageManager": "pnpm@10.24.0",
248
164
  "engines": {
249
165
  "node": ">=18.0.0",