@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.
Files changed (130) 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 +47 -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/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
  40. package/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
  41. package/.github/PULL_REQUEST_TEMPLATE.md +22 -0
  42. package/.github/workflows/ci.yml +20 -0
  43. package/.local/state/gh/device-id +1 -0
  44. package/.prettierrc +8 -0
  45. package/AGENTS.md +117 -396
  46. package/CODE_OF_CONDUCT.md +55 -0
  47. package/CONTRIBUTING.md +39 -0
  48. package/README.md +18 -5
  49. package/biome.json +25 -0
  50. package/index.ts +63 -68
  51. package/package.json +46 -13
  52. package/packages/components/ScrolltoTop.tsx +3 -3
  53. package/packages/components/SwitchCase.tsx +7 -7
  54. package/packages/components/ViewportPortal.tsx +22 -23
  55. package/packages/cookie/cookie.shared.ts +80 -110
  56. package/packages/cookie/cookieNames.shared.ts +9 -0
  57. package/packages/datetime/dateTime.client.ts +84 -86
  58. package/packages/datetime/dateTime.server.ts +84 -86
  59. package/packages/datetime/dateTime.shared.ts +249 -287
  60. package/packages/hooks/useAvoidKeyboard.ts +15 -15
  61. package/packages/hooks/useCheckInvisible.ts +16 -16
  62. package/packages/hooks/useCheckScroll.ts +10 -10
  63. package/packages/hooks/useClientDateTime.ts +24 -37
  64. package/packages/hooks/useDebounce.ts +90 -95
  65. package/packages/hooks/useDebouncedCallback.ts +45 -45
  66. package/packages/hooks/useDoubleClick.ts +43 -43
  67. package/packages/hooks/useGeolocation.ts +126 -130
  68. package/packages/hooks/useHasMounted.ts +5 -5
  69. package/packages/hooks/useIntersectionObserver.ts +20 -20
  70. package/packages/hooks/useInterval.ts +45 -45
  71. package/packages/hooks/useKeyboardHeight.ts +9 -9
  72. package/packages/hooks/useKeyboardListNavigation.ts +130 -158
  73. package/packages/hooks/useLongPress.ts +87 -98
  74. package/packages/hooks/usePreservedCallback.ts +12 -12
  75. package/packages/hooks/usePreservedReference.ts +10 -10
  76. package/packages/hooks/useRefEffect.ts +19 -19
  77. package/packages/hooks/useRelativeDateTime.ts +36 -39
  78. package/packages/hooks/useTimeout.ts +41 -41
  79. package/packages/hooks/useToggleState.ts +5 -5
  80. package/packages/hooks/useViewportHeight.ts +12 -13
  81. package/packages/hooks/useViewportMatch.ts +13 -23
  82. package/packages/utils/browserStorage.ts +26 -26
  83. package/packages/utils/buildContext.tsx +10 -11
  84. package/packages/utils/checkDevice.ts +64 -64
  85. package/packages/utils/clipboardShare.tsx +31 -31
  86. package/packages/utils/clipboardShare.types.ts +7 -7
  87. package/packages/utils/floatingMotion.ts +71 -77
  88. package/packages/utils/keyboardTarget.ts +9 -9
  89. package/packages/utils/mergeRefs.ts +9 -9
  90. package/packages/utils/seen.ts +15 -15
  91. package/packages/utils/subscribeKeyboardHeight.ts +49 -46
  92. package/tests/components/ScrolltoTop.test.tsx +15 -0
  93. package/tests/components/SwitchCase.test.tsx +33 -0
  94. package/tests/components/ViewportPortal.test.tsx +46 -0
  95. package/tests/cookie/cookie.test.ts +192 -0
  96. package/tests/datetime/datetime.test.ts +461 -0
  97. package/tests/hooks/useAvoidKeyboard.test.ts +43 -0
  98. package/tests/hooks/useCheckInvisible.test.ts +59 -0
  99. package/tests/hooks/useCheckScroll.test.ts +23 -0
  100. package/tests/hooks/useClientDateTime.test.ts +12 -0
  101. package/tests/hooks/useDebounce.test.ts +96 -0
  102. package/tests/hooks/useDebouncedCallback.test.ts +63 -0
  103. package/tests/hooks/useDoubleClick.test.ts +82 -0
  104. package/tests/hooks/useGeolocation.test.ts +201 -0
  105. package/tests/hooks/useHasMounted.test.ts +10 -0
  106. package/tests/hooks/useIntersectionObserver.test.ts +78 -0
  107. package/tests/hooks/useInterval.test.ts +56 -0
  108. package/tests/hooks/useKeyboardHeight.test.ts +10 -0
  109. package/tests/hooks/useKeyboardListNavigation.test.ts +404 -0
  110. package/tests/hooks/useLongPress.test.ts +214 -0
  111. package/tests/hooks/usePreservedCallback.test.ts +33 -0
  112. package/tests/hooks/usePreservedReference.test.ts +42 -0
  113. package/tests/hooks/useRefEffect.test.ts +67 -0
  114. package/tests/hooks/useRelativeDateTime.test.ts +21 -0
  115. package/tests/hooks/useTimeout.test.ts +72 -0
  116. package/tests/hooks/useToggleState.test.ts +43 -0
  117. package/tests/hooks/useViewportHeight.test.ts +32 -0
  118. package/tests/hooks/useViewportMatch.test.ts +35 -0
  119. package/tests/setup.ts +9 -0
  120. package/tests/utils/browserStorage.test.ts +114 -0
  121. package/tests/utils/buildContext.test.tsx +26 -0
  122. package/tests/utils/checkDevice.test.ts +140 -0
  123. package/tests/utils/clipboardShare.test.ts +152 -0
  124. package/tests/utils/floatingMotion.test.ts +135 -0
  125. package/tests/utils/keyboardTarget.test.ts +85 -0
  126. package/tests/utils/mergeRefs.test.ts +50 -0
  127. package/tests/utils/seen.test.ts +40 -0
  128. package/tests/utils/subscribeKeyboardHeight.test.ts +172 -0
  129. package/tsconfig.json +16 -16
  130. package/vitest.config.ts +22 -0
@@ -3,14 +3,14 @@
3
3
  import { useCallback, useEffect, useRef, useState } from "react";
4
4
 
5
5
  type GeolocationData = {
6
- latitude: number;
7
- longitude: number;
8
- accuracy: number;
9
- altitude: number | null;
10
- altitudeAccuracy: number | null;
11
- heading: number | null;
12
- speed: number | null;
13
- timestamp: number;
6
+ latitude: number;
7
+ longitude: number;
8
+ accuracy: number;
9
+ altitude: number | null;
10
+ altitudeAccuracy: number | null;
11
+ heading: number | null;
12
+ speed: number | null;
13
+ timestamp: number;
14
14
  };
15
15
 
16
16
  const MOUNT_BEHAVIOR = { GET: "get", WATCH: "watch" } as const;
@@ -18,129 +18,125 @@ const MOUNT_BEHAVIOR = { GET: "get", WATCH: "watch" } as const;
18
18
  type MountBehavior = (typeof MOUNT_BEHAVIOR)[keyof typeof MOUNT_BEHAVIOR];
19
19
 
20
20
  type UseGeolocationOptions = {
21
- mountBehavior?: MountBehavior;
22
- enableHighAccuracy?: boolean;
23
- timeout?: number;
24
- maximumAge?: number;
21
+ mountBehavior?: MountBehavior;
22
+ enableHighAccuracy?: boolean;
23
+ timeout?: number;
24
+ maximumAge?: number;
25
25
  };
26
26
 
27
27
  export function useGeolocation(options?: UseGeolocationOptions) {
28
- const [state, setState] = useState<{
29
- loading: boolean;
30
- error: string | null;
31
- data: GeolocationData | null;
32
- }>({
33
- loading: !!options?.mountBehavior,
34
- error: null,
35
- data: null,
36
- });
37
- const [isTracking, setIsTracking] = useState(false);
38
- const watchIdRef = useRef<number | null>(null);
39
-
40
- const isSupported = useCallback(() => {
41
- if (typeof window === "undefined" || !navigator.geolocation) {
42
- setState((prev) => ({
43
- ...prev,
44
- loading: false,
45
- error: "Geolocation is not supported by this environment.",
46
- }));
47
- return false;
48
- }
49
- return true;
50
- }, []);
51
-
52
- const handleSuccess = useCallback((position: GeolocationPosition) => {
53
- const { coords } = position;
54
- setState((prev) => ({
55
- ...prev,
56
- loading: false,
57
- error: null,
58
- data: {
59
- latitude: coords.latitude,
60
- longitude: coords.longitude,
61
- accuracy: coords.accuracy,
62
- altitude: coords.altitude,
63
- altitudeAccuracy: coords.altitudeAccuracy,
64
- heading: coords.heading,
65
- speed: coords.speed,
66
- timestamp: position.timestamp,
67
- },
68
- }));
69
- }, []);
70
-
71
- const handleError = useCallback((error: GeolocationPositionError) => {
72
- setState((prev) => ({
73
- ...prev,
74
- loading: false,
75
- error: error.message,
76
- }));
77
- }, []);
78
-
79
- const geoOptions = useCallback(
80
- () => ({
81
- enableHighAccuracy: options?.enableHighAccuracy ?? false,
82
- maximumAge: options?.maximumAge ?? 0,
83
- timeout: options?.timeout ?? Infinity,
84
- }),
85
- [options?.enableHighAccuracy, options?.maximumAge, options?.timeout],
86
- );
87
-
88
- const getCurrentPosition = useCallback(() => {
89
- if (!isSupported()) return;
90
- setState((prev) => ({ ...prev, loading: true }));
91
- navigator.geolocation.getCurrentPosition(
92
- handleSuccess,
93
- handleError,
94
- geoOptions(),
95
- );
96
- }, [handleSuccess, handleError, geoOptions, isSupported]);
97
-
98
- const startTracking = useCallback(() => {
99
- if (!isSupported()) return;
100
-
101
- if (watchIdRef.current !== null) {
102
- navigator.geolocation.clearWatch(watchIdRef.current);
103
- }
104
-
105
- setState((prev) => ({ ...prev, loading: true }));
106
-
107
- watchIdRef.current = navigator.geolocation.watchPosition(
108
- (position) => {
109
- setIsTracking(true);
110
- handleSuccess(position);
111
- },
112
- handleError,
113
- geoOptions(),
114
- );
115
- }, [handleSuccess, handleError, geoOptions, isSupported]);
116
-
117
- const stopTracking = useCallback(() => {
118
- if (watchIdRef.current === null) return;
119
- navigator.geolocation.clearWatch(watchIdRef.current);
120
- watchIdRef.current = null;
121
- setIsTracking(false);
122
- }, []);
123
-
124
- useEffect(() => {
125
- if (options?.mountBehavior === MOUNT_BEHAVIOR.WATCH) {
126
- startTracking();
127
- } else if (options?.mountBehavior === MOUNT_BEHAVIOR.GET) {
128
- getCurrentPosition();
129
- }
130
-
131
- return () => {
132
- if (watchIdRef.current !== null) {
133
- navigator.geolocation.clearWatch(watchIdRef.current);
134
- watchIdRef.current = null;
135
- }
136
- };
137
- }, [options?.mountBehavior, getCurrentPosition, startTracking]);
138
-
139
- return {
140
- ...state,
141
- getCurrentPosition,
142
- startTracking,
143
- stopTracking,
144
- isTracking,
145
- };
28
+ const [state, setState] = useState<{
29
+ loading: boolean;
30
+ error: string | null;
31
+ data: GeolocationData | null;
32
+ }>({
33
+ loading: !!options?.mountBehavior,
34
+ error: null,
35
+ data: null,
36
+ });
37
+ const [isTracking, setIsTracking] = useState(false);
38
+ const watchIdRef = useRef<number | null>(null);
39
+
40
+ const isSupported = useCallback(() => {
41
+ if (typeof window === "undefined" || !navigator.geolocation) {
42
+ setState((prev) => ({
43
+ ...prev,
44
+ loading: false,
45
+ error: "Geolocation is not supported by this environment.",
46
+ }));
47
+ return false;
48
+ }
49
+ return true;
50
+ }, []);
51
+
52
+ const handleSuccess = useCallback((position: GeolocationPosition) => {
53
+ const { coords } = position;
54
+ setState((prev) => ({
55
+ ...prev,
56
+ loading: false,
57
+ error: null,
58
+ data: {
59
+ latitude: coords.latitude,
60
+ longitude: coords.longitude,
61
+ accuracy: coords.accuracy,
62
+ altitude: coords.altitude,
63
+ altitudeAccuracy: coords.altitudeAccuracy,
64
+ heading: coords.heading,
65
+ speed: coords.speed,
66
+ timestamp: position.timestamp,
67
+ },
68
+ }));
69
+ }, []);
70
+
71
+ const handleError = useCallback((error: GeolocationPositionError) => {
72
+ setState((prev) => ({
73
+ ...prev,
74
+ loading: false,
75
+ error: error.message,
76
+ }));
77
+ }, []);
78
+
79
+ const geoOptions = useCallback(
80
+ () => ({
81
+ enableHighAccuracy: options?.enableHighAccuracy ?? false,
82
+ maximumAge: options?.maximumAge ?? 0,
83
+ timeout: options?.timeout ?? Infinity,
84
+ }),
85
+ [options?.enableHighAccuracy, options?.maximumAge, options?.timeout],
86
+ );
87
+
88
+ const getCurrentPosition = useCallback(() => {
89
+ if (!isSupported()) return;
90
+ setState((prev) => ({ ...prev, loading: true }));
91
+ navigator.geolocation.getCurrentPosition(handleSuccess, handleError, geoOptions());
92
+ }, [handleSuccess, handleError, geoOptions, isSupported]);
93
+
94
+ const startTracking = useCallback(() => {
95
+ if (!isSupported()) return;
96
+
97
+ if (watchIdRef.current !== null) {
98
+ navigator.geolocation.clearWatch(watchIdRef.current);
99
+ }
100
+
101
+ setState((prev) => ({ ...prev, loading: true }));
102
+
103
+ watchIdRef.current = navigator.geolocation.watchPosition(
104
+ (position) => {
105
+ setIsTracking(true);
106
+ handleSuccess(position);
107
+ },
108
+ handleError,
109
+ geoOptions(),
110
+ );
111
+ }, [handleSuccess, handleError, geoOptions, isSupported]);
112
+
113
+ const stopTracking = useCallback(() => {
114
+ if (watchIdRef.current === null) return;
115
+ navigator.geolocation.clearWatch(watchIdRef.current);
116
+ watchIdRef.current = null;
117
+ setIsTracking(false);
118
+ }, []);
119
+
120
+ useEffect(() => {
121
+ if (options?.mountBehavior === MOUNT_BEHAVIOR.WATCH) {
122
+ startTracking();
123
+ } else if (options?.mountBehavior === MOUNT_BEHAVIOR.GET) {
124
+ getCurrentPosition();
125
+ }
126
+
127
+ return () => {
128
+ if (watchIdRef.current !== null) {
129
+ navigator.geolocation.clearWatch(watchIdRef.current);
130
+ watchIdRef.current = null;
131
+ }
132
+ };
133
+ }, [options?.mountBehavior, getCurrentPosition, startTracking]);
134
+
135
+ return {
136
+ ...state,
137
+ getCurrentPosition,
138
+ startTracking,
139
+ stopTracking,
140
+ isTracking,
141
+ };
146
142
  }
@@ -3,11 +3,11 @@
3
3
  import { useEffect, useState } from "react";
4
4
 
5
5
  export function useHasMounted() {
6
- const [hasMounted, setHasMounted] = useState(false);
6
+ const [hasMounted, setHasMounted] = useState(false);
7
7
 
8
- useEffect(() => {
9
- setHasMounted(true);
10
- }, []);
8
+ useEffect(() => {
9
+ setHasMounted(true);
10
+ }, []);
11
11
 
12
- return hasMounted;
12
+ return hasMounted;
13
13
  }
@@ -6,29 +6,29 @@ import { usePreservedCallback } from "./usePreservedCallback";
6
6
  import { useRefEffect } from "./useRefEffect";
7
7
 
8
8
  export function useIntersectionObserver<Element extends HTMLElement>(
9
- callback: (entry: IntersectionObserverEntry) => void,
10
- options: IntersectionObserverInit,
9
+ callback: (entry: IntersectionObserverEntry) => void,
10
+ options: IntersectionObserverInit,
11
11
  ): (element: Element | null) => void {
12
- const preservedCallback = usePreservedCallback(callback);
12
+ const preservedCallback = usePreservedCallback(callback);
13
13
 
14
- const observer = useMemo(() => {
15
- if (typeof IntersectionObserver === "undefined") {
16
- return;
17
- }
14
+ const observer = useMemo(() => {
15
+ if (typeof IntersectionObserver === "undefined") {
16
+ return;
17
+ }
18
18
 
19
- return new IntersectionObserver(([entry]) => {
20
- preservedCallback(entry);
21
- }, options);
22
- }, [preservedCallback, options]);
19
+ return new IntersectionObserver(([entry]) => {
20
+ preservedCallback(entry);
21
+ }, options);
22
+ }, [preservedCallback, options]);
23
23
 
24
- return useRefEffect<Element>(
25
- (element) => {
26
- observer?.observe(element);
24
+ return useRefEffect<Element>(
25
+ (element) => {
26
+ observer?.observe(element);
27
27
 
28
- return () => {
29
- observer?.unobserve(element);
30
- };
31
- },
32
- [preservedCallback, options],
33
- );
28
+ return () => {
29
+ observer?.unobserve(element);
30
+ };
31
+ },
32
+ [preservedCallback, options],
33
+ );
34
34
  }
@@ -5,51 +5,51 @@ import { useEffect, useRef } from "react";
5
5
  import { usePreservedCallback } from "./usePreservedCallback";
6
6
 
7
7
  type IntervalOptions =
8
- | number
9
- | {
10
- delay: number;
11
- immediate?: boolean;
12
- enabled?: boolean;
13
- };
8
+ | number
9
+ | {
10
+ delay: number;
11
+ immediate?: boolean;
12
+ enabled?: boolean;
13
+ };
14
14
 
15
15
  export function useInterval(callback: () => void, options: IntervalOptions) {
16
- const delay = typeof options === "number" ? options : options.delay;
17
- const immediate = typeof options === "number" ? false : options.immediate;
18
- const enabled = typeof options === "number" ? true : (options.enabled ?? true);
19
-
20
- const preservedCallback = usePreservedCallback(callback);
21
- const immediateCalledRef = useRef(false);
22
-
23
- useEffect(
24
- function runImmediateCallback() {
25
- if (immediate !== true) {
26
- immediateCalledRef.current = false;
27
- return;
28
- }
29
-
30
- if (!enabled) {
31
- return;
32
- }
33
-
34
- if (immediateCalledRef.current) {
35
- return;
36
- }
37
-
38
- immediateCalledRef.current = true;
39
- preservedCallback();
40
- },
41
- [immediate, preservedCallback, enabled],
42
- );
43
-
44
- useEffect(
45
- function startInterval() {
46
- if (!enabled) {
47
- return;
48
- }
49
-
50
- const id = setInterval(preservedCallback, delay);
51
- return () => clearInterval(id);
52
- },
53
- [delay, preservedCallback, enabled],
54
- );
16
+ const delay = typeof options === "number" ? options : options.delay;
17
+ const immediate = typeof options === "number" ? false : options.immediate;
18
+ const enabled = typeof options === "number" ? true : (options.enabled ?? true);
19
+
20
+ const preservedCallback = usePreservedCallback(callback);
21
+ const immediateCalledRef = useRef(false);
22
+
23
+ useEffect(
24
+ function runImmediateCallback() {
25
+ if (immediate !== true) {
26
+ immediateCalledRef.current = false;
27
+ return;
28
+ }
29
+
30
+ if (!enabled) {
31
+ return;
32
+ }
33
+
34
+ if (immediateCalledRef.current) {
35
+ return;
36
+ }
37
+
38
+ immediateCalledRef.current = true;
39
+ preservedCallback();
40
+ },
41
+ [immediate, preservedCallback, enabled],
42
+ );
43
+
44
+ useEffect(
45
+ function startInterval() {
46
+ if (!enabled) {
47
+ return;
48
+ }
49
+
50
+ const id = setInterval(preservedCallback, delay);
51
+ return () => clearInterval(id);
52
+ },
53
+ [delay, preservedCallback, enabled],
54
+ );
55
55
  }
@@ -4,15 +4,15 @@ import { useEffect, useState } from "react";
4
4
  import { subscribeKeyboardHeight } from "../utils/subscribeKeyboardHeight";
5
5
 
6
6
  export function useKeyboardHeight() {
7
- const [keyboardHeight, setKeyboardHeight] = useState(0);
7
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
8
8
 
9
- useEffect(() => {
10
- const { unsubscribe } = subscribeKeyboardHeight({
11
- callback: setKeyboardHeight,
12
- immediate: true,
13
- });
14
- return unsubscribe;
15
- }, []);
9
+ useEffect(() => {
10
+ const { unsubscribe } = subscribeKeyboardHeight({
11
+ callback: setKeyboardHeight,
12
+ immediate: true,
13
+ });
14
+ return unsubscribe;
15
+ }, []);
16
16
 
17
- return { keyboardHeight };
17
+ return { keyboardHeight };
18
18
  }