@musecat/functionkit 1.0.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/.gitattributes +2 -0
- package/AGENTS.md +398 -0
- package/components/ScrolltoTop.tsx +9 -0
- package/components/SwitchCase.tsx +15 -0
- package/components/ViewportPortal.tsx +40 -0
- package/cookie/cookie.shared.ts +142 -0
- package/datetime/dateTime.client.ts +108 -0
- package/datetime/dateTime.server.ts +108 -0
- package/datetime/dateTime.shared.ts +358 -0
- package/hooks/useAvoidKeyboard.ts +28 -0
- package/hooks/useCheckInvisible.ts +25 -0
- package/hooks/useCheckScroll.ts +19 -0
- package/hooks/useClientDateTime.ts +49 -0
- package/hooks/useDebounce.ts +118 -0
- package/hooks/useDebouncedCallback.ts +69 -0
- package/hooks/useDoubleClick.ts +53 -0
- package/hooks/useGeolocation.ts +146 -0
- package/hooks/useHasMounted.ts +13 -0
- package/hooks/useIntersectionObserver.ts +34 -0
- package/hooks/useInterval.ts +55 -0
- package/hooks/useKeyboardHeight.ts +18 -0
- package/hooks/useKeyboardListNavigation.ts +170 -0
- package/hooks/useLongPress.ts +120 -0
- package/hooks/usePreservedCallback.ts +21 -0
- package/hooks/usePreservedReference.ts +21 -0
- package/hooks/useRefEffect.ts +35 -0
- package/hooks/useRelativeDateTime.ts +54 -0
- package/hooks/useTimeout.ts +56 -0
- package/hooks/useToggleState.ts +13 -0
- package/hooks/useViewportHeight.ts +23 -0
- package/hooks/useViewportMatch.ts +33 -0
- package/index.ts +111 -0
- package/package.json +15 -0
- package/tsconfig.json +18 -0
- package/utils/browserStorage.ts +59 -0
- package/utils/buildContext.tsx +19 -0
- package/utils/checkDevice.ts +74 -0
- package/utils/clipboardShare.tsx +47 -0
- package/utils/clipboardShare.types.ts +18 -0
- package/utils/floatingMotion.ts +99 -0
- package/utils/keyboardTarget.ts +13 -0
- package/utils/mergeRefs.ts +15 -0
- package/utils/seen.ts +29 -0
- package/utils/subscribeKeyboardHeight.ts +54 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
NavigatorClipboardProps,
|
|
5
|
+
NavigatorClipboardResult,
|
|
6
|
+
NavigatorShareProps,
|
|
7
|
+
NavigatorShareResult,
|
|
8
|
+
} from "./clipboardShare.types";
|
|
9
|
+
|
|
10
|
+
export function NavigatorClipboard({
|
|
11
|
+
text,
|
|
12
|
+
}: NavigatorClipboardProps): Promise<NavigatorClipboardResult> {
|
|
13
|
+
return navigator.clipboard.writeText(text).then(
|
|
14
|
+
() => ({ success: true }),
|
|
15
|
+
() => ({ success: false }),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function NavigatorShare({
|
|
20
|
+
title,
|
|
21
|
+
text,
|
|
22
|
+
link,
|
|
23
|
+
}: NavigatorShareProps): Promise<NavigatorShareResult> {
|
|
24
|
+
if (navigator.share) {
|
|
25
|
+
return navigator.share({ title, text, url: link }).then(
|
|
26
|
+
() => ({ success: true, method: "share" }),
|
|
27
|
+
(error) => {
|
|
28
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
29
|
+
return { success: false, method: "share" };
|
|
30
|
+
}
|
|
31
|
+
return clipboardFallback(link);
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return clipboardFallback(link);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function clipboardFallback(link: string): Promise<NavigatorShareResult> {
|
|
40
|
+
if (navigator.clipboard) {
|
|
41
|
+
return navigator.clipboard.writeText(link).then(
|
|
42
|
+
() => ({ success: true, method: "clipboard" }),
|
|
43
|
+
() => ({ success: false, method: "unsupported" }),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return Promise.resolve({ success: false, method: "unsupported" });
|
|
47
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type NavigatorClipboardProps = {
|
|
2
|
+
text: string;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export type NavigatorShareProps = {
|
|
6
|
+
title: string;
|
|
7
|
+
text: string;
|
|
8
|
+
link: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type NavigatorClipboardResult = {
|
|
12
|
+
success: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type NavigatorShareResult = {
|
|
16
|
+
success: boolean;
|
|
17
|
+
method: "share" | "clipboard" | "unsupported";
|
|
18
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export type FloatingMotionMode =
|
|
2
|
+
| "anchored"
|
|
3
|
+
| "center-selected"
|
|
4
|
+
| "modal-center"
|
|
5
|
+
| "mobile-sheet";
|
|
6
|
+
|
|
7
|
+
export type FloatingPlacement =
|
|
8
|
+
| "top-left"
|
|
9
|
+
| "top-center"
|
|
10
|
+
| "top-right"
|
|
11
|
+
| "middle-left"
|
|
12
|
+
| "middle-center"
|
|
13
|
+
| "middle-right"
|
|
14
|
+
| "bottom-left"
|
|
15
|
+
| "bottom-center"
|
|
16
|
+
| "bottom-right";
|
|
17
|
+
|
|
18
|
+
export interface FloatingMotionPreset {
|
|
19
|
+
enterMs: number;
|
|
20
|
+
exitMs: number;
|
|
21
|
+
ease: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const getFloatingMotionPreset = (
|
|
25
|
+
mode: FloatingMotionMode,
|
|
26
|
+
): FloatingMotionPreset => {
|
|
27
|
+
if (mode === "mobile-sheet") {
|
|
28
|
+
return {
|
|
29
|
+
enterMs: 360,
|
|
30
|
+
exitMs: 280,
|
|
31
|
+
ease: "cubic-bezier(0.2, 0.8, 0.2, 1)",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (mode === "modal-center") {
|
|
36
|
+
return {
|
|
37
|
+
enterMs: 340,
|
|
38
|
+
exitMs: 260,
|
|
39
|
+
ease: "cubic-bezier(0.22, 1, 0.36, 1)",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (mode === "center-selected") {
|
|
44
|
+
return {
|
|
45
|
+
enterMs: 190,
|
|
46
|
+
exitMs: 140,
|
|
47
|
+
ease: "cubic-bezier(0.2, 0, 0, 1)",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
enterMs: 230,
|
|
53
|
+
exitMs: 170,
|
|
54
|
+
ease: "cubic-bezier(0.16, 1, 0.3, 1)",
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const getFloatingTransformOrigin = (
|
|
59
|
+
placement?: FloatingPlacement,
|
|
60
|
+
): string => {
|
|
61
|
+
if (!placement) return "top center";
|
|
62
|
+
|
|
63
|
+
const [row, col] = placement.split("-") as [
|
|
64
|
+
"top" | "middle" | "bottom",
|
|
65
|
+
"left" | "center" | "right",
|
|
66
|
+
];
|
|
67
|
+
const y = row === "top" ? "bottom" : row === "bottom" ? "top" : "center";
|
|
68
|
+
const x = col === "right" ? "right" : col === "left" ? "left" : "center";
|
|
69
|
+
|
|
70
|
+
return `${y} ${x}`;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const getFloatingHiddenTransform = ({
|
|
74
|
+
mode,
|
|
75
|
+
placement,
|
|
76
|
+
}: {
|
|
77
|
+
mode: FloatingMotionMode;
|
|
78
|
+
placement?: FloatingPlacement;
|
|
79
|
+
}): string => {
|
|
80
|
+
if (mode === "mobile-sheet") return "translateY(1.8rem) scale(1)";
|
|
81
|
+
if (mode === "modal-center") return "translateY(.8rem) scale(.94)";
|
|
82
|
+
if (mode === "center-selected") return "translateY(.45rem) scale(.99)";
|
|
83
|
+
if (!placement) return "translateY(.4rem) scale(.975)";
|
|
84
|
+
|
|
85
|
+
const [row, col] = placement.split("-") as [
|
|
86
|
+
"top" | "middle" | "bottom",
|
|
87
|
+
"left" | "center" | "right",
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
if (row === "top") return "translateY(.4rem) scale(.975)";
|
|
91
|
+
if (row === "bottom") return "translateY(-.4rem) scale(.975)";
|
|
92
|
+
|
|
93
|
+
if (row === "middle") {
|
|
94
|
+
if (col === "left") return "translateX(.4rem) scale(.975)";
|
|
95
|
+
if (col === "right") return "translateX(-.4rem) scale(.975)";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return "translateY(.4rem) scale(.975)";
|
|
99
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
export function isEditableKeyboardTarget(target: HTMLElement) {
|
|
4
|
+
return (
|
|
5
|
+
target.isContentEditable ||
|
|
6
|
+
target.tagName === "TEXTAREA" ||
|
|
7
|
+
target.tagName === "SELECT" ||
|
|
8
|
+
(target.tagName === "INPUT" &&
|
|
9
|
+
(target as HTMLInputElement).type !== "button" &&
|
|
10
|
+
(target as HTMLInputElement).type !== "checkbox" &&
|
|
11
|
+
(target as HTMLInputElement).type !== "radio")
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Ref, RefCallback } from "react";
|
|
2
|
+
|
|
3
|
+
type ReactRef<T> = Ref<T> | undefined | null;
|
|
4
|
+
|
|
5
|
+
export function mergeRefs<T>(...refs: ReactRef<T>[]): RefCallback<T> {
|
|
6
|
+
return (value: T | null) => {
|
|
7
|
+
for (const ref of refs) {
|
|
8
|
+
if (typeof ref === "function") {
|
|
9
|
+
ref(value);
|
|
10
|
+
} else if (ref && "current" in ref) {
|
|
11
|
+
(ref as { current: T | null }).current = value;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
package/utils/seen.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const SEEN_STORAGE_KEY = "seen";
|
|
2
|
+
|
|
3
|
+
type SeenMap = Partial<Record<string, boolean>>;
|
|
4
|
+
|
|
5
|
+
export function parseSeen(raw: string | null): SeenMap {
|
|
6
|
+
if (!raw) return {};
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
10
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return parsed as SeenMap;
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function hasSeenKey(raw: string | null, key: string) {
|
|
21
|
+
const map = parseSeen(raw);
|
|
22
|
+
return map[key] === true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildSeenValue(raw: string | null, key: string): string {
|
|
26
|
+
const map = parseSeen(raw);
|
|
27
|
+
map[key] = true;
|
|
28
|
+
return JSON.stringify(map);
|
|
29
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
type SubscribeKeyboardHeightOptions = {
|
|
4
|
+
callback: (height: number) => void;
|
|
5
|
+
immediate?: boolean;
|
|
6
|
+
throttleMs?: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function subscribeKeyboardHeight({
|
|
10
|
+
callback,
|
|
11
|
+
immediate = false,
|
|
12
|
+
throttleMs = 16,
|
|
13
|
+
}: SubscribeKeyboardHeightOptions) {
|
|
14
|
+
let ticking = false;
|
|
15
|
+
let lastHeight = 0;
|
|
16
|
+
|
|
17
|
+
const notify = (height: number) => {
|
|
18
|
+
if (ticking) return;
|
|
19
|
+
ticking = true;
|
|
20
|
+
|
|
21
|
+
const run = () => {
|
|
22
|
+
if (height !== lastHeight) {
|
|
23
|
+
lastHeight = height;
|
|
24
|
+
callback(height);
|
|
25
|
+
}
|
|
26
|
+
ticking = false;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (throttleMs > 0) {
|
|
30
|
+
setTimeout(run, throttleMs);
|
|
31
|
+
} else {
|
|
32
|
+
run();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const handleResize = () => {
|
|
37
|
+
const vv = window.visualViewport;
|
|
38
|
+
if (!vv) return;
|
|
39
|
+
const diff = window.innerHeight - vv.height;
|
|
40
|
+
notify(diff > 0 ? diff : 0);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (immediate) handleResize();
|
|
44
|
+
|
|
45
|
+
window.visualViewport?.addEventListener("resize", handleResize);
|
|
46
|
+
window.visualViewport?.addEventListener("scroll", handleResize);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
unsubscribe() {
|
|
50
|
+
window.visualViewport?.removeEventListener("resize", handleResize);
|
|
51
|
+
window.visualViewport?.removeEventListener("scroll", handleResize);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|