@musecat/functionkit 1.0.2 → 1.2.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 (101) hide show
  1. package/index.ts +1 -1
  2. package/package.json +37 -2
  3. package/packages/components/ScrolltoTop.tsx +1 -1
  4. package/packages/components/ViewportPortal.tsx +1 -2
  5. package/packages/cookie/cookie.shared.ts +17 -35
  6. package/packages/cookie/cookieNames.shared.ts +9 -0
  7. package/packages/datetime/dateTime.client.ts +1 -3
  8. package/packages/datetime/dateTime.server.ts +1 -3
  9. package/packages/datetime/dateTime.shared.ts +9 -47
  10. package/packages/hooks/useCheckInvisible.ts +1 -1
  11. package/packages/hooks/useClientDateTime.ts +3 -16
  12. package/packages/hooks/useDebounce.ts +2 -7
  13. package/packages/hooks/useGeolocation.ts +1 -5
  14. package/packages/hooks/useInterval.ts +1 -2
  15. package/packages/hooks/useKeyboardListNavigation.ts +10 -38
  16. package/packages/hooks/useLongPress.ts +13 -24
  17. package/packages/hooks/useRelativeDateTime.ts +1 -4
  18. package/packages/hooks/useViewportHeight.ts +1 -2
  19. package/packages/hooks/useViewportMatch.ts +0 -4
  20. package/packages/utils/buildContext.tsx +2 -3
  21. package/packages/utils/floatingMotion.ts +15 -23
  22. package/packages/utils/subscribeKeyboardHeight.ts +3 -0
  23. package/.agents/references/components/ScrolltoTop.md +0 -36
  24. package/.agents/references/components/SwitchCase.md +0 -47
  25. package/.agents/references/components/ViewportPortal.md +0 -34
  26. package/.agents/references/cookie/cookie.md +0 -47
  27. package/.agents/references/datetime/dateTime.client.md +0 -42
  28. package/.agents/references/datetime/dateTime.server.md +0 -42
  29. package/.agents/references/datetime/dateTime.shared.md +0 -86
  30. package/.agents/references/hooks/useAvoidKeyboard.md +0 -29
  31. package/.agents/references/hooks/useCheckInvisible.md +0 -36
  32. package/.agents/references/hooks/useCheckScroll.md +0 -36
  33. package/.agents/references/hooks/useClientDateTime.md +0 -39
  34. package/.agents/references/hooks/useDebounce.md +0 -46
  35. package/.agents/references/hooks/useDebouncedCallback.md +0 -34
  36. package/.agents/references/hooks/useDoubleClick.md +0 -40
  37. package/.agents/references/hooks/useGeolocation.md +0 -37
  38. package/.agents/references/hooks/useHasMounted.md +0 -27
  39. package/.agents/references/hooks/useIntersectionObserver.md +0 -37
  40. package/.agents/references/hooks/useInterval.md +0 -40
  41. package/.agents/references/hooks/useKeyboardHeight.md +0 -33
  42. package/.agents/references/hooks/useKeyboardListNavigation.md +0 -58
  43. package/.agents/references/hooks/useLongPress.md +0 -48
  44. package/.agents/references/hooks/usePreservedCallback.md +0 -33
  45. package/.agents/references/hooks/usePreservedReference.md +0 -34
  46. package/.agents/references/hooks/useRefEffect.md +0 -35
  47. package/.agents/references/hooks/useRelativeDateTime.md +0 -35
  48. package/.agents/references/hooks/useTimeout.md +0 -42
  49. package/.agents/references/hooks/useToggleState.md +0 -39
  50. package/.agents/references/hooks/useViewportHeight.md +0 -26
  51. package/.agents/references/hooks/useViewportMatch.md +0 -32
  52. package/.agents/references/utils/browserStorage.md +0 -41
  53. package/.agents/references/utils/buildContext.md +0 -47
  54. package/.agents/references/utils/clipboardShare.md +0 -59
  55. package/.agents/references/utils/floatingMotion.md +0 -67
  56. package/.agents/references/utils/getDeviceInfo.md +0 -43
  57. package/.agents/references/utils/isEditableKeyboardTarget.md +0 -35
  58. package/.agents/references/utils/mergeRefs.md +0 -29
  59. package/.agents/references/utils/seen.md +0 -37
  60. package/.agents/references/utils/subscribeKeyboardHeight.md +0 -35
  61. package/.gitattributes +0 -2
  62. package/AGENTS.md +0 -105
  63. package/tests/components/ScrolltoTop.test.tsx +0 -15
  64. package/tests/components/SwitchCase.test.tsx +0 -34
  65. package/tests/components/ViewportPortal.test.tsx +0 -42
  66. package/tests/cookie/cookie.test.ts +0 -156
  67. package/tests/datetime/datetime.test.ts +0 -250
  68. package/tests/hooks/useAvoidKeyboard.test.ts +0 -12
  69. package/tests/hooks/useCheckInvisible.test.ts +0 -28
  70. package/tests/hooks/useCheckScroll.test.ts +0 -23
  71. package/tests/hooks/useClientDateTime.test.ts +0 -14
  72. package/tests/hooks/useDebounce.test.ts +0 -71
  73. package/tests/hooks/useDebouncedCallback.test.ts +0 -34
  74. package/tests/hooks/useDoubleClick.test.ts +0 -31
  75. package/tests/hooks/useGeolocation.test.ts +0 -50
  76. package/tests/hooks/useHasMounted.test.ts +0 -10
  77. package/tests/hooks/useIntersectionObserver.test.ts +0 -13
  78. package/tests/hooks/useInterval.test.ts +0 -31
  79. package/tests/hooks/useKeyboardHeight.test.ts +0 -10
  80. package/tests/hooks/useKeyboardListNavigation.test.ts +0 -82
  81. package/tests/hooks/useLongPress.test.ts +0 -52
  82. package/tests/hooks/usePreservedCallback.test.ts +0 -35
  83. package/tests/hooks/usePreservedReference.test.ts +0 -35
  84. package/tests/hooks/useRefEffect.test.ts +0 -30
  85. package/tests/hooks/useRelativeDateTime.test.ts +0 -19
  86. package/tests/hooks/useTimeout.test.ts +0 -34
  87. package/tests/hooks/useToggleState.test.ts +0 -35
  88. package/tests/hooks/useViewportHeight.test.ts +0 -10
  89. package/tests/hooks/useViewportMatch.test.ts +0 -35
  90. package/tests/setup.ts +0 -9
  91. package/tests/utils/browserStorage.test.ts +0 -54
  92. package/tests/utils/buildContext.test.tsx +0 -27
  93. package/tests/utils/checkDevice.test.ts +0 -84
  94. package/tests/utils/clipboardShare.test.ts +0 -78
  95. package/tests/utils/floatingMotion.test.ts +0 -84
  96. package/tests/utils/keyboardTarget.test.ts +0 -44
  97. package/tests/utils/mergeRefs.test.ts +0 -18
  98. package/tests/utils/seen.test.ts +0 -31
  99. package/tests/utils/subscribeKeyboardHeight.test.ts +0 -50
  100. package/tsconfig.json +0 -18
  101. package/vitest.config.ts +0 -16
package/index.ts CHANGED
@@ -6,9 +6,9 @@ export {
6
6
  clearAllClientCookies,
7
7
  clearClientCookie,
8
8
  getClientCookie,
9
- parseClientCookieNames,
10
9
  setClientCookie,
11
10
  } from "./packages/cookie/cookie.shared";
11
+ export { parseClientCookieNames } from "./packages/cookie/cookieNames.shared";
12
12
  export {
13
13
  formatClientDate,
14
14
  formatClientDateTime,
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "@musecat/functionkit",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
+ "description": "React 19 + TypeScript frontend utility library — hooks, components, datetime, cookie, and utils",
5
+ "keywords": [
6
+ "react",
7
+ "hooks",
8
+ "typescript",
9
+ "utility",
10
+ "frontend"
11
+ ],
12
+ "license": "MIT",
13
+ "private": false,
14
+ "author": "Musecat Team",
15
+ "engines": {
16
+ "node": ">=20.0.0"
17
+ },
18
+ "files": [
19
+ "index.ts",
20
+ "packages/",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "sideEffects": false,
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/TheTechclip/FunctionKit.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/TheTechclip/FunctionKit/issues"
31
+ },
32
+ "homepage": "https://musecat.app",
4
33
  "type": "module",
5
34
  "main": "./index.ts",
6
35
  "types": "./index.ts",
@@ -11,7 +40,12 @@
11
40
  "scripts": {
12
41
  "lint": "biome check",
13
42
  "format": "prettier --write . && biome format --write",
14
- "test": "vitest run"
43
+ "test": "vitest run",
44
+ "test:coverage": "vitest run --coverage"
45
+ },
46
+ "publishConfig": {
47
+ "registry": "https://registry.npmjs.org/",
48
+ "access": "public"
15
49
  },
16
50
  "peerDependencies": {
17
51
  "react": "^19.0.0",
@@ -22,6 +56,7 @@
22
56
  "@testing-library/jest-dom": "^6.6.3",
23
57
  "@testing-library/react": "^16.2.0",
24
58
  "@testing-library/user-event": "^14.6.1",
59
+ "@vitest/coverage-v8": "^4.1.10",
25
60
  "jsdom": "^26.0.0",
26
61
  "prettier": "^3.9.5",
27
62
  "vitest": "^4.1.10"
@@ -5,5 +5,5 @@ import { useEffect } from "react";
5
5
  export function ScrolltoTop() {
6
6
  useEffect(() => {
7
7
  window.scrollTo(0, 0);
8
- });
8
+ }, []);
9
9
  }
@@ -18,8 +18,7 @@ export function getViewportPortalRoot() {
18
18
  const root = document.createElement("div");
19
19
  root.id = VIEWPORT_PORTAL_ROOT_ID;
20
20
  root.setAttribute("data-viewport-portal-root", "true");
21
- root.style.cssText =
22
- "position:fixed;inset:0;pointer-events:none;z-index:9999;";
21
+ root.style.cssText = "position:fixed;inset:0;pointer-events:none;z-index:9999;";
23
22
  document.body.appendChild(root);
24
23
  return root;
25
24
  }
@@ -22,10 +22,7 @@ type ClearAllClientCookiesOptions = ClearClientCookieOptions & {
22
22
  cookieString?: string;
23
23
  };
24
24
 
25
- function findClientCookie(
26
- name: string,
27
- documentRef: DocumentCookieRef,
28
- ): string | undefined {
25
+ function findClientCookie(name: string, documentRef: DocumentCookieRef): string | undefined {
29
26
  const parsed = parseClientCookie(documentRef.cookie);
30
27
  return parsed[name];
31
28
  }
@@ -42,15 +39,9 @@ function parseClientCookie(cookieString: string): Record<string, string> {
42
39
  }, {});
43
40
  }
44
41
 
45
- export function parseClientCookieNames(cookieString: string): string[] {
46
- return cookieString
47
- .split(";")
48
- .map((entry) => {
49
- const eqPos = entry.indexOf("=");
50
- return eqPos > -1 ? entry.slice(0, eqPos).trim() : entry.trim();
51
- })
52
- .filter(Boolean);
53
- }
42
+ import { parseClientCookieNames } from "./cookieNames.shared";
43
+
44
+ export { parseClientCookieNames };
54
45
 
55
46
  export function getClientCookie(name: string): string | undefined {
56
47
  if (typeof document === "undefined") {
@@ -60,11 +51,18 @@ export function getClientCookie(name: string): string | undefined {
60
51
  return findClientCookie(name, document);
61
52
  }
62
53
 
63
- export function setClientCookie(name: string, value: string, days?: number) {
54
+ export function setClientCookie(
55
+ name: string,
56
+ value: string,
57
+ days?: number,
58
+ documentRef?: DocumentCookieRef,
59
+ ) {
64
60
  if (typeof document === "undefined") {
65
61
  return;
66
62
  }
67
63
 
64
+ const ref = documentRef ?? document;
65
+
68
66
  let expires = "";
69
67
  if (days) {
70
68
  const date = new Date();
@@ -72,31 +70,17 @@ export function setClientCookie(name: string, value: string, days?: number) {
72
70
  expires = `; expires=${date.toUTCString()}`;
73
71
  }
74
72
 
75
- document.cookie = `${name}=${encodeURIComponent(value)}${expires}; path=/`;
73
+ ref.cookie = `${name}=${encodeURIComponent(value)}${expires}; path=/`;
76
74
  }
77
75
 
78
- export function clearClientCookie(
79
- name: string,
80
- options: ClearClientCookieOptions = {},
81
- ): void {
82
- const {
83
- hostname = window.location.hostname,
84
- path = "/",
85
- documentRef = document,
86
- } = options;
76
+ export function clearClientCookie(name: string, options: ClearClientCookieOptions = {}): void {
77
+ const { hostname = window.location.hostname, path = "/", documentRef = document } = options;
87
78
 
88
79
  documentRef.cookie = `${name}=; expires=${EXPIRED_COOKIE_DATE}; path=${path}; domain=${hostname};`;
89
80
  }
90
81
 
91
- export function clearAllClientCookies(
92
- options: ClearAllClientCookiesOptions = {},
93
- ): string[] {
94
- const {
95
- documentRef = document,
96
- cookieStore,
97
- includeRoot = false,
98
- cookieString,
99
- } = options;
82
+ export function clearAllClientCookies(options: ClearAllClientCookiesOptions = {}): string[] {
83
+ const { documentRef = document, cookieStore, includeRoot = false, cookieString } = options;
100
84
 
101
85
  const entries = cookieString
102
86
  ? parseClientCookieNames(cookieString)
@@ -124,8 +108,6 @@ export function clearAllClientCookies(
124
108
  }
125
109
 
126
110
  for (const name of entries) {
127
- if (!name) continue;
128
-
129
111
  for (const path of paths) {
130
112
  for (let i = 0; i < hostnameParts.length; i++) {
131
113
  const domain = hostnameParts.slice(i).join(".");
@@ -0,0 +1,9 @@
1
+ export function parseClientCookieNames(cookieString: string): string[] {
2
+ return cookieString
3
+ .split(";")
4
+ .map((entry) => {
5
+ const eqPos = entry.indexOf("=");
6
+ return eqPos > -1 ? entry.slice(0, eqPos).trim() : entry.trim();
7
+ })
8
+ .filter(Boolean);
9
+ }
@@ -72,9 +72,7 @@ export function formatClientDateTime(
72
72
  timeZone: options.timeZone,
73
73
  preset: options.timePreset ?? "24h-minute",
74
74
  });
75
- return dateText && timeText
76
- ? `${dateText} ${timeText}`
77
- : dateText || timeText;
75
+ return dateText && timeText ? `${dateText} ${timeText}` : dateText || timeText;
78
76
  }
79
77
 
80
78
  export function formatClientRelative(
@@ -72,9 +72,7 @@ export function formatServerDateTime(
72
72
  timeZone: options.timeZone,
73
73
  preset: options.timePreset ?? "24h-minute",
74
74
  });
75
- return dateText && timeText
76
- ? `${dateText} ${timeText}`
77
- : dateText || timeText;
75
+ return dateText && timeText ? `${dateText} ${timeText}` : dateText || timeText;
78
76
  }
79
77
 
80
78
  export function formatServerRelative(
@@ -17,8 +17,7 @@ type TimeParts = {
17
17
  };
18
18
 
19
19
  export function normalizeAppLocale(locale?: string): AppLocale {
20
- const normalized =
21
- typeof locale === "string" ? locale.trim().toLowerCase() : "";
20
+ const normalized = typeof locale === "string" ? locale.trim().toLowerCase() : "";
22
21
  if (normalized === "kr" || normalized === "ko") return "kr";
23
22
  if (normalized === "jp" || normalized === "ja") return "jp";
24
23
  return "en";
@@ -45,15 +44,10 @@ export function toDate(value: DateInput): Date | null {
45
44
  }
46
45
 
47
46
  export function toUtcMidnight(date: Date): Date {
48
- return new Date(
49
- Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()),
50
- );
47
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
51
48
  }
52
49
 
53
- export function parseUtcDateInput(
54
- value?: DateInput | null,
55
- fallback?: DateInput,
56
- ): Date | null {
50
+ export function parseUtcDateInput(value?: DateInput | null, fallback?: DateInput): Date | null {
57
51
  if (typeof value === "string") {
58
52
  const trimmed = value.trim();
59
53
  const match = DATE_ONLY_PATTERN.exec(trimmed);
@@ -128,11 +122,7 @@ function getTimeParts(date: Date, timeZone?: string): TimeParts {
128
122
  };
129
123
  }
130
124
 
131
- export function formatLongDate(
132
- date: Date,
133
- locale?: string,
134
- timeZone?: string,
135
- ): string {
125
+ export function formatLongDate(date: Date, locale?: string, timeZone?: string): string {
136
126
  const localeKey = normalizeAppLocale(locale);
137
127
  const { year, month, day } = getDateParts(date, timeZone);
138
128
 
@@ -177,11 +167,7 @@ export function formatKoreanTime(date: Date, timeZone?: string): string {
177
167
  return `${meridiem} ${hour12}시 ${minute}분`;
178
168
  }
179
169
 
180
- export function formatTwelveHourTime(
181
- date: Date,
182
- locale?: string,
183
- timeZone?: string,
184
- ): string {
170
+ export function formatTwelveHourTime(date: Date, locale?: string, timeZone?: string): string {
185
171
  const localeKey = normalizeAppLocale(locale);
186
172
  if (localeKey === "kr") {
187
173
  const { hour, minute } = getTimeParts(date, timeZone);
@@ -298,25 +284,13 @@ function formatRelativeUnit(
298
284
  ): string {
299
285
  if (locale === "kr") {
300
286
  const suffix =
301
- unit === "second"
302
- ? "초"
303
- : unit === "minute"
304
- ? "분"
305
- : unit === "hour"
306
- ? "시간"
307
- : "일";
287
+ unit === "second" ? "초" : unit === "minute" ? "분" : unit === "hour" ? "시간" : "일";
308
288
  return `${count}${suffix} 전`;
309
289
  }
310
290
 
311
291
  if (locale === "jp") {
312
292
  const suffix =
313
- unit === "second"
314
- ? "秒"
315
- : unit === "minute"
316
- ? "分"
317
- : unit === "hour"
318
- ? "時間"
319
- : "日";
293
+ unit === "second" ? "秒" : unit === "minute" ? "分" : unit === "hour" ? "時間" : "日";
320
294
  return `${count}${suffix}前`;
321
295
  }
322
296
 
@@ -331,25 +305,13 @@ function formatRemainingUnit(
331
305
  ): string {
332
306
  if (locale === "kr") {
333
307
  const suffix =
334
- unit === "second"
335
- ? "초"
336
- : unit === "minute"
337
- ? "분"
338
- : unit === "hour"
339
- ? "시간"
340
- : "일";
308
+ unit === "second" ? "초" : unit === "minute" ? "분" : unit === "hour" ? "시간" : "일";
341
309
  return `${count}${suffix}`;
342
310
  }
343
311
 
344
312
  if (locale === "jp") {
345
313
  const suffix =
346
- unit === "second"
347
- ? "秒"
348
- : unit === "minute"
349
- ? "分"
350
- : unit === "hour"
351
- ? "時間"
352
- : "日";
314
+ unit === "second" ? "秒" : unit === "minute" ? "分" : unit === "hour" ? "時間" : "日";
353
315
  return `${count}${suffix}`;
354
316
  }
355
317
 
@@ -7,7 +7,7 @@ export function useCheckInvisible(className: string) {
7
7
 
8
8
  useEffect(() => {
9
9
  const handleScroll = () => {
10
- const el = document.querySelector(`.${className}`);
10
+ const el = document.querySelector(`.${CSS.escape(className)}`);
11
11
  if (el) {
12
12
  const { top } = el.getBoundingClientRect();
13
13
  setIsInvisible(top < 0);
@@ -2,11 +2,7 @@
2
2
 
3
3
  import { useEffect, useState } from "react";
4
4
  import { formatClientDateTime } from "../datetime/dateTime.client";
5
- import type {
6
- DateInput,
7
- DatePreset,
8
- TimePreset,
9
- } from "../datetime/dateTime.shared";
5
+ import type { DateInput, DatePreset, TimePreset } from "../datetime/dateTime.shared";
10
6
  import { toDate } from "../datetime/dateTime.shared";
11
7
 
12
8
  type UseClientDateTimeOptions = {
@@ -16,10 +12,7 @@ type UseClientDateTimeOptions = {
16
12
  timePreset?: TimePreset;
17
13
  };
18
14
 
19
- export function useClientDateTime(
20
- value: DateInput,
21
- options?: UseClientDateTimeOptions,
22
- ) {
15
+ export function useClientDateTime(value: DateInput, options?: UseClientDateTimeOptions) {
23
16
  const [ready, setReady] = useState(false);
24
17
  const [text, setText] = useState("");
25
18
 
@@ -33,13 +26,7 @@ export function useClientDateTime(
33
26
  }),
34
27
  );
35
28
  setReady(true);
36
- }, [
37
- options?.datePreset,
38
- options?.locale,
39
- options?.timePreset,
40
- options?.timeZone,
41
- value,
42
- ]);
29
+ }, [options?.datePreset, options?.locale, options?.timePreset, options?.timeZone, value]);
43
30
 
44
31
  return {
45
32
  ready,
@@ -7,9 +7,7 @@ import { usePreservedCallback } from "./usePreservedCallback";
7
7
  export function debounce<F extends (...args: unknown[]) => void>(
8
8
  func: F,
9
9
  debounceMs: number,
10
- {
11
- edges = ["leading", "trailing"],
12
- }: { edges?: Array<"leading" | "trailing"> } = {},
10
+ { edges = ["leading", "trailing"] }: { edges?: Array<"leading" | "trailing"> } = {},
13
11
  ): {
14
12
  (...args: Parameters<F>): void;
15
13
  cancel: () => void;
@@ -51,10 +49,7 @@ export function debounce<F extends (...args: unknown[]) => void>(
51
49
  }, debounceMs);
52
50
  };
53
51
 
54
- const debounced = function (
55
- this: ThisParameterType<F>,
56
- ...args: Parameters<F>
57
- ) {
52
+ const debounced = function (this: ThisParameterType<F>, ...args: Parameters<F>) {
58
53
  pendingThis = this;
59
54
  pendingArgs = args;
60
55
 
@@ -88,11 +88,7 @@ export function useGeolocation(options?: UseGeolocationOptions) {
88
88
  const getCurrentPosition = useCallback(() => {
89
89
  if (!isSupported()) return;
90
90
  setState((prev) => ({ ...prev, loading: true }));
91
- navigator.geolocation.getCurrentPosition(
92
- handleSuccess,
93
- handleError,
94
- geoOptions(),
95
- );
91
+ navigator.geolocation.getCurrentPosition(handleSuccess, handleError, geoOptions());
96
92
  }, [handleSuccess, handleError, geoOptions, isSupported]);
97
93
 
98
94
  const startTracking = useCallback(() => {
@@ -15,8 +15,7 @@ type IntervalOptions =
15
15
  export function useInterval(callback: () => void, options: IntervalOptions) {
16
16
  const delay = typeof options === "number" ? options : options.delay;
17
17
  const immediate = typeof options === "number" ? false : options.immediate;
18
- const enabled =
19
- typeof options === "number" ? true : (options.enabled ?? true);
18
+ const enabled = typeof options === "number" ? true : (options.enabled ?? true);
20
19
 
21
20
  const preservedCallback = usePreservedCallback(callback);
22
21
  const immediateCalledRef = useRef(false);
@@ -10,18 +10,13 @@ type UseKeyboardListNavigationOptions = {
10
10
  itemCount: number;
11
11
  };
12
12
 
13
- export function useKeyboardListNavigation({
14
- itemCount,
15
- }: UseKeyboardListNavigationOptions) {
13
+ export function useKeyboardListNavigation({ itemCount }: UseKeyboardListNavigationOptions) {
16
14
  const [activeIndex, setActiveIndex] = useState(-1);
17
15
  const itemRefs = useRef<(HTMLElement | null)[]>([]);
18
16
 
19
- const setItemRef = useCallback(
20
- (index: number, element: HTMLElement | null) => {
21
- itemRefs.current[index] = element;
22
- },
23
- [],
24
- );
17
+ const setItemRef = useCallback((index: number, element: HTMLElement | null) => {
18
+ itemRefs.current[index] = element;
19
+ }, []);
25
20
 
26
21
  const focusItem = useCallback((index: number) => {
27
22
  const target = itemRefs.current[index];
@@ -59,18 +54,6 @@ export function useKeyboardListNavigation({
59
54
 
60
55
  const moveActiveItem = useCallback(
61
56
  (key: string) => {
62
- if (itemCount === 0) {
63
- return false;
64
- }
65
-
66
- if (key === "Home") {
67
- return focusItem(0);
68
- }
69
-
70
- if (key === "End") {
71
- return focusItem(itemCount - 1);
72
- }
73
-
74
57
  if (key === "ArrowDown" || key === "ArrowRight") {
75
58
  const nextIndex = activeIndex < 0 ? 0 : (activeIndex + 1) % itemCount;
76
59
  return focusItem(nextIndex);
@@ -78,15 +61,13 @@ export function useKeyboardListNavigation({
78
61
 
79
62
  if (key === "ArrowUp" || key === "ArrowLeft") {
80
63
  const nextIndex =
81
- activeIndex < 0
82
- ? itemCount - 1
83
- : (activeIndex - 1 + itemCount) % itemCount;
64
+ activeIndex < 0 ? itemCount - 1 : (activeIndex - 1 + itemCount) % itemCount;
84
65
  return focusItem(nextIndex);
85
66
  }
86
67
 
87
- return false;
68
+ return focusBoundaryItem(key);
88
69
  },
89
- [activeIndex, focusItem, itemCount],
70
+ [activeIndex, focusItem, focusBoundaryItem, itemCount],
90
71
  );
91
72
 
92
73
  const clickActiveItem = useCallback(() => {
@@ -99,10 +80,7 @@ export function useKeyboardListNavigation({
99
80
  }, [activeIndex]);
100
81
 
101
82
  const handleListKeyDown = useCallback(
102
- (
103
- event: React.KeyboardEvent<HTMLElement>,
104
- options?: { container?: HTMLElement | null },
105
- ) => {
83
+ (event: React.KeyboardEvent<HTMLElement>, options?: { container?: HTMLElement | null }) => {
106
84
  if (itemCount === 0 || event.defaultPrevented) {
107
85
  return;
108
86
  }
@@ -144,15 +122,9 @@ export function useKeyboardListNavigation({
144
122
  return;
145
123
  }
146
124
 
147
- if (moveActiveItem(event.key)) {
148
- return;
149
- }
150
-
151
- if (focusBoundaryItem(event.key)) {
152
- return;
153
- }
125
+ moveActiveItem(event.key);
154
126
  },
155
- [clickActiveItem, moveActiveItem, focusBoundaryItem, itemCount],
127
+ [clickActiveItem, moveActiveItem, itemCount],
156
128
  );
157
129
 
158
130
  useEffect(() => {
@@ -14,12 +14,7 @@ export type UseLongPressOptions<E extends HTMLElement> = {
14
14
 
15
15
  export function useLongPress<E extends HTMLElement = HTMLElement>(
16
16
  onLongPress: (event: MouseEvent<E> | TouchEvent<E>) => void,
17
- {
18
- delay = 500,
19
- moveThreshold,
20
- onClick,
21
- onLongPressEnd,
22
- }: UseLongPressOptions<E> = {},
17
+ { delay = 500, moveThreshold, onClick, onLongPressEnd }: UseLongPressOptions<E> = {},
23
18
  ) {
24
19
  const timeoutRef = useRef<number | null>(null);
25
20
  const isLongPressActiveRef = useRef(false);
@@ -32,22 +27,18 @@ export function useLongPress<E extends HTMLElement = HTMLElement>(
32
27
  savedOnClick.current = onClick;
33
28
  savedOnLongPressEnd.current = onLongPressEnd;
34
29
 
35
- const hasThreshold =
36
- moveThreshold?.x !== undefined || moveThreshold?.y !== undefined;
30
+ const hasThreshold = moveThreshold?.x !== undefined || moveThreshold?.y !== undefined;
37
31
 
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
- );
32
+ const getClientPosition = useCallback((event: MouseEvent<E> | TouchEvent<E>) => {
33
+ if ("touches" in event.nativeEvent) {
34
+ const touch = event.nativeEvent.touches[0];
35
+ return { x: touch.clientX, y: touch.clientY };
36
+ }
37
+ return {
38
+ x: event.nativeEvent.clientX,
39
+ y: event.nativeEvent.clientY,
40
+ };
41
+ }, []);
51
42
 
52
43
  const isMovedBeyondThreshold = useCallback(
53
44
  (event: MouseEvent<E> | TouchEvent<E>) => {
@@ -113,8 +104,6 @@ export function useLongPress<E extends HTMLElement = HTMLElement>(
113
104
  onMouseLeave: cancelLongPress,
114
105
  onTouchStart: handlePressStart,
115
106
  onTouchEnd: handlePressEnd,
116
- ...(hasThreshold
117
- ? { onTouchMove: handlePressMove, onMouseMove: handlePressMove }
118
- : {}),
107
+ ...(hasThreshold ? { onTouchMove: handlePressMove, onMouseMove: handlePressMove } : {}),
119
108
  };
120
109
  }
@@ -12,10 +12,7 @@ type UseRelativeDateTimeOptions = {
12
12
  updateIntervalMs?: number;
13
13
  };
14
14
 
15
- export function useRelativeDateTime(
16
- value: DateInput,
17
- options?: UseRelativeDateTimeOptions,
18
- ) {
15
+ export function useRelativeDateTime(value: DateInput, options?: UseRelativeDateTimeOptions) {
19
16
  const [ready, setReady] = useState(false);
20
17
  const [text, setText] = useState("");
21
18
  const [isRelative, setIsRelative] = useState(false);
@@ -6,8 +6,7 @@ export function useViewportHeight() {
6
6
  const [height, setHeight] = useState(0);
7
7
 
8
8
  useEffect(() => {
9
- const update = () =>
10
- setHeight(window.visualViewport?.height ?? window.innerHeight);
9
+ const update = () => setHeight(window.visualViewport?.height ?? window.innerHeight);
11
10
 
12
11
  update();
13
12
  window.visualViewport?.addEventListener("resize", update);
@@ -6,10 +6,6 @@ export function useViewportMatch(mediaQuery: string): boolean {
6
6
  const [isMatched, setIsMatched] = useState(false);
7
7
 
8
8
  useEffect(() => {
9
- if (typeof window === "undefined") {
10
- return;
11
- }
12
-
13
9
  const mediaQueryList = window.matchMedia(mediaQuery);
14
10
  const syncMatchState = () => {
15
11
  setIsMatched(mediaQueryList.matches);
@@ -1,13 +1,12 @@
1
1
  "use client";
2
2
 
3
- import { createContext, type ReactNode, useContext, useMemo } from "react";
3
+ import { createContext, type ReactNode, useContext } from "react";
4
4
 
5
5
  export function buildContext<T>(defaultValue: T) {
6
6
  const Ctx = createContext<T>(defaultValue);
7
7
 
8
8
  function Provider({ value, children }: { value: T; children: ReactNode }) {
9
- const stable = useMemo(() => value, [value]);
10
- return <Ctx.Provider value={stable}>{children}</Ctx.Provider>;
9
+ return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
11
10
  }
12
11
 
13
12
  function useValue() {