@musecat/functionkit 1.0.2 → 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 (72) hide show
  1. package/.agents/references/hooks/useIntersectionObserver.md +4 -0
  2. package/.agents/references/hooks/useInterval.md +4 -0
  3. package/.agents/references/hooks/usePreservedCallback.md +4 -0
  4. package/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
  6. package/.github/PULL_REQUEST_TEMPLATE.md +22 -0
  7. package/.github/workflows/ci.yml +20 -0
  8. package/.local/state/gh/device-id +1 -0
  9. package/.prettierrc +8 -0
  10. package/AGENTS.md +14 -0
  11. package/CODE_OF_CONDUCT.md +55 -0
  12. package/CONTRIBUTING.md +39 -0
  13. package/biome.json +25 -0
  14. package/index.ts +1 -1
  15. package/package.json +21 -2
  16. package/packages/components/ScrolltoTop.tsx +1 -1
  17. package/packages/components/ViewportPortal.tsx +1 -2
  18. package/packages/cookie/cookie.shared.ts +7 -33
  19. package/packages/cookie/cookieNames.shared.ts +9 -0
  20. package/packages/datetime/dateTime.client.ts +1 -3
  21. package/packages/datetime/dateTime.server.ts +1 -3
  22. package/packages/datetime/dateTime.shared.ts +9 -47
  23. package/packages/hooks/useCheckInvisible.ts +1 -1
  24. package/packages/hooks/useClientDateTime.ts +3 -16
  25. package/packages/hooks/useDebounce.ts +2 -7
  26. package/packages/hooks/useGeolocation.ts +1 -5
  27. package/packages/hooks/useInterval.ts +1 -2
  28. package/packages/hooks/useKeyboardListNavigation.ts +10 -38
  29. package/packages/hooks/useLongPress.ts +13 -24
  30. package/packages/hooks/useRelativeDateTime.ts +1 -4
  31. package/packages/hooks/useViewportHeight.ts +1 -2
  32. package/packages/hooks/useViewportMatch.ts +0 -4
  33. package/packages/utils/buildContext.tsx +2 -3
  34. package/packages/utils/floatingMotion.ts +14 -20
  35. package/packages/utils/subscribeKeyboardHeight.ts +3 -0
  36. package/tests/components/ScrolltoTop.test.tsx +1 -1
  37. package/tests/components/SwitchCase.test.tsx +1 -2
  38. package/tests/components/ViewportPortal.test.tsx +10 -6
  39. package/tests/cookie/cookie.test.ts +47 -11
  40. package/tests/datetime/datetime.test.ts +257 -46
  41. package/tests/hooks/useAvoidKeyboard.test.ts +32 -1
  42. package/tests/hooks/useCheckInvisible.test.ts +36 -5
  43. package/tests/hooks/useCheckScroll.test.ts +2 -2
  44. package/tests/hooks/useClientDateTime.test.ts +2 -4
  45. package/tests/hooks/useDebounce.test.ts +30 -5
  46. package/tests/hooks/useDebouncedCallback.test.ts +41 -12
  47. package/tests/hooks/useDoubleClick.test.ts +58 -7
  48. package/tests/hooks/useGeolocation.test.ts +155 -4
  49. package/tests/hooks/useHasMounted.test.ts +1 -1
  50. package/tests/hooks/useIntersectionObserver.test.ts +70 -5
  51. package/tests/hooks/useInterval.test.ts +28 -3
  52. package/tests/hooks/useKeyboardHeight.test.ts +1 -1
  53. package/tests/hooks/useKeyboardListNavigation.test.ts +340 -18
  54. package/tests/hooks/useLongPress.test.ts +177 -15
  55. package/tests/hooks/usePreservedCallback.test.ts +7 -9
  56. package/tests/hooks/usePreservedReference.test.ts +19 -12
  57. package/tests/hooks/useRefEffect.test.ts +43 -6
  58. package/tests/hooks/useRelativeDateTime.test.ts +8 -6
  59. package/tests/hooks/useTimeout.test.ts +43 -5
  60. package/tests/hooks/useToggleState.test.ts +14 -6
  61. package/tests/hooks/useViewportHeight.test.ts +24 -2
  62. package/tests/hooks/useViewportMatch.test.ts +1 -1
  63. package/tests/utils/browserStorage.test.ts +68 -8
  64. package/tests/utils/buildContext.test.tsx +1 -2
  65. package/tests/utils/checkDevice.test.ts +62 -6
  66. package/tests/utils/clipboardShare.test.ts +79 -5
  67. package/tests/utils/floatingMotion.test.ts +60 -9
  68. package/tests/utils/keyboardTarget.test.ts +42 -1
  69. package/tests/utils/mergeRefs.test.ts +33 -1
  70. package/tests/utils/seen.test.ts +16 -7
  71. package/tests/utils/subscribeKeyboardHeight.test.ts +124 -2
  72. package/vitest.config.ts +7 -1
@@ -35,3 +35,7 @@ function LazyImage() {
35
35
  return <div ref={ref}>...</div>;
36
36
  }
37
37
  ```
38
+
39
+ ## Trade-off
40
+
41
+ The `options` object is used as a `useMemo` dependency. If you pass an inline object (`{ threshold: 0.5 }`) on every render, the `IntersectionObserver` is recreated each time. Memoize options with `useMemo` or define them outside the component for stable references.
@@ -38,3 +38,7 @@ function PollingComponent() {
38
38
  return <div>{data}</div>;
39
39
  }
40
40
  ```
41
+
42
+ ## Trade-off
43
+
44
+ Internally uses `usePreservedCallback` to stabilize the callback. The callback always reflects the latest closure, but during the render-to-effect gap a stale version could theoretically fire. In practice this is never an issue since interval callbacks fire asynchronously after paint. See `usePreservedCallback` trade-off for details.
@@ -31,3 +31,7 @@ function InfiniteScroll({ onLoadMore }: { onLoadMore: () => void }) {
31
31
  return <div ref={ref}>...</div>;
32
32
  }
33
33
  ```
34
+
35
+ ## Trade-off
36
+
37
+ `usePreservedCallback` uses `useRef` + `useEffect` to keep a stable callback reference that always calls the latest version. Because the ref is synced via `useEffect` (after paint), the returned callback may hold a **stale value** during the first render or in the gap between a re-render and the effect flush. In practice this is never an issue — the callback is only invoked in event handlers that fire post-paint. This is the same pattern used by react-hook-form, Radix UI, and others. If synchronous invocation during render is required, use a regular `useCallback` with inline deps instead.
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: Bug report
3
+ about: Report a bug to help us improve
4
+ title: "[BUG] "
5
+ labels: bug
6
+ ---
7
+
8
+ ## Describe the Bug
9
+
10
+ A clear and concise description of what the bug is.
11
+
12
+ ## To Reproduce
13
+
14
+ Steps to reproduce the behavior:
15
+ 1. Import '...'
16
+ 2. Call '...'
17
+ 3. See error
18
+
19
+ ## Expected Behavior
20
+
21
+ A clear and concise description of what you expected to happen.
22
+
23
+ ## Environment
24
+
25
+ - React version:
26
+ - @musecat/functionkit version:
27
+ - Browser / Node version:
28
+
29
+ ## Additional Context
30
+
31
+ Add any other context about the problem here.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea for this project
4
+ title: "[FEATURE] "
5
+ labels: enhancement
6
+ ---
7
+
8
+ ## Problem Statement
9
+
10
+ Is your feature request related to a problem? Please describe clearly.
11
+
12
+ ## Proposed Solution
13
+
14
+ A clear and concise description of what you want to happen.
15
+
16
+ ## Alternative Solutions
17
+
18
+ A description of any alternative solutions or features you've considered.
19
+
20
+ ## Additional Context
21
+
22
+ Add any other context or examples about the feature request here.
@@ -0,0 +1,22 @@
1
+ ## Description
2
+
3
+ Please include a summary of the change and which issue is fixed.
4
+
5
+ ## Type of Change
6
+
7
+ - [ ] Bug fix
8
+ - [ ] New feature
9
+ - [ ] Breaking change
10
+ - [ ] Documentation update
11
+
12
+ ## How Has This Been Tested?
13
+
14
+ Please describe the tests that you ran to verify your changes.
15
+
16
+ ## Checklist
17
+
18
+ - [ ] My code follows the project's code style
19
+ - [ ] I have added tests that prove my fix/feature works
20
+ - [ ] `npm run lint` passes
21
+ - [ ] `npm test` passes
22
+ - [ ] Barrel exports (`index.ts`) are updated if needed
@@ -0,0 +1,20 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+ branches: [master]
8
+
9
+ jobs:
10
+ quality:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-node@v4
15
+ with:
16
+ node-version: 22
17
+ cache: npm
18
+ - run: npm ci
19
+ - run: npm run lint
20
+ - run: npm test
@@ -0,0 +1 @@
1
+ f1f55548-1baa-4638-bd0f-f3da5e3262f0
package/.prettierrc ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "useTabs": true,
3
+ "tabWidth": 2,
4
+ "printWidth": 100,
5
+ "singleQuote": false,
6
+ "semi": true,
7
+ "trailingComma": "all"
8
+ }
package/AGENTS.md CHANGED
@@ -103,3 +103,17 @@ Never write raw `createContext` + `Provider` + `useContext` boilerplate. Use `bu
103
103
  - When hooks, components, or utils are added, modified, or removed, update the corresponding `.agents/references/` documentation.
104
104
  - When the barrel (`index.ts`) export list changes, update the Barrel Export Mapping section above.
105
105
  - Before using any feature, read the corresponding `.agents/references/` doc first.
106
+
107
+ ### Reference File Mapping
108
+
109
+ | Category | File(s) |
110
+ |---|---|
111
+ | Components | `.agents/references/components/SwitchCase.md`, `ScrolltoTop.md`, `ViewportPortal.md` |
112
+ | Hooks | `.agents/references/hooks/usePreservedCallback.md`, `usePreservedReference.md`, `useRefEffect.md`, `useDebounce.md`, `useDebouncedCallback.md`, `useInterval.md`, `useTimeout.md`, `useKeyboardListNavigation.md`, `useLongPress.md`, `useDoubleClick.md`, `useIntersectionObserver.md`, `useCheckInvisible.md`, `useCheckScroll.md`, `useViewportHeight.md`, `useViewportMatch.md`, `useAvoidKeyboard.md`, `useToggleState.md`, `useHasMounted.md`, `useGeolocation.md`, `useKeyboardHeight.md`, `useClientDateTime.md`, `useRelativeDateTime.md` |
113
+ | Cookie | `.agents/references/cookie/cookie.md` |
114
+ | DateTime | `.agents/references/datetime/dateTime.shared.md`, `dateTime.client.md`, `dateTime.server.md` |
115
+ | Utils | `.agents/references/utils/buildContext.md`, `mergeRefs.md`, `floatingMotion.md`, `browserStorage.md`, `getDeviceInfo.md`, `clipboardShare.md`, `isEditableKeyboardTarget.md`, `seen.md`, `subscribeKeyboardHeight.md` |
116
+
117
+ ### usePreservedCallback Trade-off
118
+
119
+ `usePreservedCallback` uses `useRef` + `useEffect` to keep a stable callback reference that always calls the latest version. Because the ref is synced via `useEffect` (after paint), the returned callback may hold a **stale value** during the first render or in the gap between a re-render and the effect flush. In practice this is never an issue — the callback is only invoked in event handlers that fire post-paint. This is the same pattern used by react-hook-form, Radix UI, and others. If synchronous invocation during render is required, use a regular `useCallback` with inline deps instead.
@@ -0,0 +1,55 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, religion, or sexual identity
10
+ and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment:
18
+
19
+ - Demonstrating empathy and kindness toward other people
20
+ - Being respectful of differing opinions, viewpoints, and experiences
21
+ - Giving and gracefully accepting constructive feedback
22
+ - Accepting responsibility and apologizing to those affected by our mistakes
23
+ - Focusing on what is best not just for us as individuals, but for the overall community
24
+
25
+ Examples of unacceptable behavior:
26
+
27
+ - The use of sexualized language or imagery, and sexual attention or advances
28
+ - Trolling, insulting or derogatory comments, and personal or political attacks
29
+ - Public or private harassment
30
+ - Publishing others' private information without explicit permission
31
+ - Other conduct which could reasonably be considered inappropriate in a professional setting
32
+
33
+ ## Enforcement Responsibilities
34
+
35
+ Project maintainers are responsible for clarifying and enforcing our standards of
36
+ acceptable behavior and will take appropriate and fair corrective action in
37
+ response to any behavior that they deem inappropriate, threatening, offensive,
38
+ or harmful.
39
+
40
+ ## Scope
41
+
42
+ This Code of Conduct applies within all community spaces, and also applies when
43
+ an individual is officially representing the community in public spaces.
44
+
45
+ ## Enforcement
46
+
47
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
48
+ reported to the project maintainers. All complaints will be reviewed and
49
+ investigated promptly and fairly.
50
+
51
+ ## Attribution
52
+
53
+ This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
54
+ version 2.1, available at
55
+ https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
@@ -0,0 +1,39 @@
1
+ # Contributing to @musecat/functionkit
2
+
3
+ Thank you for your interest in contributing! We welcome bug reports, feature suggestions, and pull requests.
4
+
5
+ ## Development Setup
6
+
7
+ ```bash
8
+ git clone https://github.com/TheTechclip/FunctionKit.git
9
+ cd FunctionKit
10
+ npm install
11
+ ```
12
+
13
+ ## Available Scripts
14
+
15
+ | Command | Description |
16
+ |---|---|
17
+ | `npm test` | Run all tests with Vitest |
18
+ | `npm run test:coverage` | Run tests with coverage report |
19
+ | `npm run lint` | Lint all files with Biome |
20
+ | `npm run format` | Format all files with Prettier + Biome |
21
+
22
+ ## Code Standards
23
+
24
+ - TypeScript strict mode is enabled — ensure your code compiles without errors.
25
+ - All new features must include corresponding tests.
26
+ - Hooks must start with `"use client"` directive. Pure utility functions must NOT.
27
+ - Barrel exports (`index.ts`) must be updated for new public APIs.
28
+ - Follow the existing code style (tabs, double quotes, semicolons).
29
+
30
+ ## Pull Request Process
31
+
32
+ 1. Create a feature branch from `master`.
33
+ 2. Write tests for your changes.
34
+ 3. Ensure `npm run lint && npm test` passes.
35
+ 4. Open a pull request describing the change and its motivation.
36
+
37
+ ## Questions?
38
+
39
+ Feel free to open a discussion or issue for any questions.
package/biome.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.5.4/schema.json",
3
+ "assist": { "actions": { "source": { "organizeImports": "on" } } },
4
+ "linter": {
5
+ "enabled": true,
6
+ "rules": {
7
+ "preset": "recommended"
8
+ }
9
+ },
10
+ "formatter": {
11
+ "enabled": true,
12
+ "indentStyle": "tab",
13
+ "indentWidth": 2,
14
+ "lineWidth": 100
15
+ },
16
+ "javascript": {
17
+ "formatter": {
18
+ "quoteStyle": "double",
19
+ "semicolons": "always"
20
+ }
21
+ },
22
+ "files": {
23
+ "includes": ["**", "!**/dist", "!**/coverage", "!**/node_modules"]
24
+ }
25
+ }
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,23 @@
1
1
  {
2
2
  "name": "@musecat/functionkit",
3
- "version": "1.0.2",
3
+ "version": "1.1.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
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/TheTechclip/FunctionKit.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/TheTechclip/FunctionKit/issues"
19
+ },
20
+ "homepage": "https://musecat.app",
4
21
  "type": "module",
5
22
  "main": "./index.ts",
6
23
  "types": "./index.ts",
@@ -11,7 +28,8 @@
11
28
  "scripts": {
12
29
  "lint": "biome check",
13
30
  "format": "prettier --write . && biome format --write",
14
- "test": "vitest run"
31
+ "test": "vitest run",
32
+ "test:coverage": "vitest run --coverage"
15
33
  },
16
34
  "peerDependencies": {
17
35
  "react": "^19.0.0",
@@ -22,6 +40,7 @@
22
40
  "@testing-library/jest-dom": "^6.6.3",
23
41
  "@testing-library/react": "^16.2.0",
24
42
  "@testing-library/user-event": "^14.6.1",
43
+ "@vitest/coverage-v8": "^4.1.10",
25
44
  "jsdom": "^26.0.0",
26
45
  "prettier": "^3.9.5",
27
46
  "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,8 @@ 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
+ export { parseClientCookieNames };
54
44
 
55
45
  export function getClientCookie(name: string): string | undefined {
56
46
  if (typeof document === "undefined") {
@@ -75,28 +65,14 @@ export function setClientCookie(name: string, value: string, days?: number) {
75
65
  document.cookie = `${name}=${encodeURIComponent(value)}${expires}; path=/`;
76
66
  }
77
67
 
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;
68
+ export function clearClientCookie(name: string, options: ClearClientCookieOptions = {}): void {
69
+ const { hostname = window.location.hostname, path = "/", documentRef = document } = options;
87
70
 
88
71
  documentRef.cookie = `${name}=; expires=${EXPIRED_COOKIE_DATE}; path=${path}; domain=${hostname};`;
89
72
  }
90
73
 
91
- export function clearAllClientCookies(
92
- options: ClearAllClientCookiesOptions = {},
93
- ): string[] {
94
- const {
95
- documentRef = document,
96
- cookieStore,
97
- includeRoot = false,
98
- cookieString,
99
- } = options;
74
+ export function clearAllClientCookies(options: ClearAllClientCookiesOptions = {}): string[] {
75
+ const { documentRef = document, cookieStore, includeRoot = false, cookieString } = options;
100
76
 
101
77
  const entries = cookieString
102
78
  ? parseClientCookieNames(cookieString)
@@ -124,8 +100,6 @@ export function clearAllClientCookies(
124
100
  }
125
101
 
126
102
  for (const name of entries) {
127
- if (!name) continue;
128
-
129
103
  for (const path of paths) {
130
104
  for (let i = 0; i < hostnameParts.length; i++) {
131
105
  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);