@musecat/functionkit 1.0.1 → 1.0.2
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/.agents/references/components/ScrolltoTop.md +36 -0
- package/.agents/references/components/SwitchCase.md +47 -0
- package/.agents/references/components/ViewportPortal.md +34 -0
- package/.agents/references/cookie/cookie.md +47 -0
- package/.agents/references/datetime/dateTime.client.md +42 -0
- package/.agents/references/datetime/dateTime.server.md +42 -0
- package/.agents/references/datetime/dateTime.shared.md +86 -0
- package/.agents/references/hooks/useAvoidKeyboard.md +29 -0
- package/.agents/references/hooks/useCheckInvisible.md +36 -0
- package/.agents/references/hooks/useCheckScroll.md +36 -0
- package/.agents/references/hooks/useClientDateTime.md +39 -0
- package/.agents/references/hooks/useDebounce.md +46 -0
- package/.agents/references/hooks/useDebouncedCallback.md +34 -0
- package/.agents/references/hooks/useDoubleClick.md +40 -0
- package/.agents/references/hooks/useGeolocation.md +37 -0
- package/.agents/references/hooks/useHasMounted.md +27 -0
- package/.agents/references/hooks/useIntersectionObserver.md +37 -0
- package/.agents/references/hooks/useInterval.md +40 -0
- package/.agents/references/hooks/useKeyboardHeight.md +33 -0
- package/.agents/references/hooks/useKeyboardListNavigation.md +58 -0
- package/.agents/references/hooks/useLongPress.md +48 -0
- package/.agents/references/hooks/usePreservedCallback.md +33 -0
- package/.agents/references/hooks/usePreservedReference.md +34 -0
- package/.agents/references/hooks/useRefEffect.md +35 -0
- package/.agents/references/hooks/useRelativeDateTime.md +35 -0
- package/.agents/references/hooks/useTimeout.md +42 -0
- package/.agents/references/hooks/useToggleState.md +39 -0
- package/.agents/references/hooks/useViewportHeight.md +26 -0
- package/.agents/references/hooks/useViewportMatch.md +32 -0
- package/.agents/references/utils/browserStorage.md +41 -0
- package/.agents/references/utils/buildContext.md +47 -0
- package/.agents/references/utils/clipboardShare.md +59 -0
- package/.agents/references/utils/floatingMotion.md +67 -0
- package/.agents/references/utils/getDeviceInfo.md +43 -0
- package/.agents/references/utils/isEditableKeyboardTarget.md +35 -0
- package/.agents/references/utils/mergeRefs.md +29 -0
- package/.agents/references/utils/seen.md +37 -0
- package/.agents/references/utils/subscribeKeyboardHeight.md +35 -0
- package/AGENTS.md +103 -396
- package/README.md +18 -5
- package/index.ts +63 -68
- package/package.json +27 -13
- package/packages/components/ScrolltoTop.tsx +3 -3
- package/packages/components/SwitchCase.tsx +7 -7
- package/packages/components/ViewportPortal.tsx +23 -23
- package/packages/cookie/cookie.shared.ts +97 -101
- package/packages/datetime/dateTime.client.ts +86 -86
- package/packages/datetime/dateTime.server.ts +86 -86
- package/packages/datetime/dateTime.shared.ts +281 -281
- package/packages/hooks/useAvoidKeyboard.ts +15 -15
- package/packages/hooks/useCheckInvisible.ts +16 -16
- package/packages/hooks/useCheckScroll.ts +10 -10
- package/packages/hooks/useClientDateTime.ts +33 -33
- package/packages/hooks/useDebounce.ts +95 -95
- package/packages/hooks/useDebouncedCallback.ts +45 -45
- package/packages/hooks/useDoubleClick.ts +43 -43
- package/packages/hooks/useGeolocation.ts +130 -130
- package/packages/hooks/useHasMounted.ts +5 -5
- package/packages/hooks/useIntersectionObserver.ts +20 -20
- package/packages/hooks/useInterval.ts +46 -45
- package/packages/hooks/useKeyboardHeight.ts +9 -9
- package/packages/hooks/useKeyboardListNavigation.ts +156 -156
- package/packages/hooks/useLongPress.ts +98 -98
- package/packages/hooks/usePreservedCallback.ts +12 -12
- package/packages/hooks/usePreservedReference.ts +10 -10
- package/packages/hooks/useRefEffect.ts +19 -19
- package/packages/hooks/useRelativeDateTime.ts +37 -37
- package/packages/hooks/useTimeout.ts +41 -41
- package/packages/hooks/useToggleState.ts +5 -5
- package/packages/hooks/useViewportHeight.ts +13 -13
- package/packages/hooks/useViewportMatch.ts +16 -22
- package/packages/utils/browserStorage.ts +26 -26
- package/packages/utils/buildContext.tsx +10 -10
- package/packages/utils/checkDevice.ts +64 -64
- package/packages/utils/clipboardShare.tsx +31 -31
- package/packages/utils/clipboardShare.types.ts +7 -7
- package/packages/utils/floatingMotion.ts +71 -71
- package/packages/utils/keyboardTarget.ts +9 -9
- package/packages/utils/mergeRefs.ts +9 -9
- package/packages/utils/seen.ts +15 -15
- package/packages/utils/subscribeKeyboardHeight.ts +46 -46
- package/tests/components/ScrolltoTop.test.tsx +15 -0
- package/tests/components/SwitchCase.test.tsx +34 -0
- package/tests/components/ViewportPortal.test.tsx +42 -0
- package/tests/cookie/cookie.test.ts +156 -0
- package/tests/datetime/datetime.test.ts +250 -0
- package/tests/hooks/useAvoidKeyboard.test.ts +12 -0
- package/tests/hooks/useCheckInvisible.test.ts +28 -0
- package/tests/hooks/useCheckScroll.test.ts +23 -0
- package/tests/hooks/useClientDateTime.test.ts +14 -0
- package/tests/hooks/useDebounce.test.ts +71 -0
- package/tests/hooks/useDebouncedCallback.test.ts +34 -0
- package/tests/hooks/useDoubleClick.test.ts +31 -0
- package/tests/hooks/useGeolocation.test.ts +50 -0
- package/tests/hooks/useHasMounted.test.ts +10 -0
- package/tests/hooks/useIntersectionObserver.test.ts +13 -0
- package/tests/hooks/useInterval.test.ts +31 -0
- package/tests/hooks/useKeyboardHeight.test.ts +10 -0
- package/tests/hooks/useKeyboardListNavigation.test.ts +82 -0
- package/tests/hooks/useLongPress.test.ts +52 -0
- package/tests/hooks/usePreservedCallback.test.ts +35 -0
- package/tests/hooks/usePreservedReference.test.ts +35 -0
- package/tests/hooks/useRefEffect.test.ts +30 -0
- package/tests/hooks/useRelativeDateTime.test.ts +19 -0
- package/tests/hooks/useTimeout.test.ts +34 -0
- package/tests/hooks/useToggleState.test.ts +35 -0
- package/tests/hooks/useViewportHeight.test.ts +10 -0
- package/tests/hooks/useViewportMatch.test.ts +35 -0
- package/tests/setup.ts +9 -0
- package/tests/utils/browserStorage.test.ts +54 -0
- package/tests/utils/buildContext.test.tsx +27 -0
- package/tests/utils/checkDevice.test.ts +84 -0
- package/tests/utils/clipboardShare.test.ts +78 -0
- package/tests/utils/floatingMotion.test.ts +84 -0
- package/tests/utils/keyboardTarget.test.ts +44 -0
- package/tests/utils/mergeRefs.test.ts +18 -0
- package/tests/utils/seen.test.ts +31 -0
- package/tests/utils/subscribeKeyboardHeight.test.ts +50 -0
- package/tsconfig.json +16 -16
- package/vitest.config.ts +16 -0
package/AGENTS.md
CHANGED
|
@@ -1,398 +1,105 @@
|
|
|
1
1
|
# FunctionKit (@musecat/functionkit)
|
|
2
2
|
|
|
3
|
-
React 19
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
- **
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
`
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
`useToggleState(initialValue?: boolean): { value, setValue, setTrue, setFalse, toggle }`
|
|
107
|
-
- `initialValue` 기본 `false`. 모든 setter 함수는 stable (useCallback deps=[]).
|
|
108
|
-
- **용례:** 모달 열림/닫힘, 사이드바 토글, 접힘/펼침 UI.
|
|
109
|
-
|
|
110
|
-
### useKeyboardHeight / useAvoidKeyboard
|
|
111
|
-
`useKeyboardHeight(): { keyboardHeight: number }`
|
|
112
|
-
- `subscribeKeyboardHeight({ immediate: true })`로 visualViewport 변화 구독.
|
|
113
|
-
- 모바일 키보드 높이를 px 단위로 반환 (닫히면 0).
|
|
114
|
-
|
|
115
|
-
`useAvoidKeyboard({ safeAreaBottom?, transitionDuration?, transitionTimingFunction? }): { style: React.CSSProperties }`
|
|
116
|
-
- `useKeyboardHeight()`로 높이 얻어 transform: translateY로 회피. keyboardHeight=0이고 safeAreaBottom=0이면 transform 없음.
|
|
117
|
-
- transition: `200ms ease-out` 기본.
|
|
118
|
-
- **용례:** 모바일에서 입력 필드가 키보드에 가려지지 않도록 뷰포트 밀어올리기.
|
|
119
|
-
```tsx
|
|
120
|
-
const { style } = useAvoidKeyboard({ safeAreaBottom: 16 })
|
|
121
|
-
return <input style={style} />
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### useKeyboardListNavigation
|
|
125
|
-
`useKeyboardListNavigation({ itemCount }): { activeIndex, setActiveIndex, setItemRef, clickActiveItem, focusBoundaryItem, handleListKeyDown }`
|
|
126
|
-
- 방향키(ArrowDown/Up)로 리스트 탐색, Home/End로 처음/끝, Enter/Space로 활성 아이템 클릭, Escape로 선택 해제(activeIndex=-1).
|
|
127
|
-
- `itemCount=0`이면 아무 동작 안 함. `event.defaultPrevented`면 스킵. editable input에서는 동작 안 함.
|
|
128
|
-
- `setItemRef(index, element)`로 각 아이템 ref 등록. `focusBoundaryItem(key)`는 탭 네비게이션용 (첫 항목/마지막 항목 focus).
|
|
129
|
-
- `clickActiveItem()`은 현재 active 아이템의 click() 호출. 없으면 false 반환.
|
|
130
|
-
- **용례:** 드롭다운 메뉴 키보드 탐색, 검색 자동완성 리스트, 커맨드 팔레트.
|
|
131
|
-
|
|
132
|
-
### useViewportHeight
|
|
133
|
-
`useViewportHeight(): { height: number }`
|
|
134
|
-
- `window.visualViewport.height` 우선, 없으면 `window.innerHeight`. SSR에선 0.
|
|
135
|
-
- `visualViewport.resize` + `window.resize` 모두 구독.
|
|
136
|
-
- **용례:** 모바일 100vh 대체 (주소창 등으로 인한 실제 뷰포트 높이), 키보드 올라왔을 때 레이아웃 대응.
|
|
137
|
-
|
|
138
|
-
### useViewportMatch
|
|
139
|
-
`useViewportMatch(mediaQuery: string): boolean`
|
|
140
|
-
- `window.matchMedia(mediaQuery).matches` 반환. SSR에선 false.
|
|
141
|
-
- media query change 이벤트 구독 → 실시간 반영.
|
|
142
|
-
- **용례:** `const isMobile = useViewportMatch("(max-width: 768px)")` — 반응형 로직.
|
|
143
|
-
|
|
144
|
-
### useClientDateTime
|
|
145
|
-
`useClientDateTime(value: DateInput, { locale?, timeZone?, datePreset?, timePreset? }): { ready, text, date }`
|
|
146
|
-
- `ready`는 hydration guard (초기 false → effect에서 true).
|
|
147
|
-
- `formatClientDateTime()`으로 텍스트 생성. value/options 바뀔 때마다 재포맷.
|
|
148
|
-
- **용례:** 클라이언트에서만 날짜 표시 (SSR 불일치 방지).
|
|
149
|
-
|
|
150
|
-
### useRelativeDateTime
|
|
151
|
-
`useRelativeDateTime(value: DateInput, { locale?, maxRelativeDays?, fallbackDatePreset?, updateIntervalMs? }): { ready, text, isRelative, date }`
|
|
152
|
-
- `maxRelativeDays`(기본 7) 초과 시 절대 날짜(fallbackDatePreset 기본 "dot")로 표시.
|
|
153
|
-
- `updateIntervalMs`(기본 1000ms)마다 갱신. `"3분 전"`, `"2시간 전"` 등 실시간 업데이트.
|
|
154
|
-
- **용례:** 게시글 작성 시간, 댓글 시간, 채팅 메시지 timestamp.
|
|
155
|
-
|
|
156
|
-
### useRefEffect
|
|
157
|
-
`useRefEffect<RefElement>(callback, deps): (element: RefElement | null) => void`
|
|
158
|
-
- callback ref 생성. element 부착 시 callback 실행, cleanup 반환 가능. unmount 시 cleanup 실행.
|
|
159
|
-
- deps 바뀌면 old cleanup → new callback.
|
|
160
|
-
-**용례:** DOM에 직접 이벤트 리스너 부착, IntersectionObserver 연결, 외부 라이브러리 DOM 초기화.
|
|
161
|
-
```tsx
|
|
162
|
-
const ref = useRefEffect((el) => {
|
|
163
|
-
const handler = () => {}
|
|
164
|
-
el.addEventListener("scroll", handler)
|
|
165
|
-
return () => el.removeEventListener("scroll", handler)
|
|
166
|
-
}, [])
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
---
|
|
170
|
-
|
|
171
|
-
## Components — 완전 분석
|
|
172
|
-
|
|
173
|
-
### `<ScrolltoTop />`
|
|
174
|
-
`export function ScrolltoTop(): undefined`
|
|
175
|
-
- `"use client"`. useEffect로 모든 렌더 시 `window.scrollTo(0, 0)` 실행. 의존성 배열 없음 → 매 렌더마다 실행.
|
|
176
|
-
- **용례:** 페이지 이동 시 스크롤 최상단 리셋. `<ScrolltoTop />`를 layout에 한 번 배치.
|
|
177
|
-
- 주의: 모든 상태 변경 시 스크롤이 리셋되므로 조건부 삽입하거나 최상위 layout에만 배치할 것.
|
|
178
|
-
|
|
179
|
-
### `<SwitchCase />`
|
|
180
|
-
`SwitchCase<T extends string | number>({ value, cases, otherwise }): ReactNode`
|
|
181
|
-
- `"use client"` 없음 → 서버에서도 사용 가능.
|
|
182
|
-
- `cases[value] ?? otherwise`. value가 cases에 없으면 otherwise, otherwise도 없으면 null.
|
|
183
|
-
- typesafe: case key가 T로 제한됨. Partial이라 모든 경우를 다룰 필요 없음.
|
|
184
|
-
- **용례:** 조건부 렌더링을 삼항연산자 대신 선언적으로.
|
|
185
|
-
```tsx
|
|
186
|
-
<SwitchCase
|
|
187
|
-
value={status}
|
|
188
|
-
cases={{
|
|
189
|
-
loading: <Spinner />,
|
|
190
|
-
error: <ErrorMsg />,
|
|
191
|
-
success: <Content />,
|
|
192
|
-
}}
|
|
193
|
-
otherwise={<NotFound />}
|
|
194
|
-
/>
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
### `<ViewportPortal />` / `getViewportPortalRoot()`
|
|
198
|
-
- `getViewportPortalRoot()`: SSR safe. 최초 호출 시 `position:fixed;inset:0;pointer-events:none;z-index:9999` div 생성.
|
|
199
|
-
- `<ViewportPortal>`: `createPortal`로 자식들을 위 div에 렌더. useEffect로 root 연결 전까진 null 반환 (hydration safe).
|
|
200
|
-
- pointer-events:none이라 portal 내부 요소는 별도로 pointer-events:auto 필요.
|
|
201
|
-
- **용례:** 모달/툴팁/드롭다운을 DOM 트리 최상단에 렌더링 (z-index 충돌 방지, overflow:hidden 탈출).
|
|
202
|
-
```tsx
|
|
203
|
-
<ViewportPortal>
|
|
204
|
-
<div style={{ pointerEvents: "auto" }}>
|
|
205
|
-
<Modal />
|
|
206
|
-
</div>
|
|
207
|
-
</ViewportPortal>
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
---
|
|
211
|
-
|
|
212
|
-
## Utils — 완전 분석
|
|
213
|
-
|
|
214
|
-
### browserStorage
|
|
215
|
-
`getLocalStorage(key): unknown` / `updateLocalStorage(key, value): void` / `removeLocalStorage(key): void`
|
|
216
|
-
- 동일한 API로 SessionStorage 버전도 있음: `getSessionStorage`, `updateSessionStorage`, `removeSessionStorage`.
|
|
217
|
-
- 내부적으로 SSR 체크 후 JSON.stringify/parse. 파싱 실패, 용량 초과 등 모든 에러 조용히 처리 (try-catch).
|
|
218
|
-
- 저장할 때 직접 `JSON.stringify`, 불러올 때 `JSON.parse`. 값이 없거나 파싱 실패 시 null.
|
|
219
|
-
- **용례:** 사용자 설정 유지 (테마, 언어), 최근 검색어 저장.
|
|
220
|
-
```tsx
|
|
221
|
-
updateLocalStorage("theme", "dark")
|
|
222
|
-
const theme = getLocalStorage("theme") // "dark"
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### buildContext
|
|
226
|
-
`buildContext<T>(defaultValue): [Provider, useValue]`
|
|
227
|
-
- `Provider`: value를 `useMemo`로 감싸 불필요한 리렌더 방지.
|
|
228
|
-
- `useValue`: 단순 `useContext(context)`. Provider 없이 호출해도 에러 안 남 (defaultValue 반환).
|
|
229
|
-
- createContext를 직접 쓰는 것보다 안전하고 간결.
|
|
230
|
-
- **용례:** 전역 상태 관리가 필요할 때 Context Provider 패턴을 간단히.
|
|
231
|
-
```tsx
|
|
232
|
-
const [ThemeProvider, useTheme] = buildContext("light")
|
|
233
|
-
// <ThemeProvider value="dark">...</ThemeProvider>
|
|
234
|
-
// const theme = useTheme() // "dark"
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
### getDeviceInfo
|
|
238
|
-
`getDeviceInfo(): DeviceInfo`
|
|
239
|
-
- 반환: `{ isMobile, isIOS, isAndroid, isSafari, isIOSSafari, isMacSafari, isSamsungBrowser, isTouchDevice, browser }`
|
|
240
|
-
- SSR: window 없으면 모든 값 false. `isTouchDevice`는 `"ontouchstart" in window` 체크.
|
|
241
|
-
- browser: samsung > safari > chrome > firefox > edge > unknown 순서로 판별.
|
|
242
|
-
- UA로 판별하므로 User-Agent 스푸핑에 취약하지만 실제 환경에선 충분히 정확.
|
|
243
|
-
- **용례:** 모바일 웹에서 iOS/Android 분기 처리, Safari 전용 스타일/로직.
|
|
244
|
-
|
|
245
|
-
### NavigatorClipboard / NavigatorShare
|
|
246
|
-
`NavigatorClipboard({ text }): Promise<{ success: boolean }>` — `navigator.clipboard.writeText` 호출.
|
|
247
|
-
`NavigatorShare({ title, text, link }): Promise<{ success: boolean, method: "share" | "clipboard" | "unsupported" }>`
|
|
248
|
-
- `navigator.share()` 우선 시도. 사용자 취소(AbortError)면 `{ success: false, method: "share" }`.
|
|
249
|
-
- share 실패 시 clipboard fallback. clipboard도 없으면 `{ method: "unsupported" }`.
|
|
250
|
-
- **용례:** 공유 버튼 → 모바일 네이티브 Share Sheet, 데스크톱에선 링크 복사.
|
|
251
|
-
```tsx
|
|
252
|
-
if (result.success && result.method === "share") trackShared()
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
### floatingMotion
|
|
256
|
-
`getFloatingMotionPreset(mode): { enterMs, exitMs, ease }`
|
|
257
|
-
|
|
258
|
-
| mode | enterMs | exitMs | ease |
|
|
259
|
-
|---|---|---|---|
|
|
260
|
-
| anchored | 230 | 170 | cubic-bezier(0.16, 1, 0.3, 1) |
|
|
261
|
-
| center-selected | 190 | 140 | cubic-bezier(0.2, 0, 0, 1) |
|
|
262
|
-
| modal-center | 340 | 260 | cubic-bezier(0.22, 1, 0.36, 1) |
|
|
263
|
-
| mobile-sheet | 360 | 280 | cubic-bezier(0.2, 0.8, 0.2, 1) |
|
|
264
|
-
|
|
265
|
-
`getFloatingTransformOrigin(placement?): string` — placement("bottom-left" 등)를 CSS transform-origin으로 변환. top→bottom, left→left, bottom→top, center→center. 기본값 "top center".
|
|
266
|
-
`getFloatingHiddenTransform({ mode, placement }): string` — hidden 상태의 transform 값.
|
|
267
|
-
- mobile-sheet: translateY(1.8rem) scale(1)
|
|
268
|
-
- modal-center: translateY(.8rem) scale(.94)
|
|
269
|
-
- center-selected: translateY(.45rem) scale(.99)
|
|
270
|
-
- anchored: placement의 위/아래/좌우에 따라 translateY 또는 translateX
|
|
271
|
-
|
|
272
|
-
**용례:** Floating UI(focus-trap, portal, auto-update)와 함께 사용하여 드롭다운/모달/시트의 enter/exit 애니메이션 구성. framer-motion의 initial/exit에 이 값들 전달.
|
|
273
|
-
|
|
274
|
-
### isEditableKeyboardTarget
|
|
275
|
-
`isEditableKeyboardTarget(target: HTMLElement): boolean`
|
|
276
|
-
- true 반환 조건: `contentEditable === "true"`, `TEXTAREA`, `SELECT`, `INPUT`(type이 hidden/file/submit/reset 등이 아닌 경우).
|
|
277
|
-
- 키보드 이벤트 핸들러에서 `isEditableKeyboardTarget(event.target)`로 체크 → 입력 중에는 단축키 동작 막기.
|
|
278
|
-
- **용례:** `onKeyDown`에서 방향키/Enter 이벤트를 입력 필드에선 무시.
|
|
279
|
-
|
|
280
|
-
### mergeRefs
|
|
281
|
-
`mergeRefs<T>(...refs: (Ref<T> | undefined | null)[]): RefCallback<T>`
|
|
282
|
-
- 함수 ref는 직접 호출, 객체 ref는 `.current` 할당. null/undefined는 안전하게 무시.
|
|
283
|
-
- **용례:** 외부 ref와 내부 ref를 동시에 한 요소에 연결.
|
|
284
|
-
```tsx
|
|
285
|
-
const internalRef = useRef(null)
|
|
286
|
-
const externalRef = useRef(null)
|
|
287
|
-
return <div ref={mergeRefs(internalRef, externalRef)} />
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
### seen
|
|
291
|
-
`SEEN_STORAGE_KEY = "seen"`
|
|
292
|
-
`parseSeen(raw: string | null): Record<string, boolean>` — JSON 파싱, 실패 시 `{}`.
|
|
293
|
-
`hasSeenKey(raw: string | null, key: string): boolean` — 특정 키를 본 적 있는지.
|
|
294
|
-
`buildSeenValue(raw: string | null, key: string): string` — 키를 seen에 추가한 JSON 문자열 반환.
|
|
295
|
-
- 모두 순수 함수, SSR 안전. localStorage에 "seen" 키로 저장하는 패턴 전제.
|
|
296
|
-
- **용례:** 공지사항/업데이트 알림 "다시 보지 않음" 체크, onboarding 진행 상태, 읽은 글 목록 관리.
|
|
297
|
-
```tsx
|
|
298
|
-
const raw = getLocalStorage(SEEN_STORAGE_KEY)
|
|
299
|
-
if (!hasSeenKey(raw, noticeId)) {
|
|
300
|
-
updateLocalStorage(SEEN_STORAGE_KEY, buildSeenValue(raw, noticeId))
|
|
301
|
-
showNotice()
|
|
302
|
-
}
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
### subscribeKeyboardHeight
|
|
306
|
-
`subscribeKeyboardHeight({ callback, immediate?, throttleMs? }): { unsubscribe }`
|
|
307
|
-
- `visualViewport.resize`/`scroll` 구독. 높이 계산: `window.innerHeight - visualViewport.height` (0 이상 clamp).
|
|
308
|
-
- throttleMs 기본 16ms(≈60fps). 0 이하로 설정하면 동기 실행.
|
|
309
|
-
- `immediate: true`면 구독 즉시 callback 실행.
|
|
310
|
-
- **용례:** `useKeyboardHeight()` 훅이 내부적으로 사용. 직접 구독이 필요한 경우 (React 외부에서 DOM 직접 조작).
|
|
311
|
-
|
|
312
|
-
---
|
|
313
|
-
|
|
314
|
-
## Cookie — 완전 분석
|
|
315
|
-
|
|
316
|
-
### cookie.shared.ts
|
|
317
|
-
모든 함수에 SSR guard 있음 → 서버에선 undefined/null 반환 또는 no-op.
|
|
318
|
-
|
|
319
|
-
`getClientCookie(name): string | undefined` — `document.cookie` 파싱하여 value 반환. 없으면 undefined.
|
|
320
|
-
|
|
321
|
-
`setClientCookie(name, value, days?): void` — `path=/`, URL 인코딩. days 생략 시 session cookie. `document.cookie = name=value; path=/; max-age=...`.
|
|
322
|
-
|
|
323
|
-
`clearClientCookie(name, { hostname?, path?, documentRef?, cookieStore? }): void` — 과거 만료일로 설정하여 삭제. 모든 서브도메인, 경로 조합을 순회하며 삭제. cookieStore가 있으면 우선 사용.
|
|
324
|
-
|
|
325
|
-
`clearAllClientCookies({ hostname?, path?, documentRef?, cookieStore?, includeRoot?, cookieString? }): string[]`
|
|
326
|
-
- hostname의 서브도메인 조합을 모두 생성하여 각각의 모든 경로(현재 path + includeRoot면 "/")에 대해 쿠키 삭제 시도.
|
|
327
|
-
- cookieStore가 있으면 `.delete()` 사용.
|
|
328
|
-
- 삭제된 쿠키 이름 배열 반환.
|
|
329
|
-
|
|
330
|
-
`parseClientCookieNames(cookieString: string): string[]` — "name=value; name2=value2" → ["name", "name2"].
|
|
331
|
-
**용례:** 로그인 토큰 관리, 사용자 설정 쿠키, A/B 테스트 배리언트 저장.
|
|
332
|
-
|
|
333
|
-
---
|
|
334
|
-
|
|
335
|
-
## DateTime — 완전 분석
|
|
336
|
-
|
|
337
|
-
### Shared (dateTime.shared.ts) — 순수 함수, 서버 안전
|
|
338
|
-
|
|
339
|
-
**타입:**
|
|
340
|
-
- `AppLocale = "kr" | "en" | "jp"`
|
|
341
|
-
- `DateInput = string | number | Date`
|
|
342
|
-
- `DatePreset = "long" | "dot"`
|
|
343
|
-
- `TimePreset = "ko" | "12h" | "24h-minute" | "24h-second"`
|
|
344
|
-
|
|
345
|
-
**유틸:**
|
|
346
|
-
- `toDate(value): Date | null` — Date 인스턴스면 복사 후 반환, 아니면 `new Date(value)`. 유효성 검사 후 null 가능.
|
|
347
|
-
- `toUtcMidnight(date): Date` — UTC 기준 해당일 00:00:00.000Z.
|
|
348
|
-
- `parseUtcDateInput(value?, fallback?): Date | null` — "YYYY-MM-DD" 문자열은 timezone 없이 UTC date로 파싱. 다른 형식은 toDate → toUtcMidnight.
|
|
349
|
-
- `addUtcDays(date, days): Date` — 새 Date 반환. `date.setUTCDate(date.getUTCDate() + days)`.
|
|
350
|
-
- `formatUtcDateKey(date): string` — "YYYY-MM-DD" UTC.
|
|
351
|
-
- `getUtcWeekdayIndex(date): number` — 0(일)~6(토) UTC.
|
|
352
|
-
- `normalizeAppLocale(locale?): AppLocale` — "ko"/"kr" → "kr", "ja"/"jp" → "jp", 나머지 → "en".
|
|
353
|
-
- `toIntlLocale(locale?): string` — "ko-KR" / "ja-JP" / "en-US".
|
|
354
|
-
|
|
355
|
-
**날짜 포맷:**
|
|
356
|
-
- `formatLongDate(date, locale?, timeZone?): string` — kr: "2024년 1월 1일", jp: "2024年1月1日", en: "January 1, 2024". Intl.DateTimeFormat.formatToParts로 커스텀 포맷.
|
|
357
|
-
- `formatDotDate(date, timeZone?): string` — "2024. 1. 1."
|
|
358
|
-
|
|
359
|
-
**시간 포맷:**
|
|
360
|
-
- `format24HourTime(date, { includeSeconds?, timeZone? }): string` — "14:30" / "14:30:00"
|
|
361
|
-
- `formatKoreanTime(date, timeZone?): string` — "오후 2시 30분"
|
|
362
|
-
- `formatTwelveHourTime(date, locale?, timeZone?): string` — kr: "오후 2:30", en/jp: Intl.DateTimeFormat hour12.
|
|
363
|
-
|
|
364
|
-
**상대 시간:**
|
|
365
|
-
- `formatRelativeText(diffMs, locale?): { text, isRelative }` — diffMs를 "N초 전", "N분 전", "N시간 전", "N일 전"(모든 locale)로 변환. diffMs는 0 이하로 clamp. 카운트는 최소 1.
|
|
366
|
-
- `formatRemainingText(diffMs, locale?, { includeSuffix? }): string` — 최대 2개 단위 조합. "2일 3시간 남음". diffMs=0이면 "마감됨" / "締切終了" / "Closed".
|
|
367
|
-
|
|
368
|
-
**용례 (공통 패턴):**
|
|
369
|
-
```tsx
|
|
370
|
-
// UTC date 키 생성
|
|
371
|
-
const dateKey = formatUtcDateKey(toUtcMidnight(new Date()))
|
|
372
|
-
// 상대 시간
|
|
373
|
-
const { text } = formatRelativeText(Date.now() - post.createdAt.getTime())
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
### Client (dateTime.client.ts) — "use client" 필요
|
|
377
|
-
|
|
378
|
-
`formatClientDate(value, { locale?, timeZone?, preset }): string` — preset으로 "long"/"dot" 선택. 유효하지 않은 날짜는 "" 반환.
|
|
379
|
-
`formatClientTime(value, { locale?, timeZone?, preset }): string` — "ko"/"12h"/"24h-minute"/"24h-second".
|
|
380
|
-
`formatClientDateTime(value, { locale?, timeZone?, datePreset?, timePreset? }): string` — date + " " + time. 기본 datePreset "long", timePreset "24h-minute". 둘 중 하나가 빈 문자열이면 다른 쪽만 반환.
|
|
381
|
-
`formatClientRelative(value, { locale?, now?, maxRelativeDays?, fallbackDatePreset? }): { text, isRelative }` — maxRelativeDays(기본 7) 초과 시 절대 날짜(fallbackDatePreset 기본 "dot"). 날짜 유효성 검사, 실패 시 `{ text: "", isRelative: false }`.
|
|
382
|
-
|
|
383
|
-
### Server (dateTime.server.ts) — 서버 렌더링용
|
|
384
|
-
`formatServerDate`, `formatServerTime`, `formatServerDateTime`, `formatServerRelative` — client와 동일한 로직. 차이점: `formatServerRelative`의 `now` 기본값이 `new Date()` (client는 `Date.now()`).
|
|
385
|
-
|
|
386
|
-
**datetime 선택 가이드:**
|
|
387
|
-
- 서버 컴포넌트에서 날짜 표시 → shared 함수 사용. timezone은 서버 timezone 기준.
|
|
388
|
-
- 클라이언트 hydration 후에도 계속 표시 → shared 함수. SSR/클라이언트 일관성 보장.
|
|
389
|
-
- 클라이언트 로케일/timezone 반영 → client 함수. "use client" 필요.
|
|
390
|
-
- 서버에서 렌더링된 HTML 그대로 → server 함수.
|
|
391
|
-
- 상대 시간(실시간 갱신) → `useRelativeDateTime` 훅.
|
|
392
|
-
|
|
393
|
-
**전체 datetime 함수 로직 요약:**
|
|
394
|
-
1. `toDate(value)`로 DateInput → Date | null 변환 (유효성 검사)
|
|
395
|
-
2. 선택적으로 `toUtcMidnight`로 UTC 자정 정규화
|
|
396
|
-
3. `normalizeAppLocale(locale)`로 AppLocale 결정
|
|
397
|
-
4. `Intl.DateTimeFormat.formatToParts` 또는 `Intl.DateTimeFormat.format`으로 포맷
|
|
398
|
-
5. 한국어/일본어는 커스텀 포맷팅 (toParts로 각 컴포넌트 추출 후 조립), 영어는 Intl 네이티브 포맷 사용
|
|
3
|
+
React 19 frontend utility library. Single package, ESM.
|
|
4
|
+
|
|
5
|
+
## Import Convention Enforcement (Mandatory)
|
|
6
|
+
|
|
7
|
+
- **Barrel imports are the rule:** `import { useDebounce } from "@musecat/functionkit"`.
|
|
8
|
+
- **Subpath imports are only allowed when:** the export is not in the barrel (`useCheckInvisible`, `SwitchCase`, `ScrolltoTop`) or tree-shaking is absolutely required.
|
|
9
|
+
- **Type imports also go through the barrel:** `import type { AppLocale, DateInput } from "@musecat/functionkit"`.
|
|
10
|
+
- **No reimplementation:** If FunctionKit already provides a feature, reuse it. Never write a duplicate.
|
|
11
|
+
|
|
12
|
+
## SSR Safety Classification
|
|
13
|
+
|
|
14
|
+
| Category | Condition | Examples |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| **SSR-safe** | No `"use client"`, pure function | `formatLongDate`, `mergeRefs`, `SwitchCase`, `getDeviceInfo`, `floatingMotion`, `seen`, `parseClientCookieNames`, `debounce` |
|
|
17
|
+
| **Client-only** | `"use client"` or `window` access | All hooks, `ViewportPortal`, `ScrolltoTop`, `browserStorage`, `cookie` (except `parseClientCookieNames`), `clipboardShare`, `isEditableKeyboardTarget` |
|
|
18
|
+
|
|
19
|
+
SSR-safe functions can be used freely in Server Components, RSC, and Client Components. Client-only items must only be used in `"use client"` components.
|
|
20
|
+
|
|
21
|
+
## Barrel Export Mapping
|
|
22
|
+
|
|
23
|
+
The barrel (`index.ts`) exports 63 named exports. **Items NOT in the barrel (require subpath imports):**
|
|
24
|
+
- `SwitchCase` — component subpath
|
|
25
|
+
- `ScrolltoTop` — component subpath
|
|
26
|
+
- `useCheckInvisible` — `@musecat/functionkit/hooks/useCheckInvisible`
|
|
27
|
+
- `useCheckScroll` — `@musecat/functionkit/hooks/useCheckScroll`
|
|
28
|
+
|
|
29
|
+
When adding new features, register them in the barrel.
|
|
30
|
+
|
|
31
|
+
## Hook Selection Guidelines
|
|
32
|
+
|
|
33
|
+
| Scenario | Hook to Use |
|
|
34
|
+
|---|---|
|
|
35
|
+
| Debounce rapid state changes | `useDebounce` or `useDebouncedCallback` |
|
|
36
|
+
| Polling / repeated execution | `useInterval` |
|
|
37
|
+
| Delayed single execution | `useTimeout` |
|
|
38
|
+
| Keyboard list navigation | `useKeyboardListNavigation` |
|
|
39
|
+
| Long press gesture | `useLongPress` |
|
|
40
|
+
| Single/double click distinction | `useDoubleClick` |
|
|
41
|
+
| Geolocation tracking | `useGeolocation` |
|
|
42
|
+
| Client date formatting | `useClientDateTime` |
|
|
43
|
+
| Relative time ("3 minutes ago") | `useRelativeDateTime` |
|
|
44
|
+
| IntersectionObserver | `useIntersectionObserver` |
|
|
45
|
+
| Keyboard avoidance (mobile) | `useAvoidKeyboard` |
|
|
46
|
+
| Viewport height | `useViewportHeight` |
|
|
47
|
+
| Media query matching | `useViewportMatch` |
|
|
48
|
+
| Scroll position detection | `useCheckScroll` (subpath) |
|
|
49
|
+
| Element visibility detection | `useCheckInvisible` (subpath) |
|
|
50
|
+
| Toggle state | `useToggleState` |
|
|
51
|
+
| Hydration guard | `useHasMounted` |
|
|
52
|
+
| Stable callback reference | `usePreservedCallback` |
|
|
53
|
+
| Deep equality reference | `usePreservedReference` |
|
|
54
|
+
| Ref lifecycle | `useRefEffect` |
|
|
55
|
+
|
|
56
|
+
## Component Selection Guidelines
|
|
57
|
+
|
|
58
|
+
- **SwitchCase** — Prefer over inline &&/ternary for multi-branch rendering. SSR-safe.
|
|
59
|
+
- **ViewportPortal** — Fixed-position overlays, modals, toasts. Client-only.
|
|
60
|
+
- **ScrolltoTop** — Scroll reset on page navigation. Client-only.
|
|
61
|
+
|
|
62
|
+
Never write raw `createContext` + `Provider` + `useContext` boilerplate. Use `buildContext` instead.
|
|
63
|
+
|
|
64
|
+
## DateTime Formatting Guidelines
|
|
65
|
+
|
|
66
|
+
| Context | Use | SSR-safe |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| Server static date | `formatServerDate` / `formatServerDateTime` | Yes |
|
|
69
|
+
| Client date | `formatClientDate` / `formatClientDateTime` | No |
|
|
70
|
+
| Relative time ("3분 전") | `formatServerRelative` (server) / `formatClientRelative` (client) | Mixed |
|
|
71
|
+
| Remaining time ("2일 3시간 남음") | `formatRemainingText` | Yes |
|
|
72
|
+
| UTC key ("YYYY-MM-DD") | `formatUtcDateKey` / `parseUtcDateInput` | Yes |
|
|
73
|
+
| Raw formatters | `formatLongDate`, `formatDotDate`, `formatKoreanTime` etc. | Yes |
|
|
74
|
+
|
|
75
|
+
**Locale handling:** `DateInput` accepts `string | number | Date`. Use `normalizeAppLocale("ko")` → `"kr"`, `toIntlLocale("kr")` → `"ko-KR"`.
|
|
76
|
+
|
|
77
|
+
## Cookie & Storage API Guidelines
|
|
78
|
+
|
|
79
|
+
| Operation | API | SSR-safe |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| Read cookie | `getClientCookie(name)` | No |
|
|
82
|
+
| Write cookie | `setClientCookie(name, value, days?)` | No |
|
|
83
|
+
| Delete cookie | `clearClientCookie(name)` / `clearAllClientCookies()` | No |
|
|
84
|
+
| Parse cookie names | `parseClientCookieNames(cookieString)` | **Yes** |
|
|
85
|
+
| localStorage | `getLocalStorage` / `updateLocalStorage` / `removeLocalStorage` | No |
|
|
86
|
+
| sessionStorage | `getSessionStorage` / `updateSessionStorage` / `removeSessionStorage` | No |
|
|
87
|
+
|
|
88
|
+
`clearAllClientCookies` tries all domain/path combinations — use with caution.
|
|
89
|
+
|
|
90
|
+
## Context & Utility Patterns
|
|
91
|
+
|
|
92
|
+
- **`buildContext`:** Creates `createContext` + Provider + `useContext` in one call. `const [useMyCtx, MyProvider] = buildContext<MyType>()`.
|
|
93
|
+
- **`mergeRefs`:** Merges multiple refs into a single callback ref. Use when forwarding refs.
|
|
94
|
+
- **`getDeviceInfo`:** UA-based device detection. Callable at module scope. Returns `isMobile`, `isIOS`, `isAndroid`, etc.
|
|
95
|
+
- **`isEditableKeyboardTarget`:** Checks if a key event target is an editable element. Use before keyboard navigation.
|
|
96
|
+
- **`NavigatorClipboard` / `NavigatorShare`:** Clipboard copy/share with automatic fallback on failure.
|
|
97
|
+
- **`floatingMotion`:** `getFloatingMotionPreset` for tooltip/popover/modal animation configs. 4 modes (anchored, center-selected, modal-center, mobile-sheet).
|
|
98
|
+
- **`seen`:** `hasSeenKey` / `buildSeenValue` for "user has already seen" state management. SSR-safe.
|
|
99
|
+
- **`subscribeKeyboardHeight`:** Low-level keyboard height change detection. Used internally by `useKeyboardHeight`.
|
|
100
|
+
|
|
101
|
+
## Documentation Maintenance
|
|
102
|
+
|
|
103
|
+
- When hooks, components, or utils are added, modified, or removed, update the corresponding `.agents/references/` documentation.
|
|
104
|
+
- When the barrel (`index.ts`) export list changes, update the Barrel Export Mapping section above.
|
|
105
|
+
- Before using any feature, read the corresponding `.agents/references/` doc first.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @musecat/functionkit
|
|
2
2
|
|
|
3
|
-
React 19
|
|
3
|
+
A React 19 + TypeScript UI F.E. utility library.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -10,16 +10,29 @@ npm install @musecat/functionkit
|
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
|
-
모든 훅, 컴포넌트, 유틸리티는 배럴 파일을 통해 직접 임포트하여 사용할 수 있습니다.
|
|
14
|
-
|
|
15
13
|
```tsx
|
|
16
14
|
import { useDebounce, formatClientDateTime } from "@musecat/functionkit";
|
|
15
|
+
|
|
16
|
+
export default function Example() {
|
|
17
|
+
const handleSearch = useDebounce((term: string) => fetchResults(term), 300);
|
|
18
|
+
const now = formatClientDateTime(new Date(), {
|
|
19
|
+
datePreset: "long",
|
|
20
|
+
timePreset: "24h-minute",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<View column gap={24}>
|
|
25
|
+
<Input onChange={(e) => handleSearch(e.target.value)} />
|
|
26
|
+
<Text type="Caption1">{now}</Text>
|
|
27
|
+
</View>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
17
30
|
```
|
|
18
31
|
|
|
19
32
|
## Acknowledgements
|
|
20
33
|
|
|
21
|
-
|
|
34
|
+
- [toss/react-simplikit](https://github.com/toss/react-simplikit/)
|
|
22
35
|
|
|
23
36
|
## License
|
|
24
37
|
|
|
25
|
-
MIT License © Musecat Team.
|
|
38
|
+
[MIT License](./LICENSE) © Musecat Team.
|