@vvi/react-hooks 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # 更新日志 📔
2
+
3
+ ## v1.0.0 (2026-7-4)
4
+
5
+ - 构建该项目
package/LICENSE ADDED
@@ -0,0 +1,16 @@
1
+ # MIT License
2
+
3
+ Copyright ©️ <2026> <MrMudBean>
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16
+
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # react-hooks
2
+
3
+ [![version](<https://img.shields.io/npm/v/@vvi/react-hooks.svg?logo=npm&logoColor=rgb(0,0,0)&label=版本号&labelColor=rgb(73,73,228)&color=rgb(0,0,0)>)](https://www.npmjs.com/package/@vvi/react-hooks) [![issues 提交](<https://img.shields.io/badge/issues-提交-rgb(255,0,63)?logo=github>)](https://github.com/MrMudBean/react-hooks/issues)
4
+
5
+ 业务无关的 hooks 。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ npm install --save @vvi/react-hooks
11
+
12
+ # pnpm
13
+ pnpm add --save @vvi/react-hooks
14
+
15
+ # yarn
16
+ yarn add --save @vvi/react-hooks
17
+ ```
18
+
19
+ ## 使用
20
+
21
+ 提供了几个简单的与业务无关的用于把玩的 react hooks :
22
+
23
+ - **`useTimeId`** :一个 `useRef` 数值,可用于储存 `setTimeout`、`setInterval` 的返回值,自动在卸载时调用 `clearTimeout(id.current)`
24
+ - **`useInputIsComposing`** : 储存了当前绑定了 ref 的输入框是否完成了输入的 `usrRef` 数据。可在桌面端设备上中文输入判定是否完成输入选择
25
+ - **`useAnimationFrame`** :一个 `useRef` 数据,数据储存了 `requestAnimationFrame`
26
+ - **`useRipples`** :一个 `useRef` 数据。储存了 [Ripples](https://www.npmjs.com/package/@vvi/ripple) 实例
27
+ - **`useLazyRipples`** :一个 `useRef` 数据,储存了 [Ripple](https://www.npmjs.com/package/@vvi/ripple) 实例
28
+
29
+ ## 状态
30
+
31
+ 此软件包是 `MrMudBean` 生态系统的一部分。
32
+ 它使用严格的 TypeScript 编写,并通过 Rollup 构建进行验证。
33
+ 虽然单元测试较少,但 API 稳定,并在生产环境中大量使用。
package/cjs/index.js ADDED
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+
3
+ var useAnimationFrame = require('./useAnimationFrame.js');
4
+ var useRipples = require('./use-ripples.js');
5
+ var useLazyRipple = require('./use-lazy-ripple.js');
6
+ var useInputIsComposing = require('./useInputIsComposing.js');
7
+ var useTimeId = require('./useTimeId.js');
8
+
9
+
10
+
11
+ exports.useAnimationFrame = useAnimationFrame.useAnimationFrame;
12
+ exports.useRipples = useRipples.useRipples;
13
+ exports.useLazyRipples = useLazyRipple.useLazyRipples;
14
+ exports.useInputIsComposing = useInputIsComposing.useInputIsComposing;
15
+ exports.useTimeId = useTimeId.useTimeId;
@@ -0,0 +1,67 @@
1
+ 'use client';
2
+ 'use strict';
3
+
4
+ var is = require('@vvi/is');
5
+ var react = require('react');
6
+
7
+ /**
8
+ * ## 懒加载使用涟漪 hook
9
+ * @param canvas
10
+ * @param option
11
+ */
12
+ function useLazyRipples(canvas, option) {
13
+ /** react dom */
14
+ const ripples = react.useRef(null);
15
+ /** 当前是否被卸载 */
16
+ const isUnmounted = react.useRef(true);
17
+ /** 是否已加载 */
18
+ const isLoaded = react.useRef(false);
19
+ // 加载状态
20
+ const [isLoading, setIsLoading] = react.useState(false);
21
+ // 错误状态
22
+ const [error, setError] = react.useState(null);
23
+ // 参数
24
+ const optionRef = react.useRef(option);
25
+ /** 卸载 */
26
+ function unmounted() {
27
+ isUnmounted.current = true;
28
+ setTimeout(() => {
29
+ // 当前是被卸载状态
30
+ if (isUnmounted.current) {
31
+ ripples.current?.destroy();
32
+ }
33
+ }, 0);
34
+ }
35
+ // 更新 option,防止加载 option 为旧数据
36
+ react.useEffect(() => {
37
+ optionRef.current = option;
38
+ }, [option]);
39
+ react.useEffect(() => {
40
+ isUnmounted.current = false;
41
+ /** 非空检验(这里一般都是有值的,除非故障) */
42
+ if (is.isNull(canvas.current) || isLoaded.current) {
43
+ return unmounted;
44
+ }
45
+ // 设置加载状态
46
+ setIsLoading(true);
47
+ isLoaded.current = true;
48
+ import('@vvi/ripple')
49
+ .then(module => {
50
+ if (is.isNull(canvas.current) || isUnmounted.current) {
51
+ return;
52
+ }
53
+ ripples.current = new module.Ripples(canvas.current, optionRef.current);
54
+ return;
55
+ })
56
+ .catch(err => {
57
+ setError(err);
58
+ })
59
+ .finally(() => {
60
+ setIsLoading(false);
61
+ });
62
+ return unmounted;
63
+ }, []);
64
+ return { ripples, isLoading, error };
65
+ }
66
+
67
+ exports.useLazyRipples = useLazyRipples;
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+ 'use strict';
3
+
4
+ var is = require('@vvi/is');
5
+ var ripple = require('@vvi/ripple');
6
+ var react = require('react');
7
+
8
+ /**
9
+ * @module @vvi/react-hooks/index
10
+ * @file use-ripples.ts
11
+ * @description ripple 的主要核心逻辑
12
+ * @author MrMudBean <Mr.MudBean@outlook.com>
13
+ * @copyright 2026 ©️ MrMudBean
14
+ * @since 2025-06-20 01:37
15
+ * @version 2.0.0-alpha.0
16
+ * @lastModified 2026-07-07 09:14
17
+ */
18
+ /**
19
+ * ## 使用绘制 ripples 上一层
20
+ * @param canvas `usrRef` 包裹的 `HTMLCanvasElement`,用于绘制图像
21
+ * @param option 初始化的
22
+ * @see [useRipples](https://lmssee.com/custom-hooks/use-ripples)
23
+ * @see [JQuery ripple](https://github.com/sirxemic/jquery.ripples)
24
+ * @example
25
+ * 下面是在 <BackgroundRipple> 中使用
26
+ * ```ts
27
+ * import { useRipples } from '@vvi/react-hooks';
28
+ *
29
+ * export function BackgroundRipple(props: BackgroundRipplesProps) {
30
+ *
31
+ * // canvas 元素
32
+ * const canvas = useRef<HTMLCanvasElement>(null);
33
+ *
34
+ * // 使用 ripples
35
+ * const ripplesRef = useRipples(canvas, props);
36
+ *
37
+ * return (<>
38
+ * <canvas ref={canvas}></canvas>
39
+ * {props.children}
40
+ * </>);
41
+ * }
42
+ * ```
43
+ */
44
+ function useRipples(canvas, option) {
45
+ /** react dom */
46
+ const ripples = react.useRef(null);
47
+ // 初始化数据
48
+ react.useEffect(() => {
49
+ /** 非空检验(这里一般都是有值的,除非故障) */
50
+ if (is.isNull(canvas.current))
51
+ return;
52
+ try {
53
+ ripples.current = new ripple.Ripples(canvas.current, option);
54
+ }
55
+ catch (error) {
56
+ }
57
+ return () => ripples.current?.destroy();
58
+ }, []);
59
+ return ripples;
60
+ }
61
+
62
+ Object.defineProperty(exports, "Ripples", {
63
+ enumerable: true,
64
+ get: function () { return ripple.Ripples; }
65
+ });
66
+ exports.useRipples = useRipples;
@@ -0,0 +1,137 @@
1
+ 'use client';
2
+ 'use strict';
3
+
4
+ var is = require('@vvi/is');
5
+ var react = require('react');
6
+
7
+ /**
8
+ * @module @vvi/react-hooks/useAnimationFrame
9
+ * @file useAnimationFrame.ts
10
+ * @description 使用 animationFrame
11
+ * @author MrMudBean <Mr.MudBean@outlook.com>
12
+ * @copyright 2026 ©️ MrMudBean
13
+ * @since 2025-01-07 11:23
14
+ * @version 2.0.0-alpha.0
15
+ * @lastModified 2026-07-06 17:16
16
+ */
17
+ /**
18
+ * # 该钩子在组件卸载时会自动调用 `window.cancelAnimationFrame` 清理传入的回调方法
19
+ *
20
+ * 默认是传入即启动调用方法
21
+ * @description 创建一个会自己关闭(组建卸载时)的 animationFrame。当然,可以通过返回值手动终止
22
+ * @param callback 使用回调函数。回调函数有两个参数,除了默认的时间,还有另一个用于暂停后的记时时间。如果没有主动暂停,两者时间总是趋近于相等的(可能有其他延迟造成时间差越来越大)。如果没有暂停需求,使用第一个默认时间就可以了。
23
+ * @param [option=false] 使用第二参数,可指定是否立即执行及是否仅执行一次
24
+ * @returns 返回值包含执行状态
25
+ * @see [useAnimationFrame](https://npm.lmssee.com/react-hooks/use-animation-frame)
26
+ * @example
27
+ * ```ts
28
+ * import { useAnimationFrame } from '@vvi/react-hooks';
29
+ * ...
30
+ *
31
+ * export function Home () {
32
+ * // 如果没有主动暂停的需要,直接使用默认的 time 最好。如果中间有使用暂停。那么 time 值是不连贯的,而 runTime 是连贯的
33
+ * const result = useAnimationFrame((time ,runTime) => {
34
+ * ... //
35
+ * });
36
+ *
37
+ * return <>
38
+ *
39
+ * <button onclick={() => result.cancel()}>暂停</button>
40
+ * <button onclick={() => result.render()}>恢复</button>
41
+ * </>
42
+ * }
43
+ * ...
44
+ * ```
45
+ * 当然,可使用第二参数执行立即执行还是仅执行一次(作用不大,不与赘述)
46
+ */
47
+ function useAnimationFrame(callback, option = false) {
48
+ if (is.isBoolean(option)) {
49
+ option = {
50
+ immediately: option,
51
+ once: false,
52
+ };
53
+ }
54
+ else if (is.isPlainObject(option)) {
55
+ option = {
56
+ once: is.isBoolean(option.once) ? option.once : false,
57
+ immediately: is.isBoolean(option.immediately) ? option.immediately : false,
58
+ };
59
+ }
60
+ else {
61
+ option = {
62
+ immediately: false,
63
+ once: false,
64
+ };
65
+ }
66
+ /** */
67
+ const animationFrame = react.useRef({
68
+ id: 0,
69
+ immediately: option.immediately ?? true,
70
+ once: option.once ?? false,
71
+ noun: false,
72
+ time: 0,
73
+ cancelTime: 0,
74
+ firstRunEffect: true,
75
+ isUnmounted: true,
76
+ result: {
77
+ cancel() {
78
+ const { id } = animationFrame.current;
79
+ if (id)
80
+ window.cancelAnimationFrame(id);
81
+ animationFrame.current.result.canceled = true;
82
+ },
83
+ canceled: false,
84
+ render() {
85
+ const { current } = animationFrame;
86
+ const { result } = current;
87
+ current.cancelTime = performance.now();
88
+ result.cancel();
89
+ result.canceled = false;
90
+ current.id = window.requestAnimationFrame(_);
91
+ },
92
+ },
93
+ });
94
+ const { current } = animationFrame;
95
+ /** 转化为函数 */
96
+ const _ = react.useCallback((time) => {
97
+ const { result } = current;
98
+ const _time = time - current.cancelTime;
99
+ current.time += _time < 0 ? 16.7 : _time;
100
+ callback(time, current.time);
101
+ // 如果不是仅执行一次,进入下一次执行
102
+ if (!current.once)
103
+ result.render();
104
+ }, [callback]);
105
+ // 首次执行
106
+ if (current.immediately && !current.noun && current.result.canceled) {
107
+ current.noun = true;
108
+ current.result.render();
109
+ }
110
+ react.useEffect(() => {
111
+ /// 非仅执行一次再次这里需要执行
112
+ if (!current.once) {
113
+ if (current.firstRunEffect) {
114
+ current.result.render();
115
+ current.firstRunEffect = false; // 标记为不允许执行
116
+ }
117
+ else if (!current.firstRunEffect && !current.result.canceled) {
118
+ /** 非第一次执行 */
119
+ current.result.render();
120
+ }
121
+ }
122
+ }, [callback]);
123
+ /// 在组件退出时保证能正确的退出
124
+ react.useEffect(() => {
125
+ current.isUnmounted = false;
126
+ return () => {
127
+ current.isUnmounted = true;
128
+ setTimeout(() => {
129
+ if (current.isUnmounted)
130
+ current.result.cancel();
131
+ }, 0);
132
+ };
133
+ }, []);
134
+ return current.result;
135
+ }
136
+
137
+ exports.useAnimationFrame = useAnimationFrame;
@@ -0,0 +1,68 @@
1
+ 'use client';
2
+ 'use strict';
3
+
4
+ var react = require('react');
5
+
6
+ /**
7
+ * @module @vvi/react-hooks/useInputIsComposing
8
+ * @file useInputIsComposing.ts
9
+ * @description 使用输入框是否完成输入状态
10
+ * @author MrMudBean <Mr.MudBean@outlook.com>
11
+ * @copyright 2026 ©️ MrMudBean
12
+ * @since 2025-01-07 11:23
13
+ * @version 2.0.0-alpha.0
14
+ * @lastModified 2026-07-06 17:16
15
+ */
16
+ /**
17
+ * # 导出一个使用 `useRef` 创建的 `boolean`
18
+ *
19
+ * *由于是包裹在 RefObject 之中,判断时务必使用 `isComposing.current` 进行判断*
20
+ * @description 用于判断当前的输入状态是否结束
21
+ * @param inputRef [RefObject<HTMLInputElement | HTMLTextAreaElement>] 输入框的 ref
22
+ * @returns RefObject<boolean>
23
+ * @see [useInputIscComposing](https://npm.lmssee.com/react-hooks/use-input-is-composing)
24
+ * @example
25
+ * ```ts
26
+ * import { useInputIsComposing } from '@vvi/react-hooks';
27
+ *
28
+ * ...
29
+ * const inputRef = useRef<HTMLInputElement>(null);
30
+ *
31
+ * const inputIsComposing = useInputIsComposing(inputRef);
32
+ * ...
33
+ * function enterDown(e: React.KeyboardEvent<HTMLInputElement>) {
34
+ * if (e.key === 'Enter') {
35
+ * if (isComposing.current) {
36
+ * console.log("此时此景,按回车键说明为了从候选词中挑选");
37
+ * } else {
38
+ * console.log("输入完毕,敲回车是为了看一些开发者是否绑定了其他事件");
39
+ * }
40
+ * }
41
+ * }
42
+ * ...
43
+ * <input type="text" onKeyDown={enterDown} ref={inputRef} />
44
+ * ...
45
+ * ```
46
+ */
47
+ function useInputIsComposing(inputRef) {
48
+ /** 当前输入框是否输入模式结束 */
49
+ const isComposing = react.useRef(false);
50
+ /** 监视当前编辑器输入状态 */
51
+ react.useEffect(() => {
52
+ const _i = inputRef.current; /// 当前元素
53
+ if (!_i)
54
+ return; /// 没有(不可能事件但是)获取元素直接返回
55
+ const handleCompositionStart = () => (isComposing.current = true); /// 键盘输入
56
+ const handleCompositionEnd = () => (isComposing.current = false); /// 键盘输入结束
57
+ // _i.focus(); /// 让该元素显示时自动聚焦
58
+ _i.addEventListener('compositionstart', handleCompositionStart); /// 监听键盘输入事件
59
+ _i.addEventListener('compositionend', handleCompositionEnd); /// 监听键盘输入结束事件
60
+ return () => {
61
+ _i.removeEventListener('compositionstart', handleCompositionStart); /// 取消监听键盘输入事件
62
+ _i.removeEventListener('compositionend', handleCompositionEnd); /// 取消监听键盘输入结束事件
63
+ };
64
+ }, []);
65
+ return isComposing;
66
+ }
67
+
68
+ exports.useInputIsComposing = useInputIsComposing;
@@ -0,0 +1,53 @@
1
+ 'use client';
2
+ 'use strict';
3
+
4
+ var react = require('react');
5
+
6
+ /**
7
+ * @module @vvi/react-hooks/useTimeId
8
+ * @file useTimeId.ts
9
+ * @description 使用定时器返回的时间戳
10
+ * @author MrMudBean <Mr.MudBean@outlook.com>
11
+ * @copyright 2026 ©️ MrMudBean
12
+ * @since 2025-01-07 11:20
13
+ * @version 2.0.0-alpha.0
14
+ * @lastModified 2026-07-06 17:13
15
+ */
16
+ /**
17
+ * # 导出一个使用 `useRef` 创建的 `NodeJS.Timeout`
18
+ *
19
+ * 该数值在组件卸载时会自动调用 `clearTimeout` 清理
20
+ * @see [useTimeId](https://npm.lmssee.com/react-hooks/use-time-id)
21
+ * @example
22
+ * ```ts
23
+ * import { useTimeId } from '@vvi/react-hooks';
24
+ *
25
+ * ...
26
+ *
27
+ * const timeoutId = useTimeId();
28
+ *
29
+ * useEffect(()=>{
30
+ * timeoutId.current = setTimeout(()=>{
31
+ * ...
32
+ * } ,delay);
33
+ * });
34
+ * ...
35
+ * ```
36
+ * 其实,正确的用法是这样的:
37
+ * ```ts
38
+ * useEffect(()=>{
39
+ * const timeId = setTimeout(()=>{
40
+ * ...
41
+ * }, delay) ;
42
+ *
43
+ * return ()=> timeId && clearTimeout(timeId);
44
+ * });
45
+ * ```
46
+ */
47
+ function useTimeId() {
48
+ const timeId = react.useRef(undefined);
49
+ react.useEffect(() => () => (timeId.current && clearTimeout(timeId.current)) || undefined, []);
50
+ return timeId;
51
+ }
52
+
53
+ exports.useTimeId = useTimeId;
package/es/index.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * @module @react-hooks/index
4
+ * @file index.ts
5
+ * @description _
6
+ * @author Mr.MudBean <Mr.MudBean@outlook.com>
7
+ * @license MIT
8
+ * @copyright 2026 ©️ Mr.MudBean
9
+ * @since 2026-07-06 16:13
10
+ * @version 0.0.0
11
+ * @lastModified 2026-07-06 16:18
12
+ */
13
+ export { useAnimationFrame } from './useAnimationFrame';
14
+ export { useRipples } from './use-ripples';
15
+ export { useLazyRipples } from './use-lazy-ripple';
16
+ export { useInputIsComposing } from './useInputIsComposing';
17
+ export { useTimeId } from './useTimeId';
package/es/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { useAnimationFrame } from './useAnimationFrame.js';
2
+ export { useRipples } from './use-ripples.js';
3
+ export { useLazyRipples } from './use-lazy-ripple.js';
4
+ export { useInputIsComposing } from './useInputIsComposing.js';
5
+ export { useTimeId } from './useTimeId.js';
@@ -0,0 +1,13 @@
1
+ import type { Ripples } from '@vvi/ripple';
2
+ import type { RipplesOptions } from '@vvi/ripple';
3
+ import type { RefObject } from 'react';
4
+ /**
5
+ * ## 懒加载使用涟漪 hook
6
+ * @param canvas
7
+ * @param option
8
+ */
9
+ export declare function useLazyRipples(canvas: RefObject<HTMLCanvasElement | null>, option?: RipplesOptions): {
10
+ ripples: RefObject<Ripples | null>;
11
+ isLoading: boolean;
12
+ error: unknown;
13
+ };
@@ -0,0 +1,65 @@
1
+ 'use client';
2
+ import { isNull } from '@vvi/is';
3
+ import { useRef, useState, useEffect } from 'react';
4
+
5
+ /**
6
+ * ## 懒加载使用涟漪 hook
7
+ * @param canvas
8
+ * @param option
9
+ */
10
+ function useLazyRipples(canvas, option) {
11
+ /** react dom */
12
+ const ripples = useRef(null);
13
+ /** 当前是否被卸载 */
14
+ const isUnmounted = useRef(true);
15
+ /** 是否已加载 */
16
+ const isLoaded = useRef(false);
17
+ // 加载状态
18
+ const [isLoading, setIsLoading] = useState(false);
19
+ // 错误状态
20
+ const [error, setError] = useState(null);
21
+ // 参数
22
+ const optionRef = useRef(option);
23
+ /** 卸载 */
24
+ function unmounted() {
25
+ isUnmounted.current = true;
26
+ setTimeout(() => {
27
+ // 当前是被卸载状态
28
+ if (isUnmounted.current) {
29
+ ripples.current?.destroy();
30
+ }
31
+ }, 0);
32
+ }
33
+ // 更新 option,防止加载 option 为旧数据
34
+ useEffect(() => {
35
+ optionRef.current = option;
36
+ }, [option]);
37
+ useEffect(() => {
38
+ isUnmounted.current = false;
39
+ /** 非空检验(这里一般都是有值的,除非故障) */
40
+ if (isNull(canvas.current) || isLoaded.current) {
41
+ return unmounted;
42
+ }
43
+ // 设置加载状态
44
+ setIsLoading(true);
45
+ isLoaded.current = true;
46
+ import('@vvi/ripple')
47
+ .then(module => {
48
+ if (isNull(canvas.current) || isUnmounted.current) {
49
+ return;
50
+ }
51
+ ripples.current = new module.Ripples(canvas.current, optionRef.current);
52
+ return;
53
+ })
54
+ .catch(err => {
55
+ setError(err);
56
+ })
57
+ .finally(() => {
58
+ setIsLoading(false);
59
+ });
60
+ return unmounted;
61
+ }, []);
62
+ return { ripples, isLoading, error };
63
+ }
64
+
65
+ export { useLazyRipples };
@@ -0,0 +1,31 @@
1
+ import { Ripples, type RipplesOptions } from '@vvi/ripple';
2
+ import type { RefObject } from 'react';
3
+ /**
4
+ * ## 使用绘制 ripples 上一层
5
+ * @param canvas `usrRef` 包裹的 `HTMLCanvasElement`,用于绘制图像
6
+ * @param option 初始化的
7
+ * @see [useRipples](https://lmssee.com/custom-hooks/use-ripples)
8
+ * @see [JQuery ripple](https://github.com/sirxemic/jquery.ripples)
9
+ * @example
10
+ * 下面是在 <BackgroundRipple> 中使用
11
+ * ```ts
12
+ * import { useRipples } from '@vvi/react-hooks';
13
+ *
14
+ * export function BackgroundRipple(props: BackgroundRipplesProps) {
15
+ *
16
+ * // canvas 元素
17
+ * const canvas = useRef<HTMLCanvasElement>(null);
18
+ *
19
+ * // 使用 ripples
20
+ * const ripplesRef = useRipples(canvas, props);
21
+ *
22
+ * return (<>
23
+ * <canvas ref={canvas}></canvas>
24
+ * {props.children}
25
+ * </>);
26
+ * }
27
+ * ```
28
+ */
29
+ export declare function useRipples(canvas: RefObject<HTMLCanvasElement | null>, option?: RipplesOptions): RefObject<Ripples | null>;
30
+ export { Ripples };
31
+ export type { RipplesOptions };
@@ -0,0 +1,61 @@
1
+ 'use client';
2
+ import { isNull } from '@vvi/is';
3
+ import { Ripples } from '@vvi/ripple';
4
+ export { Ripples } from '@vvi/ripple';
5
+ import { useRef, useEffect } from 'react';
6
+
7
+ /**
8
+ * @module @vvi/react-hooks/index
9
+ * @file use-ripples.ts
10
+ * @description ripple 的主要核心逻辑
11
+ * @author MrMudBean <Mr.MudBean@outlook.com>
12
+ * @copyright 2026 ©️ MrMudBean
13
+ * @since 2025-06-20 01:37
14
+ * @version 2.0.0-alpha.0
15
+ * @lastModified 2026-07-07 09:14
16
+ */
17
+ /**
18
+ * ## 使用绘制 ripples 上一层
19
+ * @param canvas `usrRef` 包裹的 `HTMLCanvasElement`,用于绘制图像
20
+ * @param option 初始化的
21
+ * @see [useRipples](https://lmssee.com/custom-hooks/use-ripples)
22
+ * @see [JQuery ripple](https://github.com/sirxemic/jquery.ripples)
23
+ * @example
24
+ * 下面是在 <BackgroundRipple> 中使用
25
+ * ```ts
26
+ * import { useRipples } from '@vvi/react-hooks';
27
+ *
28
+ * export function BackgroundRipple(props: BackgroundRipplesProps) {
29
+ *
30
+ * // canvas 元素
31
+ * const canvas = useRef<HTMLCanvasElement>(null);
32
+ *
33
+ * // 使用 ripples
34
+ * const ripplesRef = useRipples(canvas, props);
35
+ *
36
+ * return (<>
37
+ * <canvas ref={canvas}></canvas>
38
+ * {props.children}
39
+ * </>);
40
+ * }
41
+ * ```
42
+ */
43
+ function useRipples(canvas, option) {
44
+ /** react dom */
45
+ const ripples = useRef(null);
46
+ // 初始化数据
47
+ useEffect(() => {
48
+ /** 非空检验(这里一般都是有值的,除非故障) */
49
+ if (isNull(canvas.current))
50
+ return;
51
+ try {
52
+ ripples.current = new Ripples(canvas.current, option);
53
+ }
54
+ catch (error) {
55
+ }
56
+ return () => ripples.current?.destroy();
57
+ }, []);
58
+ return ripples;
59
+ }
60
+
61
+ export { useRipples };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @module @vvi/react-hooks/useAnimationFrame
3
+ * @file useAnimationFrame.ts
4
+ * @description 使用 animationFrame
5
+ * @author MrMudBean <Mr.MudBean@outlook.com>
6
+ * @copyright 2026 ©️ MrMudBean
7
+ * @since 2025-01-07 11:23
8
+ * @version 2.0.0-alpha.0
9
+ * @lastModified 2026-07-06 17:16
10
+ */
11
+ /** 使用动画结果 */
12
+ export interface UseAnimationFrameResult {
13
+ /** 取消执行动画 */
14
+ cancel(): void;
15
+ /** 当前取消的状态 */
16
+ canceled: boolean;
17
+ /** 再次执行 */
18
+ render(): void;
19
+ }
20
+ export interface UseAnimationFrame {
21
+ /** 当前动画的 id */
22
+ id: null | number;
23
+ /** 执行的时间 */
24
+ time: number;
25
+ /** 暂停的时间 */
26
+ cancelTime: number;
27
+ /** 返回值 */
28
+ result: UseAnimationFrameResult;
29
+ immediately: boolean;
30
+ /** 是否完成了额外的首次执行 */
31
+ noun: boolean;
32
+ /** 是否仅执行一次 */
33
+ once: boolean;
34
+ /** 是否首次执行 effect */
35
+ firstRunEffect: boolean;
36
+ /** 是否已卸载 */
37
+ isUnmounted: boolean;
38
+ }
39
+ export type AnimationFrameOption = boolean | {
40
+ immediately?: boolean;
41
+ once?: boolean;
42
+ };
43
+ /**
44
+ * # 该钩子在组件卸载时会自动调用 `window.cancelAnimationFrame` 清理传入的回调方法
45
+ *
46
+ * 默认是传入即启动调用方法
47
+ * @description 创建一个会自己关闭(组建卸载时)的 animationFrame。当然,可以通过返回值手动终止
48
+ * @param callback 使用回调函数。回调函数有两个参数,除了默认的时间,还有另一个用于暂停后的记时时间。如果没有主动暂停,两者时间总是趋近于相等的(可能有其他延迟造成时间差越来越大)。如果没有暂停需求,使用第一个默认时间就可以了。
49
+ * @param [option=false] 使用第二参数,可指定是否立即执行及是否仅执行一次
50
+ * @returns 返回值包含执行状态
51
+ * @see [useAnimationFrame](https://npm.lmssee.com/react-hooks/use-animation-frame)
52
+ * @example
53
+ * ```ts
54
+ * import { useAnimationFrame } from '@vvi/react-hooks';
55
+ * ...
56
+ *
57
+ * export function Home () {
58
+ * // 如果没有主动暂停的需要,直接使用默认的 time 最好。如果中间有使用暂停。那么 time 值是不连贯的,而 runTime 是连贯的
59
+ * const result = useAnimationFrame((time ,runTime) => {
60
+ * ... //
61
+ * });
62
+ *
63
+ * return <>
64
+ *
65
+ * <button onclick={() => result.cancel()}>暂停</button>
66
+ * <button onclick={() => result.render()}>恢复</button>
67
+ * </>
68
+ * }
69
+ * ...
70
+ * ```
71
+ * 当然,可使用第二参数执行立即执行还是仅执行一次(作用不大,不与赘述)
72
+ */
73
+ export declare function useAnimationFrame(callback: (time: number, runtime: number) => void, option?: AnimationFrameOption): UseAnimationFrameResult;
@@ -0,0 +1,135 @@
1
+ 'use client';
2
+ import { isBoolean, isPlainObject } from '@vvi/is';
3
+ import { useRef, useCallback, useEffect } from 'react';
4
+
5
+ /**
6
+ * @module @vvi/react-hooks/useAnimationFrame
7
+ * @file useAnimationFrame.ts
8
+ * @description 使用 animationFrame
9
+ * @author MrMudBean <Mr.MudBean@outlook.com>
10
+ * @copyright 2026 ©️ MrMudBean
11
+ * @since 2025-01-07 11:23
12
+ * @version 2.0.0-alpha.0
13
+ * @lastModified 2026-07-06 17:16
14
+ */
15
+ /**
16
+ * # 该钩子在组件卸载时会自动调用 `window.cancelAnimationFrame` 清理传入的回调方法
17
+ *
18
+ * 默认是传入即启动调用方法
19
+ * @description 创建一个会自己关闭(组建卸载时)的 animationFrame。当然,可以通过返回值手动终止
20
+ * @param callback 使用回调函数。回调函数有两个参数,除了默认的时间,还有另一个用于暂停后的记时时间。如果没有主动暂停,两者时间总是趋近于相等的(可能有其他延迟造成时间差越来越大)。如果没有暂停需求,使用第一个默认时间就可以了。
21
+ * @param [option=false] 使用第二参数,可指定是否立即执行及是否仅执行一次
22
+ * @returns 返回值包含执行状态
23
+ * @see [useAnimationFrame](https://npm.lmssee.com/react-hooks/use-animation-frame)
24
+ * @example
25
+ * ```ts
26
+ * import { useAnimationFrame } from '@vvi/react-hooks';
27
+ * ...
28
+ *
29
+ * export function Home () {
30
+ * // 如果没有主动暂停的需要,直接使用默认的 time 最好。如果中间有使用暂停。那么 time 值是不连贯的,而 runTime 是连贯的
31
+ * const result = useAnimationFrame((time ,runTime) => {
32
+ * ... //
33
+ * });
34
+ *
35
+ * return <>
36
+ *
37
+ * <button onclick={() => result.cancel()}>暂停</button>
38
+ * <button onclick={() => result.render()}>恢复</button>
39
+ * </>
40
+ * }
41
+ * ...
42
+ * ```
43
+ * 当然,可使用第二参数执行立即执行还是仅执行一次(作用不大,不与赘述)
44
+ */
45
+ function useAnimationFrame(callback, option = false) {
46
+ if (isBoolean(option)) {
47
+ option = {
48
+ immediately: option,
49
+ once: false,
50
+ };
51
+ }
52
+ else if (isPlainObject(option)) {
53
+ option = {
54
+ once: isBoolean(option.once) ? option.once : false,
55
+ immediately: isBoolean(option.immediately) ? option.immediately : false,
56
+ };
57
+ }
58
+ else {
59
+ option = {
60
+ immediately: false,
61
+ once: false,
62
+ };
63
+ }
64
+ /** */
65
+ const animationFrame = useRef({
66
+ id: 0,
67
+ immediately: option.immediately ?? true,
68
+ once: option.once ?? false,
69
+ noun: false,
70
+ time: 0,
71
+ cancelTime: 0,
72
+ firstRunEffect: true,
73
+ isUnmounted: true,
74
+ result: {
75
+ cancel() {
76
+ const { id } = animationFrame.current;
77
+ if (id)
78
+ window.cancelAnimationFrame(id);
79
+ animationFrame.current.result.canceled = true;
80
+ },
81
+ canceled: false,
82
+ render() {
83
+ const { current } = animationFrame;
84
+ const { result } = current;
85
+ current.cancelTime = performance.now();
86
+ result.cancel();
87
+ result.canceled = false;
88
+ current.id = window.requestAnimationFrame(_);
89
+ },
90
+ },
91
+ });
92
+ const { current } = animationFrame;
93
+ /** 转化为函数 */
94
+ const _ = useCallback((time) => {
95
+ const { result } = current;
96
+ const _time = time - current.cancelTime;
97
+ current.time += _time < 0 ? 16.7 : _time;
98
+ callback(time, current.time);
99
+ // 如果不是仅执行一次,进入下一次执行
100
+ if (!current.once)
101
+ result.render();
102
+ }, [callback]);
103
+ // 首次执行
104
+ if (current.immediately && !current.noun && current.result.canceled) {
105
+ current.noun = true;
106
+ current.result.render();
107
+ }
108
+ useEffect(() => {
109
+ /// 非仅执行一次再次这里需要执行
110
+ if (!current.once) {
111
+ if (current.firstRunEffect) {
112
+ current.result.render();
113
+ current.firstRunEffect = false; // 标记为不允许执行
114
+ }
115
+ else if (!current.firstRunEffect && !current.result.canceled) {
116
+ /** 非第一次执行 */
117
+ current.result.render();
118
+ }
119
+ }
120
+ }, [callback]);
121
+ /// 在组件退出时保证能正确的退出
122
+ useEffect(() => {
123
+ current.isUnmounted = false;
124
+ return () => {
125
+ current.isUnmounted = true;
126
+ setTimeout(() => {
127
+ if (current.isUnmounted)
128
+ current.result.cancel();
129
+ }, 0);
130
+ };
131
+ }, []);
132
+ return current.result;
133
+ }
134
+
135
+ export { useAnimationFrame };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @module @vvi/react-hooks/useInputIsComposing
3
+ * @file useInputIsComposing.ts
4
+ * @description 使用输入框是否完成输入状态
5
+ * @author MrMudBean <Mr.MudBean@outlook.com>
6
+ * @copyright 2026 ©️ MrMudBean
7
+ * @since 2025-01-07 11:23
8
+ * @version 2.0.0-alpha.0
9
+ * @lastModified 2026-07-06 17:16
10
+ */
11
+ import type { RefObject } from 'react';
12
+ /**
13
+ * # 导出一个使用 `useRef` 创建的 `boolean`
14
+ *
15
+ * *由于是包裹在 RefObject 之中,判断时务必使用 `isComposing.current` 进行判断*
16
+ * @description 用于判断当前的输入状态是否结束
17
+ * @param inputRef [RefObject<HTMLInputElement | HTMLTextAreaElement>] 输入框的 ref
18
+ * @returns RefObject<boolean>
19
+ * @see [useInputIscComposing](https://npm.lmssee.com/react-hooks/use-input-is-composing)
20
+ * @example
21
+ * ```ts
22
+ * import { useInputIsComposing } from '@vvi/react-hooks';
23
+ *
24
+ * ...
25
+ * const inputRef = useRef<HTMLInputElement>(null);
26
+ *
27
+ * const inputIsComposing = useInputIsComposing(inputRef);
28
+ * ...
29
+ * function enterDown(e: React.KeyboardEvent<HTMLInputElement>) {
30
+ * if (e.key === 'Enter') {
31
+ * if (isComposing.current) {
32
+ * console.log("此时此景,按回车键说明为了从候选词中挑选");
33
+ * } else {
34
+ * console.log("输入完毕,敲回车是为了看一些开发者是否绑定了其他事件");
35
+ * }
36
+ * }
37
+ * }
38
+ * ...
39
+ * <input type="text" onKeyDown={enterDown} ref={inputRef} />
40
+ * ...
41
+ * ```
42
+ */
43
+ export declare function useInputIsComposing(inputRef: RefObject<HTMLInputElement | HTMLTextAreaElement | null>): RefObject<boolean>;
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+ import { useRef, useEffect } from 'react';
3
+
4
+ /**
5
+ * @module @vvi/react-hooks/useInputIsComposing
6
+ * @file useInputIsComposing.ts
7
+ * @description 使用输入框是否完成输入状态
8
+ * @author MrMudBean <Mr.MudBean@outlook.com>
9
+ * @copyright 2026 ©️ MrMudBean
10
+ * @since 2025-01-07 11:23
11
+ * @version 2.0.0-alpha.0
12
+ * @lastModified 2026-07-06 17:16
13
+ */
14
+ /**
15
+ * # 导出一个使用 `useRef` 创建的 `boolean`
16
+ *
17
+ * *由于是包裹在 RefObject 之中,判断时务必使用 `isComposing.current` 进行判断*
18
+ * @description 用于判断当前的输入状态是否结束
19
+ * @param inputRef [RefObject<HTMLInputElement | HTMLTextAreaElement>] 输入框的 ref
20
+ * @returns RefObject<boolean>
21
+ * @see [useInputIscComposing](https://npm.lmssee.com/react-hooks/use-input-is-composing)
22
+ * @example
23
+ * ```ts
24
+ * import { useInputIsComposing } from '@vvi/react-hooks';
25
+ *
26
+ * ...
27
+ * const inputRef = useRef<HTMLInputElement>(null);
28
+ *
29
+ * const inputIsComposing = useInputIsComposing(inputRef);
30
+ * ...
31
+ * function enterDown(e: React.KeyboardEvent<HTMLInputElement>) {
32
+ * if (e.key === 'Enter') {
33
+ * if (isComposing.current) {
34
+ * console.log("此时此景,按回车键说明为了从候选词中挑选");
35
+ * } else {
36
+ * console.log("输入完毕,敲回车是为了看一些开发者是否绑定了其他事件");
37
+ * }
38
+ * }
39
+ * }
40
+ * ...
41
+ * <input type="text" onKeyDown={enterDown} ref={inputRef} />
42
+ * ...
43
+ * ```
44
+ */
45
+ function useInputIsComposing(inputRef) {
46
+ /** 当前输入框是否输入模式结束 */
47
+ const isComposing = useRef(false);
48
+ /** 监视当前编辑器输入状态 */
49
+ useEffect(() => {
50
+ const _i = inputRef.current; /// 当前元素
51
+ if (!_i)
52
+ return; /// 没有(不可能事件但是)获取元素直接返回
53
+ const handleCompositionStart = () => (isComposing.current = true); /// 键盘输入
54
+ const handleCompositionEnd = () => (isComposing.current = false); /// 键盘输入结束
55
+ // _i.focus(); /// 让该元素显示时自动聚焦
56
+ _i.addEventListener('compositionstart', handleCompositionStart); /// 监听键盘输入事件
57
+ _i.addEventListener('compositionend', handleCompositionEnd); /// 监听键盘输入结束事件
58
+ return () => {
59
+ _i.removeEventListener('compositionstart', handleCompositionStart); /// 取消监听键盘输入事件
60
+ _i.removeEventListener('compositionend', handleCompositionEnd); /// 取消监听键盘输入结束事件
61
+ };
62
+ }, []);
63
+ return isComposing;
64
+ }
65
+
66
+ export { useInputIsComposing };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @module @vvi/react-hooks/useTimeId
3
+ * @file useTimeId.ts
4
+ * @description 使用定时器返回的时间戳
5
+ * @author MrMudBean <Mr.MudBean@outlook.com>
6
+ * @copyright 2026 ©️ MrMudBean
7
+ * @since 2025-01-07 11:20
8
+ * @version 2.0.0-alpha.0
9
+ * @lastModified 2026-07-06 17:13
10
+ */
11
+ /**
12
+ * # 导出一个使用 `useRef` 创建的 `NodeJS.Timeout`
13
+ *
14
+ * 该数值在组件卸载时会自动调用 `clearTimeout` 清理
15
+ * @see [useTimeId](https://npm.lmssee.com/react-hooks/use-time-id)
16
+ * @example
17
+ * ```ts
18
+ * import { useTimeId } from '@vvi/react-hooks';
19
+ *
20
+ * ...
21
+ *
22
+ * const timeoutId = useTimeId();
23
+ *
24
+ * useEffect(()=>{
25
+ * timeoutId.current = setTimeout(()=>{
26
+ * ...
27
+ * } ,delay);
28
+ * });
29
+ * ...
30
+ * ```
31
+ * 其实,正确的用法是这样的:
32
+ * ```ts
33
+ * useEffect(()=>{
34
+ * const timeId = setTimeout(()=>{
35
+ * ...
36
+ * }, delay) ;
37
+ *
38
+ * return ()=> timeId && clearTimeout(timeId);
39
+ * });
40
+ * ```
41
+ */
42
+ export declare function useTimeId(): import("react").RefObject<number | undefined>;
@@ -0,0 +1,51 @@
1
+ 'use client';
2
+ import { useRef, useEffect } from 'react';
3
+
4
+ /**
5
+ * @module @vvi/react-hooks/useTimeId
6
+ * @file useTimeId.ts
7
+ * @description 使用定时器返回的时间戳
8
+ * @author MrMudBean <Mr.MudBean@outlook.com>
9
+ * @copyright 2026 ©️ MrMudBean
10
+ * @since 2025-01-07 11:20
11
+ * @version 2.0.0-alpha.0
12
+ * @lastModified 2026-07-06 17:13
13
+ */
14
+ /**
15
+ * # 导出一个使用 `useRef` 创建的 `NodeJS.Timeout`
16
+ *
17
+ * 该数值在组件卸载时会自动调用 `clearTimeout` 清理
18
+ * @see [useTimeId](https://npm.lmssee.com/react-hooks/use-time-id)
19
+ * @example
20
+ * ```ts
21
+ * import { useTimeId } from '@vvi/react-hooks';
22
+ *
23
+ * ...
24
+ *
25
+ * const timeoutId = useTimeId();
26
+ *
27
+ * useEffect(()=>{
28
+ * timeoutId.current = setTimeout(()=>{
29
+ * ...
30
+ * } ,delay);
31
+ * });
32
+ * ...
33
+ * ```
34
+ * 其实,正确的用法是这样的:
35
+ * ```ts
36
+ * useEffect(()=>{
37
+ * const timeId = setTimeout(()=>{
38
+ * ...
39
+ * }, delay) ;
40
+ *
41
+ * return ()=> timeId && clearTimeout(timeId);
42
+ * });
43
+ * ```
44
+ */
45
+ function useTimeId() {
46
+ const timeId = useRef(undefined);
47
+ useEffect(() => () => (timeId.current && clearTimeout(timeId.current)) || undefined, []);
48
+ return timeId;
49
+ }
50
+
51
+ export { useTimeId };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @module @react-hooks/qqi
3
+ * @file dog.ts
4
+ * @description _
5
+ * @author Mr.MudBean <Mr.MudBean@outlook.com>
6
+ * @copyright 2026 ©️ Mr.MudBean
7
+ * @since 2026-07-06 16:04
8
+ * @version 0.0.0
9
+ * @lastModified 2026-07-06 16:05
10
+ */
11
+ export declare const dog: import("@vvi/log").DevLog;
package/package.json ADDED
@@ -0,0 +1,118 @@
1
+ {
2
+ "name": "@vvi/react-hooks",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "cjs/index.js",
6
+ "module": "es/index.js",
7
+ "types": "es/index.d.ts",
8
+ "author": {
9
+ "name": "泥豆君",
10
+ "email": "Mr.MudBean@outlook.com",
11
+ "url": "https://mudbean.cn"
12
+ },
13
+ "description": "React hooks of vvi",
14
+ "sideEffects": false,
15
+ "license": "MIT",
16
+ "files": [
17
+ "cjs",
18
+ "es",
19
+ "LICENSE",
20
+ "README.md",
21
+ "CHANGELOG.md"
22
+ ],
23
+ "exports": {
24
+ ".": {
25
+ "import": {
26
+ "default": "./es/index.js",
27
+ "types": "./es/index.d.ts"
28
+ },
29
+ "require": {
30
+ "default": "./cjs/index.js",
31
+ "types": "./es/index.d.ts"
32
+ }
33
+ },
34
+ "./use-lazy-ripple": {
35
+ "import": {
36
+ "default": "./es/use-lazy-ripple.js",
37
+ "types": "./es/use-lazy-ripple.d.ts"
38
+ },
39
+ "require": {
40
+ "default": "./cjs/use-lazy-ripple.js",
41
+ "types": "./es/use-lazy-ripple.d.ts"
42
+ }
43
+ },
44
+ "./use-ripples": {
45
+ "import": {
46
+ "default": "./es/use-ripples.js",
47
+ "types": "./es/use-ripples.d.ts"
48
+ },
49
+ "require": {
50
+ "default": "./cjs/use-ripples.js",
51
+ "types": "./es/use-ripples.d.ts"
52
+ }
53
+ },
54
+ "./useAnimationFrame": {
55
+ "import": {
56
+ "default": "./es/useAnimationFrame.js",
57
+ "types": "./es/useAnimationFrame.d.ts"
58
+ },
59
+ "require": {
60
+ "default": "./cjs/useAnimationFrame.js",
61
+ "types": "./es/useAnimationFrame.d.ts"
62
+ }
63
+ },
64
+ "./useInputIsComposing": {
65
+ "import": {
66
+ "default": "./es/useInputIsComposing.js",
67
+ "types": "./es/useInputIsComposing.d.ts"
68
+ },
69
+ "require": {
70
+ "default": "./cjs/useInputIsComposing.js",
71
+ "types": "./es/useInputIsComposing.d.ts"
72
+ }
73
+ },
74
+ "./useTimeId": {
75
+ "import": {
76
+ "default": "./es/useTimeId.js",
77
+ "types": "./es/useTimeId.d.ts"
78
+ },
79
+ "require": {
80
+ "default": "./cjs/useTimeId.js",
81
+ "types": "./es/useTimeId.d.ts"
82
+ }
83
+ }
84
+ },
85
+ "keywords": [
86
+ "react-hooks",
87
+ "mudbean",
88
+ "vvi"
89
+ ],
90
+ "homepage": "https://npm.lmssee.com/react-hooks",
91
+ "dependencies": {
92
+ "@vvi/is": "^2.0.5",
93
+ "@vvi/ripple": "^1.0.0",
94
+ "@vvi/state": "^0.0.2"
95
+ },
96
+ "peerDependencies": {
97
+ "react": "^19.2"
98
+ },
99
+ "bugs": {
100
+ "url": "https://github.com/MrMudBean/react-hooks/issues",
101
+ "email": "Mr.MudBean@outlook.com"
102
+ },
103
+ "repository": {
104
+ "type": "git",
105
+ "url": "git+https://github.com/MrMudBean/react-hooks.git"
106
+ },
107
+ "publishConfig": {
108
+ "access": "public",
109
+ "registry": "https://registry.npmjs.org/"
110
+ },
111
+ "browserslist": [
112
+ "> 1%",
113
+ "last 2 versions"
114
+ ],
115
+ "engines": {
116
+ "node": ">=18.0.0"
117
+ }
118
+ }