@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.
Files changed (44) hide show
  1. package/.gitattributes +2 -0
  2. package/AGENTS.md +398 -0
  3. package/components/ScrolltoTop.tsx +9 -0
  4. package/components/SwitchCase.tsx +15 -0
  5. package/components/ViewportPortal.tsx +40 -0
  6. package/cookie/cookie.shared.ts +142 -0
  7. package/datetime/dateTime.client.ts +108 -0
  8. package/datetime/dateTime.server.ts +108 -0
  9. package/datetime/dateTime.shared.ts +358 -0
  10. package/hooks/useAvoidKeyboard.ts +28 -0
  11. package/hooks/useCheckInvisible.ts +25 -0
  12. package/hooks/useCheckScroll.ts +19 -0
  13. package/hooks/useClientDateTime.ts +49 -0
  14. package/hooks/useDebounce.ts +118 -0
  15. package/hooks/useDebouncedCallback.ts +69 -0
  16. package/hooks/useDoubleClick.ts +53 -0
  17. package/hooks/useGeolocation.ts +146 -0
  18. package/hooks/useHasMounted.ts +13 -0
  19. package/hooks/useIntersectionObserver.ts +34 -0
  20. package/hooks/useInterval.ts +55 -0
  21. package/hooks/useKeyboardHeight.ts +18 -0
  22. package/hooks/useKeyboardListNavigation.ts +170 -0
  23. package/hooks/useLongPress.ts +120 -0
  24. package/hooks/usePreservedCallback.ts +21 -0
  25. package/hooks/usePreservedReference.ts +21 -0
  26. package/hooks/useRefEffect.ts +35 -0
  27. package/hooks/useRelativeDateTime.ts +54 -0
  28. package/hooks/useTimeout.ts +56 -0
  29. package/hooks/useToggleState.ts +13 -0
  30. package/hooks/useViewportHeight.ts +23 -0
  31. package/hooks/useViewportMatch.ts +33 -0
  32. package/index.ts +111 -0
  33. package/package.json +15 -0
  34. package/tsconfig.json +18 -0
  35. package/utils/browserStorage.ts +59 -0
  36. package/utils/buildContext.tsx +19 -0
  37. package/utils/checkDevice.ts +74 -0
  38. package/utils/clipboardShare.tsx +47 -0
  39. package/utils/clipboardShare.types.ts +18 -0
  40. package/utils/floatingMotion.ts +99 -0
  41. package/utils/keyboardTarget.ts +13 -0
  42. package/utils/mergeRefs.ts +15 -0
  43. package/utils/seen.ts +29 -0
  44. package/utils/subscribeKeyboardHeight.ts +54 -0
@@ -0,0 +1,120 @@
1
+ "use client";
2
+
3
+ import { type MouseEvent, type TouchEvent, useCallback, useRef } from "react";
4
+
5
+ export type UseLongPressOptions<E extends HTMLElement> = {
6
+ delay?: number;
7
+ moveThreshold?: {
8
+ x?: number;
9
+ y?: number;
10
+ };
11
+ onClick?: (event: MouseEvent<E> | TouchEvent<E>) => void;
12
+ onLongPressEnd?: (event: MouseEvent<E> | TouchEvent<E>) => void;
13
+ };
14
+
15
+ export function useLongPress<E extends HTMLElement = HTMLElement>(
16
+ onLongPress: (event: MouseEvent<E> | TouchEvent<E>) => void,
17
+ {
18
+ delay = 500,
19
+ moveThreshold,
20
+ onClick,
21
+ onLongPressEnd,
22
+ }: UseLongPressOptions<E> = {},
23
+ ) {
24
+ const timeoutRef = useRef<number | null>(null);
25
+ const isLongPressActiveRef = useRef(false);
26
+ const initialPositionRef = useRef({ x: 0, y: 0 });
27
+
28
+ const savedOnLongPress = useRef(onLongPress);
29
+ const savedOnClick = useRef(onClick);
30
+ const savedOnLongPressEnd = useRef(onLongPressEnd);
31
+ savedOnLongPress.current = onLongPress;
32
+ savedOnClick.current = onClick;
33
+ savedOnLongPressEnd.current = onLongPressEnd;
34
+
35
+ const hasThreshold =
36
+ moveThreshold?.x !== undefined || moveThreshold?.y !== undefined;
37
+
38
+ const getClientPosition = useCallback(
39
+ (event: MouseEvent<E> | TouchEvent<E>) => {
40
+ if ("touches" in event.nativeEvent) {
41
+ const touch = event.nativeEvent.touches[0];
42
+ return { x: touch.clientX, y: touch.clientY };
43
+ }
44
+ return {
45
+ x: event.nativeEvent.clientX,
46
+ y: event.nativeEvent.clientY,
47
+ };
48
+ },
49
+ [],
50
+ );
51
+
52
+ const isMovedBeyondThreshold = useCallback(
53
+ (event: MouseEvent<E> | TouchEvent<E>) => {
54
+ const { x, y } = getClientPosition(event);
55
+ const deltaX = Math.abs(x - initialPositionRef.current.x);
56
+ const deltaY = Math.abs(y - initialPositionRef.current.y);
57
+
58
+ return (
59
+ (moveThreshold?.x !== undefined && deltaX > moveThreshold.x) ||
60
+ (moveThreshold?.y !== undefined && deltaY > moveThreshold.y)
61
+ );
62
+ },
63
+ [getClientPosition, moveThreshold],
64
+ );
65
+
66
+ const cancelLongPress = useCallback(() => {
67
+ if (timeoutRef.current !== null) {
68
+ window.clearTimeout(timeoutRef.current);
69
+ timeoutRef.current = null;
70
+ }
71
+ }, []);
72
+
73
+ const handlePressStart = useCallback(
74
+ (event: MouseEvent<E> | TouchEvent<E>) => {
75
+ cancelLongPress();
76
+ initialPositionRef.current = getClientPosition(event);
77
+ isLongPressActiveRef.current = false;
78
+
79
+ timeoutRef.current = window.setTimeout(() => {
80
+ isLongPressActiveRef.current = true;
81
+ savedOnLongPress.current(event);
82
+ }, delay);
83
+ },
84
+ [cancelLongPress, delay, getClientPosition],
85
+ );
86
+
87
+ const handlePressEnd = useCallback(
88
+ (event: MouseEvent<E> | TouchEvent<E>) => {
89
+ if (isLongPressActiveRef.current) {
90
+ savedOnLongPressEnd.current?.(event);
91
+ } else if (timeoutRef.current !== null) {
92
+ savedOnClick.current?.(event);
93
+ }
94
+
95
+ cancelLongPress();
96
+ isLongPressActiveRef.current = false;
97
+ },
98
+ [cancelLongPress],
99
+ );
100
+
101
+ const handlePressMove = useCallback(
102
+ (event: MouseEvent<E> | TouchEvent<E>) => {
103
+ if (timeoutRef.current !== null && isMovedBeyondThreshold(event)) {
104
+ cancelLongPress();
105
+ }
106
+ },
107
+ [cancelLongPress, isMovedBeyondThreshold],
108
+ );
109
+
110
+ return {
111
+ onMouseDown: handlePressStart,
112
+ onMouseUp: handlePressEnd,
113
+ onMouseLeave: cancelLongPress,
114
+ onTouchStart: handlePressStart,
115
+ onTouchEnd: handlePressEnd,
116
+ ...(hasThreshold
117
+ ? { onTouchMove: handlePressMove, onMouseMove: handlePressMove }
118
+ : {}),
119
+ };
120
+ }
@@ -0,0 +1,21 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef } from "react";
4
+
5
+ export function usePreservedCallback<
6
+ Arguments extends unknown[] = unknown[],
7
+ ReturnValue = unknown,
8
+ >(callback: (...args: Arguments) => ReturnValue) {
9
+ const callbackRef = useRef(callback);
10
+
11
+ useEffect(
12
+ function syncCallbackRef() {
13
+ callbackRef.current = callback;
14
+ },
15
+ [callback],
16
+ );
17
+
18
+ return useCallback((...args: Arguments) => {
19
+ return callbackRef.current(...args);
20
+ }, []);
21
+ }
@@ -0,0 +1,21 @@
1
+ "use client";
2
+
3
+ import { useMemo, useRef } from "react";
4
+
5
+ function areDeeplyEqual<T>(a: T, b: T): boolean {
6
+ return JSON.stringify(a) === JSON.stringify(b);
7
+ }
8
+
9
+ export function usePreservedReference<T>(
10
+ value: T,
11
+ areValuesEqual: (a: T, b: T) => boolean = areDeeplyEqual,
12
+ ): T {
13
+ const ref = useRef(value);
14
+
15
+ return useMemo(() => {
16
+ if (!areValuesEqual(ref.current, value)) {
17
+ ref.current = value;
18
+ }
19
+ return ref.current;
20
+ }, [areValuesEqual, value]);
21
+ }
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import { type DependencyList, useCallback, useRef } from "react";
4
+
5
+ import { usePreservedCallback } from "./usePreservedCallback";
6
+
7
+ export type CleanupCallback = () => void;
8
+
9
+ export function useRefEffect<RefElement extends HTMLElement = HTMLElement>(
10
+ callback: (element: RefElement) => CleanupCallback | undefined,
11
+ deps: DependencyList,
12
+ ): (element: RefElement | null) => void {
13
+ const preservedCallback = usePreservedCallback(callback);
14
+ const cleanupCallbackRef = useRef<CleanupCallback>(() => {});
15
+
16
+ const effect = useCallback(
17
+ (element: RefElement | null) => {
18
+ cleanupCallbackRef.current();
19
+ cleanupCallbackRef.current = () => {};
20
+
21
+ if (element == null) {
22
+ return;
23
+ }
24
+
25
+ const cleanup = preservedCallback(element);
26
+
27
+ if (typeof cleanup === "function") {
28
+ cleanupCallbackRef.current = cleanup;
29
+ }
30
+ },
31
+ [preservedCallback, ...deps],
32
+ );
33
+
34
+ return effect;
35
+ }
@@ -0,0 +1,54 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { formatClientRelative } from "../datetime/dateTime.client";
5
+ import type { DateInput, DatePreset } from "../datetime/dateTime.shared";
6
+ import { toDate } from "../datetime/dateTime.shared";
7
+
8
+ type UseRelativeDateTimeOptions = {
9
+ locale?: string;
10
+ maxRelativeDays?: number;
11
+ fallbackDatePreset?: DatePreset;
12
+ updateIntervalMs?: number;
13
+ };
14
+
15
+ export function useRelativeDateTime(
16
+ value: DateInput,
17
+ options?: UseRelativeDateTimeOptions,
18
+ ) {
19
+ const [ready, setReady] = useState(false);
20
+ const [text, setText] = useState("");
21
+ const [isRelative, setIsRelative] = useState(false);
22
+
23
+ useEffect(() => {
24
+ const update = () => {
25
+ const next = formatClientRelative(value, {
26
+ locale: options?.locale,
27
+ maxRelativeDays: options?.maxRelativeDays,
28
+ fallbackDatePreset: options?.fallbackDatePreset,
29
+ now: Date.now(),
30
+ });
31
+ setText(next.text);
32
+ setIsRelative(next.isRelative);
33
+ setReady(true);
34
+ };
35
+
36
+ update();
37
+
38
+ const timer = window.setInterval(update, options?.updateIntervalMs ?? 1000);
39
+ return () => window.clearInterval(timer);
40
+ }, [
41
+ options?.fallbackDatePreset,
42
+ options?.locale,
43
+ options?.maxRelativeDays,
44
+ options?.updateIntervalMs,
45
+ value,
46
+ ]);
47
+
48
+ return {
49
+ ready,
50
+ text,
51
+ isRelative,
52
+ date: toDate(value),
53
+ };
54
+ }
@@ -0,0 +1,56 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef } from "react";
4
+
5
+ import { usePreservedCallback } from "./usePreservedCallback";
6
+
7
+ type UseTimeoutOptions = {
8
+ enabled?: boolean;
9
+ };
10
+
11
+ export type UseTimeoutControls = {
12
+ start: (overrideDelay?: number) => void;
13
+ reset: (overrideDelay?: number) => void;
14
+ clear: () => void;
15
+ isPending: () => boolean;
16
+ };
17
+
18
+ export function useTimeout(
19
+ callback: () => void,
20
+ delay = 0,
21
+ options?: UseTimeoutOptions,
22
+ ): UseTimeoutControls {
23
+ const { enabled = true } = options ?? {};
24
+ const preservedCallback = usePreservedCallback(callback);
25
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
26
+
27
+ const clear = useCallback(() => {
28
+ if (timeoutRef.current !== null) {
29
+ clearTimeout(timeoutRef.current);
30
+ timeoutRef.current = null;
31
+ }
32
+ }, []);
33
+
34
+ const start = useCallback(
35
+ (overrideDelay?: number) => {
36
+ clear();
37
+ timeoutRef.current = setTimeout(() => {
38
+ timeoutRef.current = null;
39
+ preservedCallback();
40
+ }, overrideDelay ?? delay);
41
+ },
42
+ [clear, delay, preservedCallback],
43
+ );
44
+
45
+ const isPending = useCallback(() => timeoutRef.current !== null, []);
46
+
47
+ useEffect(() => {
48
+ if (!enabled) return;
49
+ start();
50
+ return clear;
51
+ }, [enabled, start, clear]);
52
+
53
+ useEffect(() => clear, [clear]);
54
+
55
+ return { start, reset: start, clear, isPending };
56
+ }
@@ -0,0 +1,13 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+
5
+ export function useToggleState(initialValue = false) {
6
+ const [value, setValue] = useState(initialValue);
7
+
8
+ const setTrue = useCallback(() => setValue(true), []);
9
+ const setFalse = useCallback(() => setValue(false), []);
10
+ const toggle = useCallback(() => setValue((v) => !v), []);
11
+
12
+ return { value, setValue, setTrue, setFalse, toggle };
13
+ }
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+
5
+ export function useViewportHeight() {
6
+ const [height, setHeight] = useState(0);
7
+
8
+ useEffect(() => {
9
+ const update = () =>
10
+ setHeight(window.visualViewport?.height ?? window.innerHeight);
11
+
12
+ update();
13
+ window.visualViewport?.addEventListener("resize", update);
14
+ window.addEventListener("resize", update);
15
+
16
+ return () => {
17
+ window.visualViewport?.removeEventListener("resize", update);
18
+ window.removeEventListener("resize", update);
19
+ };
20
+ }, []);
21
+
22
+ return { height };
23
+ }
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+
5
+ /**
6
+ * Tracks whether the current viewport matches a media query.
7
+ *
8
+ * @param mediaQuery CSS media query string.
9
+ * @returns True when current viewport matches the given query.
10
+ */
11
+ export function useViewportMatch(mediaQuery: string): boolean {
12
+ const [isMatched, setIsMatched] = useState(false);
13
+
14
+ useEffect(() => {
15
+ if (typeof window === "undefined") {
16
+ return;
17
+ }
18
+
19
+ const mediaQueryList = window.matchMedia(mediaQuery);
20
+ const syncMatchState = () => {
21
+ setIsMatched(mediaQueryList.matches);
22
+ };
23
+
24
+ syncMatchState();
25
+ mediaQueryList.addEventListener("change", syncMatchState);
26
+
27
+ return () => {
28
+ mediaQueryList.removeEventListener("change", syncMatchState);
29
+ };
30
+ }, [mediaQuery]);
31
+
32
+ return isMatched;
33
+ }
package/index.ts ADDED
@@ -0,0 +1,111 @@
1
+ // 컴포넌트
2
+ export {
3
+ getViewportPortalRoot,
4
+ ViewportPortal
5
+ } from "./components/ViewportPortal";
6
+ // 쿠키
7
+ export {
8
+ clearAllClientCookies,
9
+ clearClientCookie,
10
+ getClientCookie,
11
+ parseClientCookieNames,
12
+ setClientCookie
13
+ } from "./cookie/cookie.shared";
14
+ // 날짜/시간
15
+ export {
16
+ formatClientDate,
17
+ formatClientDateTime,
18
+ formatClientRelative,
19
+ formatClientTime
20
+ } from "./datetime/dateTime.client";
21
+ export {
22
+ formatServerDate,
23
+ formatServerDateTime,
24
+ formatServerRelative,
25
+ formatServerTime
26
+ } from "./datetime/dateTime.server";
27
+ export {
28
+ addUtcDays,
29
+ format24HourTime,
30
+ formatDotDate,
31
+ formatKoreanTime,
32
+ formatLongDate,
33
+ formatRelativeText,
34
+ formatRemainingText,
35
+ formatTwelveHourTime,
36
+ formatUtcDateKey,
37
+ getUtcWeekdayIndex,
38
+ normalizeAppLocale,
39
+ parseUtcDateInput,
40
+ toDate,
41
+ toIntlLocale,
42
+ toUtcMidnight
43
+ } from "./datetime/dateTime.shared";
44
+ export type {
45
+ AppLocale,
46
+ DateInput,
47
+ DatePreset,
48
+ TimePreset
49
+ } from "./datetime/dateTime.shared";
50
+ // hooks
51
+ export { useAvoidKeyboard } from "./hooks/useAvoidKeyboard";
52
+ export { useClientDateTime } from "./hooks/useClientDateTime";
53
+ export { useDebounce } from "./hooks/useDebounce";
54
+ export { useDebouncedCallback } from "./hooks/useDebouncedCallback";
55
+ export { useDoubleClick } from "./hooks/useDoubleClick";
56
+ export { useGeolocation } from "./hooks/useGeolocation";
57
+ export { useHasMounted } from "./hooks/useHasMounted";
58
+ export { useIntersectionObserver } from "./hooks/useIntersectionObserver";
59
+ export { useInterval } from "./hooks/useInterval";
60
+ export { useKeyboardHeight } from "./hooks/useKeyboardHeight";
61
+ export { useKeyboardListNavigation } from "./hooks/useKeyboardListNavigation";
62
+ export { useLongPress } from "./hooks/useLongPress";
63
+ export { usePreservedCallback } from "./hooks/usePreservedCallback";
64
+ export { usePreservedReference } from "./hooks/usePreservedReference";
65
+ export { useRefEffect } from "./hooks/useRefEffect";
66
+ export { useRelativeDateTime } from "./hooks/useRelativeDateTime";
67
+ export { useTimeout } from "./hooks/useTimeout";
68
+ export { useToggleState } from "./hooks/useToggleState";
69
+ export { useViewportHeight } from "./hooks/useViewportHeight";
70
+ export { useViewportMatch } from "./hooks/useViewportMatch";
71
+ // 유틸리티
72
+ export {
73
+ getLocalStorage,
74
+ getSessionStorage,
75
+ removeLocalStorage,
76
+ removeSessionStorage,
77
+ updateLocalStorage,
78
+ updateSessionStorage
79
+ } from "./utils/browserStorage";
80
+ // 유틸리티
81
+ export { buildContext } from "./utils/buildContext";
82
+ export { getDeviceInfo } from "./utils/checkDevice";
83
+ export type { DeviceInfo } from "./utils/checkDevice";
84
+ export { NavigatorClipboard, NavigatorShare } from "./utils/clipboardShare";
85
+ export type {
86
+ NavigatorClipboardProps,
87
+ NavigatorClipboardResult,
88
+ NavigatorShareProps,
89
+ NavigatorShareResult
90
+ } from "./utils/clipboardShare.types";
91
+ export {
92
+ getFloatingHiddenTransform,
93
+ getFloatingMotionPreset,
94
+ getFloatingTransformOrigin
95
+ } from "./utils/floatingMotion";
96
+ export type {
97
+ FloatingMotionMode,
98
+ FloatingMotionPreset,
99
+ FloatingPlacement
100
+ } from "./utils/floatingMotion";
101
+ // 유틸리티
102
+ export { isEditableKeyboardTarget } from "./utils/keyboardTarget";
103
+ export { mergeRefs } from "./utils/mergeRefs";
104
+ export {
105
+ buildSeenValue,
106
+ hasSeenKey,
107
+ parseSeen,
108
+ SEEN_STORAGE_KEY
109
+ } from "./utils/seen";
110
+ export { subscribeKeyboardHeight } from "./utils/subscribeKeyboardHeight";
111
+
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@musecat/functionkit",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "./index.ts",
6
+ "types": "./index.ts",
7
+ "exports": {
8
+ ".": "./index.ts",
9
+ "./*": "./*"
10
+ },
11
+ "peerDependencies": {
12
+ "react": "^19.0.0",
13
+ "react-dom": "^19.0.0"
14
+ }
15
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true
16
+ },
17
+ "include": ["**/*.ts", "**/*.tsx"]
18
+ }
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ type StorageKind = "localStorage" | "sessionStorage";
4
+
5
+ function getStorage(kind: StorageKind): Storage | null {
6
+ if (typeof window === "undefined") return null;
7
+ return window[kind];
8
+ }
9
+
10
+ function saveStorage(kind: StorageKind, key: string, value: unknown) {
11
+ const storage = getStorage(kind);
12
+ if (!storage) return;
13
+ try {
14
+ storage.setItem(key, JSON.stringify(value));
15
+ } catch {}
16
+ }
17
+
18
+ function loadStorage(kind: StorageKind, key: string): unknown {
19
+ const storage = getStorage(kind);
20
+ if (!storage) return null;
21
+ try {
22
+ const raw = storage.getItem(key);
23
+ return raw === null ? null : (JSON.parse(raw) as unknown);
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ function removeStorage(kind: StorageKind, key: string) {
30
+ const storage = getStorage(kind);
31
+ if (!storage) return;
32
+ try {
33
+ storage.removeItem(key);
34
+ } catch {}
35
+ }
36
+
37
+ export function getLocalStorage(key: string): unknown {
38
+ return loadStorage("localStorage", key);
39
+ }
40
+
41
+ export function updateLocalStorage(key: string, value: unknown) {
42
+ saveStorage("localStorage", key, value);
43
+ }
44
+
45
+ export function removeLocalStorage(key: string) {
46
+ removeStorage("localStorage", key);
47
+ }
48
+
49
+ export function getSessionStorage(key: string): unknown {
50
+ return loadStorage("sessionStorage", key);
51
+ }
52
+
53
+ export function updateSessionStorage(key: string, value: unknown) {
54
+ saveStorage("sessionStorage", key, value);
55
+ }
56
+
57
+ export function removeSessionStorage(key: string) {
58
+ removeStorage("sessionStorage", key);
59
+ }
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ import { createContext, type ReactNode, useContext, useMemo } from "react";
4
+
5
+ export function buildContext<T>(defaultValue: T) {
6
+ const Ctx = createContext<T>(defaultValue);
7
+
8
+ function Provider({ value, children }: { value: T; children: ReactNode }) {
9
+ const stable = useMemo(() => value, [value]);
10
+ return <Ctx.Provider value={stable}>{children}</Ctx.Provider>;
11
+ }
12
+
13
+ function useValue() {
14
+ const ctx = useContext(Ctx);
15
+ return ctx;
16
+ }
17
+
18
+ return [Provider, useValue] as const;
19
+ }
@@ -0,0 +1,74 @@
1
+ export type DeviceInfo = {
2
+ isMobile: boolean;
3
+ isIOS: boolean;
4
+ isAndroid: boolean;
5
+ isSafari: boolean;
6
+ isIOSSafari: boolean;
7
+ isMacSafari: boolean;
8
+ isSamsungBrowser: boolean;
9
+ isTouchDevice: boolean;
10
+ browser: string;
11
+ };
12
+
13
+ export function getDeviceInfo(): DeviceInfo {
14
+ let isMobile = false;
15
+ let isIOS = false;
16
+ let isAndroid = false;
17
+ let isSafari = false;
18
+ let isIOSSafari = false;
19
+ let isMacSafari = false;
20
+ let isSamsungBrowser = false;
21
+ let isTouchDevice = false;
22
+ let browser = "unknown";
23
+
24
+ if (typeof window === "undefined") {
25
+ return {
26
+ isMobile,
27
+ isIOS,
28
+ isAndroid,
29
+ isSafari,
30
+ isIOSSafari,
31
+ isMacSafari,
32
+ isSamsungBrowser,
33
+ isTouchDevice,
34
+ browser,
35
+ };
36
+ }
37
+
38
+ const ua = navigator.userAgent;
39
+
40
+ isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(ua);
41
+ isIOS = /iPhone|iPad|iPod/i.test(ua);
42
+ isAndroid = /Android/i.test(ua);
43
+ isSafari = /Safari/i.test(ua) && !/Chrome/i.test(ua);
44
+ isIOSSafari = isIOS && isSafari;
45
+ isMacSafari = /Macintosh/i.test(ua) && isSafari && !isIOS;
46
+ isSamsungBrowser = /SamsungBrowser/i.test(ua);
47
+ isTouchDevice = "ontouchstart" in window;
48
+
49
+ if (isSamsungBrowser) {
50
+ browser = "samsung";
51
+ } else if (isSafari) {
52
+ browser = "safari";
53
+ } else if (/Chrome/i.test(ua)) {
54
+ browser = "chrome";
55
+ } else if (/Firefox/i.test(ua)) {
56
+ browser = "firefox";
57
+ } else if (/Edge/i.test(ua)) {
58
+ browser = "edge";
59
+ } else {
60
+ browser = "unknown";
61
+ }
62
+
63
+ return {
64
+ isMobile,
65
+ isIOS,
66
+ isAndroid,
67
+ isSafari,
68
+ isIOSSafari,
69
+ isMacSafari,
70
+ isSamsungBrowser,
71
+ isTouchDevice,
72
+ browser,
73
+ };
74
+ }