@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 +187 -64
- package/dist/test/setup.cjs +98 -0
- package/dist/test/setup.d.ts +5 -0
- package/dist/test/setup.js +98 -0
- package/dist/test/utils.cjs +73 -0
- package/dist/test/utils.js +73 -0
- package/package.json +57 -141
package/README.md
CHANGED
|
@@ -1,102 +1,225 @@
|
|
|
1
1
|

|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# @opensite/hooks
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Performance-first React hooks for UI state, storage, events, and responsive behavior.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
[](https://www.npmjs.com/package/@opensite/hooks)
|
|
11
|
-
[](./LICENSE)
|
|
12
|
-
[](./tsconfig.json)
|
|
13
|
-
[](#tree-shaking)
|
|
7
|
+
[](https://www.npmjs.com/package/@opensite/hooks)
|
|
8
|
+
[](https://bundlephobia.com/package/@opensite/hooks)
|
|
9
|
+
[](./LICENSE)
|
|
14
10
|
|
|
15
11
|
## Overview
|
|
16
12
|
|
|
17
|
-
`@opensite/hooks` provides a
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
import { useBoolean, useDebounceValue } from "@opensite/hooks";
|
|
43
|
+
### Barrel Import
|
|
31
44
|
|
|
32
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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={
|
|
109
|
+
<button onClick={open}>Open Modal</button>
|
|
46
110
|
{isOpen && (
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
<button onClick={
|
|
50
|
-
</
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
142
|
+
### useMediaQuery
|
|
86
143
|
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
##
|
|
196
|
+
## Contributing
|
|
197
|
+
|
|
198
|
+
We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md) for details.
|
|
95
199
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
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,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.
|
|
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
|
|
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
|
-
"./
|
|
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
|
-
"./
|
|
57
|
-
"import": "./dist/
|
|
58
|
-
"require": "./dist/
|
|
59
|
-
"types": "./dist/
|
|
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
|
-
"./
|
|
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
|
-
"./
|
|
77
|
-
"import": "./dist/
|
|
78
|
-
"require": "./dist/
|
|
79
|
-
"types": "./dist/
|
|
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
|
-
"./
|
|
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
|
-
"./
|
|
127
|
-
"import": "./dist/
|
|
128
|
-
"require": "./dist/
|
|
129
|
-
"types": "./dist/
|
|
66
|
+
"./useHover": {
|
|
67
|
+
"import": "./dist/core/useHover.js",
|
|
68
|
+
"require": "./dist/core/useHover.cjs",
|
|
69
|
+
"types": "./dist/core/useHover.d.ts"
|
|
130
70
|
},
|
|
131
|
-
"./
|
|
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
|
-
"./
|
|
137
|
-
"import": "./dist/
|
|
138
|
-
"require": "./dist/
|
|
139
|
-
"types": "./dist/
|
|
76
|
+
"./useIsomorphicLayoutEffect": {
|
|
77
|
+
"import": "./dist/core/useIsomorphicLayoutEffect.js",
|
|
78
|
+
"require": "./dist/core/useIsomorphicLayoutEffect.cjs",
|
|
79
|
+
"types": "./dist/core/useIsomorphicLayoutEffect.d.ts"
|
|
140
80
|
},
|
|
141
|
-
"./
|
|
142
|
-
"import": "./dist/core/
|
|
143
|
-
"require": "./dist/core/
|
|
144
|
-
"types": "./dist/core/
|
|
81
|
+
"./useLocalStorage": {
|
|
82
|
+
"import": "./dist/core/useLocalStorage.js",
|
|
83
|
+
"require": "./dist/core/useLocalStorage.cjs",
|
|
84
|
+
"types": "./dist/core/useLocalStorage.d.ts"
|
|
145
85
|
},
|
|
146
|
-
"./
|
|
147
|
-
"import": "./dist/
|
|
148
|
-
"require": "./dist/
|
|
149
|
-
"types": "./dist/
|
|
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
|
-
"./
|
|
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
|
-
"./
|
|
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
|
-
"./
|
|
177
|
-
"import": "./dist/
|
|
178
|
-
"require": "./dist/
|
|
179
|
-
"types": "./dist/
|
|
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
|
-
"./
|
|
207
|
-
"import": "./dist/
|
|
208
|
-
"require": "./dist/
|
|
209
|
-
"types": "./dist/
|
|
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",
|