@musecat/functionkit 1.0.0 → 1.0.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.
- 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 +37 -0
- package/.agents/references/hooks/useInterval.md +40 -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 +33 -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/AGENTS.md +103 -396
- package/LICENSE +9 -0
- package/README.md +38 -0
- package/index.ts +97 -102
- package/package.json +27 -13
- package/{components → packages/components}/ScrolltoTop.tsx +3 -3
- package/{components → packages/components}/SwitchCase.tsx +7 -7
- package/packages/components/ViewportPortal.tsx +40 -0
- package/packages/cookie/cookie.shared.ts +138 -0
- package/packages/datetime/dateTime.client.ts +108 -0
- package/packages/datetime/dateTime.server.ts +108 -0
- package/packages/datetime/dateTime.shared.ts +358 -0
- package/packages/hooks/useAvoidKeyboard.ts +28 -0
- package/packages/hooks/useCheckInvisible.ts +25 -0
- package/packages/hooks/useCheckScroll.ts +19 -0
- package/packages/hooks/useClientDateTime.ts +49 -0
- package/packages/hooks/useDebounce.ts +118 -0
- package/packages/hooks/useDebouncedCallback.ts +69 -0
- package/packages/hooks/useDoubleClick.ts +53 -0
- package/packages/hooks/useGeolocation.ts +146 -0
- package/packages/hooks/useHasMounted.ts +13 -0
- package/packages/hooks/useIntersectionObserver.ts +34 -0
- package/packages/hooks/useInterval.ts +56 -0
- package/packages/hooks/useKeyboardHeight.ts +18 -0
- package/packages/hooks/useKeyboardListNavigation.ts +170 -0
- package/packages/hooks/useLongPress.ts +120 -0
- package/packages/hooks/usePreservedCallback.ts +21 -0
- package/packages/hooks/usePreservedReference.ts +21 -0
- package/packages/hooks/useRefEffect.ts +35 -0
- package/packages/hooks/useRelativeDateTime.ts +54 -0
- package/packages/hooks/useTimeout.ts +56 -0
- package/packages/hooks/useToggleState.ts +13 -0
- package/packages/hooks/useViewportHeight.ts +23 -0
- package/packages/hooks/useViewportMatch.ts +27 -0
- package/packages/utils/browserStorage.ts +59 -0
- package/packages/utils/buildContext.tsx +19 -0
- package/packages/utils/checkDevice.ts +74 -0
- package/packages/utils/clipboardShare.tsx +47 -0
- package/{utils → packages/utils}/clipboardShare.types.ts +7 -7
- package/packages/utils/floatingMotion.ts +99 -0
- package/packages/utils/keyboardTarget.ts +13 -0
- package/packages/utils/mergeRefs.ts +15 -0
- package/packages/utils/seen.ts +29 -0
- package/packages/utils/subscribeKeyboardHeight.ts +54 -0
- package/tests/components/ScrolltoTop.test.tsx +15 -0
- package/tests/components/SwitchCase.test.tsx +34 -0
- package/tests/components/ViewportPortal.test.tsx +42 -0
- package/tests/cookie/cookie.test.ts +156 -0
- package/tests/datetime/datetime.test.ts +250 -0
- package/tests/hooks/useAvoidKeyboard.test.ts +12 -0
- package/tests/hooks/useCheckInvisible.test.ts +28 -0
- package/tests/hooks/useCheckScroll.test.ts +23 -0
- package/tests/hooks/useClientDateTime.test.ts +14 -0
- package/tests/hooks/useDebounce.test.ts +71 -0
- package/tests/hooks/useDebouncedCallback.test.ts +34 -0
- package/tests/hooks/useDoubleClick.test.ts +31 -0
- package/tests/hooks/useGeolocation.test.ts +50 -0
- package/tests/hooks/useHasMounted.test.ts +10 -0
- package/tests/hooks/useIntersectionObserver.test.ts +13 -0
- package/tests/hooks/useInterval.test.ts +31 -0
- package/tests/hooks/useKeyboardHeight.test.ts +10 -0
- package/tests/hooks/useKeyboardListNavigation.test.ts +82 -0
- package/tests/hooks/useLongPress.test.ts +52 -0
- package/tests/hooks/usePreservedCallback.test.ts +35 -0
- package/tests/hooks/usePreservedReference.test.ts +35 -0
- package/tests/hooks/useRefEffect.test.ts +30 -0
- package/tests/hooks/useRelativeDateTime.test.ts +19 -0
- package/tests/hooks/useTimeout.test.ts +34 -0
- package/tests/hooks/useToggleState.test.ts +35 -0
- package/tests/hooks/useViewportHeight.test.ts +10 -0
- package/tests/hooks/useViewportMatch.test.ts +35 -0
- package/tests/setup.ts +9 -0
- package/tests/utils/browserStorage.test.ts +54 -0
- package/tests/utils/buildContext.test.tsx +27 -0
- package/tests/utils/checkDevice.test.ts +84 -0
- package/tests/utils/clipboardShare.test.ts +78 -0
- package/tests/utils/floatingMotion.test.ts +84 -0
- package/tests/utils/keyboardTarget.test.ts +44 -0
- package/tests/utils/mergeRefs.test.ts +18 -0
- package/tests/utils/seen.test.ts +31 -0
- package/tests/utils/subscribeKeyboardHeight.test.ts +50 -0
- package/tsconfig.json +16 -16
- package/vitest.config.ts +16 -0
- package/components/ViewportPortal.tsx +0 -40
- package/cookie/cookie.shared.ts +0 -142
- package/datetime/dateTime.client.ts +0 -108
- package/datetime/dateTime.server.ts +0 -108
- package/datetime/dateTime.shared.ts +0 -358
- package/hooks/useAvoidKeyboard.ts +0 -28
- package/hooks/useCheckInvisible.ts +0 -25
- package/hooks/useCheckScroll.ts +0 -19
- package/hooks/useClientDateTime.ts +0 -49
- package/hooks/useDebounce.ts +0 -118
- package/hooks/useDebouncedCallback.ts +0 -69
- package/hooks/useDoubleClick.ts +0 -53
- package/hooks/useGeolocation.ts +0 -146
- package/hooks/useHasMounted.ts +0 -13
- package/hooks/useIntersectionObserver.ts +0 -34
- package/hooks/useInterval.ts +0 -55
- package/hooks/useKeyboardHeight.ts +0 -18
- package/hooks/useKeyboardListNavigation.ts +0 -170
- package/hooks/useLongPress.ts +0 -120
- package/hooks/usePreservedCallback.ts +0 -21
- package/hooks/usePreservedReference.ts +0 -21
- package/hooks/useRefEffect.ts +0 -35
- package/hooks/useRelativeDateTime.ts +0 -54
- package/hooks/useTimeout.ts +0 -56
- package/hooks/useToggleState.ts +0 -13
- package/hooks/useViewportHeight.ts +0 -23
- package/hooks/useViewportMatch.ts +0 -33
- package/utils/browserStorage.ts +0 -59
- package/utils/buildContext.tsx +0 -19
- package/utils/checkDevice.ts +0 -74
- package/utils/clipboardShare.tsx +0 -47
- package/utils/floatingMotion.ts +0 -99
- package/utils/keyboardTarget.ts +0 -13
- package/utils/mergeRefs.ts +0 -15
- package/utils/seen.ts +0 -29
- package/utils/subscribeKeyboardHeight.ts +0 -54
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, test, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { NavigatorClipboard, NavigatorShare } from "@/packages/utils/clipboardShare";
|
|
3
|
+
|
|
4
|
+
describe("NavigatorClipboard", () => {
|
|
5
|
+
test("resolves with success: true on write", async () => {
|
|
6
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
7
|
+
value: { writeText: vi.fn(() => Promise.resolve()) },
|
|
8
|
+
writable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const result = await NavigatorClipboard({ text: "hello" });
|
|
13
|
+
expect(result).toEqual({ success: true });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("resolves with success: false on failure", async () => {
|
|
17
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
18
|
+
value: { writeText: vi.fn(() => Promise.reject(new Error("denied"))) },
|
|
19
|
+
writable: true,
|
|
20
|
+
configurable: true,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const result = await NavigatorClipboard({ text: "hello" });
|
|
24
|
+
expect(result).toEqual({ success: false });
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("NavigatorShare", () => {
|
|
29
|
+
afterEach(() => { vi.restoreAllMocks(); });
|
|
30
|
+
|
|
31
|
+
test("uses Web Share API when available", async () => {
|
|
32
|
+
Object.defineProperty(navigator, "share", {
|
|
33
|
+
value: vi.fn(() => Promise.resolve()),
|
|
34
|
+
writable: true,
|
|
35
|
+
configurable: true,
|
|
36
|
+
});
|
|
37
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
38
|
+
value: { writeText: vi.fn() },
|
|
39
|
+
writable: true,
|
|
40
|
+
configurable: true,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const result = await NavigatorShare({ title: "Test", text: "Hello", link: "https://example.com" });
|
|
44
|
+
expect(result).toEqual({ success: true, method: "share" });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("falls back to clipboard when share is unavailable", async () => {
|
|
48
|
+
Object.defineProperty(navigator, "share", {
|
|
49
|
+
value: undefined,
|
|
50
|
+
writable: true,
|
|
51
|
+
configurable: true,
|
|
52
|
+
});
|
|
53
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
54
|
+
value: { writeText: vi.fn(() => Promise.resolve()) },
|
|
55
|
+
writable: true,
|
|
56
|
+
configurable: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const result = await NavigatorShare({ title: "Test", text: "Hello", link: "https://example.com" });
|
|
60
|
+
expect(result).toEqual({ success: true, method: "clipboard" });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("returns unsupported when neither share nor clipboard", async () => {
|
|
64
|
+
Object.defineProperty(navigator, "share", {
|
|
65
|
+
value: undefined,
|
|
66
|
+
writable: true,
|
|
67
|
+
configurable: true,
|
|
68
|
+
});
|
|
69
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
70
|
+
value: undefined,
|
|
71
|
+
writable: true,
|
|
72
|
+
configurable: true,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = await NavigatorShare({ title: "Test", text: "Hello", link: "https://example.com" });
|
|
76
|
+
expect(result).toEqual({ success: false, method: "unsupported" });
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
getFloatingMotionPreset,
|
|
4
|
+
getFloatingTransformOrigin,
|
|
5
|
+
getFloatingHiddenTransform,
|
|
6
|
+
} from "@/packages/utils/floatingMotion";
|
|
7
|
+
|
|
8
|
+
describe("getFloatingMotionPreset", () => {
|
|
9
|
+
test("returns anchored preset", () => {
|
|
10
|
+
const preset = getFloatingMotionPreset("anchored");
|
|
11
|
+
expect(preset.enterMs).toBe(230);
|
|
12
|
+
expect(preset.exitMs).toBe(170);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("returns modal-center preset", () => {
|
|
16
|
+
const preset = getFloatingMotionPreset("modal-center");
|
|
17
|
+
expect(preset.enterMs).toBe(340);
|
|
18
|
+
expect(preset.exitMs).toBe(260);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("returns center-selected preset", () => {
|
|
22
|
+
const preset = getFloatingMotionPreset("center-selected");
|
|
23
|
+
expect(preset.enterMs).toBe(190);
|
|
24
|
+
expect(preset.exitMs).toBe(140);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("returns mobile-sheet preset", () => {
|
|
28
|
+
const preset = getFloatingMotionPreset("mobile-sheet");
|
|
29
|
+
expect(preset.enterMs).toBe(360);
|
|
30
|
+
expect(preset.exitMs).toBe(280);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("getFloatingTransformOrigin", () => {
|
|
35
|
+
test("returns default when no placement", () => {
|
|
36
|
+
expect(getFloatingTransformOrigin()).toBe("top center");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("returns correct origin for top-left", () => {
|
|
40
|
+
expect(getFloatingTransformOrigin("top-left")).toBe("bottom left");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("returns correct origin for bottom-right", () => {
|
|
44
|
+
expect(getFloatingTransformOrigin("bottom-right")).toBe("top right");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("returns correct origin for middle-center", () => {
|
|
48
|
+
expect(getFloatingTransformOrigin("middle-center")).toBe("center center");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("getFloatingHiddenTransform", () => {
|
|
53
|
+
test("returns mobile-sheet transform", () => {
|
|
54
|
+
expect(getFloatingHiddenTransform({ mode: "mobile-sheet" })).toBe("translateY(1.8rem) scale(1)");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("returns modal-center transform", () => {
|
|
58
|
+
expect(getFloatingHiddenTransform({ mode: "modal-center" })).toBe("translateY(.8rem) scale(.94)");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("returns center-selected transform", () => {
|
|
62
|
+
expect(getFloatingHiddenTransform({ mode: "center-selected" })).toBe("translateY(.45rem) scale(.99)");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("anchored with no placement", () => {
|
|
66
|
+
expect(getFloatingHiddenTransform({ mode: "anchored" })).toBe("translateY(.4rem) scale(.975)");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("anchored with top placement", () => {
|
|
70
|
+
expect(getFloatingHiddenTransform({ mode: "anchored", placement: "top-left" })).toBe("translateY(.4rem) scale(.975)");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("anchored with bottom placement", () => {
|
|
74
|
+
expect(getFloatingHiddenTransform({ mode: "anchored", placement: "bottom-center" })).toBe("translateY(-.4rem) scale(.975)");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("anchored with middle-left placement", () => {
|
|
78
|
+
expect(getFloatingHiddenTransform({ mode: "anchored", placement: "middle-left" })).toBe("translateX(.4rem) scale(.975)");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("anchored with middle-right placement", () => {
|
|
82
|
+
expect(getFloatingHiddenTransform({ mode: "anchored", placement: "middle-right" })).toBe("translateX(-.4rem) scale(.975)");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { isEditableKeyboardTarget } from "@/packages/utils/keyboardTarget";
|
|
3
|
+
|
|
4
|
+
describe("isEditableKeyboardTarget", () => {
|
|
5
|
+
test("returns true for contentEditable", () => {
|
|
6
|
+
const el = document.createElement("div");
|
|
7
|
+
Object.defineProperty(el, "isContentEditable", { value: true });
|
|
8
|
+
expect(isEditableKeyboardTarget(el)).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("returns true for TEXTAREA", () => {
|
|
12
|
+
const el = document.createElement("textarea");
|
|
13
|
+
expect(isEditableKeyboardTarget(el)).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("returns true for text INPUT", () => {
|
|
17
|
+
const el = document.createElement("input");
|
|
18
|
+
el.type = "text";
|
|
19
|
+
expect(isEditableKeyboardTarget(el)).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("returns false for button INPUT", () => {
|
|
23
|
+
const el = document.createElement("input");
|
|
24
|
+
el.type = "button";
|
|
25
|
+
expect(isEditableKeyboardTarget(el)).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("returns false for checkbox INPUT", () => {
|
|
29
|
+
const el = document.createElement("input");
|
|
30
|
+
el.type = "checkbox";
|
|
31
|
+
expect(isEditableKeyboardTarget(el)).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("returns false for radio INPUT", () => {
|
|
35
|
+
const el = document.createElement("input");
|
|
36
|
+
el.type = "radio";
|
|
37
|
+
expect(isEditableKeyboardTarget(el)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("returns false for regular div", () => {
|
|
41
|
+
const el = document.createElement("div");
|
|
42
|
+
expect(isEditableKeyboardTarget(el)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, test, expect, vi } from "vitest";
|
|
2
|
+
import { mergeRefs } from "@/packages/utils/mergeRefs";
|
|
3
|
+
|
|
4
|
+
describe("mergeRefs", () => {
|
|
5
|
+
test("merges multiple refs", () => {
|
|
6
|
+
const ref1 = vi.fn();
|
|
7
|
+
const ref2 = { current: null };
|
|
8
|
+
const ref3 = null;
|
|
9
|
+
|
|
10
|
+
const merged = mergeRefs(ref1, ref2, ref3);
|
|
11
|
+
const element = document.createElement("div");
|
|
12
|
+
|
|
13
|
+
merged(element as any);
|
|
14
|
+
|
|
15
|
+
expect(ref1).toHaveBeenCalledWith(element);
|
|
16
|
+
expect(ref2.current).toBe(element);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
parseSeen,
|
|
4
|
+
hasSeenKey,
|
|
5
|
+
buildSeenValue,
|
|
6
|
+
SEEN_STORAGE_KEY,
|
|
7
|
+
} from "@/packages/utils/seen";
|
|
8
|
+
|
|
9
|
+
describe("seen", () => {
|
|
10
|
+
test("SEEN_STORAGE_KEY", () => {
|
|
11
|
+
expect(SEEN_STORAGE_KEY).toBe("seen");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("parseSeen", () => {
|
|
15
|
+
expect(parseSeen(null)).toEqual({});
|
|
16
|
+
expect(parseSeen('{"a":true}')).toEqual({ a: true });
|
|
17
|
+
expect(parseSeen("invalid json")).toEqual({});
|
|
18
|
+
expect(parseSeen("[]")).toEqual({});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("hasSeenKey", () => {
|
|
22
|
+
expect(hasSeenKey('{"a":true}', "a")).toBe(true);
|
|
23
|
+
expect(hasSeenKey('{"a":true}', "b")).toBe(false);
|
|
24
|
+
expect(hasSeenKey(null, "a")).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("buildSeenValue", () => {
|
|
28
|
+
const result = buildSeenValue('{"a":true}', "b");
|
|
29
|
+
expect(JSON.parse(result)).toEqual({ a: true, b: true });
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { subscribeKeyboardHeight } from "@/packages/utils/subscribeKeyboardHeight";
|
|
3
|
+
|
|
4
|
+
describe("subscribeKeyboardHeight", () => {
|
|
5
|
+
let visualViewport: any;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
visualViewport = {
|
|
9
|
+
height: 600,
|
|
10
|
+
width: 390,
|
|
11
|
+
offsetTop: 0,
|
|
12
|
+
addEventListener: vi.fn(),
|
|
13
|
+
removeEventListener: vi.fn(),
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(window, "visualViewport", {
|
|
16
|
+
value: visualViewport,
|
|
17
|
+
writable: true,
|
|
18
|
+
configurable: true,
|
|
19
|
+
});
|
|
20
|
+
Object.defineProperty(window, "innerHeight", {
|
|
21
|
+
value: 800,
|
|
22
|
+
writable: true,
|
|
23
|
+
configurable: true,
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => { vi.useRealTimers(); });
|
|
28
|
+
|
|
29
|
+
test("subscribes to resize events", () => {
|
|
30
|
+
const callback = vi.fn();
|
|
31
|
+
subscribeKeyboardHeight({ callback });
|
|
32
|
+
expect(visualViewport.addEventListener).toHaveBeenCalledWith("resize", expect.any(Function));
|
|
33
|
+
expect(visualViewport.addEventListener).toHaveBeenCalledWith("scroll", expect.any(Function));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("calls immediately when immediate is true", () => {
|
|
37
|
+
vi.useFakeTimers();
|
|
38
|
+
const callback = vi.fn();
|
|
39
|
+
subscribeKeyboardHeight({ callback, immediate: true });
|
|
40
|
+
vi.advanceTimersByTime(16);
|
|
41
|
+
expect(callback).toHaveBeenCalled();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("returns unsubscribe function", () => {
|
|
45
|
+
const callback = vi.fn();
|
|
46
|
+
const { unsubscribe } = subscribeKeyboardHeight({ callback });
|
|
47
|
+
unsubscribe();
|
|
48
|
+
expect(visualViewport.removeEventListener).toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
});
|
package/tsconfig.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
18
|
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
test: {
|
|
6
|
+
environment: "jsdom",
|
|
7
|
+
setupFiles: ["./tests/setup.ts"],
|
|
8
|
+
include: ["tests/**/*.test.{ts,tsx}"],
|
|
9
|
+
globals: true,
|
|
10
|
+
},
|
|
11
|
+
resolve: {
|
|
12
|
+
alias: {
|
|
13
|
+
"@": path.resolve(__dirname, "./"),
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
4
|
-
import { createPortal } from "react-dom";
|
|
5
|
-
|
|
6
|
-
const VIEWPORT_PORTAL_ROOT_ID = "viewport-portal-root";
|
|
7
|
-
|
|
8
|
-
export function getViewportPortalRoot() {
|
|
9
|
-
if (typeof document === "undefined") {
|
|
10
|
-
return null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const existingRoot = document.getElementById(VIEWPORT_PORTAL_ROOT_ID);
|
|
14
|
-
if (existingRoot instanceof HTMLDivElement) {
|
|
15
|
-
return existingRoot;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const root = document.createElement("div");
|
|
19
|
-
root.id = VIEWPORT_PORTAL_ROOT_ID;
|
|
20
|
-
root.setAttribute("data-viewport-portal-root", "true");
|
|
21
|
-
root.style.cssText =
|
|
22
|
-
"position:fixed;inset:0;pointer-events:none;z-index:9999;";
|
|
23
|
-
document.body.appendChild(root);
|
|
24
|
-
return root;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
type ViewportPortalProps = {
|
|
28
|
-
children: React.ReactNode;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export function ViewportPortal({ children }: ViewportPortalProps) {
|
|
32
|
-
const [root, setRoot] = useState<HTMLElement | null>(null);
|
|
33
|
-
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
setRoot(getViewportPortalRoot());
|
|
36
|
-
}, []);
|
|
37
|
-
|
|
38
|
-
if (!root) return null;
|
|
39
|
-
return createPortal(children, root);
|
|
40
|
-
}
|
package/cookie/cookie.shared.ts
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
const EXPIRED_COOKIE_DATE = "Thu, 01 Jan 1970 00:00:00 GMT";
|
|
4
|
-
|
|
5
|
-
type CookieStoreLike = {
|
|
6
|
-
delete: (name: string) => Promise<void>;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
type DocumentCookieRef = {
|
|
10
|
-
cookie: string;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
type ClearClientCookieOptions = {
|
|
14
|
-
hostname?: string;
|
|
15
|
-
path?: string;
|
|
16
|
-
documentRef?: DocumentCookieRef;
|
|
17
|
-
cookieStore?: CookieStoreLike;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
type ClearAllClientCookiesOptions = ClearClientCookieOptions & {
|
|
21
|
-
/**
|
|
22
|
-
* If true, cookies in the hostname's root path ("/") are also deleted.
|
|
23
|
-
* Use with caution.
|
|
24
|
-
*/
|
|
25
|
-
includeRoot?: boolean;
|
|
26
|
-
cookieString?: string;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
function findClientCookie(
|
|
30
|
-
name: string,
|
|
31
|
-
documentRef: DocumentCookieRef,
|
|
32
|
-
): string | undefined {
|
|
33
|
-
const parsed = parseClientCookie(documentRef.cookie);
|
|
34
|
-
return parsed[name];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function parseClientCookie(cookieString: string): Record<string, string> {
|
|
38
|
-
return cookieString
|
|
39
|
-
.split(";")
|
|
40
|
-
.map((pair) => pair.trim().split("=") as [string, string])
|
|
41
|
-
.reduce<Record<string, string>>((acc, [key, value]) => {
|
|
42
|
-
if (key) {
|
|
43
|
-
acc[key] = value;
|
|
44
|
-
}
|
|
45
|
-
return acc;
|
|
46
|
-
}, {});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function parseClientCookieNames(cookieString: string): string[] {
|
|
50
|
-
return cookieString
|
|
51
|
-
.split(";")
|
|
52
|
-
.map((entry) => {
|
|
53
|
-
const eqPos = entry.indexOf("=");
|
|
54
|
-
return eqPos > -1 ? entry.slice(0, eqPos).trim() : entry.trim();
|
|
55
|
-
})
|
|
56
|
-
.filter(Boolean);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function getClientCookie(name: string): string | undefined {
|
|
60
|
-
if (typeof document === "undefined") {
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return findClientCookie(name, document);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function setClientCookie(name: string, value: string, days?: number) {
|
|
68
|
-
if (typeof document === "undefined") {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
let expires = "";
|
|
73
|
-
if (days) {
|
|
74
|
-
const date = new Date();
|
|
75
|
-
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
|
76
|
-
expires = `; expires=${date.toUTCString()}`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
document.cookie = `${name}=${encodeURIComponent(value)}${expires}; path=/`;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function clearClientCookie(
|
|
83
|
-
name: string,
|
|
84
|
-
options: ClearClientCookieOptions = {},
|
|
85
|
-
): void {
|
|
86
|
-
const {
|
|
87
|
-
hostname = window.location.hostname,
|
|
88
|
-
path = "/",
|
|
89
|
-
documentRef = document,
|
|
90
|
-
} = options;
|
|
91
|
-
|
|
92
|
-
documentRef.cookie = `${name}=; expires=${EXPIRED_COOKIE_DATE}; path=${path}; domain=${hostname};`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function clearAllClientCookies(
|
|
96
|
-
options: ClearAllClientCookiesOptions = {},
|
|
97
|
-
): string[] {
|
|
98
|
-
const {
|
|
99
|
-
documentRef = document,
|
|
100
|
-
cookieStore,
|
|
101
|
-
includeRoot = false,
|
|
102
|
-
cookieString,
|
|
103
|
-
} = options;
|
|
104
|
-
|
|
105
|
-
const entries = cookieString
|
|
106
|
-
? parseClientCookieNames(cookieString)
|
|
107
|
-
: documentRef.cookie
|
|
108
|
-
.split(/;\s*/)
|
|
109
|
-
.map((entry) => {
|
|
110
|
-
const eqPos = entry.indexOf("=");
|
|
111
|
-
return eqPos > -1 ? entry.slice(0, eqPos).trim() : entry.trim();
|
|
112
|
-
})
|
|
113
|
-
.filter(Boolean);
|
|
114
|
-
|
|
115
|
-
if (cookieStore) {
|
|
116
|
-
for (const name of entries) {
|
|
117
|
-
cookieStore.delete(name);
|
|
118
|
-
}
|
|
119
|
-
return entries;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const hostnameParts = window.location.hostname.split(".");
|
|
123
|
-
const { pathname } = window.location;
|
|
124
|
-
|
|
125
|
-
const paths = [pathname];
|
|
126
|
-
if (includeRoot) {
|
|
127
|
-
paths.unshift("/");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
for (const name of entries) {
|
|
131
|
-
if (!name) continue;
|
|
132
|
-
|
|
133
|
-
for (const path of paths) {
|
|
134
|
-
for (let i = 0; i < hostnameParts.length; i++) {
|
|
135
|
-
const domain = hostnameParts.slice(i).join(".");
|
|
136
|
-
documentRef.cookie = `${name}=; expires=${EXPIRED_COOKIE_DATE}; path=${path}; domain=${domain};`;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return entries;
|
|
142
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type DateInput,
|
|
3
|
-
type DatePreset,
|
|
4
|
-
format24HourTime,
|
|
5
|
-
formatDotDate,
|
|
6
|
-
formatKoreanTime,
|
|
7
|
-
formatLongDate,
|
|
8
|
-
formatRelativeText,
|
|
9
|
-
formatTwelveHourTime,
|
|
10
|
-
type TimePreset,
|
|
11
|
-
toDate,
|
|
12
|
-
} from "./dateTime.shared";
|
|
13
|
-
|
|
14
|
-
export function formatClientDate(
|
|
15
|
-
value: DateInput,
|
|
16
|
-
options: {
|
|
17
|
-
locale?: string;
|
|
18
|
-
timeZone?: string;
|
|
19
|
-
preset: DatePreset;
|
|
20
|
-
},
|
|
21
|
-
): string {
|
|
22
|
-
const date = toDate(value);
|
|
23
|
-
if (!date) return "";
|
|
24
|
-
|
|
25
|
-
return options.preset === "dot"
|
|
26
|
-
? formatDotDate(date, options.timeZone)
|
|
27
|
-
: formatLongDate(date, options.locale, options.timeZone);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function formatClientTime(
|
|
31
|
-
value: DateInput,
|
|
32
|
-
options: {
|
|
33
|
-
locale?: string;
|
|
34
|
-
timeZone?: string;
|
|
35
|
-
preset: TimePreset;
|
|
36
|
-
},
|
|
37
|
-
): string {
|
|
38
|
-
const date = toDate(value);
|
|
39
|
-
if (!date) return "";
|
|
40
|
-
|
|
41
|
-
switch (options.preset) {
|
|
42
|
-
case "ko":
|
|
43
|
-
return formatKoreanTime(date, options.timeZone);
|
|
44
|
-
case "12h":
|
|
45
|
-
return formatTwelveHourTime(date, options.locale, options.timeZone);
|
|
46
|
-
case "24h-second":
|
|
47
|
-
return format24HourTime(date, {
|
|
48
|
-
includeSeconds: true,
|
|
49
|
-
timeZone: options.timeZone,
|
|
50
|
-
});
|
|
51
|
-
default:
|
|
52
|
-
return format24HourTime(date, { timeZone: options.timeZone });
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function formatClientDateTime(
|
|
57
|
-
value: DateInput,
|
|
58
|
-
options: {
|
|
59
|
-
locale?: string;
|
|
60
|
-
timeZone?: string;
|
|
61
|
-
datePreset?: DatePreset;
|
|
62
|
-
timePreset?: TimePreset;
|
|
63
|
-
},
|
|
64
|
-
): string {
|
|
65
|
-
const dateText = formatClientDate(value, {
|
|
66
|
-
locale: options.locale,
|
|
67
|
-
timeZone: options.timeZone,
|
|
68
|
-
preset: options.datePreset ?? "long",
|
|
69
|
-
});
|
|
70
|
-
const timeText = formatClientTime(value, {
|
|
71
|
-
locale: options.locale,
|
|
72
|
-
timeZone: options.timeZone,
|
|
73
|
-
preset: options.timePreset ?? "24h-minute",
|
|
74
|
-
});
|
|
75
|
-
return dateText && timeText
|
|
76
|
-
? `${dateText} ${timeText}`
|
|
77
|
-
: dateText || timeText;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function formatClientRelative(
|
|
81
|
-
value: DateInput,
|
|
82
|
-
options: {
|
|
83
|
-
locale?: string;
|
|
84
|
-
now?: DateInput;
|
|
85
|
-
maxRelativeDays?: number;
|
|
86
|
-
fallbackDatePreset?: DatePreset;
|
|
87
|
-
},
|
|
88
|
-
): { text: string; isRelative: boolean } {
|
|
89
|
-
const date = toDate(value);
|
|
90
|
-
const now = toDate(options.now ?? Date.now());
|
|
91
|
-
if (!date || !now) {
|
|
92
|
-
return { text: "", isRelative: false };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const diffMs = Math.max(0, now.getTime() - date.getTime());
|
|
96
|
-
const maxRelativeMs = (options.maxRelativeDays ?? 7) * 24 * 60 * 60 * 1000;
|
|
97
|
-
if (diffMs > maxRelativeMs) {
|
|
98
|
-
return {
|
|
99
|
-
text: formatClientDate(date, {
|
|
100
|
-
locale: options.locale,
|
|
101
|
-
preset: options.fallbackDatePreset ?? "dot",
|
|
102
|
-
}),
|
|
103
|
-
isRelative: false,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return formatRelativeText(diffMs, options.locale);
|
|
108
|
-
}
|