@jbpark/use-hooks 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # use-hooks
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@jbpark/use-hooks.svg)](https://www.npmjs.com/package/@jbpark/use-hooks)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@jbpark/use-hooks.svg)](https://www.npmjs.com/package/@jbpark/use-hooks)
5
+ [![GitHub issues](https://img.shields.io/github/issues/pjb0811/use-hooks)](https://github.com/pjb0811/use-hooks/issues)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ 일반적인 UI 및 상호작용 패턴을 위한 재사용 가능한 React 19 훅 모음입니다. TypeScript와 Vite로 빌드되었으며, 서버 사이드 렌더링과 클라이언트 사이드 애플리케이션 모두에 최적화되어 있습니다.
9
+
10
+ ## 기능
11
+
12
+ - 📦 **10개 프로덕션 레디 훅** - 스크롤, 뷰포트, 스토리지 등 다양한 유틸리티
13
+ - 🎯 **TypeScript 지원** - 완전한 타입 지원으로 더 나은 개발 경험
14
+ - ⚡ **트리 셰이킹 지원** - 필요한 것만 임포트하세요
15
+ - 🔒 **SSR 안전** - window/document 전역 변수에 대한 보호
16
+ - 📱 **iOS 최적화** - 모바일 뷰포트 특성에 대한 특별 처리
17
+ - 🧹 **완벽한 정리** - 모든 리스너와 옵저버가 정리됩니다
18
+
19
+ ## 설치
20
+
21
+ ```bash
22
+ npm install @jbpark/use-hooks
23
+ ```
24
+
25
+ ## 사용 방법
26
+
27
+ ```tsx
28
+ import {
29
+ useElementSize,
30
+ useLocalStorage,
31
+ useWindowScroll,
32
+ } from '@jbpark/use-hooks';
33
+
34
+ function MyComponent() {
35
+ // localStorage를 사용한 영속적 상태
36
+ const [count, setCount] = useLocalStorage('count', 0);
37
+
38
+ // 윈도우 스크롤 위치 추적
39
+ const { y, percent } = useWindowScroll();
40
+
41
+ // 브레이크포인트를 포함한 요소 크기 모니터링
42
+ const { size, breakpoint, ref } = useElementSize();
43
+
44
+ return (
45
+ <div ref={ref}>
46
+ <p>Count: {count}</p>
47
+ <p>Scroll: {percent.y}%</p>
48
+ <p>Breakpoint: {breakpoint.current}</p>
49
+ <button onClick={() => setCount(count + 1)}>+</button>
50
+ </div>
51
+ );
52
+ }
53
+ ```
54
+
55
+ ## 사용 가능한 훅
56
+
57
+ | 훅 | 설명 |
58
+ | --------------------- | --------------------------------------------------------------- |
59
+ | `useLocalStorage` | 에러 핸들링이 포함된 JSON 기반 영속 상태 (SSR 안전) |
60
+ | `useWindowScroll` | 윈도우 스크롤 위치 및 백분율 추적 (iOS visualViewport 대응) |
61
+ | `useScrollPosition` | ResizeObserver를 사용한 특정 요소의 스크롤 상태 추적 |
62
+ | `useElementRect` | 스크롤/리사이즈 시 요소의 바운딩 렉트 모니터링 (요소 참조 지원) |
63
+ | `useElementSize` | Tailwind 유사 브레이크포인트를 포함한 요소 크기 추적 (debounce) |
64
+ | `useBodyScrollLock` | 스타일 보존을 포함한 바디 스크롤 잠금/해제 (iOS 특별 처리) |
65
+ | `useScrollToElements` | 인덱스별로 특정 요소로 스크롤 (오프셋 조절 가능) |
66
+ | `useImageLoader` | 이미지 사전로드 및 로딩/에러 상태 노출 |
67
+ | `useRecursiveTimeout` | 비동기/동기 콜백을 재귀적으로 스케줄링 |
68
+ | `useViewport` | visualViewport 지원, 인앱 모드 옵션, debounce 포함 |
69
+
70
+ ## 개발
71
+
72
+ ```bash
73
+ # HMR이 포함된 개발 서버 시작
74
+ npm run dev
75
+
76
+ # 라이브러리 빌드 (tsc + vite)
77
+ npm run build
78
+
79
+ # 빌드된 라이브러리 미리보기
80
+ npm run preview
81
+
82
+ # 린트 및 타입 체크
83
+ npm run lint
84
+
85
+ # prettier로 포맷팅
86
+ npx prettier --write .
87
+ ```
88
+
89
+ ## 프로젝트 구조
90
+
91
+ ```
92
+ src/
93
+ ├── hooks/ # 개별 훅 구현
94
+ │ ├── useBodyScrollLock/
95
+ │ ├── useElementRect/
96
+ │ ├── useElementSize/
97
+ │ ├── useImageLoader/
98
+ │ ├── useLocalStorage/
99
+ │ ├── useRecursiveTimeout/
100
+ │ ├── useScrollPosition/
101
+ │ ├── useScrollToElements/
102
+ │ ├── useViewport/
103
+ │ ├── useWindowScroll/
104
+ │ └── index.ts # 배럴 익스포트
105
+ └── index.ts # 패키지 진입점
106
+
107
+ dist/ # 빌드된 라이브러리 (ES + CJS + types)
108
+ .changeset/ # 버저닝을 위한 Changesets
109
+ ```
110
+
111
+ ## 빌드 및 배포
112
+
113
+ 이 프로젝트는 버전 관리를 위해 Changesets를 사용합니다:
114
+
115
+ ```bash
116
+ # 변경사항 기록
117
+ npx changeset
118
+
119
+ # 버전 업데이트 및 CHANGELOG 생성
120
+ npx changeset version
121
+
122
+ # npm에 배포
123
+ npx changeset publish
124
+
125
+ # 태그 푸시
126
+ git push --follow-tags
127
+ ```
128
+
129
+ 라이브러리는 다음과 같이 빌드됩니다:
130
+
131
+ - **ES Module**: `dist/index.js`
132
+ - **CommonJS**: `dist/index.cjs`
133
+ - **타입 정의**: `dist/index.d.ts`
134
+
135
+ ## 주요 패턴
136
+
137
+ - **Window 보호**: `window`/`document`에 접근하는 훅은 SSR 안전성을 위해 `typeof window` 체크 (예: `useLocalStorage`)
138
+ - **이벤트 리스너**: 모든 스크롤/리사이즈 리스너는 가능한 한 passive 플래그 사용
139
+ - **ResizeObserver**: `useElementSize`와 `useScrollPosition`에서 사용하여 성능 최적화
140
+ - **requestAnimationFrame**: 스크롤/리사이즈 콜백에서 레이아웃 스래싱 방지
141
+ - **iOS 대응**: `useBodyScrollLock`, `useWindowScroll`, `useViewport`에서 iOS의 visualViewport 특성 처리
142
+ - **Debounce**: `useElementSize`와 `useViewport`에서 리사이즈 이벤트 디바운싱 지원
143
+
144
+ ## 브라우저 지원
145
+
146
+ - 최신 브라우저 (Chrome, Firefox, Safari, Edge)
147
+ - iOS 12+ (특수한 `visualViewport` 처리 포함)
148
+ - SSR 준비 완료 (적절한 보호 포함)
149
+
150
+ ## 기여하기
151
+
152
+ 버그 리포트, 기능 제안, 또는 코드 기여를 환영합니다!
153
+
154
+ - 🐛 **버그 리포트**: [Issues](https://github.com/pjb0811/use-hooks/issues)에서 버그를 리포트해주세요
155
+ - 💡 **기능 제안**: 새로운 기능 아이디어가 있으시면 [Issues](https://github.com/pjb0811/use-hooks/issues)에 제안해주세요
156
+ - 🔧 **코드 기여**: Pull Request를 보내주시면 검토 후 반영하겠습니다
157
+
158
+ 이슈를 생성하기 전에 기존 이슈를 확인해주시면 중복을 방지할 수 있습니다.
159
+
160
+ ## 라이선스
161
+
162
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const n=require("react"),E=(e,{delay:o,autoInvoke:d=!0},t=[])=>{const r=n.useRef(null),c=n.useRef(e),i=n.useRef(t);n.useEffect(()=>{c.current=e}),n.useEffect(()=>{i.current=t});const l=n.useRef(null);l.current||(l.current=((...u)=>{r.current&&clearTimeout(r.current),r.current=setTimeout(()=>{c.current(...u)},o)}));const s=n.useRef(t);return n.useEffect(()=>{const u=!s.current||s.current.length!==t.length||s.current.some((a,w)=>a!==t[w]);u&&r.current&&clearTimeout(r.current),d&&u&&l.current&&l.current(),s.current=t}),n.useEffect(()=>()=>{r.current&&clearTimeout(r.current)},[]),l.current},S=(e=!0)=>{n.useEffect(()=>{if(!e)return;const o=window.scrollY,d=/iPad|iPhone|iPod/.test(navigator.userAgent),t={documentElement:{overflow:document.documentElement.style.overflow,height:document.documentElement.style.height,position:document.documentElement.style.position,width:document.documentElement.style.width},body:{overflow:document.body.style.overflow,height:document.body.style.height,position:document.body.style.position,top:document.body.style.top,left:document.body.style.left,right:document.body.style.right,width:document.body.style.width,webkitOverflowScrolling:document.body.style.getPropertyValue("-webkit-overflow-scrolling")}};if(document.documentElement.style.overflow="hidden",document.documentElement.style.height="100%",document.documentElement.style.position="fixed",document.documentElement.style.width="100%",document.body.style.overflow="hidden",document.body.style.height="100%",document.body.style.position="fixed",document.body.style.top=`-${o}px`,document.body.style.left="0",document.body.style.right="0",document.body.style.width="100%",d){document.body.style.setProperty("-webkit-overflow-scrolling","touch");const r=s=>{(s.target===document.body||s.target===document.documentElement||s.target===window)&&(s.preventDefault(),s.stopPropagation(),s.stopImmediatePropagation())},c=()=>{window.scrollTo(0,0),document.body.scrollTop=0,document.documentElement.scrollTop=0},i=["scroll","touchmove","touchstart","touchend"];i.forEach(s=>{window.addEventListener(s,r,{passive:!1,capture:!0}),document.addEventListener(s,r,{passive:!1,capture:!0}),document.body.addEventListener(s,r,{passive:!1,capture:!0})});const l=setInterval(c,16);return()=>{clearInterval(l),i.forEach(s=>{window.removeEventListener(s,r,{capture:!0}),document.removeEventListener(s,r,{capture:!0}),document.body.removeEventListener(s,r,{capture:!0})}),document.documentElement.style.overflow=t.documentElement.overflow,document.documentElement.style.height=t.documentElement.height,document.documentElement.style.position=t.documentElement.position,document.documentElement.style.width=t.documentElement.width,document.body.style.overflow=t.body.overflow,document.body.style.height=t.body.height,document.body.style.position=t.body.position,document.body.style.top=t.body.top,document.body.style.left=t.body.left,document.body.style.right=t.body.right,document.body.style.width=t.body.width,t.body.webkitOverflowScrolling?document.body.style.setProperty("-webkit-overflow-scrolling",t.body.webkitOverflowScrolling):document.body.style.removeProperty("-webkit-overflow-scrolling"),window.scrollTo(0,o)}}return()=>{document.documentElement.style.overflow=t.documentElement.overflow,document.documentElement.style.height=t.documentElement.height,document.documentElement.style.position=t.documentElement.position,document.documentElement.style.width=t.documentElement.width,document.body.style.overflow=t.body.overflow,document.body.style.height=t.body.height,document.body.style.position=t.body.position,document.body.style.top=t.body.top,document.body.style.left=t.body.left,document.body.style.right=t.body.right,document.body.style.width=t.body.width,window.scrollTo(0,o)}},[e])},L=e=>{const[o,d]=n.useState(null);return n.useEffect(()=>{const t=i=>typeof i=="string"?document.querySelector(i):i.current,r=()=>{const i=t(e);d(i?i.getBoundingClientRect():null)},c=()=>{requestAnimationFrame(r)};return r(),window.addEventListener("scroll",c,{passive:!0}),window.addEventListener("resize",c,{passive:!0}),()=>{window.removeEventListener("scroll",c),window.removeEventListener("resize",c)}},[e]),o},T=()=>{const[e,o]=n.useState(null),[d,t]=n.useState({scrollY:0,scrollPercentage:0,isAtTop:!0,isAtBottom:!1,scrollableHeight:0,clientHeight:0,scrollHeight:0}),r=n.useCallback(c=>{o(c)},[]);return n.useEffect(()=>{if(!e)return;const c=()=>{const{scrollTop:s,scrollHeight:u,clientHeight:a}=e,w=u-a;if(w<=0){t({scrollY:0,scrollPercentage:0,isAtTop:!0,isAtBottom:!0,scrollableHeight:0,clientHeight:a,scrollHeight:u});return}const m=Math.min(100,Math.max(0,s/w*100));t({scrollY:s,scrollPercentage:m,isAtTop:s<=0,isAtBottom:s>=w-1,scrollableHeight:w,clientHeight:a,scrollHeight:u})};c();const i=()=>{c()};e.addEventListener("scroll",i,{passive:!0});const l=new ResizeObserver(()=>{c()});return l.observe(e),()=>{e.removeEventListener("scroll",i),l.unobserve(e)}},[e]),{...d,element:e,setRef:r}},f={sm:640,md:768,lg:1024,xl:1280,"2xl":1536},x=e=>{let o="xs";return e>=f["2xl"]?o="2xl":e>=f.xl?o="xl":e>=f.lg?o="lg":e>=f.md?o="md":e>=f.sm?o="sm":o="xs",{current:o,xs:e<f.sm,sm:e>=f.sm&&e<f.md,md:e>=f.md&&e<f.lg,lg:e>=f.lg&&e<f.xl,xl:e>=f.xl&&e<f["2xl"],"2xl":e>=f["2xl"]}},V=e=>{const{delay:o=100,container:d}=e||{},[t,r]=n.useState({width:0,height:0}),[c,i]=n.useState({current:"xs",xs:!0,sm:!1,md:!1,lg:!1,xl:!1,"2xl":!1}),[l,s]=n.useState({width:0,height:0}),u=n.useRef(null),a=n.useRef(null);return E(()=>{s(t)},{delay:o},[t]),n.useEffect(()=>{const w=()=>{const h=d??u.current??document.body;if(!h)return;const{offsetWidth:y,offsetHeight:v}=h;r(p=>p.width!==y||p.height!==v?{width:y,height:v}:p),i(p=>{const b=x(y);return p.current!==b.current?b:p})},m=()=>{const h=d??u.current??document.body;h&&(w(),a.current&&a.current.disconnect(),a.current=new ResizeObserver(()=>{requestAnimationFrame(()=>{w()})}),a.current.observe(h))},g=()=>{a.current&&(a.current.disconnect(),a.current=null)};return m(),()=>{g()}},[d]),{size:l,breakpoint:c,ref:u}},R=(e,o={})=>{const{retryCount:d=0,retryDelay:t=1e3}=o,[r,c]=n.useState(!0),[i,l]=n.useState(null),[s,u]=n.useState(!1),[a,w]=n.useState(0),m=n.useCallback(()=>{if(!e){c(!1),u(!1);return}c(!0),l(null);const h=new Image;h.src=e,h.onload=()=>{c(!1),u(!0),l(null),w(0)},h.onerror=y=>{c(!1),u(!1),l(y),a<d&&setTimeout(()=>{w(v=>v+1)},t)}},[e,a,d,t]);n.useEffect(()=>{m()},[m]);const g=n.useCallback(()=>{w(0),m()},[m]);return{loading:r,error:i,loaded:s,retry:g}},z=(e,o)=>{const d=n.useRef(o),[t,r]=n.useState(()=>{if(typeof window>"u")return o;try{const i=window.localStorage.getItem(e);return i?JSON.parse(i):o}catch{return o}});n.useEffect(()=>{if(!(typeof window>"u"))try{const i=window.localStorage.getItem(e);i?r(JSON.parse(i)):window.localStorage.setItem(e,JSON.stringify(d.current))}catch(i){console.error(`Error reading localStorage key "${e}":`,i)}},[e]);const c=n.useCallback(i=>{try{r(l=>{const s=i instanceof Function?i(l):i;return localStorage.setItem(e,JSON.stringify(s)),s})}catch(l){console.error(`Error setting localStorage key "${e}":`,l)}},[e]);return[t,c]},P=(e,o)=>{const d=n.useRef(e);n.useEffect(()=>{d.current=e},[e]),n.useEffect(()=>{let t;function r(){const c=d.current();c instanceof Promise?c.then(()=>{o&&(t=setTimeout(r,o))}):o&&(t=setTimeout(r,o))}if(o)return t=setTimeout(r,o),()=>t&&clearTimeout(t)},[o])},H=e=>{const o=n.useRef([]),d=n.useCallback(r=>{if(o.current[r]&&(o.current[r].scrollIntoView({behavior:"smooth",block:"start",inline:"start",...e}),e?.offset)){const c=o.current[r].getBoundingClientRect().top+window.scrollY-e.offset;window.scrollTo({top:c,behavior:e.behavior||"smooth"})}},[e]),t=n.useCallback((r,c)=>{o.current[c]=r},[]);return{elementRefs:o,setElementRef:t,scrollToElement:d}},I=()=>{const[e,o]=n.useState({x:0,y:0,percent:{x:0,y:0}});return n.useEffect(()=>{const d=()=>{const i=window.scrollX||0,l=window.scrollY||0,s=/iPad|iPhone|iPod/.test(navigator.userAgent),u=window.visualViewport,a=s&&u?u.width:window.innerWidth,w=s&&u?u.height:window.innerHeight,m=Math.max(0,document.documentElement.scrollWidth-a),g=Math.max(0,document.documentElement.scrollHeight-w),h=m===0?0:Math.min(100,i/m*100),y=g===0?0:Math.min(100,l/g*100);o({x:i,y:l,percent:{x:Math.floor(Math.max(0,h)),y:Math.floor(Math.max(0,y))}})};d();const t=()=>{d()},r=()=>{setTimeout(d,100)},c=()=>{setTimeout(d,50)};return window.addEventListener("scroll",t,{passive:!0}),window.addEventListener("resize",r),window.addEventListener("orientationchange",r),window.visualViewport&&window.visualViewport.addEventListener("resize",c),()=>{window.removeEventListener("scroll",t),window.removeEventListener("resize",r),window.removeEventListener("orientationchange",r),window.visualViewport&&window.visualViewport.removeEventListener("resize",c)}},[]),e},k=(e={})=>{const{isInApp:o=!1,debounce:d=100}=e,[t,r]=n.useState({width:0,height:0,offsetLeft:0,offsetTop:0,pageLeft:0,pageTop:0,scale:1}),c=n.useCallback(()=>{const l=window.innerHeight,s=window.visualViewport?.height||l,u=document.documentElement.clientHeight,a=document.body.clientHeight;return window.visualViewport&&Math.abs(s-l)>100?s:Math.max(l,u,a)},[]),i=n.useCallback(()=>{if(window.visualViewport&&!o)return window.visualViewport;const l=window.visualViewport?.width||window.innerWidth,s=o?c():window.visualViewport?.height||window.innerHeight;return{width:l,height:s,offsetLeft:window.visualViewport?.offsetLeft||0,offsetTop:window.visualViewport?.offsetTop||0,pageLeft:window.scrollX??window.pageXOffset??0,pageTop:window.scrollY??window.pageYOffset??0,scale:window.visualViewport?.scale||1}},[o,c]);return n.useEffect(()=>{let l;const s=()=>{clearTimeout(l),l=setTimeout(()=>{r(i())},d)},u=()=>r(i());u();const a=["resize","orientationchange"];o&&a.push("focus","blur","touchstart","touchend"),a.forEach(m=>{m==="resize"||m==="orientationchange"?window.addEventListener(m,s):window.addEventListener(m,u,{passive:!0})}),window.visualViewport&&(window.visualViewport.addEventListener("resize",u),window.visualViewport.addEventListener("scroll",u));let w;if(o){let m=i().height;w=setInterval(()=>{const g=i().height;Math.abs(g-m)>50&&(m=g,u())},500)}return()=>{clearTimeout(l),w&&clearInterval(w),a.forEach(m=>{window.removeEventListener(m,m==="resize"||m==="orientationchange"?s:u)}),window.visualViewport&&(window.visualViewport.removeEventListener("resize",u),window.visualViewport.removeEventListener("scroll",u))}},[i,o,d]),t};exports.useBodyScrollLock=S;exports.useDebounce=E;exports.useElementPosition=L;exports.useElementScroll=T;exports.useImage=R;exports.useLocalStorage=z;exports.useRecursiveTimeout=P;exports.useResponsiveSize=V;exports.useScrollToElements=H;exports.useViewport=k;exports.useWindowScroll=I;
@@ -0,0 +1,106 @@
1
+ import { RefObject } from 'react';
2
+
3
+ declare type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
4
+
5
+ declare interface BreakpointInfo {
6
+ current: Breakpoint;
7
+ xs: boolean;
8
+ sm: boolean;
9
+ md: boolean;
10
+ lg: boolean;
11
+ xl: boolean;
12
+ '2xl': boolean;
13
+ }
14
+
15
+ declare type ElementReference<T> = string | RefObject<T>;
16
+
17
+ declare interface Options {
18
+ delay: number;
19
+ autoInvoke?: boolean;
20
+ }
21
+
22
+ declare interface Options_2 {
23
+ delay?: number;
24
+ container?: HTMLElement | null;
25
+ }
26
+
27
+ declare interface Options_3 {
28
+ retryCount?: number;
29
+ retryDelay?: number;
30
+ }
31
+
32
+ declare interface Options_4 extends ScrollIntoViewOptions {
33
+ offset?: number;
34
+ }
35
+
36
+ declare interface Options_5 {
37
+ isInApp?: boolean;
38
+ debounce?: number;
39
+ }
40
+
41
+ export declare const useBodyScrollLock: (enabled?: boolean) => void;
42
+
43
+ export declare const useDebounce: <T extends (...args: unknown[]) => unknown>(callback: T, { delay, autoInvoke }: Options, deps?: React.DependencyList) => T;
44
+
45
+ export declare const useElementPosition: <T>(elementRef: ElementReference<T>) => DOMRect | null;
46
+
47
+ export declare const useElementScroll: () => {
48
+ element: HTMLElement | null;
49
+ setRef: (el: HTMLElement | null) => void;
50
+ scrollY: number;
51
+ scrollPercentage: number;
52
+ isAtTop: boolean;
53
+ isAtBottom: boolean;
54
+ scrollableHeight: number;
55
+ clientHeight: number;
56
+ scrollHeight: number;
57
+ };
58
+
59
+ export declare const useImage: (src: string, options?: Options_3) => {
60
+ loading: boolean;
61
+ error: string | Event | null;
62
+ loaded: boolean;
63
+ retry: () => void;
64
+ };
65
+
66
+ export declare const useLocalStorage: <T>(key: string, initialValue: T) => readonly [T, (value: T | ((val: T) => T)) => void];
67
+
68
+ export declare const useRecursiveTimeout: <T>(callback: () => Promise<T> | (() => void), delay: number | null) => void;
69
+
70
+ export declare const useResponsiveSize: <T extends HTMLElement>(options?: Options_2) => {
71
+ size: {
72
+ width: number;
73
+ height: number;
74
+ };
75
+ breakpoint: BreakpointInfo;
76
+ ref: RefObject<T | null>;
77
+ };
78
+
79
+ export declare const useScrollToElements: (options?: Options_4) => {
80
+ elementRefs: RefObject<HTMLElement[]>;
81
+ setElementRef: (element: HTMLElement, index: number) => void;
82
+ scrollToElement: (index: number) => void;
83
+ };
84
+
85
+ export declare const useViewport: (options?: Options_5) => ViewportInfo;
86
+
87
+ export declare const useWindowScroll: () => {
88
+ x: number;
89
+ y: number;
90
+ percent: {
91
+ x: number;
92
+ y: number;
93
+ };
94
+ };
95
+
96
+ declare type ViewportInfo = VisualViewport | {
97
+ width: number;
98
+ height: number;
99
+ offsetLeft: number;
100
+ offsetTop: number;
101
+ pageLeft: number;
102
+ pageTop: number;
103
+ scale: number;
104
+ };
105
+
106
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,424 @@
1
+ import { useRef as y, useEffect as f, useState as h, useCallback as v } from "react";
2
+ const x = (e, { delay: o, autoInvoke: u = !0 }, t = []) => {
3
+ const n = y(null), i = y(e), s = y(t);
4
+ f(() => {
5
+ i.current = e;
6
+ }), f(() => {
7
+ s.current = t;
8
+ });
9
+ const l = y(null);
10
+ l.current || (l.current = ((...c) => {
11
+ n.current && clearTimeout(n.current), n.current = setTimeout(() => {
12
+ i.current(...c);
13
+ }, o);
14
+ }));
15
+ const r = y(t);
16
+ return f(() => {
17
+ const c = !r.current || r.current.length !== t.length || r.current.some((d, a) => d !== t[a]);
18
+ c && n.current && clearTimeout(n.current), u && c && l.current && l.current(), r.current = t;
19
+ }), f(() => () => {
20
+ n.current && clearTimeout(n.current);
21
+ }, []), l.current;
22
+ }, z = (e = !0) => {
23
+ f(() => {
24
+ if (!e)
25
+ return;
26
+ const o = window.scrollY, u = /iPad|iPhone|iPod/.test(navigator.userAgent), t = {
27
+ documentElement: {
28
+ overflow: document.documentElement.style.overflow,
29
+ height: document.documentElement.style.height,
30
+ position: document.documentElement.style.position,
31
+ width: document.documentElement.style.width
32
+ },
33
+ body: {
34
+ overflow: document.body.style.overflow,
35
+ height: document.body.style.height,
36
+ position: document.body.style.position,
37
+ top: document.body.style.top,
38
+ left: document.body.style.left,
39
+ right: document.body.style.right,
40
+ width: document.body.style.width,
41
+ webkitOverflowScrolling: document.body.style.getPropertyValue(
42
+ "-webkit-overflow-scrolling"
43
+ )
44
+ }
45
+ };
46
+ if (document.documentElement.style.overflow = "hidden", document.documentElement.style.height = "100%", document.documentElement.style.position = "fixed", document.documentElement.style.width = "100%", document.body.style.overflow = "hidden", document.body.style.height = "100%", document.body.style.position = "fixed", document.body.style.top = `-${o}px`, document.body.style.left = "0", document.body.style.right = "0", document.body.style.width = "100%", u) {
47
+ document.body.style.setProperty("-webkit-overflow-scrolling", "touch");
48
+ const n = (r) => {
49
+ (r.target === document.body || r.target === document.documentElement || r.target === window) && (r.preventDefault(), r.stopPropagation(), r.stopImmediatePropagation());
50
+ }, i = () => {
51
+ window.scrollTo(0, 0), document.body.scrollTop = 0, document.documentElement.scrollTop = 0;
52
+ }, s = ["scroll", "touchmove", "touchstart", "touchend"];
53
+ s.forEach((r) => {
54
+ window.addEventListener(r, n, {
55
+ passive: !1,
56
+ capture: !0
57
+ }), document.addEventListener(r, n, {
58
+ passive: !1,
59
+ capture: !0
60
+ }), document.body.addEventListener(r, n, {
61
+ passive: !1,
62
+ capture: !0
63
+ });
64
+ });
65
+ const l = setInterval(i, 16);
66
+ return () => {
67
+ clearInterval(l), s.forEach((r) => {
68
+ window.removeEventListener(r, n, { capture: !0 }), document.removeEventListener(r, n, { capture: !0 }), document.body.removeEventListener(r, n, {
69
+ capture: !0
70
+ });
71
+ }), document.documentElement.style.overflow = t.documentElement.overflow, document.documentElement.style.height = t.documentElement.height, document.documentElement.style.position = t.documentElement.position, document.documentElement.style.width = t.documentElement.width, document.body.style.overflow = t.body.overflow, document.body.style.height = t.body.height, document.body.style.position = t.body.position, document.body.style.top = t.body.top, document.body.style.left = t.body.left, document.body.style.right = t.body.right, document.body.style.width = t.body.width, t.body.webkitOverflowScrolling ? document.body.style.setProperty(
72
+ "-webkit-overflow-scrolling",
73
+ t.body.webkitOverflowScrolling
74
+ ) : document.body.style.removeProperty("-webkit-overflow-scrolling"), window.scrollTo(0, o);
75
+ };
76
+ }
77
+ return () => {
78
+ document.documentElement.style.overflow = t.documentElement.overflow, document.documentElement.style.height = t.documentElement.height, document.documentElement.style.position = t.documentElement.position, document.documentElement.style.width = t.documentElement.width, document.body.style.overflow = t.body.overflow, document.body.style.height = t.body.height, document.body.style.position = t.body.position, document.body.style.top = t.body.top, document.body.style.left = t.body.left, document.body.style.right = t.body.right, document.body.style.width = t.body.width, window.scrollTo(0, o);
79
+ };
80
+ }, [e]);
81
+ }, H = (e) => {
82
+ const [o, u] = h(null);
83
+ return f(() => {
84
+ const t = (s) => typeof s == "string" ? document.querySelector(s) : s.current, n = () => {
85
+ const s = t(e);
86
+ u(s ? s.getBoundingClientRect() : null);
87
+ }, i = () => {
88
+ requestAnimationFrame(n);
89
+ };
90
+ return n(), window.addEventListener("scroll", i, { passive: !0 }), window.addEventListener("resize", i, { passive: !0 }), () => {
91
+ window.removeEventListener("scroll", i), window.removeEventListener("resize", i);
92
+ };
93
+ }, [e]), o;
94
+ }, I = () => {
95
+ const [e, o] = h(null), [u, t] = h({
96
+ scrollY: 0,
97
+ scrollPercentage: 0,
98
+ isAtTop: !0,
99
+ isAtBottom: !1,
100
+ scrollableHeight: 0,
101
+ clientHeight: 0,
102
+ scrollHeight: 0
103
+ }), n = v((i) => {
104
+ o(i);
105
+ }, []);
106
+ return f(() => {
107
+ if (!e)
108
+ return;
109
+ const i = () => {
110
+ const { scrollTop: r, scrollHeight: c, clientHeight: d } = e, a = c - d;
111
+ if (a <= 0) {
112
+ t({
113
+ scrollY: 0,
114
+ scrollPercentage: 0,
115
+ isAtTop: !0,
116
+ isAtBottom: !0,
117
+ scrollableHeight: 0,
118
+ clientHeight: d,
119
+ scrollHeight: c
120
+ });
121
+ return;
122
+ }
123
+ const m = Math.min(
124
+ 100,
125
+ Math.max(0, r / a * 100)
126
+ );
127
+ t({
128
+ scrollY: r,
129
+ scrollPercentage: m,
130
+ isAtTop: r <= 0,
131
+ isAtBottom: r >= a - 1,
132
+ scrollableHeight: a,
133
+ clientHeight: d,
134
+ scrollHeight: c
135
+ });
136
+ };
137
+ i();
138
+ const s = () => {
139
+ i();
140
+ };
141
+ e.addEventListener("scroll", s, { passive: !0 });
142
+ const l = new ResizeObserver(() => {
143
+ i();
144
+ });
145
+ return l.observe(e), () => {
146
+ e.removeEventListener("scroll", s), l.unobserve(e);
147
+ };
148
+ }, [e]), { ...u, element: e, setRef: n };
149
+ }, w = {
150
+ // < 640px
151
+ sm: 640,
152
+ // >= 640px
153
+ md: 768,
154
+ // >= 768px
155
+ lg: 1024,
156
+ // >= 1024px
157
+ xl: 1280,
158
+ // >= 1280px
159
+ "2xl": 1536
160
+ // >= 1536px
161
+ }, T = (e) => {
162
+ let o = "xs";
163
+ return e >= w["2xl"] ? o = "2xl" : e >= w.xl ? o = "xl" : e >= w.lg ? o = "lg" : e >= w.md ? o = "md" : e >= w.sm ? o = "sm" : o = "xs", {
164
+ current: o,
165
+ xs: e < w.sm,
166
+ sm: e >= w.sm && e < w.md,
167
+ md: e >= w.md && e < w.lg,
168
+ lg: e >= w.lg && e < w.xl,
169
+ xl: e >= w.xl && e < w["2xl"],
170
+ "2xl": e >= w["2xl"]
171
+ };
172
+ }, P = (e) => {
173
+ const { delay: o = 100, container: u } = e || {}, [t, n] = h({ width: 0, height: 0 }), [i, s] = h({
174
+ current: "xs",
175
+ xs: !0,
176
+ sm: !1,
177
+ md: !1,
178
+ lg: !1,
179
+ xl: !1,
180
+ "2xl": !1
181
+ }), [l, r] = h({ width: 0, height: 0 }), c = y(null), d = y(null);
182
+ return x(
183
+ () => {
184
+ r(t);
185
+ },
186
+ { delay: o },
187
+ [t]
188
+ ), f(() => {
189
+ const a = () => {
190
+ const g = u ?? c.current ?? document.body;
191
+ if (!g)
192
+ return;
193
+ const { offsetWidth: b, offsetHeight: S } = g;
194
+ n((E) => E.width !== b || E.height !== S ? { width: b, height: S } : E), s((E) => {
195
+ const L = T(b);
196
+ return E.current !== L.current ? L : E;
197
+ });
198
+ }, m = () => {
199
+ const g = u ?? c.current ?? document.body;
200
+ g && (a(), d.current && d.current.disconnect(), d.current = new ResizeObserver(() => {
201
+ requestAnimationFrame(() => {
202
+ a();
203
+ });
204
+ }), d.current.observe(g));
205
+ }, p = () => {
206
+ d.current && (d.current.disconnect(), d.current = null);
207
+ };
208
+ return m(), () => {
209
+ p();
210
+ };
211
+ }, [u]), {
212
+ size: l,
213
+ breakpoint: i,
214
+ ref: c
215
+ };
216
+ }, R = (e, o = {}) => {
217
+ const { retryCount: u = 0, retryDelay: t = 1e3 } = o, [n, i] = h(!0), [s, l] = h(null), [r, c] = h(!1), [d, a] = h(0), m = v(() => {
218
+ if (!e) {
219
+ i(!1), c(!1);
220
+ return;
221
+ }
222
+ i(!0), l(null);
223
+ const g = new Image();
224
+ g.src = e, g.onload = () => {
225
+ i(!1), c(!0), l(null), a(0);
226
+ }, g.onerror = (b) => {
227
+ i(!1), c(!1), l(b), d < u && setTimeout(() => {
228
+ a((S) => S + 1);
229
+ }, t);
230
+ };
231
+ }, [e, d, u, t]);
232
+ f(() => {
233
+ m();
234
+ }, [m]);
235
+ const p = v(() => {
236
+ a(0), m();
237
+ }, [m]);
238
+ return {
239
+ loading: n,
240
+ error: s,
241
+ loaded: r,
242
+ retry: p
243
+ };
244
+ }, A = (e, o) => {
245
+ const u = y(o), [t, n] = h(() => {
246
+ if (typeof window > "u")
247
+ return o;
248
+ try {
249
+ const s = window.localStorage.getItem(e);
250
+ return s ? JSON.parse(s) : o;
251
+ } catch {
252
+ return o;
253
+ }
254
+ });
255
+ f(() => {
256
+ if (!(typeof window > "u"))
257
+ try {
258
+ const s = window.localStorage.getItem(e);
259
+ s ? n(JSON.parse(s)) : window.localStorage.setItem(e, JSON.stringify(u.current));
260
+ } catch (s) {
261
+ console.error(`Error reading localStorage key "${e}":`, s);
262
+ }
263
+ }, [e]);
264
+ const i = v(
265
+ (s) => {
266
+ try {
267
+ n((l) => {
268
+ const r = s instanceof Function ? s(l) : s;
269
+ return localStorage.setItem(e, JSON.stringify(r)), r;
270
+ });
271
+ } catch (l) {
272
+ console.error(`Error setting localStorage key "${e}":`, l);
273
+ }
274
+ },
275
+ [e]
276
+ );
277
+ return [t, i];
278
+ }, O = (e, o) => {
279
+ const u = y(e);
280
+ f(() => {
281
+ u.current = e;
282
+ }, [e]), f(() => {
283
+ let t;
284
+ function n() {
285
+ const i = u.current();
286
+ i instanceof Promise ? i.then(() => {
287
+ o && (t = setTimeout(n, o));
288
+ }) : o && (t = setTimeout(n, o));
289
+ }
290
+ if (o)
291
+ return t = setTimeout(n, o), () => t && clearTimeout(t);
292
+ }, [o]);
293
+ }, M = (e) => {
294
+ const o = y([]), u = v(
295
+ (n) => {
296
+ if (o.current[n] && (o.current[n].scrollIntoView({
297
+ behavior: "smooth",
298
+ block: "start",
299
+ inline: "start",
300
+ ...e
301
+ }), e?.offset)) {
302
+ const i = o.current[n].getBoundingClientRect().top + window.scrollY - e.offset;
303
+ window.scrollTo({
304
+ top: i,
305
+ behavior: e.behavior || "smooth"
306
+ });
307
+ }
308
+ },
309
+ [e]
310
+ ), t = v((n, i) => {
311
+ o.current[i] = n;
312
+ }, []);
313
+ return { elementRefs: o, setElementRef: t, scrollToElement: u };
314
+ }, Y = () => {
315
+ const [e, o] = h({
316
+ x: 0,
317
+ y: 0,
318
+ percent: {
319
+ x: 0,
320
+ y: 0
321
+ }
322
+ });
323
+ return f(() => {
324
+ const u = () => {
325
+ const s = window.scrollX || 0, l = window.scrollY || 0, r = /iPad|iPhone|iPod/.test(navigator.userAgent), c = window.visualViewport, d = r && c ? c.width : window.innerWidth, a = r && c ? c.height : window.innerHeight, m = Math.max(
326
+ 0,
327
+ document.documentElement.scrollWidth - d
328
+ ), p = Math.max(
329
+ 0,
330
+ document.documentElement.scrollHeight - a
331
+ ), g = m === 0 ? 0 : Math.min(100, s / m * 100), b = p === 0 ? 0 : Math.min(100, l / p * 100);
332
+ o({
333
+ x: s,
334
+ y: l,
335
+ percent: {
336
+ x: Math.floor(Math.max(0, g)),
337
+ y: Math.floor(Math.max(0, b))
338
+ }
339
+ });
340
+ };
341
+ u();
342
+ const t = () => {
343
+ u();
344
+ }, n = () => {
345
+ setTimeout(u, 100);
346
+ }, i = () => {
347
+ setTimeout(u, 50);
348
+ };
349
+ return window.addEventListener("scroll", t, { passive: !0 }), window.addEventListener("resize", n), window.addEventListener("orientationchange", n), window.visualViewport && window.visualViewport.addEventListener("resize", i), () => {
350
+ window.removeEventListener("scroll", t), window.removeEventListener("resize", n), window.removeEventListener("orientationchange", n), window.visualViewport && window.visualViewport.removeEventListener(
351
+ "resize",
352
+ i
353
+ );
354
+ };
355
+ }, []), e;
356
+ }, C = (e = {}) => {
357
+ const { isInApp: o = !1, debounce: u = 100 } = e, [t, n] = h({
358
+ width: 0,
359
+ height: 0,
360
+ offsetLeft: 0,
361
+ offsetTop: 0,
362
+ pageLeft: 0,
363
+ pageTop: 0,
364
+ scale: 1
365
+ }), i = v(() => {
366
+ const l = window.innerHeight, r = window.visualViewport?.height || l, c = document.documentElement.clientHeight, d = document.body.clientHeight;
367
+ return window.visualViewport && Math.abs(r - l) > 100 ? r : Math.max(l, c, d);
368
+ }, []), s = v(() => {
369
+ if (window.visualViewport && !o)
370
+ return window.visualViewport;
371
+ const l = window.visualViewport?.width || window.innerWidth, r = o ? i() : window.visualViewport?.height || window.innerHeight;
372
+ return {
373
+ width: l,
374
+ height: r,
375
+ offsetLeft: window.visualViewport?.offsetLeft || 0,
376
+ offsetTop: window.visualViewport?.offsetTop || 0,
377
+ pageLeft: window.scrollX ?? window.pageXOffset ?? 0,
378
+ pageTop: window.scrollY ?? window.pageYOffset ?? 0,
379
+ scale: window.visualViewport?.scale || 1
380
+ };
381
+ }, [o, i]);
382
+ return f(() => {
383
+ let l;
384
+ const r = () => {
385
+ clearTimeout(l), l = setTimeout(() => {
386
+ n(s());
387
+ }, u);
388
+ }, c = () => n(s());
389
+ c();
390
+ const d = ["resize", "orientationchange"];
391
+ o && d.push("focus", "blur", "touchstart", "touchend"), d.forEach((m) => {
392
+ m === "resize" || m === "orientationchange" ? window.addEventListener(m, r) : window.addEventListener(m, c, { passive: !0 });
393
+ }), window.visualViewport && (window.visualViewport.addEventListener("resize", c), window.visualViewport.addEventListener("scroll", c));
394
+ let a;
395
+ if (o) {
396
+ let m = s().height;
397
+ a = setInterval(() => {
398
+ const p = s().height;
399
+ Math.abs(p - m) > 50 && (m = p, c());
400
+ }, 500);
401
+ }
402
+ return () => {
403
+ clearTimeout(l), a && clearInterval(a), d.forEach((m) => {
404
+ window.removeEventListener(
405
+ m,
406
+ m === "resize" || m === "orientationchange" ? r : c
407
+ );
408
+ }), window.visualViewport && (window.visualViewport.removeEventListener("resize", c), window.visualViewport.removeEventListener("scroll", c));
409
+ };
410
+ }, [s, o, u]), t;
411
+ };
412
+ export {
413
+ z as useBodyScrollLock,
414
+ x as useDebounce,
415
+ H as useElementPosition,
416
+ I as useElementScroll,
417
+ R as useImage,
418
+ A as useLocalStorage,
419
+ O as useRecursiveTimeout,
420
+ P as useResponsiveSize,
421
+ M as useScrollToElements,
422
+ C as useViewport,
423
+ Y as useWindowScroll
424
+ };
package/dist/vite.svg ADDED
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@jbpark/use-hooks",
3
+ "version": "1.1.1",
4
+ "description": "A collection of reusable React 19 hooks for common UI and interaction patterns",
5
+ "keywords": [
6
+ "react",
7
+ "hooks",
8
+ "typescript",
9
+ "react-hooks",
10
+ "utilities",
11
+ "custom-hooks"
12
+ ],
13
+ "author": "jbpark",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/pjb0811/use-hooks.git"
18
+ },
19
+ "homepage": "https://github.com/pjb0811/use-hooks#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/pjb0811/use-hooks/issues"
22
+ },
23
+ "private": false,
24
+ "type": "module",
25
+ "main": "./dist/index.cjs",
26
+ "module": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "source": "./src/index.ts",
31
+ "types": "./dist/index.d.ts",
32
+ "import": "./dist/index.js",
33
+ "require": "./dist/index.cjs"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "peerDependencies": {
40
+ "react": "^19.1.1",
41
+ "react-dom": "^19.1.1"
42
+ },
43
+ "devDependencies": {
44
+ "@changesets/cli": "^2.29.8",
45
+ "@eslint/js": "^9.33.0",
46
+ "@trivago/prettier-plugin-sort-imports": "^5.2.2",
47
+ "@types/node": "^24.5.2",
48
+ "@types/react": "^19.1.10",
49
+ "@types/react-dom": "^19.1.7",
50
+ "@vitejs/plugin-react": "^5.0.0",
51
+ "eslint": "^9.33.0",
52
+ "eslint-plugin-react-hooks": "^5.2.0",
53
+ "eslint-plugin-react-refresh": "^0.4.20",
54
+ "globals": "^16.3.0",
55
+ "husky": "^9.1.7",
56
+ "lint-staged": "^16.1.6",
57
+ "prettier": "^3.8.0",
58
+ "prettier-plugin-classnames": "^0.8.3",
59
+ "prettier-plugin-merge": "^0.8.0",
60
+ "prettier-plugin-tailwindcss": "^0.6.14",
61
+ "react": "^19.1.1",
62
+ "react-dom": "^19.1.1",
63
+ "typescript": "~5.8.3",
64
+ "typescript-eslint": "^8.39.1",
65
+ "vite": "^7.1.2",
66
+ "vite-plugin-dts": "^4.5.4"
67
+ },
68
+ "lint-staged": {
69
+ "**/*.{js,jsx,ts,tsx,css,scss,md}": "prettier --write",
70
+ "**/*.{js,jsx,ts,tsx}": [
71
+ "bash -c 'pnpm lint'",
72
+ "bash -c 'tsc -b'"
73
+ ]
74
+ },
75
+ "scripts": {
76
+ "dev": "vite",
77
+ "build": "tsc -b && vite build",
78
+ "lint": "eslint .",
79
+ "preview": "vite preview"
80
+ }
81
+ }