@musecat/functionkit 1.2.0 → 1.2.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.
Files changed (42) hide show
  1. package/.agents/references/components/ScrolltoTop.md +36 -0
  2. package/.agents/references/components/SwitchCase.md +47 -0
  3. package/.agents/references/components/ViewportPortal.md +34 -0
  4. package/.agents/references/cookie/cookie.md +53 -0
  5. package/.agents/references/datetime/dateTime.client.md +42 -0
  6. package/.agents/references/datetime/dateTime.server.md +42 -0
  7. package/.agents/references/datetime/dateTime.shared.md +86 -0
  8. package/.agents/references/hooks/useAvoidKeyboard.md +29 -0
  9. package/.agents/references/hooks/useCheckInvisible.md +36 -0
  10. package/.agents/references/hooks/useCheckScroll.md +36 -0
  11. package/.agents/references/hooks/useClientDateTime.md +39 -0
  12. package/.agents/references/hooks/useDebounce.md +46 -0
  13. package/.agents/references/hooks/useDebouncedCallback.md +34 -0
  14. package/.agents/references/hooks/useDoubleClick.md +40 -0
  15. package/.agents/references/hooks/useGeolocation.md +37 -0
  16. package/.agents/references/hooks/useHasMounted.md +27 -0
  17. package/.agents/references/hooks/useIntersectionObserver.md +41 -0
  18. package/.agents/references/hooks/useInterval.md +44 -0
  19. package/.agents/references/hooks/useKeyboardHeight.md +33 -0
  20. package/.agents/references/hooks/useKeyboardListNavigation.md +58 -0
  21. package/.agents/references/hooks/useLongPress.md +48 -0
  22. package/.agents/references/hooks/usePreservedCallback.md +37 -0
  23. package/.agents/references/hooks/usePreservedReference.md +34 -0
  24. package/.agents/references/hooks/useRefEffect.md +35 -0
  25. package/.agents/references/hooks/useRelativeDateTime.md +35 -0
  26. package/.agents/references/hooks/useTimeout.md +42 -0
  27. package/.agents/references/hooks/useToggleState.md +39 -0
  28. package/.agents/references/hooks/useViewportHeight.md +26 -0
  29. package/.agents/references/hooks/useViewportMatch.md +32 -0
  30. package/.agents/references/utils/browserStorage.md +41 -0
  31. package/.agents/references/utils/buildContext.md +47 -0
  32. package/.agents/references/utils/clipboardShare.md +59 -0
  33. package/.agents/references/utils/floatingMotion.md +67 -0
  34. package/.agents/references/utils/getDeviceInfo.md +43 -0
  35. package/.agents/references/utils/isEditableKeyboardTarget.md +35 -0
  36. package/.agents/references/utils/mergeRefs.md +29 -0
  37. package/.agents/references/utils/seen.md +37 -0
  38. package/.agents/references/utils/subscribeKeyboardHeight.md +35 -0
  39. package/AGENTS.md +128 -0
  40. package/CLAUDE.md +1 -0
  41. package/README.md +4 -0
  42. package/package.json +4 -1
@@ -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
+ ```