@musecat/functionkit 1.0.1 → 1.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/.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 +41 -0
- package/.agents/references/hooks/useInterval.md +44 -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 +37 -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/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +22 -0
- package/.github/workflows/ci.yml +20 -0
- package/.local/state/gh/device-id +1 -0
- package/.prettierrc +8 -0
- package/AGENTS.md +117 -396
- package/CODE_OF_CONDUCT.md +55 -0
- package/CONTRIBUTING.md +39 -0
- package/README.md +18 -5
- package/biome.json +25 -0
- package/index.ts +63 -68
- package/package.json +46 -13
- package/packages/components/ScrolltoTop.tsx +3 -3
- package/packages/components/SwitchCase.tsx +7 -7
- package/packages/components/ViewportPortal.tsx +22 -23
- package/packages/cookie/cookie.shared.ts +80 -110
- package/packages/cookie/cookieNames.shared.ts +9 -0
- package/packages/datetime/dateTime.client.ts +84 -86
- package/packages/datetime/dateTime.server.ts +84 -86
- package/packages/datetime/dateTime.shared.ts +249 -287
- 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 +24 -37
- package/packages/hooks/useDebounce.ts +90 -95
- package/packages/hooks/useDebouncedCallback.ts +45 -45
- package/packages/hooks/useDoubleClick.ts +43 -43
- package/packages/hooks/useGeolocation.ts +126 -130
- package/packages/hooks/useHasMounted.ts +5 -5
- package/packages/hooks/useIntersectionObserver.ts +20 -20
- package/packages/hooks/useInterval.ts +45 -45
- package/packages/hooks/useKeyboardHeight.ts +9 -9
- package/packages/hooks/useKeyboardListNavigation.ts +130 -158
- package/packages/hooks/useLongPress.ts +87 -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 +36 -39
- package/packages/hooks/useTimeout.ts +41 -41
- package/packages/hooks/useToggleState.ts +5 -5
- package/packages/hooks/useViewportHeight.ts +12 -13
- package/packages/hooks/useViewportMatch.ts +13 -23
- package/packages/utils/browserStorage.ts +26 -26
- package/packages/utils/buildContext.tsx +10 -11
- 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 -77
- 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 +49 -46
- package/tests/components/ScrolltoTop.test.tsx +15 -0
- package/tests/components/SwitchCase.test.tsx +33 -0
- package/tests/components/ViewportPortal.test.tsx +46 -0
- package/tests/cookie/cookie.test.ts +192 -0
- package/tests/datetime/datetime.test.ts +461 -0
- package/tests/hooks/useAvoidKeyboard.test.ts +43 -0
- package/tests/hooks/useCheckInvisible.test.ts +59 -0
- package/tests/hooks/useCheckScroll.test.ts +23 -0
- package/tests/hooks/useClientDateTime.test.ts +12 -0
- package/tests/hooks/useDebounce.test.ts +96 -0
- package/tests/hooks/useDebouncedCallback.test.ts +63 -0
- package/tests/hooks/useDoubleClick.test.ts +82 -0
- package/tests/hooks/useGeolocation.test.ts +201 -0
- package/tests/hooks/useHasMounted.test.ts +10 -0
- package/tests/hooks/useIntersectionObserver.test.ts +78 -0
- package/tests/hooks/useInterval.test.ts +56 -0
- package/tests/hooks/useKeyboardHeight.test.ts +10 -0
- package/tests/hooks/useKeyboardListNavigation.test.ts +404 -0
- package/tests/hooks/useLongPress.test.ts +214 -0
- package/tests/hooks/usePreservedCallback.test.ts +33 -0
- package/tests/hooks/usePreservedReference.test.ts +42 -0
- package/tests/hooks/useRefEffect.test.ts +67 -0
- package/tests/hooks/useRelativeDateTime.test.ts +21 -0
- package/tests/hooks/useTimeout.test.ts +72 -0
- package/tests/hooks/useToggleState.test.ts +43 -0
- package/tests/hooks/useViewportHeight.test.ts +32 -0
- package/tests/hooks/useViewportMatch.test.ts +35 -0
- package/tests/setup.ts +9 -0
- package/tests/utils/browserStorage.test.ts +114 -0
- package/tests/utils/buildContext.test.tsx +26 -0
- package/tests/utils/checkDevice.test.ts +140 -0
- package/tests/utils/clipboardShare.test.ts +152 -0
- package/tests/utils/floatingMotion.test.ts +135 -0
- package/tests/utils/keyboardTarget.test.ts +85 -0
- package/tests/utils/mergeRefs.test.ts +50 -0
- package/tests/utils/seen.test.ts +40 -0
- package/tests/utils/subscribeKeyboardHeight.test.ts +172 -0
- package/tsconfig.json +16 -16
- package/vitest.config.ts +22 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# useIntersectionObserver
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Provides a convenient ref callback pattern for `IntersectionObserver`. Calls a callback when the observed element's intersection status changes.
|
|
6
|
+
|
|
7
|
+
## Usage Logic
|
|
8
|
+
|
|
9
|
+
Uses `useRefEffect` internally to attach the observer when the ref receives an element and clean up when it unmounts. Accepts standard `IntersectionObserverInit` options. The callback is stabilized with `usePreservedCallback`.
|
|
10
|
+
|
|
11
|
+
## Type Signature
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
function useIntersectionObserver(
|
|
15
|
+
callback: IntersectionObserverCallback,
|
|
16
|
+
options?: IntersectionObserverInit
|
|
17
|
+
): (node: HTMLElement | null) => void;
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Returns a callback ref to assign to the target element.
|
|
21
|
+
|
|
22
|
+
## Example Code
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { useIntersectionObserver } from "@musecat/functionkit";
|
|
26
|
+
|
|
27
|
+
function LazyImage() {
|
|
28
|
+
const ref = useIntersectionObserver(
|
|
29
|
+
([entry]) => {
|
|
30
|
+
if (entry.isIntersecting) loadImage();
|
|
31
|
+
},
|
|
32
|
+
{ threshold: 0.1 }
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return <div ref={ref}>...</div>;
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Trade-off
|
|
40
|
+
|
|
41
|
+
The `options` object is used as a `useMemo` dependency. If you pass an inline object (`{ threshold: 0.5 }`) on every render, the `IntersectionObserver` is recreated each time. Memoize options with `useMemo` or define them outside the component for stable references.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# useInterval
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
A `setInterval` wrapper hook that handles lifecycle cleanup and provides toggle control. Ideal for polling, timers, or any repeated execution.
|
|
6
|
+
|
|
7
|
+
## Usage Logic
|
|
8
|
+
|
|
9
|
+
Starts the interval immediately on mount (unless `enabled: false` is passed). The interval pauses when `delay` is `null` or when `enabled` is `false`. Supports `immediate` mode — fires the callback once immediately before the first interval tick.
|
|
10
|
+
|
|
11
|
+
## Type Signature
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
function useInterval(
|
|
15
|
+
callback: () => void,
|
|
16
|
+
delay: number | null,
|
|
17
|
+
options?: {
|
|
18
|
+
immediate?: boolean;
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
}
|
|
21
|
+
): void;
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Example Code
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { useInterval } from "@musecat/functionkit";
|
|
28
|
+
|
|
29
|
+
function PollingComponent() {
|
|
30
|
+
const [data, setData] = useState(null);
|
|
31
|
+
|
|
32
|
+
useInterval(
|
|
33
|
+
() => fetch("/api/status").then(setData),
|
|
34
|
+
5000,
|
|
35
|
+
{ immediate: true }
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return <div>{data}</div>;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Trade-off
|
|
43
|
+
|
|
44
|
+
Internally uses `usePreservedCallback` to stabilize the callback. The callback always reflects the latest closure, but during the render-to-effect gap a stale version could theoretically fire. In practice this is never an issue since interval callbacks fire asynchronously after paint. See `usePreservedCallback` trade-off for details.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# useKeyboardHeight
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Returns the current virtual keyboard height in pixels by subscribing to `visualViewport` changes. Useful for adjusting layout when the keyboard opens on mobile.
|
|
6
|
+
|
|
7
|
+
## Usage Logic
|
|
8
|
+
|
|
9
|
+
Internally uses `subscribeKeyboardHeight` which listens to `visualViewport.resize` and `visualViewport.scroll` events. Computes the height as the difference between the full viewport and the visual viewport.
|
|
10
|
+
|
|
11
|
+
## Type Signature
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
function useKeyboardHeight(): number;
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Returns the keyboard height in pixels (0 when the keyboard is closed).
|
|
18
|
+
|
|
19
|
+
## Example Code
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { useKeyboardHeight } from "@musecat/functionkit";
|
|
23
|
+
|
|
24
|
+
function ChatLayout() {
|
|
25
|
+
const kbHeight = useKeyboardHeight();
|
|
26
|
+
return (
|
|
27
|
+
<div style={{ paddingBottom: kbHeight }}>
|
|
28
|
+
<MessageList />
|
|
29
|
+
<InputBar />
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# useKeyboardListNavigation
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Adds keyboard-based navigation (ArrowUp, ArrowDown, Home, End, Enter) to a list of items. Returns the active index, a ref setter, and a keydown handler.
|
|
6
|
+
|
|
7
|
+
## Usage Logic
|
|
8
|
+
|
|
9
|
+
Manages an `activeIndex` state updated by arrow keys. `Home`/`End` jump to the first/last item. `Enter` triggers the `onSelect` callback with the active index. Automatically skips navigation when the event target is an editable element (`isEditableKeyboardTarget`).
|
|
10
|
+
|
|
11
|
+
| Option | Effect |
|
|
12
|
+
|--------|--------|
|
|
13
|
+
| `loop` | When `true`, navigating past the last item wraps to the first |
|
|
14
|
+
| `disable` | When `true`, all keyboard handling is suspended |
|
|
15
|
+
|
|
16
|
+
## Type Signature
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
function useKeyboardListNavigation(options: {
|
|
20
|
+
itemCount: number;
|
|
21
|
+
onSelect: (index: number) => void;
|
|
22
|
+
disable?: boolean;
|
|
23
|
+
loop?: boolean;
|
|
24
|
+
}): {
|
|
25
|
+
activeIndex: number;
|
|
26
|
+
setItemRef: (index: number) => (el: HTMLElement | null) => void;
|
|
27
|
+
handleListKeyDown: (e: React.KeyboardEvent) => void;
|
|
28
|
+
};
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Example Code
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { useKeyboardListNavigation } from "@musecat/functionkit";
|
|
35
|
+
|
|
36
|
+
function MenuList({ items }: { items: string[] }) {
|
|
37
|
+
const { activeIndex, setItemRef, handleListKeyDown } = useKeyboardListNavigation({
|
|
38
|
+
itemCount: items.length,
|
|
39
|
+
onSelect: (i) => console.log("selected:", items[i]),
|
|
40
|
+
loop: true,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<ul onKeyDown={handleListKeyDown}>
|
|
45
|
+
{items.map((item, i) => (
|
|
46
|
+
<li
|
|
47
|
+
key={i}
|
|
48
|
+
ref={setItemRef(i)}
|
|
49
|
+
className={i === activeIndex ? "active" : ""}
|
|
50
|
+
tabIndex={-1}
|
|
51
|
+
>
|
|
52
|
+
{item}
|
|
53
|
+
</li>
|
|
54
|
+
))}
|
|
55
|
+
</ul>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# useLongPress
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Detects a long press gesture on an element. Fires `onLongPress` after the user presses and holds for a configurable delay, with optional `onClick` for tap and `onLongPressEnd` for release.
|
|
6
|
+
|
|
7
|
+
## Usage Logic
|
|
8
|
+
|
|
9
|
+
Tracks both mouse and touch events. On `mousedown`/`touchstart`, starts a timer. If the pointer moves beyond `moveThreshold` pixels, the gesture is cancelled. If the timer expires without cancellation, `onLongPress` fires. On release, `onLongPressEnd` fires (if it was a long press), or `onClick` fires (if it was a short tap).
|
|
10
|
+
|
|
11
|
+
## Type Signature
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
function useLongPress(
|
|
15
|
+
handlers: {
|
|
16
|
+
onLongPress: (e: MouseEvent | TouchEvent) => void;
|
|
17
|
+
onClick?: (e: MouseEvent | TouchEvent) => void;
|
|
18
|
+
onLongPressEnd?: (e: MouseEvent | TouchEvent) => void;
|
|
19
|
+
},
|
|
20
|
+
options?: {
|
|
21
|
+
delay?: number;
|
|
22
|
+
moveThreshold?: number;
|
|
23
|
+
}
|
|
24
|
+
): {
|
|
25
|
+
onMouseDown: (e: React.MouseEvent) => void;
|
|
26
|
+
onTouchStart: (e: React.TouchEvent) => void;
|
|
27
|
+
onMouseUp: (e: React.MouseEvent) => void;
|
|
28
|
+
onTouchEnd: (e: React.TouchEvent) => void;
|
|
29
|
+
onMouseMove: (e: React.MouseEvent) => void;
|
|
30
|
+
onTouchMove: (e: React.TouchEvent) => void;
|
|
31
|
+
};
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Example Code
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { useLongPress } from "@musecat/functionkit";
|
|
38
|
+
|
|
39
|
+
function LongPressButton() {
|
|
40
|
+
const handlers = useLongPress({
|
|
41
|
+
onLongPress: () => console.log("hold complete"),
|
|
42
|
+
onClick: () => console.log("tap"),
|
|
43
|
+
onLongPressEnd: () => console.log("released"),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return <button {...handlers}>Press & hold</button>;
|
|
47
|
+
}
|
|
48
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# usePreservedCallback
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Returns a stable function identity while always calling the latest version of the provided callback. Eliminates the need to list callback dependencies in `useEffect` or `useMemo` dependency arrays.
|
|
6
|
+
|
|
7
|
+
## Usage Logic
|
|
8
|
+
|
|
9
|
+
Stores the latest callback in a `useRef`, then returns a `useCallback`-wrapped proxy that always invokes `ref.current`. The returned function never changes identity, preventing unnecessary re-renders of memoized children.
|
|
10
|
+
|
|
11
|
+
## Type Signature
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
function usePreservedCallback<T extends (...args: any[]) => any>(
|
|
15
|
+
callback: T
|
|
16
|
+
): T;
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Example Code
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { usePreservedCallback, useIntersectionObserver } from "@musecat/functionkit";
|
|
23
|
+
|
|
24
|
+
function InfiniteScroll({ onLoadMore }: { onLoadMore: () => void }) {
|
|
25
|
+
const stableLoadMore = usePreservedCallback(onLoadMore);
|
|
26
|
+
|
|
27
|
+
const ref = useIntersectionObserver(([entry]) => {
|
|
28
|
+
if (entry.isIntersecting) stableLoadMore();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return <div ref={ref}>...</div>;
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Trade-off
|
|
36
|
+
|
|
37
|
+
`usePreservedCallback` uses `useRef` + `useEffect` to keep a stable callback reference that always calls the latest version. Because the ref is synced via `useEffect` (after paint), the returned callback may hold a **stale value** during the first render or in the gap between a re-render and the effect flush. In practice this is never an issue — the callback is only invoked in event handlers that fire post-paint. This is the same pattern used by react-hook-form, Radix UI, and others. If synchronous invocation during render is required, use a regular `useCallback` with inline deps instead.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# usePreservedReference
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Maintains a stable reference to a value, only updating it when the value meaningfully changes. Avoids unnecessary re-renders caused by new object/array identities on every render.
|
|
6
|
+
|
|
7
|
+
## Usage Logic
|
|
8
|
+
|
|
9
|
+
Compares the current and previous values using a custom comparator (defaults to `JSON.stringify`). If they are considered equal, the previous reference is returned instead of the new one.
|
|
10
|
+
|
|
11
|
+
## Type Signature
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
function usePreservedReference<T>(
|
|
15
|
+
value: T,
|
|
16
|
+
compare?: (a: T, b: T) => boolean
|
|
17
|
+
): T;
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Example Code
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { usePreservedReference } from "@musecat/functionkit";
|
|
24
|
+
|
|
25
|
+
function Filters({ filters }: { filters: { sort: string; page: number } }) {
|
|
26
|
+
const stableFilters = usePreservedReference(filters);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
fetchResults(stableFilters);
|
|
30
|
+
}, [stableFilters]); // Only fires when actual values change
|
|
31
|
+
|
|
32
|
+
return <div>...</div>;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# useRefEffect
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Provides a ref callback that runs an effect when the element mounts and optionally runs a cleanup when it unmounts or when dependencies change. Similar to `useEffect` but keyed to a DOM element's lifecycle.
|
|
6
|
+
|
|
7
|
+
## Usage Logic
|
|
8
|
+
|
|
9
|
+
Returns a callback ref. When React calls the ref with an element, the effect function runs. When it's called with `null` (unmount) or when `deps` change, the previous cleanup is called before running the new effect.
|
|
10
|
+
|
|
11
|
+
## Type Signatures
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
type CleanupCallback = () => void;
|
|
15
|
+
|
|
16
|
+
function useRefEffect(
|
|
17
|
+
effect: (el: HTMLElement) => CleanupCallback | void,
|
|
18
|
+
deps?: any[]
|
|
19
|
+
): (el: HTMLElement | null) => void;
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Example Code
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { useRefEffect } from "@musecat/functionkit";
|
|
26
|
+
|
|
27
|
+
function AutoFocus() {
|
|
28
|
+
const ref = useRefEffect((el) => {
|
|
29
|
+
el.focus();
|
|
30
|
+
return () => console.log("unmounted");
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
return <input ref={ref} />;
|
|
34
|
+
}
|
|
35
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# useRelativeDateTime
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Displays a date as a relative time string ("3 minutes ago", "2 hours ago") that updates automatically. Falls back to a formatted date string when the relative time exceeds a configurable threshold.
|
|
6
|
+
|
|
7
|
+
## Usage Logic
|
|
8
|
+
|
|
9
|
+
Computes the difference between now and the given date, then formats it using `formatRelativeText`. Recomputes on a configurable interval (`updateIntervalMs`, default 1000ms). If the difference exceeds `maxRelativeDays`, returns a standard date format instead.
|
|
10
|
+
|
|
11
|
+
## Type Signature
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
function useRelativeDateTime(
|
|
15
|
+
date: DateInput,
|
|
16
|
+
locale: AppLocale,
|
|
17
|
+
options?: {
|
|
18
|
+
updateIntervalMs?: number;
|
|
19
|
+
maxRelativeDays?: number;
|
|
20
|
+
}
|
|
21
|
+
): string;
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Example Code
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { useRelativeDateTime } from "@musecat/functionkit";
|
|
28
|
+
|
|
29
|
+
function Timestamp({ time }: { time: string }) {
|
|
30
|
+
const relative = useRelativeDateTime(time, "kr", {
|
|
31
|
+
maxRelativeDays: 7,
|
|
32
|
+
});
|
|
33
|
+
return <span>{relative}</span>;
|
|
34
|
+
}
|
|
35
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# useTimeout
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
A `setTimeout` wrapper hook that provides imperative controls: `start`, `reset`, `clear`, and an `isPending` state. Allows conditionally enabling the timeout.
|
|
6
|
+
|
|
7
|
+
## Usage Logic
|
|
8
|
+
|
|
9
|
+
Creates a timer on mount (unless `enabled: false`). The returned `start` begins the timeout, `clear` cancels it, and `reset` restarts it. `isPending` reflects whether a timeout is currently active.
|
|
10
|
+
|
|
11
|
+
## Type Signatures
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
interface UseTimeoutControls {
|
|
15
|
+
start: () => void;
|
|
16
|
+
reset: () => void;
|
|
17
|
+
clear: () => void;
|
|
18
|
+
isPending: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function useTimeout(
|
|
22
|
+
callback: () => void,
|
|
23
|
+
delay: number,
|
|
24
|
+
options?: { enabled?: boolean }
|
|
25
|
+
): UseTimeoutControls;
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Example Code
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { useTimeout } from "@musecat/functionkit";
|
|
32
|
+
|
|
33
|
+
function AutoDismiss({ onDismiss }: { onDismiss: () => void }) {
|
|
34
|
+
const { reset, isPending } = useTimeout(onDismiss, 5000);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div onMouseEnter={reset}>
|
|
38
|
+
Dismissing in {isPending ? "..." : "done"}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# useToggleState
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Simplifies boolean state management with explicit `setTrue`, `setFalse`, and `toggle` setters. A more expressive alternative to raw `useState(false)`.
|
|
6
|
+
|
|
7
|
+
## Usage Logic
|
|
8
|
+
|
|
9
|
+
Wraps `useState<boolean>` with named setter functions. Accepts an optional initial value (default `false`).
|
|
10
|
+
|
|
11
|
+
## Type Signature
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
function useToggleState(
|
|
15
|
+
initial?: boolean | (() => boolean)
|
|
16
|
+
): {
|
|
17
|
+
value: boolean;
|
|
18
|
+
setTrue: () => void;
|
|
19
|
+
setFalse: () => void;
|
|
20
|
+
toggle: () => void;
|
|
21
|
+
};
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Example Code
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { useToggleState } from "@musecat/functionkit";
|
|
28
|
+
|
|
29
|
+
function Collapsible({ children }: { children: React.ReactNode }) {
|
|
30
|
+
const { value: isOpen, toggle } = useToggleState(false);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div>
|
|
34
|
+
<button onClick={toggle}>{isOpen ? "Hide" : "Show"}</button>
|
|
35
|
+
{isOpen && children}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# useViewportHeight
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Returns the current viewport height in pixels, reacting to resize events. Uses `visualViewport` when available, falling back to `window.innerHeight`.
|
|
6
|
+
|
|
7
|
+
## Usage Logic
|
|
8
|
+
|
|
9
|
+
Subscribes to `visualViewport.resize` (or `window.resize`) events and updates state accordingly. Debounces updates to avoid excessive re-renders during rapid resize.
|
|
10
|
+
|
|
11
|
+
## Type Signature
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
function useViewportHeight(): number;
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Example Code
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { useViewportHeight } from "@musecat/functionkit";
|
|
21
|
+
|
|
22
|
+
function FullHeight() {
|
|
23
|
+
const height = useViewportHeight();
|
|
24
|
+
return <div style={{ height }}>Content</div>;
|
|
25
|
+
}
|
|
26
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# useViewportMatch
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Returns whether the current viewport matches a CSS media query string. Reactive — updates when the match status changes (e.g., window resize).
|
|
6
|
+
|
|
7
|
+
## Usage Logic
|
|
8
|
+
|
|
9
|
+
Uses `window.matchMedia` to create a `MediaQueryList`, then subscribes to its `change` event to track live changes.
|
|
10
|
+
|
|
11
|
+
## Type Signature
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
function useViewportMatch(query: string): boolean;
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
| Parameter | Type | Example |
|
|
18
|
+
|-----------|------|---------|
|
|
19
|
+
| `query` | `string` | `"(min-width: 768px)"`, `"(prefers-color-scheme: dark)"` |
|
|
20
|
+
|
|
21
|
+
## Example Code
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { useViewportMatch } from "@musecat/functionkit";
|
|
25
|
+
|
|
26
|
+
function ResponsiveComponent() {
|
|
27
|
+
const isDesktop = useViewportMatch("(min-width: 1024px)");
|
|
28
|
+
const isDark = useViewportMatch("(prefers-color-scheme: dark)");
|
|
29
|
+
|
|
30
|
+
return <div className={isDesktop ? "desktop" : "mobile"}>...</div>;
|
|
31
|
+
}
|
|
32
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# browserStorage
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Provides typed JSON-serialized read/write/delete access to `localStorage` and `sessionStorage`. Gracefully handles missing `window` (SSR) by returning `null` silently.
|
|
6
|
+
|
|
7
|
+
## Usage Logic
|
|
8
|
+
|
|
9
|
+
Each function wraps the native Storage API with `JSON.stringify`/`JSON.parse`. SSR-safe at the call site — checks for `typeof window` before accessing storage.
|
|
10
|
+
|
|
11
|
+
## Type Signatures
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// localStorage
|
|
15
|
+
function getLocalStorage<T = string>(key: string): T | null;
|
|
16
|
+
function updateLocalStorage<T>(key: string, value: T): void;
|
|
17
|
+
function removeLocalStorage(key: string): void;
|
|
18
|
+
|
|
19
|
+
// sessionStorage
|
|
20
|
+
function getSessionStorage<T = string>(key: string): T | null;
|
|
21
|
+
function updateSessionStorage<T>(key: string, value: T): void;
|
|
22
|
+
function removeSessionStorage(key: string): void;
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Example Code
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { getLocalStorage, updateLocalStorage, removeLocalStorage } from "@musecat/functionkit";
|
|
29
|
+
|
|
30
|
+
function useDraft() {
|
|
31
|
+
const draft = getLocalStorage<{ text: string }>("draft") ?? { text: "" };
|
|
32
|
+
|
|
33
|
+
const saveDraft = (text: string) => {
|
|
34
|
+
updateLocalStorage("draft", { text });
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const clearDraft = () => removeLocalStorage("draft");
|
|
38
|
+
|
|
39
|
+
return { draft, saveDraft, clearDraft };
|
|
40
|
+
}
|
|
41
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# buildContext
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Eliminates `createContext` + `Provider` + `useContext` boilerplate. Creates a type-safe context pair in a single call: a typed accessor hook and a Provider component with automatic value memoization.
|
|
6
|
+
|
|
7
|
+
## Usage Logic
|
|
8
|
+
|
|
9
|
+
Creates a React context with the generic type `T`. Returns a tuple `[useCtx, Provider]`. The Provider wraps its `value` in `useMemo` to prevent unnecessary re-renders. If `displayName` is provided, it's assigned to the context for DevTools.
|
|
10
|
+
|
|
11
|
+
## Type Signature
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
function buildContext<T>(
|
|
15
|
+
displayName?: string
|
|
16
|
+
): [
|
|
17
|
+
useCtx: () => T,
|
|
18
|
+
Provider: React.FC<{ value: T; children: ReactNode }>
|
|
19
|
+
];
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Example Code
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { buildContext } from "@musecat/functionkit";
|
|
26
|
+
|
|
27
|
+
interface Theme {
|
|
28
|
+
color: string;
|
|
29
|
+
bg: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const [useTheme, ThemeProvider] = buildContext<Theme>("ThemeContext");
|
|
33
|
+
|
|
34
|
+
// Usage
|
|
35
|
+
function App() {
|
|
36
|
+
return (
|
|
37
|
+
<ThemeProvider value={{ color: "#000", bg: "#fff" }}>
|
|
38
|
+
<ThemedButton />
|
|
39
|
+
</ThemeProvider>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function ThemedButton() {
|
|
44
|
+
const theme = useTheme();
|
|
45
|
+
return <button style={{ color: theme.color, background: theme.bg }}>Click</button>;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# NavigatorClipboard / NavigatorShare (clipboardShare)
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Provides easy-to-use React components for clipboard copy and native share. Handles error cases with automatic fallback (e.g., if `navigator.share` fails, falls back to clipboard).
|
|
6
|
+
|
|
7
|
+
## Usage Logic
|
|
8
|
+
|
|
9
|
+
**NavigatorClipboard**: On click, calls `navigator.clipboard.writeText(text)`. Calls `onComplete` with the result.
|
|
10
|
+
|
|
11
|
+
**NavigatorShare**: On click, calls `navigator.share({ title, text, url })`. If share fails (or is unavailable), falls back to clipboard copy and calls `onComplete` with fallback info.
|
|
12
|
+
|
|
13
|
+
## Type Signatures
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
interface NavigatorClipboardProps {
|
|
17
|
+
text: string;
|
|
18
|
+
onComplete?: (result: NavigatorClipboardResult) => void;
|
|
19
|
+
children?: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface NavigatorShareProps {
|
|
23
|
+
title?: string;
|
|
24
|
+
text?: string;
|
|
25
|
+
url?: string;
|
|
26
|
+
onComplete?: (result: NavigatorShareResult) => void;
|
|
27
|
+
children?: ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface NavigatorClipboardResult {
|
|
31
|
+
copied: boolean;
|
|
32
|
+
error?: Error;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface NavigatorShareResult {
|
|
36
|
+
shared: boolean;
|
|
37
|
+
copied: boolean;
|
|
38
|
+
error?: Error;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Example Code
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { NavigatorClipboard, NavigatorShare } from "@musecat/functionkit";
|
|
46
|
+
|
|
47
|
+
function ShareButton({ url }: { url: string }) {
|
|
48
|
+
return (
|
|
49
|
+
<div>
|
|
50
|
+
<NavigatorClipboard text={url}>
|
|
51
|
+
<button>Copy Link</button>
|
|
52
|
+
</NavigatorClipboard>
|
|
53
|
+
<NavigatorShare title="Check this out" url={url}>
|
|
54
|
+
<button>Share</button>
|
|
55
|
+
</NavigatorShare>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|