@tarsis/toolkit 0.6.5 → 0.7.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/dist/Container-BVX2MW1U.cjs +138 -0
- package/dist/Container-BirkN1fA.js +119 -0
- package/dist/SelectBase-BC6WKZVF.cjs +448 -0
- package/dist/SelectBase-DPcXvMTa.js +399 -0
- package/dist/Slot-SOe-b2n6.cjs +77 -0
- package/dist/Slot-z71j7q57.js +65 -0
- package/dist/animation-BFpILbqb.js +102 -0
- package/dist/animation-BauloIgQ.cjs +119 -0
- package/dist/assets-BMqH4phf.cjs +52 -0
- package/dist/assets-huTvlamy.js +29 -0
- package/dist/audio/fail.mp3 +0 -0
- package/dist/audio/fail.ogg +0 -0
- package/dist/audio/hover.mp3 +0 -0
- package/dist/audio/hover.ogg +0 -0
- package/dist/audio/lock/fail.mp3 +0 -0
- package/dist/audio/lock/fail.ogg +0 -0
- package/dist/audio/lock/hover.mp3 +0 -0
- package/dist/audio/lock/hover.ogg +0 -0
- package/dist/audio/lock/prev-next.mp3 +0 -0
- package/dist/audio/lock/prev-next.ogg +0 -0
- package/dist/audio/lock/select.mp3 +0 -0
- package/dist/audio/lock/select.ogg +0 -0
- package/dist/audio/lock/success.mp3 +0 -0
- package/dist/audio/lock/success.ogg +0 -0
- package/dist/audio/prev-next.mp3 +0 -0
- package/dist/audio/prev-next.ogg +0 -0
- package/dist/audio/select.mp3 +0 -0
- package/dist/audio/select.ogg +0 -0
- package/dist/audio/success.mp3 +0 -0
- package/dist/audio/success.ogg +0 -0
- package/dist/chunk-CKQMccvm.cjs +28 -0
- package/dist/fonts/orbitron/orbitron-black.fnt +426 -0
- package/dist/fonts/orbitron/orbitron-black.png +0 -0
- package/dist/fonts/orbitron-black.fnt +426 -0
- package/dist/fonts/orbitron-black.png +0 -0
- package/dist/gl-B0NhVYRl.cjs +177 -0
- package/dist/gl-BipoEx9s.js +171 -0
- package/dist/hooks.cjs +661 -24
- package/dist/hooks.d.ts +72 -0
- package/dist/hooks.js +635 -1
- package/dist/index.cjs +26708 -384
- package/dist/index.d.ts +913 -27
- package/dist/index.js +26282 -3
- package/dist/layout.cjs +5 -0
- package/dist/layout.d.ts +45 -0
- package/dist/layout.js +2 -0
- package/dist/primitives.cjs +13 -0
- package/dist/primitives.d.ts +178 -0
- package/dist/primitives.js +3 -0
- package/dist/server.cjs +25 -0
- package/dist/server.d.ts +70 -0
- package/dist/server.js +2 -0
- package/dist/styles.css +3872 -2798
- package/dist/tokens-B2AxRYyF.js +434 -0
- package/dist/tokens-DlMougUi.cjs +469 -0
- package/dist/tokens.cjs +12 -0
- package/dist/tokens.d.ts +435 -0
- package/dist/tokens.js +3 -0
- package/dist/useMergeRefs-BM2-gSLn.js +16 -0
- package/dist/useMergeRefs-C_l6omwU.cjs +28 -0
- package/dist/utils-BGgmkNY4.cjs +330 -0
- package/dist/utils-Dw5El_3G.js +222 -0
- package/dist/utils.cjs +44 -38
- package/dist/utils.d.ts +75 -0
- package/dist/utils.js +3 -1
- package/dist/values-BTw18-W5.js +138 -0
- package/dist/values-BqSJ0h9o.cjs +275 -0
- package/package.json +88 -36
- package/dist/gl-Bp3e3vph.js +0 -3258
- package/dist/gl-Duf2UKsB.cjs +0 -3262
- package/dist/index-BcIzOPR7.cjs +0 -116866
- package/dist/index-BjG_vCX_.js +0 -3910
- package/dist/index-ZBjz1bHI.cjs +0 -3912
- package/dist/index-ss50SEnC.js +0 -116503
- package/dist/svg-BT_esDTZ.cjs +0 -236
- package/dist/svg-CQLdTbLk.js +0 -205
- package/dist/useWindowReady-6kIdYolB.cjs +0 -9317
- package/dist/useWindowReady-tUs-ONyG.js +0 -9224
package/dist/hooks.js
CHANGED
|
@@ -1 +1,635 @@
|
|
|
1
|
-
|
|
1
|
+
import { n as useMergeRefs, t as mergeRefs } from "./useMergeRefs-BM2-gSLn.js";
|
|
2
|
+
import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
3
|
+
import gsap from "gsap";
|
|
4
|
+
import { animate, useMotionValue } from "motion/react";
|
|
5
|
+
import Bowser from "bowser";
|
|
6
|
+
//#region src/hooks/usePrevious.ts
|
|
7
|
+
var usePreviousState = (value) => {
|
|
8
|
+
const previousRef = useRef(void 0);
|
|
9
|
+
const currentRef = useRef(void 0);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (value !== currentRef.current) {
|
|
12
|
+
previousRef.current = currentRef.current;
|
|
13
|
+
currentRef.current = value;
|
|
14
|
+
}
|
|
15
|
+
}, [value]);
|
|
16
|
+
return previousRef.current;
|
|
17
|
+
};
|
|
18
|
+
var usePreviousRender = (value) => {
|
|
19
|
+
const previousRef = useRef(void 0);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
previousRef.current = value;
|
|
22
|
+
}, [value]);
|
|
23
|
+
return previousRef.current;
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/hooks/useAnimatedText/useAnimatedText.ts
|
|
27
|
+
function useAnimatedText(text, delimiter) {
|
|
28
|
+
const resolvedDelimiter = delimiter === "word" ? " " : "";
|
|
29
|
+
const animatedCursor = useMotionValue(0);
|
|
30
|
+
const [cursor, setCursor] = useState(0);
|
|
31
|
+
const prevText = usePreviousRender(text);
|
|
32
|
+
const isSameText = text.startsWith(prevText ?? "");
|
|
33
|
+
const mountedRef = useRef(true);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
mountedRef.current = true;
|
|
36
|
+
return () => {
|
|
37
|
+
mountedRef.current = false;
|
|
38
|
+
};
|
|
39
|
+
}, []);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!isSameText && mountedRef.current) {
|
|
42
|
+
animatedCursor.jump(0);
|
|
43
|
+
requestAnimationFrame(() => {
|
|
44
|
+
if (mountedRef.current) setCursor(0);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}, [isSameText, animatedCursor]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!mountedRef.current) return;
|
|
50
|
+
const controls = animate(animatedCursor, text.split(resolvedDelimiter).length, {
|
|
51
|
+
duration: 3,
|
|
52
|
+
ease: "easeOut",
|
|
53
|
+
onUpdate(latest) {
|
|
54
|
+
if (mountedRef.current) setCursor(Math.floor(latest));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
return () => {
|
|
58
|
+
controls.stop();
|
|
59
|
+
};
|
|
60
|
+
}, [
|
|
61
|
+
animatedCursor,
|
|
62
|
+
isSameText,
|
|
63
|
+
text,
|
|
64
|
+
resolvedDelimiter
|
|
65
|
+
]);
|
|
66
|
+
return text.split(resolvedDelimiter).slice(0, cursor).join(resolvedDelimiter);
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/hooks/useControllableState.ts
|
|
70
|
+
function useControllableState(controlledValue, defaultValue, onChange) {
|
|
71
|
+
const isControlled = controlledValue !== void 0;
|
|
72
|
+
const isControlledRef = useRef(isControlled);
|
|
73
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (process.env.NODE_ENV !== "production") {
|
|
76
|
+
if (isControlledRef.current !== isControlled) console.warn("Component switched between controlled and uncontrolled. Decide between using a controlled or uncontrolled value for the lifetime of the component.");
|
|
77
|
+
}
|
|
78
|
+
isControlledRef.current = isControlled;
|
|
79
|
+
});
|
|
80
|
+
return [isControlled ? controlledValue : internalValue, useCallback((next) => {
|
|
81
|
+
const resolve = (prev) => typeof next === "function" ? next(prev) : next;
|
|
82
|
+
if (!isControlledRef.current) setInternalValue((prev) => {
|
|
83
|
+
const nextValue = resolve(prev);
|
|
84
|
+
onChange?.(nextValue);
|
|
85
|
+
return nextValue;
|
|
86
|
+
});
|
|
87
|
+
else onChange?.(resolve(controlledValue));
|
|
88
|
+
}, [onChange, controlledValue])];
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/hooks/useBowser.ts
|
|
92
|
+
var useBowser = () => {
|
|
93
|
+
const [browser] = useState(() => typeof window === "undefined" ? null : Bowser.parse(window.navigator.userAgent));
|
|
94
|
+
const [parser] = useState(() => typeof window === "undefined" ? null : Bowser.getParser(window.navigator.userAgent));
|
|
95
|
+
return useMemo(() => {
|
|
96
|
+
if (!parser || !browser) return null;
|
|
97
|
+
const browserName = parser.getBrowser().name;
|
|
98
|
+
const isDesktop = browser.platform.type === "desktop";
|
|
99
|
+
return {
|
|
100
|
+
isMobile: browser.platform.type === "mobile",
|
|
101
|
+
isDesktop,
|
|
102
|
+
isIOS: browser.os.name?.toLowerCase() === "ios",
|
|
103
|
+
isMacOS: browser.platform.type?.toLocaleLowerCase() === "desktop" && browser.platform.vendor?.toLowerCase() === "apple" && browser.os.name?.toLocaleLowerCase() === "macos",
|
|
104
|
+
isSafari: browserName ? Boolean(browserName.toLowerCase().match(/safari/)) : false,
|
|
105
|
+
isFirefox: browserName ? Boolean(browserName.toLowerCase().match(/firefox/)) : false,
|
|
106
|
+
isChrome: browserName ? Boolean(browserName.toLowerCase().match(/chrome/)) : false
|
|
107
|
+
};
|
|
108
|
+
}, [browser, parser]);
|
|
109
|
+
};
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/hooks/useLiveRef.ts
|
|
112
|
+
var useLiveRef = (value) => {
|
|
113
|
+
const ref = useRef(value);
|
|
114
|
+
ref.current = value;
|
|
115
|
+
return ref;
|
|
116
|
+
};
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/hooks/useDebounce.ts
|
|
119
|
+
/**
|
|
120
|
+
* Custom hook that debounces a callback function to delay its execution until after
|
|
121
|
+
* a specified wait time has elapsed since the last time it was invoked.
|
|
122
|
+
*
|
|
123
|
+
* Debouncing ensures that the callback is only executed once after a series of rapid calls,
|
|
124
|
+
* waiting for a pause in the calls before executing.
|
|
125
|
+
*
|
|
126
|
+
* @param callback - The function to debounce
|
|
127
|
+
* @param delay - The delay time (in milliseconds) to wait before executing the callback
|
|
128
|
+
* @param options - Optional configuration object
|
|
129
|
+
* @param options.maxWait - Maximum time before function must be invoked
|
|
130
|
+
* @param options.leading - If true, invoke on the leading edge
|
|
131
|
+
* @returns A debounced version of the callback function with a `cancel` method
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```tsx
|
|
135
|
+
* const debouncedSearch = useDebounce((query: string) => {
|
|
136
|
+
* console.log('Searching for:', query)
|
|
137
|
+
* }, 300)
|
|
138
|
+
*
|
|
139
|
+
* return <input onChange={(e) => debouncedSearch(e.target.value)} />
|
|
140
|
+
* ```
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```tsx
|
|
144
|
+
* // With maxWait and leading options
|
|
145
|
+
* const debouncedSearch = useDebounce(
|
|
146
|
+
* (query: string) => console.log('Searching for:', query),
|
|
147
|
+
* 300,
|
|
148
|
+
* { maxWait: 1000, leading: true }
|
|
149
|
+
* )
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```tsx
|
|
154
|
+
* // Cancel pending debounced call
|
|
155
|
+
* const debouncedSearch = useDebounce((query: string) => {
|
|
156
|
+
* console.log('Searching for:', query)
|
|
157
|
+
* }, 300)
|
|
158
|
+
*
|
|
159
|
+
* debouncedSearch('test')
|
|
160
|
+
* debouncedSearch.cancel() // Cancels the pending call
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
var useDebounce = (callback, delay, { maxWait, leading = false } = {}) => {
|
|
164
|
+
const timeoutRef = useRef(null);
|
|
165
|
+
const maxTimeoutMetadataRef = useRef(null);
|
|
166
|
+
const callbackRef = useLiveRef(callback);
|
|
167
|
+
const clearTimers = useCallback(() => {
|
|
168
|
+
if (timeoutRef.current) {
|
|
169
|
+
clearTimeout(timeoutRef.current);
|
|
170
|
+
timeoutRef.current = null;
|
|
171
|
+
}
|
|
172
|
+
if (maxTimeoutMetadataRef.current) {
|
|
173
|
+
clearTimeout(maxTimeoutMetadataRef.current.timeout);
|
|
174
|
+
maxTimeoutMetadataRef.current = null;
|
|
175
|
+
}
|
|
176
|
+
}, []);
|
|
177
|
+
const cancel = useCallback(() => {
|
|
178
|
+
clearTimers();
|
|
179
|
+
}, [clearTimers]);
|
|
180
|
+
const invokeCallback = useCallback((args) => {
|
|
181
|
+
clearTimers();
|
|
182
|
+
callbackRef.current(...args);
|
|
183
|
+
}, [clearTimers, callbackRef]);
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
return clearTimers;
|
|
186
|
+
}, [clearTimers]);
|
|
187
|
+
const debounced = useCallback((...args) => {
|
|
188
|
+
const isNewCycle = timeoutRef.current === null;
|
|
189
|
+
if (leading && isNewCycle) callbackRef.current(...args);
|
|
190
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
191
|
+
if (maxWait && !maxTimeoutMetadataRef.current) maxTimeoutMetadataRef.current = {
|
|
192
|
+
timeout: setTimeout(() => {
|
|
193
|
+
if (maxTimeoutMetadataRef.current) invokeCallback(maxTimeoutMetadataRef.current.args);
|
|
194
|
+
}, maxWait),
|
|
195
|
+
args
|
|
196
|
+
};
|
|
197
|
+
else if (maxTimeoutMetadataRef.current) maxTimeoutMetadataRef.current.args = args;
|
|
198
|
+
timeoutRef.current = setTimeout(() => {
|
|
199
|
+
invokeCallback(args);
|
|
200
|
+
}, delay);
|
|
201
|
+
}, [
|
|
202
|
+
delay,
|
|
203
|
+
maxWait,
|
|
204
|
+
leading,
|
|
205
|
+
invokeCallback,
|
|
206
|
+
callbackRef
|
|
207
|
+
]);
|
|
208
|
+
return Object.assign(debounced, { cancel });
|
|
209
|
+
};
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region src/hooks/useEffectEvent.ts
|
|
212
|
+
/**
|
|
213
|
+
* Universal layout effect that uses useLayoutEffect in browser environments
|
|
214
|
+
* and falls back to useEffect in SSR environments
|
|
215
|
+
*/
|
|
216
|
+
var useUniversalLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;
|
|
217
|
+
/**
|
|
218
|
+
* Use `toString()` to prevent bundlers from trying to `import { useInsertionEffect } from 'react';`
|
|
219
|
+
* @see https://github.com/radix-ui/primitives/blob/main/packages/react/id/src/id.tsx
|
|
220
|
+
*/
|
|
221
|
+
var useInsertionEffect = React["useInsertionEffect".toString()] || useUniversalLayoutEffect;
|
|
222
|
+
function useEffectEvent(callback) {
|
|
223
|
+
const ref = React.useRef(() => {
|
|
224
|
+
throw new Error("Cannot call an event handler while rendering.");
|
|
225
|
+
});
|
|
226
|
+
useInsertionEffect(() => {
|
|
227
|
+
ref.current = callback;
|
|
228
|
+
}, [callback]);
|
|
229
|
+
return React.useCallback((...args) => {
|
|
230
|
+
return ref.current?.(...args);
|
|
231
|
+
}, []);
|
|
232
|
+
}
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/hooks/useInterval.ts
|
|
235
|
+
var DEFAULT_DEPENDENCIES$1 = [];
|
|
236
|
+
/**
|
|
237
|
+
* Custom hook that manages an interval with automatic cleanup.
|
|
238
|
+
* Provides a reliable way to handle intervals that are properly cleaned up
|
|
239
|
+
* on component unmount or when dependencies change.
|
|
240
|
+
*
|
|
241
|
+
* @param callback - The function to execute on each interval
|
|
242
|
+
* @param delay - The delay in milliseconds (null to clear the interval)
|
|
243
|
+
* @param deps - Optional dependency array. If provided, interval restarts when deps change.
|
|
244
|
+
* If not provided, callback updates via ref without restarting interval.
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```tsx
|
|
248
|
+
* // Runs continuously, callback updates via ref
|
|
249
|
+
* useInterval(() => {
|
|
250
|
+
* console.log('This runs every second')
|
|
251
|
+
* }, 1000)
|
|
252
|
+
*
|
|
253
|
+
* // Restarts interval when count changes
|
|
254
|
+
* useInterval(() => {
|
|
255
|
+
* console.log('Count:', count)
|
|
256
|
+
* }, 1000, [count])
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
var useInterval = (callback, delay = null, deps = DEFAULT_DEPENDENCIES$1) => {
|
|
260
|
+
const intervalRef = useRef(null);
|
|
261
|
+
const callbackRef = useRef(callback);
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
callbackRef.current = callback;
|
|
264
|
+
}, [callback]);
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
if (delay === null) return;
|
|
267
|
+
intervalRef.current = setInterval(() => {
|
|
268
|
+
callbackRef.current();
|
|
269
|
+
}, delay);
|
|
270
|
+
return () => {
|
|
271
|
+
if (intervalRef.current) {
|
|
272
|
+
clearInterval(intervalRef.current);
|
|
273
|
+
intervalRef.current = null;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}, [delay, ...deps]);
|
|
277
|
+
};
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region src/hooks/useMatchMedia.ts
|
|
280
|
+
var hasMatchMedia = () => {
|
|
281
|
+
return typeof window?.matchMedia === "function";
|
|
282
|
+
};
|
|
283
|
+
var useMatchMedia = (query) => {
|
|
284
|
+
const [matches, setMatches] = useState(() => {
|
|
285
|
+
if (!hasMatchMedia()) return null;
|
|
286
|
+
try {
|
|
287
|
+
return window.matchMedia(query).matches;
|
|
288
|
+
} catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
const matchMedia = useMemo(() => {
|
|
293
|
+
if (!hasMatchMedia()) return null;
|
|
294
|
+
try {
|
|
295
|
+
return window.matchMedia(query);
|
|
296
|
+
} catch {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
}, [query]);
|
|
300
|
+
const handleChange = useCallback((event) => {
|
|
301
|
+
setMatches(event.matches);
|
|
302
|
+
}, []);
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
if (!matchMedia) return;
|
|
305
|
+
requestAnimationFrame(() => {
|
|
306
|
+
setMatches(matchMedia.matches);
|
|
307
|
+
});
|
|
308
|
+
if (matchMedia.addEventListener) {
|
|
309
|
+
matchMedia.addEventListener("change", handleChange);
|
|
310
|
+
return () => matchMedia.removeEventListener("change", handleChange);
|
|
311
|
+
}
|
|
312
|
+
const legacyHandler = (event) => {
|
|
313
|
+
handleChange(event);
|
|
314
|
+
};
|
|
315
|
+
matchMedia.addListener(legacyHandler);
|
|
316
|
+
return () => matchMedia.removeListener(legacyHandler);
|
|
317
|
+
}, [matchMedia, handleChange]);
|
|
318
|
+
return matches;
|
|
319
|
+
};
|
|
320
|
+
//#endregion
|
|
321
|
+
//#region src/hooks/useOklch.ts
|
|
322
|
+
var globalCounterRef = { current: 0 };
|
|
323
|
+
var useOklch = () => {
|
|
324
|
+
const hasSetP3Ref = useRef(false);
|
|
325
|
+
useEffect(() => {
|
|
326
|
+
if (typeof window === "undefined") return;
|
|
327
|
+
globalCounterRef.current += 1;
|
|
328
|
+
hasSetP3Ref.current = true;
|
|
329
|
+
document.body.dataset.p3 = "false";
|
|
330
|
+
return () => {
|
|
331
|
+
globalCounterRef.current -= 1;
|
|
332
|
+
if (globalCounterRef.current < 1 && hasSetP3Ref.current) {
|
|
333
|
+
document.body.dataset.p3 = void 0;
|
|
334
|
+
hasSetP3Ref.current = false;
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}, []);
|
|
338
|
+
};
|
|
339
|
+
//#endregion
|
|
340
|
+
//#region src/hooks/useOutsideClick.ts
|
|
341
|
+
/**
|
|
342
|
+
* Custom hook that detects clicks outside of a specified element and triggers a callback.
|
|
343
|
+
*
|
|
344
|
+
* Useful for implementing dropdown menus, modals, or any component that should close
|
|
345
|
+
* when the user clicks outside of it.
|
|
346
|
+
*
|
|
347
|
+
* @param ref - React ref object pointing to the element to monitor
|
|
348
|
+
* @param callback - Function to call when a click occurs outside the element
|
|
349
|
+
* @param isActive - Whether the outside click detection should be active (default: true)
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ```tsx
|
|
353
|
+
* const ref = useRef<HTMLDivElement>(null)
|
|
354
|
+
* const [isOpen, setIsOpen] = useState(false)
|
|
355
|
+
*
|
|
356
|
+
* useOutsideClick(ref, () => setIsOpen(false), isOpen)
|
|
357
|
+
*
|
|
358
|
+
* return <div ref={ref}>...</div>
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
var useOutsideClick = (ref, callback, isActive = true) => {
|
|
362
|
+
const liveCallbackRef = useLiveRef(callback);
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
if (!isActive) return;
|
|
365
|
+
const handleClick = (event) => {
|
|
366
|
+
if (ref.current && !ref.current.contains(event.target)) liveCallbackRef.current();
|
|
367
|
+
};
|
|
368
|
+
document.addEventListener("pointerdown", handleClick, true);
|
|
369
|
+
return () => {
|
|
370
|
+
document.removeEventListener("pointerdown", handleClick, true);
|
|
371
|
+
};
|
|
372
|
+
}, [
|
|
373
|
+
ref,
|
|
374
|
+
isActive,
|
|
375
|
+
liveCallbackRef
|
|
376
|
+
]);
|
|
377
|
+
};
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region src/hooks/useRaf.ts
|
|
380
|
+
/**
|
|
381
|
+
* Custom hook that provides a requestAnimationFrame-based callback execution.
|
|
382
|
+
* Useful for performance-critical animations and continuous updates that should
|
|
383
|
+
* be synchronized with the browser's refresh rate.
|
|
384
|
+
*
|
|
385
|
+
* The callback will be executed on the next animation frame, and can be
|
|
386
|
+
* controlled with an enabled flag for conditional execution.
|
|
387
|
+
*
|
|
388
|
+
* @param callback - The function to execute on each animation frame
|
|
389
|
+
* @param enabled - Whether the animation loop should be active (default: true)
|
|
390
|
+
* @returns A function to manually trigger the animation frame callback
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* ```tsx
|
|
394
|
+
* const animate = useRaf(() => {
|
|
395
|
+
* // Animation logic here
|
|
396
|
+
* console.log('Animation frame')
|
|
397
|
+
* }, isAnimating)
|
|
398
|
+
*
|
|
399
|
+
* // Manually trigger
|
|
400
|
+
* animate()
|
|
401
|
+
* ```
|
|
402
|
+
*/
|
|
403
|
+
var useRaf = (callback, enabled = true) => {
|
|
404
|
+
const rafIdRef = useRef(null);
|
|
405
|
+
const isRunningRef = useRef(false);
|
|
406
|
+
const enabledRef = useLiveRef(enabled);
|
|
407
|
+
const cleanup = useCallback(() => {
|
|
408
|
+
if (rafIdRef.current) {
|
|
409
|
+
cancelAnimationFrame(rafIdRef.current);
|
|
410
|
+
rafIdRef.current = null;
|
|
411
|
+
}
|
|
412
|
+
isRunningRef.current = false;
|
|
413
|
+
}, []);
|
|
414
|
+
const animate = useCallback(() => {
|
|
415
|
+
if (!enabledRef.current) return;
|
|
416
|
+
cleanup();
|
|
417
|
+
isRunningRef.current = true;
|
|
418
|
+
const loop = () => {
|
|
419
|
+
if (!enabledRef.current || !isRunningRef.current) {
|
|
420
|
+
isRunningRef.current = false;
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
callback();
|
|
424
|
+
rafIdRef.current = requestAnimationFrame(loop);
|
|
425
|
+
};
|
|
426
|
+
rafIdRef.current = requestAnimationFrame(loop);
|
|
427
|
+
}, [
|
|
428
|
+
callback,
|
|
429
|
+
cleanup,
|
|
430
|
+
enabledRef
|
|
431
|
+
]);
|
|
432
|
+
useEffect(() => {
|
|
433
|
+
if (enabled) animate();
|
|
434
|
+
else cleanup();
|
|
435
|
+
return cleanup;
|
|
436
|
+
}, [
|
|
437
|
+
enabled,
|
|
438
|
+
animate,
|
|
439
|
+
cleanup
|
|
440
|
+
]);
|
|
441
|
+
return animate;
|
|
442
|
+
};
|
|
443
|
+
//#endregion
|
|
444
|
+
//#region src/hooks/useThrottle.ts
|
|
445
|
+
/**
|
|
446
|
+
* Custom hook that throttles a callback function to limit how often it can be invoked.
|
|
447
|
+
*
|
|
448
|
+
* Throttling ensures that the callback is executed at most once per specified time interval,
|
|
449
|
+
* regardless of how many times the throttled function is called.
|
|
450
|
+
*
|
|
451
|
+
* @param callback - The function to throttle
|
|
452
|
+
* @param delay - The minimum time interval (in milliseconds) between executions
|
|
453
|
+
* @returns A throttled version of the callback function
|
|
454
|
+
*
|
|
455
|
+
* @example
|
|
456
|
+
* ```tsx
|
|
457
|
+
* const throttledClick = useThrottle(() => {
|
|
458
|
+
* console.log('Button clicked')
|
|
459
|
+
* }, 500)
|
|
460
|
+
*
|
|
461
|
+
* return <button onClick={throttledClick}>Click me</button>
|
|
462
|
+
* ```
|
|
463
|
+
*/
|
|
464
|
+
var useThrottle = (callback, delay) => {
|
|
465
|
+
const lastExecuted = useRef(0);
|
|
466
|
+
const timeoutRef = useRef(null);
|
|
467
|
+
useEffect(() => {
|
|
468
|
+
return () => {
|
|
469
|
+
if (timeoutRef.current) {
|
|
470
|
+
clearTimeout(timeoutRef.current);
|
|
471
|
+
timeoutRef.current = null;
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
}, [callback, delay]);
|
|
475
|
+
return useCallback((...args) => {
|
|
476
|
+
const now = Date.now();
|
|
477
|
+
if (now - lastExecuted.current >= delay) {
|
|
478
|
+
callback(...args);
|
|
479
|
+
lastExecuted.current = now;
|
|
480
|
+
} else {
|
|
481
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
482
|
+
timeoutRef.current = setTimeout(() => {
|
|
483
|
+
callback(...args);
|
|
484
|
+
lastExecuted.current = Date.now();
|
|
485
|
+
timeoutRef.current = null;
|
|
486
|
+
}, delay - (now - lastExecuted.current));
|
|
487
|
+
}
|
|
488
|
+
}, [callback, delay]);
|
|
489
|
+
};
|
|
490
|
+
//#endregion
|
|
491
|
+
//#region src/hooks/useTimeout.ts
|
|
492
|
+
var DEFAULT_DEPENDENCIES = [];
|
|
493
|
+
/**
|
|
494
|
+
* Custom hook that manages a timeout with automatic cleanup.
|
|
495
|
+
* Provides a reliable way to handle timeouts that are properly cleaned up
|
|
496
|
+
* on component unmount or when dependencies change.
|
|
497
|
+
*
|
|
498
|
+
* @param callback - The function to execute after the delay
|
|
499
|
+
* @param delay - The delay in milliseconds (null to clear the timeout)
|
|
500
|
+
* @param deps - Optional dependency array. If provided, timeout restarts when deps change.
|
|
501
|
+
* If not provided, callback updates via ref without restarting timeout.
|
|
502
|
+
*
|
|
503
|
+
* @example
|
|
504
|
+
* ```tsx
|
|
505
|
+
* // Runs once, callback updates via ref
|
|
506
|
+
* useTimeout(() => {
|
|
507
|
+
* console.log('This runs after 1 second')
|
|
508
|
+
* }, 1000)
|
|
509
|
+
*
|
|
510
|
+
* // Restarts timeout when count changes
|
|
511
|
+
* useTimeout(() => {
|
|
512
|
+
* console.log('Count:', count)
|
|
513
|
+
* }, 1000, [count])
|
|
514
|
+
* ```
|
|
515
|
+
*/
|
|
516
|
+
var useTimeout = (callback, delay = null, deps = DEFAULT_DEPENDENCIES) => {
|
|
517
|
+
const timeoutRef = useRef(null);
|
|
518
|
+
const callbackRef = useRef(callback);
|
|
519
|
+
useEffect(() => {
|
|
520
|
+
callbackRef.current = callback;
|
|
521
|
+
}, [callback]);
|
|
522
|
+
useEffect(() => {
|
|
523
|
+
if (delay === null) return;
|
|
524
|
+
timeoutRef.current = setTimeout(() => {
|
|
525
|
+
callbackRef.current();
|
|
526
|
+
}, delay);
|
|
527
|
+
return () => {
|
|
528
|
+
if (timeoutRef.current) {
|
|
529
|
+
clearTimeout(timeoutRef.current);
|
|
530
|
+
timeoutRef.current = null;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
}, [delay, ...deps]);
|
|
534
|
+
};
|
|
535
|
+
//#endregion
|
|
536
|
+
//#region src/hooks/useWindowReady.tsx
|
|
537
|
+
var useWindowReady = () => {
|
|
538
|
+
const [ready, setReady] = useState(false);
|
|
539
|
+
useEffect(() => {
|
|
540
|
+
const timer = setTimeout(() => setReady(true), 0);
|
|
541
|
+
return () => clearTimeout(timer);
|
|
542
|
+
}, []);
|
|
543
|
+
return ready;
|
|
544
|
+
};
|
|
545
|
+
//#endregion
|
|
546
|
+
//#region src/hooks/useReducedMotion.ts
|
|
547
|
+
var QUERY = "(prefers-reduced-motion: reduce)";
|
|
548
|
+
function useReducedMotion() {
|
|
549
|
+
const [reduced, setReduced] = useState(() => {
|
|
550
|
+
if (typeof window === "undefined") return false;
|
|
551
|
+
return window.matchMedia(QUERY).matches;
|
|
552
|
+
});
|
|
553
|
+
useEffect(() => {
|
|
554
|
+
const mql = window.matchMedia(QUERY);
|
|
555
|
+
const handler = (e) => setReduced(e.matches);
|
|
556
|
+
mql.addEventListener("change", handler);
|
|
557
|
+
return () => mql.removeEventListener("change", handler);
|
|
558
|
+
}, []);
|
|
559
|
+
return reduced;
|
|
560
|
+
}
|
|
561
|
+
//#endregion
|
|
562
|
+
//#region src/hooks/useGsapContext.ts
|
|
563
|
+
/**
|
|
564
|
+
* Creates a GSAP context scoped to the given ref.
|
|
565
|
+
* All animations created inside the callback are automatically
|
|
566
|
+
* reverted on unmount — preventing DOM leak bugs.
|
|
567
|
+
*/
|
|
568
|
+
function useGsapContext(scope, callback, deps = []) {
|
|
569
|
+
const ctxRef = useRef(null);
|
|
570
|
+
useEffect(() => {
|
|
571
|
+
if (!scope.current) return;
|
|
572
|
+
const ctx = gsap.context((context) => {
|
|
573
|
+
const typedContext = context;
|
|
574
|
+
ctxRef.current = typedContext;
|
|
575
|
+
const cleanup = callback(typedContext);
|
|
576
|
+
if (typeof cleanup === "function") typedContext.add(cleanup);
|
|
577
|
+
}, scope.current);
|
|
578
|
+
ctxRef.current = ctx;
|
|
579
|
+
return () => {
|
|
580
|
+
ctx.revert();
|
|
581
|
+
ctxRef.current = null;
|
|
582
|
+
};
|
|
583
|
+
}, [scope, ...deps]);
|
|
584
|
+
return ctxRef;
|
|
585
|
+
}
|
|
586
|
+
//#endregion
|
|
587
|
+
//#region src/hooks/useFormField.ts
|
|
588
|
+
function useFormField(options = {}) {
|
|
589
|
+
const { id: externalId, hasError = false, hasHint = false } = options;
|
|
590
|
+
const autoId = useId();
|
|
591
|
+
const fieldId = externalId ?? autoId;
|
|
592
|
+
const ids = useMemo(() => ({
|
|
593
|
+
fieldId,
|
|
594
|
+
labelId: `${fieldId}-label`,
|
|
595
|
+
errorId: `${fieldId}-error`,
|
|
596
|
+
hintId: `${fieldId}-hint`,
|
|
597
|
+
descriptionId: `${fieldId}-description`
|
|
598
|
+
}), [fieldId]);
|
|
599
|
+
const describedBy = useMemo(() => {
|
|
600
|
+
const parts = [];
|
|
601
|
+
if (hasError) parts.push(ids.errorId);
|
|
602
|
+
if (hasHint && !hasError) parts.push(ids.hintId);
|
|
603
|
+
return parts.length > 0 ? parts.join(" ") : void 0;
|
|
604
|
+
}, [
|
|
605
|
+
hasError,
|
|
606
|
+
hasHint,
|
|
607
|
+
ids.errorId,
|
|
608
|
+
ids.hintId
|
|
609
|
+
]);
|
|
610
|
+
return useMemo(() => ({
|
|
611
|
+
ids,
|
|
612
|
+
labelProps: {
|
|
613
|
+
htmlFor: fieldId,
|
|
614
|
+
id: ids.labelId
|
|
615
|
+
},
|
|
616
|
+
fieldProps: {
|
|
617
|
+
id: fieldId,
|
|
618
|
+
"aria-describedby": describedBy,
|
|
619
|
+
"aria-invalid": hasError || void 0,
|
|
620
|
+
"aria-errormessage": hasError ? ids.errorId : void 0
|
|
621
|
+
},
|
|
622
|
+
errorProps: {
|
|
623
|
+
id: ids.errorId,
|
|
624
|
+
role: "alert"
|
|
625
|
+
},
|
|
626
|
+
hintProps: { id: ids.hintId }
|
|
627
|
+
}), [
|
|
628
|
+
ids,
|
|
629
|
+
fieldId,
|
|
630
|
+
describedBy,
|
|
631
|
+
hasError
|
|
632
|
+
]);
|
|
633
|
+
}
|
|
634
|
+
//#endregion
|
|
635
|
+
export { mergeRefs, useAnimatedText, useBowser, useControllableState, useDebounce, useEffectEvent, useFormField, useGsapContext, useInterval, useLiveRef, useMatchMedia, useMergeRefs, useOklch, useOutsideClick, usePreviousRender, usePreviousState, useRaf, useReducedMotion, useThrottle, useTimeout, useUniversalLayoutEffect, useWindowReady };
|