@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.
- package/.agents/references/hooks/useIntersectionObserver.md +4 -0
- package/.agents/references/hooks/useInterval.md +4 -0
- package/.agents/references/hooks/usePreservedCallback.md +4 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +22 -0
- package/.github/workflows/ci.yml +20 -0
- package/.local/state/gh/device-id +1 -0
- package/.prettierrc +8 -0
- package/AGENTS.md +14 -0
- package/CODE_OF_CONDUCT.md +55 -0
- package/CONTRIBUTING.md +39 -0
- package/biome.json +25 -0
- package/index.ts +1 -1
- package/package.json +21 -2
- package/packages/components/ScrolltoTop.tsx +1 -1
- package/packages/components/ViewportPortal.tsx +1 -2
- package/packages/cookie/cookie.shared.ts +7 -33
- package/packages/cookie/cookieNames.shared.ts +9 -0
- package/packages/datetime/dateTime.client.ts +1 -3
- package/packages/datetime/dateTime.server.ts +1 -3
- package/packages/datetime/dateTime.shared.ts +9 -47
- package/packages/hooks/useCheckInvisible.ts +1 -1
- package/packages/hooks/useClientDateTime.ts +3 -16
- package/packages/hooks/useDebounce.ts +2 -7
- package/packages/hooks/useGeolocation.ts +1 -5
- package/packages/hooks/useInterval.ts +1 -2
- package/packages/hooks/useKeyboardListNavigation.ts +10 -38
- package/packages/hooks/useLongPress.ts +13 -24
- package/packages/hooks/useRelativeDateTime.ts +1 -4
- package/packages/hooks/useViewportHeight.ts +1 -2
- package/packages/hooks/useViewportMatch.ts +0 -4
- package/packages/utils/buildContext.tsx +2 -3
- package/packages/utils/floatingMotion.ts +14 -20
- package/packages/utils/subscribeKeyboardHeight.ts +3 -0
- package/tests/components/ScrolltoTop.test.tsx +1 -1
- package/tests/components/SwitchCase.test.tsx +1 -2
- package/tests/components/ViewportPortal.test.tsx +10 -6
- package/tests/cookie/cookie.test.ts +47 -11
- package/tests/datetime/datetime.test.ts +257 -46
- package/tests/hooks/useAvoidKeyboard.test.ts +32 -1
- package/tests/hooks/useCheckInvisible.test.ts +36 -5
- package/tests/hooks/useCheckScroll.test.ts +2 -2
- package/tests/hooks/useClientDateTime.test.ts +2 -4
- package/tests/hooks/useDebounce.test.ts +30 -5
- package/tests/hooks/useDebouncedCallback.test.ts +41 -12
- package/tests/hooks/useDoubleClick.test.ts +58 -7
- package/tests/hooks/useGeolocation.test.ts +155 -4
- package/tests/hooks/useHasMounted.test.ts +1 -1
- package/tests/hooks/useIntersectionObserver.test.ts +70 -5
- package/tests/hooks/useInterval.test.ts +28 -3
- package/tests/hooks/useKeyboardHeight.test.ts +1 -1
- package/tests/hooks/useKeyboardListNavigation.test.ts +340 -18
- package/tests/hooks/useLongPress.test.ts +177 -15
- package/tests/hooks/usePreservedCallback.test.ts +7 -9
- package/tests/hooks/usePreservedReference.test.ts +19 -12
- package/tests/hooks/useRefEffect.test.ts +43 -6
- package/tests/hooks/useRelativeDateTime.test.ts +8 -6
- package/tests/hooks/useTimeout.test.ts +43 -5
- package/tests/hooks/useToggleState.test.ts +14 -6
- package/tests/hooks/useViewportHeight.test.ts +24 -2
- package/tests/hooks/useViewportMatch.test.ts +1 -1
- package/tests/utils/browserStorage.test.ts +68 -8
- package/tests/utils/buildContext.test.tsx +1 -2
- package/tests/utils/checkDevice.test.ts +62 -6
- package/tests/utils/clipboardShare.test.ts +79 -5
- package/tests/utils/floatingMotion.test.ts +60 -9
- package/tests/utils/keyboardTarget.test.ts +42 -1
- package/tests/utils/mergeRefs.test.ts +33 -1
- package/tests/utils/seen.test.ts +16 -7
- package/tests/utils/subscribeKeyboardHeight.test.ts +124 -2
- 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
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.
|
package/CONTRIBUTING.md
ADDED
|
@@ -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
|
|
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"
|
|
@@ -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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(".");
|
|
@@ -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);
|