@jbpark/use-hooks 4.0.1 → 4.1.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.ko.md CHANGED
@@ -7,7 +7,9 @@
7
7
  [![GitHub issues](https://img.shields.io/github/issues/pjb0811/use-hooks)](https://github.com/pjb0811/use-hooks/issues)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
9
 
10
- 일반적인 UI 및 상호작용 패턴을 위한 재사용 가능한 React 19 훅 모음입니다. TypeScript와 Vite로 빌드되었으며, 서버 사이드 렌더링과 클라이언트 사이드 애플리케이션 모두에 최적화되어 있습니다.
10
+ 📖 **문서 및 라이브 데모:** https://use-hooks-lab.vercel.app
11
+
12
+ 일반적인 UI 및 상호작용 패턴을 위한 재사용 가능한 React 19 훅 모음입니다. TypeScript로 빌드되었으며, 서버 사이드 렌더링과 클라이언트 사이드 애플리케이션 모두에 최적화되어 있습니다.
11
13
 
12
14
  ## 기능
13
15
 
@@ -80,7 +82,7 @@ function MyComponent() {
80
82
  | `useElementPosition` | 스크롤/리사이즈 시 요소의 바운딩 렉트 모니터링 (요소 참조 지원) |
81
83
  | `useResponsiveSize` | Tailwind 유사 브레이크포인트를 포함한 요소 크기 추적 (debounce) |
82
84
  | `useViewport` | visualViewport 지원, 인앱 모드 옵션, debounce 포함 |
83
- | `useScrollToElements` | 키로 요소를 등록하고 키로 스크롤 (오프셋 조절, 컨테이너 지정 가능) |
85
+ | `useScrollToElements` | 키로 요소를 등록하고 키로 스크롤 (오프셋 조절, 가장 가까운 스크롤 조상을 스크롤) |
84
86
  | `useBodyScrollLock` | 스타일 보존을 포함한 바디 스크롤 잠금/해제 (iOS 특별 처리) |
85
87
  | `useIntersectionObserver` | 뷰포트 교차 추적, `[ref, { entry, isIntersecting }]` 반환, `freezeOnceVisible` 옵션 |
86
88
  | `useResizeObserver` | 콜백 ref로 요소 자체의 너비/높이 추적 (content-box/border-box) |
@@ -99,6 +101,7 @@ function MyComponent() {
99
101
  | `useRecursiveTimeout` | 비동기/동기 콜백을 재귀적으로 스케줄링 |
100
102
  | `useMergedRef` | 여러 객체/콜백 ref를 하나의 콜백 ref로 병합 |
101
103
  | `useImage` | 이미지 사전로드 및 `loading`/`error`(`Error`)/`loaded`/`attemptCount`/`retry` 노출 |
104
+ | `useMediaQuery` | `matchMedia`로 CSS 미디어 쿼리 추적 (SSR 안전, iframe/포털 창을 위한 `target` 옵션) |
102
105
 
103
106
  ## v2에서 마이그레이션
104
107
 
@@ -108,22 +111,26 @@ v3.0.0은 breaking change가 여럿 포함된 메이저 릴리스입니다. 전
108
111
  ## 개발
109
112
 
110
113
  ```bash
111
- # HMR이 포함된 개발 서버 시작
112
- pnpm dev
113
-
114
- # 라이브러리 빌드 (tsc + vite)
114
+ # 라이브러리 빌드 (tsc + tsdown)
115
115
  pnpm build
116
116
 
117
- # 빌드된 라이브러리 미리보기
118
- pnpm preview
119
-
120
117
  # 린트 및 타입 체크
121
118
  pnpm lint
122
119
 
123
120
  # prettier로 포맷팅
124
121
  pnpm exec prettier --write .
122
+
123
+ # 문서/데모 사이트 시작 (각 훅의 데모는 해당 훅의 문서 페이지에 있습니다)
124
+ pnpm --dir website start
125
+
126
+ # 문서 사이트 의존성 설치 (별도의 pnpm 프로젝트입니다)
127
+ pnpm --dir website install
125
128
  ```
126
129
 
130
+ 두 `package.json` 모두 `packageManager`에 같은 pnpm 버전을 핀하고 있어,
131
+ `website/` 안에서 pnpm을 실행해도 `PATH`에 깔린 버전이 아니라 핀된 버전이
132
+ 쓰입니다. 버전을 올릴 때는 두 곳을 함께 올립니다.
133
+
127
134
  ## 프로젝트 구조
128
135
 
129
136
  ```
@@ -149,6 +156,9 @@ src/
149
156
  └── index.ts # 패키지 진입점
150
157
 
151
158
  dist/ # 빌드된 라이브러리 (ESM + types)
159
+ website/ # Docusaurus 문서 및 데모 사이트
160
+ └── src/demo/ # 훅별 인터랙티브 데모 컴포넌트 1개씩
161
+ └── use-x-demo.tsx # 해당 훅의 문서 페이지에 그대로 렌더링됨
152
162
  ```
153
163
 
154
164
  ## 빌드 및 배포
@@ -164,10 +174,13 @@ dist/ # 빌드된 라이브러리 (ESM + types)
164
174
  - **ES Module**: `dist/index.mjs`
165
175
  - **타입 정의**: `dist/index.d.ts`
166
176
 
177
+ Docusaurus 문서 사이트는 `website/`에서 빌드되어 Vercel로 배포됩니다
178
+ (`vercel.json` 참고).
179
+
167
180
  ## 주요 패턴
168
181
 
169
182
  - **훅 1개 = 파일 1개**: 각 훅은 `src/hooks/use-x.ts` 형태의 단일 파일로 존재하며 (폴더/`index.ts` 없음), `src/hooks/index.ts`에서 재수출됩니다
170
- - **데모 파일 네이밍**: 각 훅의 데모 페이지 파일명은 훅 파일명에 `-demo`를 붙인 `src/demo/use-x-demo.tsx` 형태를 따릅니다 (컴포넌트명 자체는 기존처럼 PascalCase 유지, 예: `ClickOutsideDemo`)
183
+ - **데모 파일 네이밍**: 각 훅의 데모 페이지 파일명은 훅 파일명에 `-demo`를 붙인 `website/src/demo/use-x-demo.tsx` 형태를 따릅니다 (컴포넌트명 자체는 기존처럼 PascalCase 유지, 예: `ClickOutsideDemo`)
171
184
  - **Window 보호**: `window`/`document`에 접근하는 훅은 SSR 안전성을 위해 `typeof window` 체크 (예: `useLocalStorage`)
172
185
  - **이벤트 리스너**: 모든 스크롤/리사이즈 리스너는 가능한 한 passive 플래그 사용
173
186
  - **ResizeObserver**: `useResponsiveSize`와 `useElementPosition`에서 사용하여 성능 최적화
package/README.md CHANGED
@@ -7,7 +7,9 @@
7
7
  [![GitHub issues](https://img.shields.io/github/issues/pjb0811/use-hooks)](https://github.com/pjb0811/use-hooks/issues)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
9
 
10
- A collection of reusable React 19 hooks for common UI and interaction patterns. Built with TypeScript and Vite, optimized for both server-side rendering and client-side applications.
10
+ 📖 **Documentation & live demos:** https://use-hooks-lab.vercel.app
11
+
12
+ A collection of reusable React 19 hooks for common UI and interaction patterns. Built with TypeScript, optimized for both server-side rendering and client-side applications.
11
13
 
12
14
  ## Features
13
15
 
@@ -80,7 +82,7 @@ function MyComponent() {
80
82
  | `useElementPosition` | Monitor element bounding rect on scroll/resize (element ref support) |
81
83
  | `useResponsiveSize` | Track element size with Tailwind-like breakpoints (debounced) |
82
84
  | `useViewport` | visualViewport support with in-app mode option and debounce |
83
- | `useScrollToElements` | Register elements by key and scroll to them by key (adjustable offset, optional container) |
85
+ | `useScrollToElements` | Register elements by key and scroll to them by key (adjustable offset, scrolls the nearest scrollable ancestor) |
84
86
  | `useBodyScrollLock` | Lock/unlock body scroll with style preservation (iOS-specific handling) |
85
87
  | `useIntersectionObserver` | Track viewport intersection; returns `[ref, { entry, isIntersecting }]` with optional `freezeOnceVisible` |
86
88
  | `useResizeObserver` | Track an element's own width/height via a callback ref (content-box or border-box) |
@@ -99,6 +101,7 @@ function MyComponent() {
99
101
  | `useRecursiveTimeout` | Recursively schedule async/sync callbacks |
100
102
  | `useMergedRef` | Merge multiple object/callback refs into one callback ref |
101
103
  | `useImage` | Preload an image and expose `loading`/`error` (an `Error`)/`loaded`/`attemptCount`/`retry` |
104
+ | `useMediaQuery` | Track a CSS media query via `matchMedia` (SSR-safe, optional `target` for iframe/portal windows) |
102
105
 
103
106
  ## Migrating from v2
104
107
 
@@ -108,22 +111,26 @@ v3.0.0 is a major release with several breaking changes. See the
108
111
  ## Development
109
112
 
110
113
  ```bash
111
- # Start development server with HMR
112
- pnpm dev
113
-
114
- # Build library (tsc + vite)
114
+ # Build library (tsc + tsdown)
115
115
  pnpm build
116
116
 
117
- # Preview built library
118
- pnpm preview
119
-
120
117
  # Run lint and type check
121
118
  pnpm lint
122
119
 
123
120
  # Format code with prettier
124
121
  pnpm exec prettier --write .
122
+
123
+ # Start the docs/demo site (each hook's demo lives on its docs page)
124
+ pnpm --dir website start
125
+
126
+ # Install the docs site's own dependencies (separate pnpm project)
127
+ pnpm --dir website install
125
128
  ```
126
129
 
130
+ Both `package.json` files pin `packageManager` to the same pnpm version, so
131
+ running pnpm inside `website/` picks up that version rather than whatever is
132
+ on `PATH`. Bump the two together.
133
+
127
134
  ## Project Structure
128
135
 
129
136
  ```
@@ -131,7 +138,7 @@ src/
131
138
  ├── hooks/ # Individual hook implementations (one file per hook)
132
139
  │ ├── use-body-scroll-lock.ts
133
140
  │ ├── use-click-outside.ts
134
- │ ├── use-debounce.ts
141
+ │ ├── use-debounced-value.ts
135
142
  │ ├── use-element-position.ts
136
143
  │ ├── use-element-scroll.ts
137
144
  │ ├── use-history-state.ts
@@ -142,13 +149,16 @@ src/
142
149
  │ ├── use-recursive-timeout.ts
143
150
  │ ├── use-responsive-size.ts
144
151
  │ ├── use-scroll-to-elements.ts
145
- │ ├── use-throttle.ts
152
+ │ ├── use-throttled-value.ts
146
153
  │ ├── use-viewport.ts
147
154
  │ ├── use-window-scroll.ts
148
155
  │ └── index.ts # Barrel export
149
156
  └── index.ts # Package entry point
150
157
 
151
158
  dist/ # Built library (ESM + types)
159
+ website/ # Docusaurus documentation & demo site
160
+ └── src/demo/ # One interactive demo component per hook
161
+ └── use-x-demo.tsx # Rendered directly on that hook's docs page
152
162
  ```
153
163
 
154
164
  ## Build & Deployment
@@ -164,10 +174,13 @@ The library is built as:
164
174
  - **ES Module**: `dist/index.mjs`
165
175
  - **Type Definitions**: `dist/index.d.ts`
166
176
 
177
+ The Docusaurus documentation site is built from `website/` and deployed via
178
+ Vercel (see `vercel.json`).
179
+
167
180
  ## Key Patterns
168
181
 
169
182
  - **One File Per Hook**: Each hook lives in a single flat file at `src/hooks/use-x.ts` (no per-hook folder/`index.ts`) and is re-exported from `src/hooks/index.ts`
170
- - **Demo File Naming**: Each hook's demo page file is named `src/demo/use-x-demo.tsx`, matching the hook's own filename with a `-demo` suffix (the component name itself stays PascalCase, e.g. `ClickOutsideDemo`)
183
+ - **Demo File Naming**: Each hook's demo page file is named `website/src/demo/use-x-demo.tsx`, matching the hook's own filename with a `-demo` suffix (the component name itself stays PascalCase, e.g. `ClickOutsideDemo`)
171
184
  - **Window Protection**: Hooks accessing `window`/`document` check `typeof window` for SSR safety (e.g., `useLocalStorage`)
172
185
  - **Event Listeners**: All scroll/resize listeners use passive flag when possible
173
186
  - **ResizeObserver**: Used in `useResponsiveSize` and `useElementPosition` for performance
@@ -1,4 +1,4 @@
1
- import useDebouncedCallback from "./use-debounced-callback.mjs";
1
+ import useDebouncedCallback, { DebouncedCallback } from "./use-debounced-callback.mjs";
2
2
  import useDebouncedValue from "./use-debounced-value.mjs";
3
3
  import useBodyScrollLock from "./use-body-scroll-lock.mjs";
4
4
  import useClickOutside from "./use-click-outside.mjs";
@@ -15,6 +15,7 @@ import useResponsiveSize from "./use-responsive-size.mjs";
15
15
  import useImage from "./use-image.mjs";
16
16
  import useKeyPress from "./use-key-press.mjs";
17
17
  import useLocalStorage from "./use-local-storage.mjs";
18
+ import useMediaQuery from "./use-media-query.mjs";
18
19
  import useMergedRef from "./use-merged-ref.mjs";
19
20
  import useMultiSelect from "./use-multi-select.mjs";
20
21
  import useMutationObserver from "./use-mutation-observer.mjs";
@@ -28,4 +29,4 @@ import useTimeout from "./use-timeout.mjs";
28
29
  import useToggle from "./use-toggle.mjs";
29
30
  import useWindowScroll from "./use-window-scroll.mjs";
30
31
  import useViewport from "./use-viewport.mjs";
31
- export { useBodyScrollLock, useClickOutside, useControllableState, useDebouncedCallback as useDebounce, useDebouncedCallback, useDebouncedValue, useElementPosition, useElementScroll, useEventListener, useFileDrop, useFileToDataUrl, useHistoryState, useImage, useIntersectionObserver, useInterval, useKeyPress, useLocalStorage, useMergedRef, useMultiSelect, useMutationObserver, usePrevious, useRecursiveTimeout, useResizeObserver, useResponsiveSize, useScrollToElements, useThrottledValue as useThrottle, useThrottledCallback, useThrottledValue, useTimeout, useToggle, useViewport, useWindowScroll };
32
+ export { type DebouncedCallback, useBodyScrollLock, useClickOutside, useControllableState, useDebouncedCallback as useDebounce, useDebouncedCallback, useDebouncedValue, useElementPosition, useElementScroll, useEventListener, useFileDrop, useFileToDataUrl, useHistoryState, useImage, useIntersectionObserver, useInterval, useKeyPress, useLocalStorage, useMediaQuery, useMergedRef, useMultiSelect, useMutationObserver, usePrevious, useRecursiveTimeout, useResizeObserver, useResponsiveSize, useScrollToElements, useThrottledValue as useThrottle, useThrottledCallback, useThrottledValue, useTimeout, useToggle, useViewport, useWindowScroll };
@@ -15,6 +15,7 @@ import useResponsiveSize from "./use-responsive-size.mjs";
15
15
  import useImage from "./use-image.mjs";
16
16
  import useKeyPress from "./use-key-press.mjs";
17
17
  import useLocalStorage from "./use-local-storage.mjs";
18
+ import useMediaQuery from "./use-media-query.mjs";
18
19
  import useMergedRef from "./use-merged-ref.mjs";
19
20
  import useMultiSelect from "./use-multi-select.mjs";
20
21
  import useMutationObserver from "./use-mutation-observer.mjs";
@@ -28,4 +29,4 @@ import useTimeout from "./use-timeout.mjs";
28
29
  import useToggle from "./use-toggle.mjs";
29
30
  import useWindowScroll from "./use-window-scroll.mjs";
30
31
  import useViewport from "./use-viewport.mjs";
31
- export { useBodyScrollLock, useClickOutside, useControllableState, useDebouncedCallback as useDebounce, useDebouncedCallback, useDebouncedValue, useElementPosition, useElementScroll, useEventListener, useFileDrop, useFileToDataUrl, useHistoryState, useImage, useIntersectionObserver, useInterval, useKeyPress, useLocalStorage, useMergedRef, useMultiSelect, useMutationObserver, usePrevious, useRecursiveTimeout, useResizeObserver, useResponsiveSize, useScrollToElements, useThrottledValue as useThrottle, useThrottledCallback, useThrottledValue, useTimeout, useToggle, useViewport, useWindowScroll };
32
+ export { useBodyScrollLock, useClickOutside, useControllableState, useDebouncedCallback as useDebounce, useDebouncedCallback, useDebouncedValue, useElementPosition, useElementScroll, useEventListener, useFileDrop, useFileToDataUrl, useHistoryState, useImage, useIntersectionObserver, useInterval, useKeyPress, useLocalStorage, useMediaQuery, useMergedRef, useMultiSelect, useMutationObserver, usePrevious, useRecursiveTimeout, useResizeObserver, useResponsiveSize, useScrollToElements, useThrottledValue as useThrottle, useThrottledCallback, useThrottledValue, useTimeout, useToggle, useViewport, useWindowScroll };
@@ -17,7 +17,6 @@ const lockScroll = (token) => {
17
17
  width: document.documentElement.style.width
18
18
  },
19
19
  body: {
20
- overflow: document.body.style.overflow,
21
20
  height: document.body.style.height,
22
21
  position: document.body.style.position,
23
22
  top: document.body.style.top,
@@ -32,7 +31,6 @@ const lockScroll = (token) => {
32
31
  document.documentElement.style.height = "100%";
33
32
  document.documentElement.style.position = "fixed";
34
33
  document.documentElement.style.width = "100%";
35
- document.body.style.overflow = "hidden";
36
34
  document.body.style.height = "100%";
37
35
  document.body.style.position = "fixed";
38
36
  document.body.style.top = `-${originalScrollY}px`;
@@ -51,7 +49,6 @@ const unlockScroll = (token) => {
51
49
  document.documentElement.style.height = originalStyles.documentElement.height;
52
50
  document.documentElement.style.position = originalStyles.documentElement.position;
53
51
  document.documentElement.style.width = originalStyles.documentElement.width;
54
- document.body.style.overflow = originalStyles.body.overflow;
55
52
  document.body.style.height = originalStyles.body.height;
56
53
  document.body.style.position = originalStyles.body.position;
57
54
  document.body.style.top = originalStyles.body.top;
@@ -1 +1 @@
1
- {"version":3,"file":"use-body-scroll-lock.mjs","names":[],"sources":["../../src/hooks/use-body-scroll-lock.ts"],"sourcesContent":["import { useEffect } from 'react';\n\ninterface LockedStyles {\n documentElement: {\n overflow: string;\n height: string;\n position: string;\n width: string;\n };\n body: {\n overflow: string;\n height: string;\n position: string;\n top: string;\n left: string;\n right: string;\n width: string;\n paddingRight: string;\n webkitOverflowScrolling: string;\n };\n}\n\n// A Set of per-instance tokens instead of a raw counter — deleting the same\n// token twice (or locking with a token that's already in the set) can't\n// drift the count the way increment/decrement pairs could.\nconst activeLocks = new Set<symbol>();\nlet originalStyles: LockedStyles | null = null;\nlet originalScrollY = 0;\n\nconst isIOS = () =>\n typeof navigator !== 'undefined' &&\n /iPad|iPhone|iPod/.test(navigator.userAgent);\n\nconst lockScroll = (token: symbol) => {\n if (activeLocks.size === 0) {\n originalScrollY = window.scrollY;\n const scrollbarWidth =\n window.innerWidth - document.documentElement.clientWidth;\n const currentPaddingRight =\n parseFloat(getComputedStyle(document.body).paddingRight) || 0;\n\n originalStyles = {\n documentElement: {\n overflow: document.documentElement.style.overflow,\n height: document.documentElement.style.height,\n position: document.documentElement.style.position,\n width: document.documentElement.style.width,\n },\n body: {\n overflow: document.body.style.overflow,\n height: document.body.style.height,\n position: document.body.style.position,\n top: document.body.style.top,\n left: document.body.style.left,\n right: document.body.style.right,\n width: document.body.style.width,\n paddingRight: document.body.style.paddingRight,\n webkitOverflowScrolling: document.body.style.getPropertyValue(\n '-webkit-overflow-scrolling',\n ),\n },\n };\n\n document.documentElement.style.overflow = 'hidden';\n document.documentElement.style.height = '100%';\n document.documentElement.style.position = 'fixed';\n document.documentElement.style.width = '100%';\n\n document.body.style.overflow = 'hidden';\n document.body.style.height = '100%';\n document.body.style.position = 'fixed';\n document.body.style.top = `-${originalScrollY}px`;\n document.body.style.left = '0';\n document.body.style.right = '0';\n document.body.style.width = '100%';\n\n if (scrollbarWidth > 0) {\n document.body.style.paddingRight = `${currentPaddingRight + scrollbarWidth}px`;\n }\n\n if (isIOS()) {\n document.body.style.setProperty('-webkit-overflow-scrolling', 'touch');\n }\n }\n\n activeLocks.add(token);\n};\n\nconst unlockScroll = (token: symbol) => {\n activeLocks.delete(token);\n\n if (activeLocks.size > 0 || !originalStyles) {\n return;\n }\n\n document.documentElement.style.overflow =\n originalStyles.documentElement.overflow;\n document.documentElement.style.height = originalStyles.documentElement.height;\n document.documentElement.style.position =\n originalStyles.documentElement.position;\n document.documentElement.style.width = originalStyles.documentElement.width;\n\n document.body.style.overflow = originalStyles.body.overflow;\n document.body.style.height = originalStyles.body.height;\n document.body.style.position = originalStyles.body.position;\n document.body.style.top = originalStyles.body.top;\n document.body.style.left = originalStyles.body.left;\n document.body.style.right = originalStyles.body.right;\n document.body.style.width = originalStyles.body.width;\n document.body.style.paddingRight = originalStyles.body.paddingRight;\n\n if (originalStyles.body.webkitOverflowScrolling) {\n document.body.style.setProperty(\n '-webkit-overflow-scrolling',\n originalStyles.body.webkitOverflowScrolling,\n );\n } else {\n document.body.style.removeProperty('-webkit-overflow-scrolling');\n }\n\n window.scrollTo(0, originalScrollY);\n originalStyles = null;\n};\n\nconst useBodyScrollLock = (enabled: boolean = true) => {\n useEffect(() => {\n if (typeof window === 'undefined' || !enabled) {\n return;\n }\n\n const token = Symbol('body-scroll-lock');\n lockScroll(token);\n\n // The position:fixed styles above are the real lock — this is only a\n // narrow safety net for iOS Safari, which can still rubber-band the\n // page on a touchmove that starts directly on the body/html background.\n // Only that one event is handled (no capture, no stopPropagation), so\n // a scrollable element inside the locked content (e.g. a modal body)\n // keeps scrolling normally — its touchmove target isn't body/html.\n let removeTouchListener: (() => void) | undefined;\n\n if (isIOS()) {\n const preventTouchMove = (event: TouchEvent) => {\n if (\n event.target === document.body ||\n event.target === document.documentElement\n ) {\n event.preventDefault();\n }\n };\n\n document.addEventListener('touchmove', preventTouchMove, {\n passive: false,\n });\n\n removeTouchListener = () => {\n document.removeEventListener('touchmove', preventTouchMove);\n };\n }\n\n return () => {\n removeTouchListener?.();\n unlockScroll(token);\n };\n }, [enabled]);\n};\n\nexport default useBodyScrollLock;\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-body-scroll-lock.mjs","names":[],"sources":["../../src/hooks/use-body-scroll-lock.ts"],"sourcesContent":["import { useEffect } from 'react';\n\ninterface LockedStyles {\n documentElement: {\n overflow: string;\n height: string;\n position: string;\n width: string;\n };\n body: {\n height: string;\n position: string;\n top: string;\n left: string;\n right: string;\n width: string;\n paddingRight: string;\n webkitOverflowScrolling: string;\n };\n}\n\n// A Set of per-instance tokens instead of a raw counter — deleting the same\n// token twice (or locking with a token that's already in the set) can't\n// drift the count the way increment/decrement pairs could.\nconst activeLocks = new Set<symbol>();\nlet originalStyles: LockedStyles | null = null;\nlet originalScrollY = 0;\n\nconst isIOS = () =>\n typeof navigator !== 'undefined' &&\n /iPad|iPhone|iPod/.test(navigator.userAgent);\n\nconst lockScroll = (token: symbol) => {\n if (activeLocks.size === 0) {\n originalScrollY = window.scrollY;\n const scrollbarWidth =\n window.innerWidth - document.documentElement.clientWidth;\n const currentPaddingRight =\n parseFloat(getComputedStyle(document.body).paddingRight) || 0;\n\n originalStyles = {\n documentElement: {\n overflow: document.documentElement.style.overflow,\n height: document.documentElement.style.height,\n position: document.documentElement.style.position,\n width: document.documentElement.style.width,\n },\n body: {\n height: document.body.style.height,\n position: document.body.style.position,\n top: document.body.style.top,\n left: document.body.style.left,\n right: document.body.style.right,\n width: document.body.style.width,\n paddingRight: document.body.style.paddingRight,\n webkitOverflowScrolling: document.body.style.getPropertyValue(\n '-webkit-overflow-scrolling',\n ),\n },\n };\n\n document.documentElement.style.overflow = 'hidden';\n document.documentElement.style.height = '100%';\n document.documentElement.style.position = 'fixed';\n document.documentElement.style.width = '100%';\n\n // `overflow: hidden` belongs on the root element only. Setting it on the\n // body as well clips the body's own box — and that box is shifted up by\n // `top: -scrollY`, so everything the user was looking at gets clipped out\n // of view and the page goes blank behind the modal. The overlay survives\n // because a `position: fixed` element isn't clipped by an ancestor's\n // overflow, which makes the failure look like an opaque backdrop rather\n // than a rendering bug.\n document.body.style.height = '100%';\n document.body.style.position = 'fixed';\n document.body.style.top = `-${originalScrollY}px`;\n document.body.style.left = '0';\n document.body.style.right = '0';\n document.body.style.width = '100%';\n\n if (scrollbarWidth > 0) {\n document.body.style.paddingRight = `${currentPaddingRight + scrollbarWidth}px`;\n }\n\n if (isIOS()) {\n document.body.style.setProperty('-webkit-overflow-scrolling', 'touch');\n }\n }\n\n activeLocks.add(token);\n};\n\nconst unlockScroll = (token: symbol) => {\n activeLocks.delete(token);\n\n if (activeLocks.size > 0 || !originalStyles) {\n return;\n }\n\n document.documentElement.style.overflow =\n originalStyles.documentElement.overflow;\n document.documentElement.style.height = originalStyles.documentElement.height;\n document.documentElement.style.position =\n originalStyles.documentElement.position;\n document.documentElement.style.width = originalStyles.documentElement.width;\n\n document.body.style.height = originalStyles.body.height;\n document.body.style.position = originalStyles.body.position;\n document.body.style.top = originalStyles.body.top;\n document.body.style.left = originalStyles.body.left;\n document.body.style.right = originalStyles.body.right;\n document.body.style.width = originalStyles.body.width;\n document.body.style.paddingRight = originalStyles.body.paddingRight;\n\n if (originalStyles.body.webkitOverflowScrolling) {\n document.body.style.setProperty(\n '-webkit-overflow-scrolling',\n originalStyles.body.webkitOverflowScrolling,\n );\n } else {\n document.body.style.removeProperty('-webkit-overflow-scrolling');\n }\n\n window.scrollTo(0, originalScrollY);\n originalStyles = null;\n};\n\nconst useBodyScrollLock = (enabled: boolean = true) => {\n useEffect(() => {\n if (typeof window === 'undefined' || !enabled) {\n return;\n }\n\n const token = Symbol('body-scroll-lock');\n lockScroll(token);\n\n // The position:fixed styles above are the real lock — this is only a\n // narrow safety net for iOS Safari, which can still rubber-band the\n // page on a touchmove that starts directly on the body/html background.\n // Only that one event is handled (no capture, no stopPropagation), so\n // a scrollable element inside the locked content (e.g. a modal body)\n // keeps scrolling normally — its touchmove target isn't body/html.\n let removeTouchListener: (() => void) | undefined;\n\n if (isIOS()) {\n const preventTouchMove = (event: TouchEvent) => {\n if (\n event.target === document.body ||\n event.target === document.documentElement\n ) {\n event.preventDefault();\n }\n };\n\n document.addEventListener('touchmove', preventTouchMove, {\n passive: false,\n });\n\n removeTouchListener = () => {\n document.removeEventListener('touchmove', preventTouchMove);\n };\n }\n\n return () => {\n removeTouchListener?.();\n unlockScroll(token);\n };\n }, [enabled]);\n};\n\nexport default useBodyScrollLock;\n"],"mappings":"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"}
@@ -3,8 +3,14 @@ interface Options {
3
3
  delay?: number;
4
4
  autoInvoke?: boolean;
5
5
  leading?: boolean;
6
+ flushOnUnmount?: boolean;
6
7
  }
7
- declare const useDebouncedCallback: (callback: () => unknown, { delay, autoInvoke, leading }: Options, deps?: React.DependencyList) => () => void;
8
+ interface DebouncedCallback {
9
+ (): void;
10
+ flush: () => void;
11
+ cancel: () => void;
12
+ }
13
+ declare const useDebouncedCallback: (callback: () => unknown, { delay, autoInvoke, leading, flushOnUnmount }: Options, deps?: React.DependencyList) => DebouncedCallback;
8
14
  //#endregion
9
- export { useDebouncedCallback as default };
15
+ export { DebouncedCallback, useDebouncedCallback as default };
10
16
  //# sourceMappingURL=use-debounced-callback.d.mts.map
@@ -1,23 +1,42 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
2
  //#region src/hooks/use-debounced-callback.ts
3
- const useDebouncedCallback = (callback, { delay = 100, autoInvoke = true, leading = true }, deps = []) => {
3
+ const useDebouncedCallback = (callback, { delay = 100, autoInvoke = true, leading = true, flushOnUnmount = false }, deps = []) => {
4
4
  const timeoutRef = useRef(null);
5
5
  const callbackRef = useRef(callback);
6
6
  const delayRef = useRef(delay);
7
+ const flushOnUnmountRef = useRef(flushOnUnmount);
7
8
  const prevDeps = useRef(void 0);
8
9
  useEffect(() => {
9
10
  callbackRef.current = callback;
10
11
  delayRef.current = delay;
12
+ flushOnUnmountRef.current = flushOnUnmount;
11
13
  });
12
- const [stableDebouncedCallback] = useState(() => () => {
14
+ const [cancel] = useState(() => () => {
15
+ if (timeoutRef.current) {
16
+ clearTimeout(timeoutRef.current);
17
+ timeoutRef.current = null;
18
+ }
19
+ });
20
+ const [flush] = useState(() => () => {
21
+ if (!timeoutRef.current) return;
22
+ clearTimeout(timeoutRef.current);
23
+ timeoutRef.current = null;
24
+ callbackRef.current();
25
+ });
26
+ const [schedule] = useState(() => () => {
13
27
  if (timeoutRef.current) clearTimeout(timeoutRef.current);
14
28
  timeoutRef.current = setTimeout(() => {
29
+ timeoutRef.current = null;
15
30
  callbackRef.current();
16
31
  }, delayRef.current);
17
32
  });
33
+ const [stableDebouncedCallback] = useState(() => Object.assign(() => schedule(), {
34
+ flush,
35
+ cancel
36
+ }));
18
37
  useEffect(() => {
19
38
  const depsChanged = prevDeps.current === void 0 || prevDeps.current.length !== deps.length || prevDeps.current.some((dep, i) => dep !== deps[i]);
20
- if (depsChanged && timeoutRef.current) clearTimeout(timeoutRef.current);
39
+ if (depsChanged) stableDebouncedCallback.cancel();
21
40
  if (autoInvoke && depsChanged) {
22
41
  if (prevDeps.current === void 0 && leading) callbackRef.current();
23
42
  else stableDebouncedCallback();
@@ -26,9 +45,10 @@ const useDebouncedCallback = (callback, { delay = 100, autoInvoke = true, leadin
26
45
  });
27
46
  useEffect(() => {
28
47
  return () => {
29
- if (timeoutRef.current) clearTimeout(timeoutRef.current);
48
+ if (flushOnUnmountRef.current) stableDebouncedCallback.flush();
49
+ else stableDebouncedCallback.cancel();
30
50
  };
31
- }, []);
51
+ }, [stableDebouncedCallback]);
32
52
  return stableDebouncedCallback;
33
53
  };
34
54
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"use-debounced-callback.mjs","names":[],"sources":["../../src/hooks/use-debounced-callback.ts"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\n\ninterface Options {\n delay?: number;\n autoInvoke?: boolean;\n leading?: boolean;\n}\n\n// Auto-invokes `callback` (debounced by `delay`ms) whenever `deps` change,\n// and also returns a stable debounced version of `callback` for manual use.\n// `callback` intentionally takes no arguments — the deps-triggered\n// auto-invoke has no natural argument to supply, so an arg-taking callback\n// here was a latent type hole (it was always invoked with zero args\n// regardless of what the callback's own signature claimed).\nconst useDebouncedCallback = (\n callback: () => unknown,\n { delay = 100, autoInvoke = true, leading = true }: Options,\n deps: React.DependencyList = [],\n) => {\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const callbackRef = useRef(callback);\n // Read the latest `delay` when the timeout actually fires, instead of\n // baking in whatever `delay` was on the render that created the stable\n // debounced function (which otherwise never updates again).\n const delayRef = useRef(delay);\n const prevDeps = useRef<React.DependencyList | undefined>(undefined);\n\n useEffect(() => {\n callbackRef.current = callback;\n delayRef.current = delay;\n });\n\n // Built once via useState's lazy initializer (runs only on mount) rather\n // than the ref-guarded-by-if pattern this used to use — same \"create\n // once, keep a stable identity\" effect, but without ever reading/writing\n // a ref during render, which react-hooks/refs now disallows even for the\n // otherwise-idempotent lazy-ref-init form (see facebook/react#36896).\n const [stableDebouncedCallback] = useState(() => () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n\n timeoutRef.current = setTimeout(() => {\n callbackRef.current();\n }, delayRef.current);\n });\n\n useEffect(() => {\n const depsChanged =\n prevDeps.current === undefined ||\n prevDeps.current.length !== deps.length ||\n prevDeps.current.some((dep, i) => dep !== deps[i]);\n\n if (depsChanged && timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n\n if (autoInvoke && depsChanged) {\n const isFirstRender = prevDeps.current === undefined;\n\n // `leading` makes the \"first invocation fires immediately, without\n // waiting `delay`ms\" behavior explicit and opt-out-able, instead of\n // an unconditional special case baked into the first render.\n if (isFirstRender && leading) {\n callbackRef.current();\n } else {\n stableDebouncedCallback();\n }\n }\n\n prevDeps.current = deps;\n });\n\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n };\n }, []);\n\n return stableDebouncedCallback;\n};\n\nexport default useDebouncedCallback;\n"],"mappings":";;AAcA,MAAM,wBACJ,UACA,EAAE,QAAQ,KAAK,aAAa,MAAM,UAAU,QAC5C,OAA6B,CAAC,MAC3B;CACH,MAAM,aAAa,OAA6C,IAAI;CACpE,MAAM,cAAc,OAAO,QAAQ;CAInC,MAAM,WAAW,OAAO,KAAK;CAC7B,MAAM,WAAW,OAAyC,KAAA,CAAS;CAEnE,gBAAgB;EACd,YAAY,UAAU;EACtB,SAAS,UAAU;CACrB,CAAC;CAOD,MAAM,CAAC,2BAA2B,qBAAqB;EACrD,IAAI,WAAW,SACb,aAAa,WAAW,OAAO;EAGjC,WAAW,UAAU,iBAAiB;GACpC,YAAY,QAAQ;EACtB,GAAG,SAAS,OAAO;CACrB,CAAC;CAED,gBAAgB;EACd,MAAM,cACJ,SAAS,YAAY,KAAA,KACrB,SAAS,QAAQ,WAAW,KAAK,UACjC,SAAS,QAAQ,MAAM,KAAK,MAAM,QAAQ,KAAK,EAAE;EAEnD,IAAI,eAAe,WAAW,SAC5B,aAAa,WAAW,OAAO;EAGjC,IAAI,cAAc,aAAa;GAM7B,IALsB,SAAS,YAAY,KAAA,KAKtB,SACnB,YAAY,QAAQ;QAEpB,wBAAwB;EAE5B;EAEA,SAAS,UAAU;CACrB,CAAC;CAED,gBAAgB;EACd,aAAa;GACX,IAAI,WAAW,SACb,aAAa,WAAW,OAAO;EAEnC;CACF,GAAG,CAAC,CAAC;CAEL,OAAO;AACT"}
1
+ {"version":3,"file":"use-debounced-callback.mjs","names":[],"sources":["../../src/hooks/use-debounced-callback.ts"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\n\ninterface Options {\n delay?: number;\n autoInvoke?: boolean;\n leading?: boolean;\n // Run a call that is still waiting when the component unmounts, instead of\n // dropping it. For work that must not be lost when its UI goes away, such\n // as saving the last edit of a field that is being closed.\n flushOnUnmount?: boolean;\n}\n\n// The returned function schedules `callback`. Its methods act on a call that\n// is waiting for the delay to pass: `flush` runs it now, `cancel` drops it.\n// Both do nothing when no call is waiting.\nexport interface DebouncedCallback {\n (): void;\n flush: () => void;\n cancel: () => void;\n}\n\n// Auto-invokes `callback` (debounced by `delay`ms) whenever `deps` change,\n// and also returns a stable debounced version of `callback` for manual use.\n// `callback` intentionally takes no arguments — the deps-triggered\n// auto-invoke has no natural argument to supply, so an arg-taking callback\n// here was a latent type hole (it was always invoked with zero args\n// regardless of what the callback's own signature claimed).\nconst useDebouncedCallback = (\n callback: () => unknown,\n {\n delay = 100,\n autoInvoke = true,\n leading = true,\n flushOnUnmount = false,\n }: Options,\n deps: React.DependencyList = [],\n): DebouncedCallback => {\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const callbackRef = useRef(callback);\n // Read the latest `delay` when the timeout actually fires, instead of\n // baking in whatever `delay` was on the render that created the stable\n // debounced function (which otherwise never updates again).\n const delayRef = useRef(delay);\n const flushOnUnmountRef = useRef(flushOnUnmount);\n const prevDeps = useRef<React.DependencyList | undefined>(undefined);\n\n useEffect(() => {\n callbackRef.current = callback;\n delayRef.current = delay;\n flushOnUnmountRef.current = flushOnUnmount;\n });\n\n // Built once via useState's lazy initializer (runs only on mount) rather\n // than the ref-guarded-by-if pattern this used to use — same \"create\n // once, keep a stable identity\" effect, but without ever reading/writing\n // a ref during render, which react-hooks/refs now disallows even for the\n // otherwise-idempotent lazy-ref-init form (see facebook/react#36896).\n //\n // `timeoutRef` holding a timer is what \"a call is waiting\" means, so it is\n // cleared before the callback runs, whichever way it runs.\n const [cancel] = useState(() => () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = null;\n }\n });\n\n const [flush] = useState(() => () => {\n if (!timeoutRef.current) {\n return;\n }\n\n clearTimeout(timeoutRef.current);\n timeoutRef.current = null;\n callbackRef.current();\n });\n\n const [schedule] = useState(() => () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n\n timeoutRef.current = setTimeout(() => {\n timeoutRef.current = null;\n callbackRef.current();\n }, delayRef.current);\n });\n\n // Each piece above is its own lazy initializer that only returns a\n // function: building them inside one initializer that also calls\n // `Object.assign` reads to the refs rule as touching refs during render.\n const [stableDebouncedCallback] = useState<DebouncedCallback>(() =>\n Object.assign(() => schedule(), { flush, cancel }),\n );\n\n useEffect(() => {\n const depsChanged =\n prevDeps.current === undefined ||\n prevDeps.current.length !== deps.length ||\n prevDeps.current.some((dep, i) => dep !== deps[i]);\n\n if (depsChanged) {\n stableDebouncedCallback.cancel();\n }\n\n if (autoInvoke && depsChanged) {\n const isFirstRender = prevDeps.current === undefined;\n\n // `leading` makes the \"first invocation fires immediately, without\n // waiting `delay`ms\" behavior explicit and opt-out-able, instead of\n // an unconditional special case baked into the first render.\n if (isFirstRender && leading) {\n callbackRef.current();\n } else {\n stableDebouncedCallback();\n }\n }\n\n prevDeps.current = deps;\n });\n\n useEffect(() => {\n return () => {\n if (flushOnUnmountRef.current) {\n stableDebouncedCallback.flush();\n } else {\n stableDebouncedCallback.cancel();\n }\n };\n }, [stableDebouncedCallback]);\n\n return stableDebouncedCallback;\n};\n\nexport default useDebouncedCallback;\n"],"mappings":";;AA2BA,MAAM,wBACJ,UACA,EACE,QAAQ,KACR,aAAa,MACb,UAAU,MACV,iBAAiB,SAEnB,OAA6B,CAAC,MACR;CACtB,MAAM,aAAa,OAA6C,IAAI;CACpE,MAAM,cAAc,OAAO,QAAQ;CAInC,MAAM,WAAW,OAAO,KAAK;CAC7B,MAAM,oBAAoB,OAAO,cAAc;CAC/C,MAAM,WAAW,OAAyC,KAAA,CAAS;CAEnE,gBAAgB;EACd,YAAY,UAAU;EACtB,SAAS,UAAU;EACnB,kBAAkB,UAAU;CAC9B,CAAC;CAUD,MAAM,CAAC,UAAU,qBAAqB;EACpC,IAAI,WAAW,SAAS;GACtB,aAAa,WAAW,OAAO;GAC/B,WAAW,UAAU;EACvB;CACF,CAAC;CAED,MAAM,CAAC,SAAS,qBAAqB;EACnC,IAAI,CAAC,WAAW,SACd;EAGF,aAAa,WAAW,OAAO;EAC/B,WAAW,UAAU;EACrB,YAAY,QAAQ;CACtB,CAAC;CAED,MAAM,CAAC,YAAY,qBAAqB;EACtC,IAAI,WAAW,SACb,aAAa,WAAW,OAAO;EAGjC,WAAW,UAAU,iBAAiB;GACpC,WAAW,UAAU;GACrB,YAAY,QAAQ;EACtB,GAAG,SAAS,OAAO;CACrB,CAAC;CAKD,MAAM,CAAC,2BAA2B,eAChC,OAAO,aAAa,SAAS,GAAG;EAAE;EAAO;CAAO,CAAC,CACnD;CAEA,gBAAgB;EACd,MAAM,cACJ,SAAS,YAAY,KAAA,KACrB,SAAS,QAAQ,WAAW,KAAK,UACjC,SAAS,QAAQ,MAAM,KAAK,MAAM,QAAQ,KAAK,EAAE;EAEnD,IAAI,aACF,wBAAwB,OAAO;EAGjC,IAAI,cAAc,aAAa;GAM7B,IALsB,SAAS,YAAY,KAAA,KAKtB,SACnB,YAAY,QAAQ;QAEpB,wBAAwB;EAE5B;EAEA,SAAS,UAAU;CACrB,CAAC;CAED,gBAAgB;EACd,aAAa;GACX,IAAI,kBAAkB,SACpB,wBAAwB,MAAM;QAE9B,wBAAwB,OAAO;EAEnC;CACF,GAAG,CAAC,uBAAuB,CAAC;CAE5B,OAAO;AACT"}
@@ -0,0 +1,11 @@
1
+ import { RefObject } from "react";
2
+ //#region src/hooks/use-media-query.d.ts
3
+ type MediaQueryTarget = RefObject<Element | null> | Element | null;
4
+ interface Options {
5
+ defaultValue?: boolean;
6
+ target?: MediaQueryTarget;
7
+ }
8
+ declare const useMediaQuery: (query: string, options?: Options) => boolean;
9
+ //#endregion
10
+ export { useMediaQuery as default };
11
+ //# sourceMappingURL=use-media-query.d.mts.map
@@ -0,0 +1,32 @@
1
+ import { useCallback, useSyncExternalStore } from "react";
2
+ //#region src/hooks/use-media-query.ts
3
+ const resolveWindow = (target) => {
4
+ if (typeof window === "undefined") return null;
5
+ return (target && "current" in target ? target.current : target)?.ownerDocument.defaultView ?? window;
6
+ };
7
+ const getMediaQueryList = (query, target) => {
8
+ const view = resolveWindow(target);
9
+ return view?.matchMedia ? view.matchMedia(query) : null;
10
+ };
11
+ const useMediaQuery = (query, options = {}) => {
12
+ const { defaultValue = false, target } = options;
13
+ const subscribe = useCallback((onStoreChange) => {
14
+ const mql = getMediaQueryList(query, target);
15
+ if (!mql) return () => {};
16
+ mql.addEventListener("change", onStoreChange);
17
+ return () => {
18
+ mql.removeEventListener("change", onStoreChange);
19
+ };
20
+ }, [query, target]);
21
+ const getSnapshot = useCallback(() => getMediaQueryList(query, target)?.matches ?? defaultValue, [
22
+ query,
23
+ target,
24
+ defaultValue
25
+ ]);
26
+ const getServerSnapshot = useCallback(() => defaultValue, [defaultValue]);
27
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
28
+ };
29
+ //#endregion
30
+ export { useMediaQuery as default };
31
+
32
+ //# sourceMappingURL=use-media-query.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-media-query.mjs","names":[],"sources":["../../src/hooks/use-media-query.ts"],"sourcesContent":["import { type RefObject, useCallback, useSyncExternalStore } from 'react';\n\ntype MediaQueryTarget = RefObject<Element | null> | Element | null;\n\ninterface Options {\n // Returned while `matchMedia` is unreachable — during SSR and on the\n // hydration render, where guessing wrong only costs one corrected\n // render once the real value is readable.\n defaultValue?: boolean;\n // Pass a ref to an element rendered inside the window the query should\n // be evaluated against (e.g. a node portaled into an iframe preview,\n // whose viewport is not the host one). Defaults to the host `window`.\n target?: MediaQueryTarget;\n}\n\nconst resolveWindow = (target: MediaQueryTarget | undefined): Window | null => {\n if (typeof window === 'undefined') {\n return null;\n }\n\n const element = target && 'current' in target ? target.current : target;\n\n return element?.ownerDocument.defaultView ?? window;\n};\n\nconst getMediaQueryList = (\n query: string,\n target: MediaQueryTarget | undefined,\n) => {\n const view = resolveWindow(target);\n\n return view?.matchMedia ? view.matchMedia(query) : null;\n};\n\nconst useMediaQuery = (query: string, options: Options = {}) => {\n const { defaultValue = false, target } = options;\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n const mql = getMediaQueryList(query, target);\n\n if (!mql) {\n return () => {};\n }\n\n mql.addEventListener('change', onStoreChange);\n\n return () => {\n mql.removeEventListener('change', onStoreChange);\n };\n },\n [query, target],\n );\n\n // A fresh MediaQueryList per read is fine (and cheaper than caching one\n // per query): the snapshot handed to React is the boolean `matches`, so\n // there is no new object identity to churn re-renders.\n const getSnapshot = useCallback(\n () => getMediaQueryList(query, target)?.matches ?? defaultValue,\n [query, target, defaultValue],\n );\n\n const getServerSnapshot = useCallback(() => defaultValue, [defaultValue]);\n\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n};\n\nexport default useMediaQuery;\n"],"mappings":";;AAeA,MAAM,iBAAiB,WAAwD;CAC7E,IAAI,OAAO,WAAW,aACpB,OAAO;CAKT,QAFgB,UAAU,aAAa,SAAS,OAAO,UAAU,OAAA,EAEjD,cAAc,eAAe;AAC/C;AAEA,MAAM,qBACJ,OACA,WACG;CACH,MAAM,OAAO,cAAc,MAAM;CAEjC,OAAO,MAAM,aAAa,KAAK,WAAW,KAAK,IAAI;AACrD;AAEA,MAAM,iBAAiB,OAAe,UAAmB,CAAC,MAAM;CAC9D,MAAM,EAAE,eAAe,OAAO,WAAW;CAEzC,MAAM,YAAY,aACf,kBAA8B;EAC7B,MAAM,MAAM,kBAAkB,OAAO,MAAM;EAE3C,IAAI,CAAC,KACH,aAAa,CAAC;EAGhB,IAAI,iBAAiB,UAAU,aAAa;EAE5C,aAAa;GACX,IAAI,oBAAoB,UAAU,aAAa;EACjD;CACF,GACA,CAAC,OAAO,MAAM,CAChB;CAKA,MAAM,cAAc,kBACZ,kBAAkB,OAAO,MAAM,CAAC,EAAE,WAAW,cACnD;EAAC;EAAO;EAAQ;CAAY,CAC9B;CAEA,MAAM,oBAAoB,kBAAkB,cAAc,CAAC,YAAY,CAAC;CAExE,OAAO,qBAAqB,WAAW,aAAa,iBAAiB;AACvE"}
@@ -4,6 +4,18 @@ const resolveContainer = (container) => {
4
4
  if (!container) return null;
5
5
  return "current" in container ? container.current : container;
6
6
  };
7
+ const isScrollable = (element) => {
8
+ const { overflowY } = getComputedStyle(element);
9
+ return (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") && element.scrollHeight > element.clientHeight;
10
+ };
11
+ const findScrollableAncestor = (element) => {
12
+ let parent = element.parentElement;
13
+ while (parent && parent !== document.body && parent !== document.documentElement) {
14
+ if (isScrollable(parent)) return parent;
15
+ parent = parent.parentElement;
16
+ }
17
+ return null;
18
+ };
7
19
  const useScrollToElements = (defaultOptions) => {
8
20
  const elementsRef = useRef(/* @__PURE__ */ new Map());
9
21
  const registerCallbacksRef = useRef(/* @__PURE__ */ new Map());
@@ -31,7 +43,7 @@ const useScrollToElements = (defaultOptions) => {
31
43
  ...options
32
44
  };
33
45
  if (offset != null) {
34
- const containerElement = resolveContainer(container ?? null);
46
+ const containerElement = resolveContainer(container ?? null) ?? findScrollableAncestor(element);
35
47
  if (containerElement) {
36
48
  const containerRect = containerElement.getBoundingClientRect();
37
49
  const elementRect = element.getBoundingClientRect();
@@ -1 +1 @@
1
- {"version":3,"file":"use-scroll-to-elements.mjs","names":[],"sources":["../../src/hooks/use-scroll-to-elements.ts"],"sourcesContent":["import { type RefObject, useCallback, useEffect, useRef } from 'react';\n\ntype ElementKey = string;\n\ntype ScrollContainer = HTMLElement | RefObject<HTMLElement | null> | null;\n\ninterface Options extends ScrollIntoViewOptions {\n offset?: number;\n // Element to scroll instead of `window` when `offset` is set — e.g. a\n // modal's own scrollable body. `scrollIntoView` (the no-offset path)\n // already finds its nearest scrollable ancestor on its own and doesn't\n // need this.\n container?: ScrollContainer;\n}\n\nconst resolveContainer = (container: ScrollContainer): HTMLElement | null => {\n if (!container) {\n return null;\n }\n return 'current' in container ? container.current : container;\n};\n\nconst useScrollToElements = (defaultOptions?: Options) => {\n const elementsRef = useRef(new Map<ElementKey, HTMLElement>());\n const registerCallbacksRef = useRef(\n new Map<ElementKey, (node: HTMLElement | null) => void>(),\n );\n\n // `defaultOptions` is almost always an inline object literal at the call\n // site, so depending on it directly would recreate `scrollTo` on every\n // render — read the latest value from a ref instead.\n const defaultOptionsRef = useRef(defaultOptions);\n\n useEffect(() => {\n defaultOptionsRef.current = defaultOptions;\n });\n\n // Returns a stable callback per key so React doesn't detach/reattach the\n // ref (and thrash the Map) on every render — only a genuine unmount\n // (node === null) removes the entry.\n const register = useCallback((key: ElementKey) => {\n let callback = registerCallbacksRef.current.get(key);\n\n if (!callback) {\n callback = (node: HTMLElement | null) => {\n if (node) {\n elementsRef.current.set(key, node);\n } else {\n elementsRef.current.delete(key);\n }\n };\n registerCallbacksRef.current.set(key, callback);\n }\n\n return callback;\n }, []);\n\n const scrollTo = useCallback((key: ElementKey, options?: Options) => {\n const element = elementsRef.current.get(key);\n\n if (!element) {\n return;\n }\n\n const { offset, container, ...scrollIntoViewOptions } = {\n ...defaultOptionsRef.current,\n ...options,\n };\n\n // With an offset, `scrollIntoView` and `scrollTo` would both animate\n // the same scroll, fighting each other and settling in the wrong\n // place — use `scrollTo` alone, computed from the element's position\n // before any scrolling starts (not after `scrollIntoView`, where the\n // rect would reflect a mid-animation position).\n if (offset != null) {\n const containerElement = resolveContainer(container ?? null);\n\n if (containerElement) {\n const containerRect = containerElement.getBoundingClientRect();\n const elementRect = element.getBoundingClientRect();\n const top =\n containerElement.scrollTop +\n (elementRect.top - containerRect.top) -\n offset;\n\n containerElement.scrollTo({\n top,\n behavior: scrollIntoViewOptions.behavior || 'smooth',\n });\n return;\n }\n\n const top = element.getBoundingClientRect().top + window.scrollY - offset;\n\n window.scrollTo({\n top,\n behavior: scrollIntoViewOptions.behavior || 'smooth',\n });\n return;\n }\n\n element.scrollIntoView({\n behavior: 'smooth',\n block: 'start',\n inline: 'start',\n ...scrollIntoViewOptions,\n });\n }, []);\n\n return { register, scrollTo };\n};\n\nexport default useScrollToElements;\n"],"mappings":";;AAeA,MAAM,oBAAoB,cAAmD;CAC3E,IAAI,CAAC,WACH,OAAO;CAET,OAAO,aAAa,YAAY,UAAU,UAAU;AACtD;AAEA,MAAM,uBAAuB,mBAA6B;CACxD,MAAM,cAAc,uBAAO,IAAI,IAA6B,CAAC;CAC7D,MAAM,uBAAuB,uBAC3B,IAAI,IAAoD,CAC1D;CAKA,MAAM,oBAAoB,OAAO,cAAc;CAE/C,gBAAgB;EACd,kBAAkB,UAAU;CAC9B,CAAC;CA0ED,OAAO;EAAE,UArEQ,aAAa,QAAoB;GAChD,IAAI,WAAW,qBAAqB,QAAQ,IAAI,GAAG;GAEnD,IAAI,CAAC,UAAU;IACb,YAAY,SAA6B;KACvC,IAAI,MACF,YAAY,QAAQ,IAAI,KAAK,IAAI;UAEjC,YAAY,QAAQ,OAAO,GAAG;IAElC;IACA,qBAAqB,QAAQ,IAAI,KAAK,QAAQ;GAChD;GAEA,OAAO;EACT,GAAG,CAAC,CAsDY;EAAG,UApDF,aAAa,KAAiB,YAAsB;GACnE,MAAM,UAAU,YAAY,QAAQ,IAAI,GAAG;GAE3C,IAAI,CAAC,SACH;GAGF,MAAM,EAAE,QAAQ,WAAW,GAAG,0BAA0B;IACtD,GAAG,kBAAkB;IACrB,GAAG;GACL;GAOA,IAAI,UAAU,MAAM;IAClB,MAAM,mBAAmB,iBAAiB,aAAa,IAAI;IAE3D,IAAI,kBAAkB;KACpB,MAAM,gBAAgB,iBAAiB,sBAAsB;KAC7D,MAAM,cAAc,QAAQ,sBAAsB;KAClD,MAAM,MACJ,iBAAiB,aAChB,YAAY,MAAM,cAAc,OACjC;KAEF,iBAAiB,SAAS;MACxB;MACA,UAAU,sBAAsB,YAAY;KAC9C,CAAC;KACD;IACF;IAEA,MAAM,MAAM,QAAQ,sBAAsB,CAAC,CAAC,MAAM,OAAO,UAAU;IAEnE,OAAO,SAAS;KACd;KACA,UAAU,sBAAsB,YAAY;IAC9C,CAAC;IACD;GACF;GAEA,QAAQ,eAAe;IACrB,UAAU;IACV,OAAO;IACP,QAAQ;IACR,GAAG;GACL,CAAC;EACH,GAAG,CAAC,CAEsB;CAAE;AAC9B"}
1
+ {"version":3,"file":"use-scroll-to-elements.mjs","names":[],"sources":["../../src/hooks/use-scroll-to-elements.ts"],"sourcesContent":["import { type RefObject, useCallback, useEffect, useRef } from 'react';\n\ntype ElementKey = string;\n\ntype ScrollContainer = HTMLElement | RefObject<HTMLElement | null> | null;\n\ninterface Options extends ScrollIntoViewOptions {\n offset?: number;\n // Explicit scroll container for the `offset` path, for the cases where the\n // automatic lookup below can't pick the right one — e.g. a container that\n // isn't an ancestor of the target, or one that only becomes scrollable\n // later. Leave it unset and the nearest scrollable ancestor is used, with\n // `window` as the final fallback.\n container?: ScrollContainer;\n}\n\nconst resolveContainer = (container: ScrollContainer): HTMLElement | null => {\n if (!container) {\n return null;\n }\n return 'current' in container ? container.current : container;\n};\n\n// `overlay` is a legacy alias of `auto` that still shows up in older WebKit.\n// `hidden` is deliberately excluded: it is programmatically scrollable, but an\n// author who wrote `overflow: hidden` is describing a clipped box, not a\n// scroller, so moving it would be the surprising choice here.\nconst isScrollable = (element: HTMLElement) => {\n const { overflowY } = getComputedStyle(element);\n\n return (\n (overflowY === 'auto' ||\n overflowY === 'scroll' ||\n overflowY === 'overlay') &&\n element.scrollHeight > element.clientHeight\n );\n};\n\n// The no-offset path hands the work to `scrollIntoView`, which finds the\n// nearest scrollable ancestor by itself. The offset path can't use it (see the\n// comment in `scrollTo`), so it has to repeat that lookup — otherwise passing\n// `offset` would silently switch the scroll from the element's own container\n// to `window`, which is never what the call site means.\nconst findScrollableAncestor = (element: HTMLElement): HTMLElement | null => {\n let parent = element.parentElement;\n\n while (\n parent &&\n parent !== document.body &&\n parent !== document.documentElement\n ) {\n if (isScrollable(parent)) {\n return parent;\n }\n parent = parent.parentElement;\n }\n\n return null;\n};\n\nconst useScrollToElements = (defaultOptions?: Options) => {\n const elementsRef = useRef(new Map<ElementKey, HTMLElement>());\n const registerCallbacksRef = useRef(\n new Map<ElementKey, (node: HTMLElement | null) => void>(),\n );\n\n // `defaultOptions` is almost always an inline object literal at the call\n // site, so depending on it directly would recreate `scrollTo` on every\n // render — read the latest value from a ref instead.\n const defaultOptionsRef = useRef(defaultOptions);\n\n useEffect(() => {\n defaultOptionsRef.current = defaultOptions;\n });\n\n // Returns a stable callback per key so React doesn't detach/reattach the\n // ref (and thrash the Map) on every render — only a genuine unmount\n // (node === null) removes the entry.\n const register = useCallback((key: ElementKey) => {\n let callback = registerCallbacksRef.current.get(key);\n\n if (!callback) {\n callback = (node: HTMLElement | null) => {\n if (node) {\n elementsRef.current.set(key, node);\n } else {\n elementsRef.current.delete(key);\n }\n };\n registerCallbacksRef.current.set(key, callback);\n }\n\n return callback;\n }, []);\n\n const scrollTo = useCallback((key: ElementKey, options?: Options) => {\n const element = elementsRef.current.get(key);\n\n if (!element) {\n return;\n }\n\n const { offset, container, ...scrollIntoViewOptions } = {\n ...defaultOptionsRef.current,\n ...options,\n };\n\n // With an offset, `scrollIntoView` and `scrollTo` would both animate\n // the same scroll, fighting each other and settling in the wrong\n // place — use `scrollTo` alone, computed from the element's position\n // before any scrolling starts (not after `scrollIntoView`, where the\n // rect would reflect a mid-animation position).\n if (offset != null) {\n const containerElement =\n resolveContainer(container ?? null) ?? findScrollableAncestor(element);\n\n if (containerElement) {\n const containerRect = containerElement.getBoundingClientRect();\n const elementRect = element.getBoundingClientRect();\n const top =\n containerElement.scrollTop +\n (elementRect.top - containerRect.top) -\n offset;\n\n containerElement.scrollTo({\n top,\n behavior: scrollIntoViewOptions.behavior || 'smooth',\n });\n return;\n }\n\n const top = element.getBoundingClientRect().top + window.scrollY - offset;\n\n window.scrollTo({\n top,\n behavior: scrollIntoViewOptions.behavior || 'smooth',\n });\n return;\n }\n\n element.scrollIntoView({\n behavior: 'smooth',\n block: 'start',\n inline: 'start',\n ...scrollIntoViewOptions,\n });\n }, []);\n\n return { register, scrollTo };\n};\n\nexport default useScrollToElements;\n"],"mappings":";;AAgBA,MAAM,oBAAoB,cAAmD;CAC3E,IAAI,CAAC,WACH,OAAO;CAET,OAAO,aAAa,YAAY,UAAU,UAAU;AACtD;AAMA,MAAM,gBAAgB,YAAyB;CAC7C,MAAM,EAAE,cAAc,iBAAiB,OAAO;CAE9C,QACG,cAAc,UACb,cAAc,YACd,cAAc,cAChB,QAAQ,eAAe,QAAQ;AAEnC;AAOA,MAAM,0BAA0B,YAA6C;CAC3E,IAAI,SAAS,QAAQ;CAErB,OACE,UACA,WAAW,SAAS,QACpB,WAAW,SAAS,iBACpB;EACA,IAAI,aAAa,MAAM,GACrB,OAAO;EAET,SAAS,OAAO;CAClB;CAEA,OAAO;AACT;AAEA,MAAM,uBAAuB,mBAA6B;CACxD,MAAM,cAAc,uBAAO,IAAI,IAA6B,CAAC;CAC7D,MAAM,uBAAuB,uBAC3B,IAAI,IAAoD,CAC1D;CAKA,MAAM,oBAAoB,OAAO,cAAc;CAE/C,gBAAgB;EACd,kBAAkB,UAAU;CAC9B,CAAC;CA2ED,OAAO;EAAE,UAtEQ,aAAa,QAAoB;GAChD,IAAI,WAAW,qBAAqB,QAAQ,IAAI,GAAG;GAEnD,IAAI,CAAC,UAAU;IACb,YAAY,SAA6B;KACvC,IAAI,MACF,YAAY,QAAQ,IAAI,KAAK,IAAI;UAEjC,YAAY,QAAQ,OAAO,GAAG;IAElC;IACA,qBAAqB,QAAQ,IAAI,KAAK,QAAQ;GAChD;GAEA,OAAO;EACT,GAAG,CAAC,CAuDY;EAAG,UArDF,aAAa,KAAiB,YAAsB;GACnE,MAAM,UAAU,YAAY,QAAQ,IAAI,GAAG;GAE3C,IAAI,CAAC,SACH;GAGF,MAAM,EAAE,QAAQ,WAAW,GAAG,0BAA0B;IACtD,GAAG,kBAAkB;IACrB,GAAG;GACL;GAOA,IAAI,UAAU,MAAM;IAClB,MAAM,mBACJ,iBAAiB,aAAa,IAAI,KAAK,uBAAuB,OAAO;IAEvE,IAAI,kBAAkB;KACpB,MAAM,gBAAgB,iBAAiB,sBAAsB;KAC7D,MAAM,cAAc,QAAQ,sBAAsB;KAClD,MAAM,MACJ,iBAAiB,aAChB,YAAY,MAAM,cAAc,OACjC;KAEF,iBAAiB,SAAS;MACxB;MACA,UAAU,sBAAsB,YAAY;KAC9C,CAAC;KACD;IACF;IAEA,MAAM,MAAM,QAAQ,sBAAsB,CAAC,CAAC,MAAM,OAAO,UAAU;IAEnE,OAAO,SAAS;KACd;KACA,UAAU,sBAAsB,YAAY;IAC9C,CAAC;IACD;GACF;GAEA,QAAQ,eAAe;IACrB,UAAU;IACV,OAAO;IACP,QAAQ;IACR,GAAG;GACL,CAAC;EACH,GAAG,CAAC,CAEsB;CAAE;AAC9B"}
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import useDebouncedCallback from "./hooks/use-debounced-callback.mjs";
1
+ import useDebouncedCallback, { DebouncedCallback } from "./hooks/use-debounced-callback.mjs";
2
2
  import useDebouncedValue from "./hooks/use-debounced-value.mjs";
3
3
  import useBodyScrollLock from "./hooks/use-body-scroll-lock.mjs";
4
4
  import useClickOutside from "./hooks/use-click-outside.mjs";
@@ -15,6 +15,7 @@ import useResponsiveSize from "./hooks/use-responsive-size.mjs";
15
15
  import useImage from "./hooks/use-image.mjs";
16
16
  import useKeyPress from "./hooks/use-key-press.mjs";
17
17
  import useLocalStorage from "./hooks/use-local-storage.mjs";
18
+ import useMediaQuery from "./hooks/use-media-query.mjs";
18
19
  import useMergedRef from "./hooks/use-merged-ref.mjs";
19
20
  import useMultiSelect from "./hooks/use-multi-select.mjs";
20
21
  import useMutationObserver from "./hooks/use-mutation-observer.mjs";
@@ -29,4 +30,4 @@ import useToggle from "./hooks/use-toggle.mjs";
29
30
  import useWindowScroll from "./hooks/use-window-scroll.mjs";
30
31
  import useViewport from "./hooks/use-viewport.mjs";
31
32
  import "./hooks/index.mjs";
32
- export { useBodyScrollLock, useClickOutside, useControllableState, useDebouncedCallback as useDebounce, useDebouncedCallback, useDebouncedValue, useElementPosition, useElementScroll, useEventListener, useFileDrop, useFileToDataUrl, useHistoryState, useImage, useIntersectionObserver, useInterval, useKeyPress, useLocalStorage, useMergedRef, useMultiSelect, useMutationObserver, usePrevious, useRecursiveTimeout, useResizeObserver, useResponsiveSize, useScrollToElements, useThrottledValue as useThrottle, useThrottledCallback, useThrottledValue, useTimeout, useToggle, useViewport, useWindowScroll };
33
+ export { type DebouncedCallback, useBodyScrollLock, useClickOutside, useControllableState, useDebouncedCallback as useDebounce, useDebouncedCallback, useDebouncedValue, useElementPosition, useElementScroll, useEventListener, useFileDrop, useFileToDataUrl, useHistoryState, useImage, useIntersectionObserver, useInterval, useKeyPress, useLocalStorage, useMediaQuery, useMergedRef, useMultiSelect, useMutationObserver, usePrevious, useRecursiveTimeout, useResizeObserver, useResponsiveSize, useScrollToElements, useThrottledValue as useThrottle, useThrottledCallback, useThrottledValue, useTimeout, useToggle, useViewport, useWindowScroll };
package/dist/index.mjs CHANGED
@@ -15,6 +15,7 @@ import useResponsiveSize from "./hooks/use-responsive-size.mjs";
15
15
  import useImage from "./hooks/use-image.mjs";
16
16
  import useKeyPress from "./hooks/use-key-press.mjs";
17
17
  import useLocalStorage from "./hooks/use-local-storage.mjs";
18
+ import useMediaQuery from "./hooks/use-media-query.mjs";
18
19
  import useMergedRef from "./hooks/use-merged-ref.mjs";
19
20
  import useMultiSelect from "./hooks/use-multi-select.mjs";
20
21
  import useMutationObserver from "./hooks/use-mutation-observer.mjs";
@@ -29,4 +30,4 @@ import useToggle from "./hooks/use-toggle.mjs";
29
30
  import useWindowScroll from "./hooks/use-window-scroll.mjs";
30
31
  import useViewport from "./hooks/use-viewport.mjs";
31
32
  import "./hooks/index.mjs";
32
- export { useBodyScrollLock, useClickOutside, useControllableState, useDebouncedCallback as useDebounce, useDebouncedCallback, useDebouncedValue, useElementPosition, useElementScroll, useEventListener, useFileDrop, useFileToDataUrl, useHistoryState, useImage, useIntersectionObserver, useInterval, useKeyPress, useLocalStorage, useMergedRef, useMultiSelect, useMutationObserver, usePrevious, useRecursiveTimeout, useResizeObserver, useResponsiveSize, useScrollToElements, useThrottledValue as useThrottle, useThrottledCallback, useThrottledValue, useTimeout, useToggle, useViewport, useWindowScroll };
33
+ export { useBodyScrollLock, useClickOutside, useControllableState, useDebouncedCallback as useDebounce, useDebouncedCallback, useDebouncedValue, useElementPosition, useElementScroll, useEventListener, useFileDrop, useFileToDataUrl, useHistoryState, useImage, useIntersectionObserver, useInterval, useKeyPress, useLocalStorage, useMediaQuery, useMergedRef, useMultiSelect, useMutationObserver, usePrevious, useRecursiveTimeout, useResizeObserver, useResponsiveSize, useScrollToElements, useThrottledValue as useThrottle, useThrottledCallback, useThrottledValue, useTimeout, useToggle, useViewport, useWindowScroll };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbpark/use-hooks",
3
- "version": "4.0.1",
3
+ "version": "4.1.0",
4
4
  "description": "A collection of reusable React 19 hooks for common UI and interaction patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -39,11 +39,8 @@
39
39
  "dist"
40
40
  ],
41
41
  "scripts": {
42
- "dev": "vite",
43
42
  "build": "tsc -b && tsdown",
44
- "build:demo": "vite build",
45
43
  "lint": "eslint .",
46
- "preview": "vite preview",
47
44
  "prepublishOnly": "pnpm build",
48
45
  "prepare": "husky"
49
46
  },
@@ -54,13 +51,10 @@
54
51
  "devDependencies": {
55
52
  "@changesets/cli": "^3.0.0",
56
53
  "@eslint/js": "^9.39.5",
57
- "@jbpark/ui-kit": "^5.2.0",
58
- "@radix-ui/react-icons": "^1.3.2",
59
54
  "@trivago/prettier-plugin-sort-imports": "^6.0.2",
60
55
  "@types/node": "^26.2.0",
61
56
  "@types/react": "^19.2.18",
62
57
  "@types/react-dom": "^19.2.4",
63
- "@vitejs/plugin-react": "^6.0.5",
64
58
  "eslint": "^9.39.5",
65
59
  "eslint-plugin-react-hooks": "^6.1.1",
66
60
  "eslint-plugin-react-refresh": "^0.5.4",
@@ -73,12 +67,9 @@
73
67
  "prettier-plugin-tailwindcss": "^0.8.1",
74
68
  "react": "^19.2.8",
75
69
  "react-dom": "^19.2.8",
76
- "react-router-dom": "^7.18.2",
77
70
  "tsdown": "^0.22.14",
78
71
  "typescript": "~6.0.3",
79
- "typescript-eslint": "^8.67.0",
80
- "vite": "^8.2.1",
81
- "vite-plugin-dts": "^5.0.3"
72
+ "typescript-eslint": "^8.67.0"
82
73
  },
83
74
  "lint-staged": {
84
75
  "**/*.{js,jsx,ts,tsx,css,scss,md}": "prettier --write",