@vitus-labs/hooks 2.0.0-alpha.25 → 2.0.0-alpha.27

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @vitus-labs/hooks
2
2
 
3
- Lightweight React hooks for common UI interactions.
3
+ 28 React hooks for UI interactions, state management, DOM observation, accessibility, and theming. 2.15KB gzipped.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@vitus-labs/hooks)](https://www.npmjs.com/package/@vitus-labs/hooks)
6
6
  [![license](https://img.shields.io/npm/l/@vitus-labs/hooks)](https://github.com/vitus-labs/ui-system/blob/main/LICENSE)
@@ -13,63 +13,288 @@ npm install @vitus-labs/hooks
13
13
 
14
14
  ## Hooks
15
15
 
16
- ### useHover
16
+ ### Primitives
17
17
 
18
- Tracks hover state with stable callback references.
18
+ #### useLatest
19
19
 
20
- ```tsx
21
- import { useHover } from '@vitus-labs/hooks'
20
+ Returns a ref that always holds the latest value. Avoids stale closures in callbacks and effects.
21
+
22
+ ```ts
23
+ const ref = useLatest(callback)
24
+ // ref.current is always the latest callback
25
+ ```
22
26
 
23
- const MyComponent = () => {
24
- const { hover, onMouseEnter, onMouseLeave } = useHover()
27
+ #### useToggle
25
28
 
26
- return (
27
- <div
28
- onMouseEnter={onMouseEnter}
29
- onMouseLeave={onMouseLeave}
30
- style={{ background: hover ? '#f0f0f0' : '#fff' }}
31
- >
32
- {hover ? 'Hovered' : 'Hover me'}
33
- </div>
34
- )
35
- }
29
+ Boolean state with `toggle`, `setTrue`, and `setFalse` helpers.
30
+
31
+ ```ts
32
+ const [isOpen, toggle, open, close] = useToggle(false)
36
33
  ```
37
34
 
38
- **Parameters:**
35
+ #### usePrevious
39
36
 
40
- | Param | Type | Default | Description |
41
- | ----- | ---- | ------- | ----------- |
42
- | initialValue | `boolean` | `false` | Initial hover state |
37
+ Returns the value from the previous render.
43
38
 
44
- **Returns:** `{ hover: boolean, onMouseEnter: () => void, onMouseLeave: () => void }`
39
+ ```ts
40
+ const prev = usePrevious(count)
41
+ // undefined on first render, then the previous value
42
+ ```
45
43
 
46
- ### useWindowResize
44
+ ### Callbacks
47
45
 
48
- Tracks viewport dimensions with throttled updates.
46
+ #### useDebouncedCallback
47
+
48
+ Stable debounced function with `.cancel()` and `.flush()`.
49
+
50
+ ```ts
51
+ const search = useDebouncedCallback((query: string) => {
52
+ fetchResults(query)
53
+ }, 300)
54
+
55
+ search('hello') // fires after 300ms of inactivity
56
+ search.cancel() // cancel pending
57
+ search.flush() // fire immediately
58
+ ```
59
+
60
+ #### useThrottledCallback
61
+
62
+ Stable throttled function with `.cancel()`. Uses `throttle` from `@vitus-labs/core`.
63
+
64
+ ```ts
65
+ const handleScroll = useThrottledCallback(() => {
66
+ updatePosition()
67
+ }, 100)
68
+ ```
69
+
70
+ ### State
71
+
72
+ #### useDebouncedValue
73
+
74
+ Returns a debounced version of the value that only updates after `delay` ms of inactivity.
75
+
76
+ ```ts
77
+ const [search, setSearch] = useState('')
78
+ const debouncedSearch = useDebouncedValue(search, 300)
79
+ ```
80
+
81
+ #### useControllableState
82
+
83
+ Unified controlled/uncontrolled state pattern.
84
+
85
+ ```ts
86
+ const [value, setValue] = useControllableState({
87
+ value: props.value, // controlled (optional)
88
+ defaultValue: '', // uncontrolled fallback
89
+ onChange: props.onChange, // fires in both modes
90
+ })
91
+ ```
92
+
93
+ ### Effects
94
+
95
+ #### useUpdateEffect
96
+
97
+ Like `useEffect` but skips the initial mount.
98
+
99
+ ```ts
100
+ useUpdateEffect(() => {
101
+ // only fires on updates, not on mount
102
+ saveToStorage(value)
103
+ }, [value])
104
+ ```
105
+
106
+ #### useIsomorphicLayoutEffect
107
+
108
+ `useLayoutEffect` on the client, `useEffect` on the server. Avoids SSR warnings.
109
+
110
+ ```ts
111
+ useIsomorphicLayoutEffect(() => {
112
+ measureElement()
113
+ }, [])
114
+ ```
115
+
116
+ #### useInterval
117
+
118
+ Declarative `setInterval` with auto-cleanup. Pass `null` to pause.
119
+
120
+ ```ts
121
+ useInterval(() => tick(), 1000) // every second
122
+ useInterval(() => tick(), null) // paused
123
+ ```
124
+
125
+ #### useTimeout
126
+
127
+ Declarative `setTimeout` with `reset` and `clear` controls.
128
+
129
+ ```ts
130
+ const { reset, clear } = useTimeout(() => {
131
+ showNotification()
132
+ }, 5000)
133
+ ```
134
+
135
+ ### DOM & Observers
136
+
137
+ #### useElementSize
138
+
139
+ Tracks element `width` and `height` via `ResizeObserver`.
140
+
141
+ ```tsx
142
+ const [ref, { width, height }] = useElementSize()
143
+ return <div ref={ref}>Size: {width}x{height}</div>
144
+ ```
145
+
146
+ #### useIntersection
147
+
148
+ `IntersectionObserver` wrapper for visibility detection.
49
149
 
50
150
  ```tsx
51
- import { useWindowResize } from '@vitus-labs/hooks'
151
+ const [ref, entry] = useIntersection({ threshold: 0.5 })
152
+ const isVisible = entry?.isIntersecting
153
+ return <div ref={ref}>{isVisible ? 'Visible' : 'Hidden'}</div>
154
+ ```
155
+
156
+ ### Interaction
157
+
158
+ #### useHover
159
+
160
+ Tracks hover state with stable callback references.
161
+
162
+ ```ts
163
+ const { hover, onMouseEnter, onMouseLeave } = useHover()
164
+ ```
52
165
 
53
- const Layout = () => {
54
- const { width, height } = useWindowResize({
55
- throttleDelay: 300,
56
- onChange: ({ width }) => console.log('Width:', width),
57
- })
166
+ #### useFocus
58
167
 
59
- return <div>Viewport: {width} x {height}</div>
60
- }
168
+ Tracks focus state with stable callback references.
169
+
170
+ ```ts
171
+ const { focused, onFocus, onBlur } = useFocus()
172
+ ```
173
+
174
+ #### useClickOutside
175
+
176
+ Calls handler when a click occurs outside the referenced element.
177
+
178
+ ```ts
179
+ const ref = useRef<HTMLDivElement>(null)
180
+ useClickOutside(ref, () => setOpen(false))
181
+ ```
182
+
183
+ #### useScrollLock
184
+
185
+ Locks page scroll by setting `overflow: hidden` on `document.body`.
186
+
187
+ ```ts
188
+ useScrollLock(isModalOpen)
189
+ ```
190
+
191
+ #### useKeyboard
192
+
193
+ Listens for a specific keyboard key.
194
+
195
+ ```ts
196
+ useKeyboard('Escape', () => setOpen(false))
197
+ ```
198
+
199
+ #### useFocusTrap
200
+
201
+ Traps Tab/Shift+Tab focus within a container. Essential for modals and dialogs.
202
+
203
+ ```ts
204
+ const ref = useRef<HTMLDivElement>(null)
205
+ useFocusTrap(ref, isOpen)
206
+ ```
207
+
208
+ ### Responsive
209
+
210
+ #### useMediaQuery
211
+
212
+ Subscribes to a CSS media query and returns whether it matches.
213
+
214
+ ```ts
215
+ const isDesktop = useMediaQuery('(min-width: 1024px)')
216
+ ```
217
+
218
+ #### useBreakpoint
219
+
220
+ Returns the currently active breakpoint name from the theme context.
221
+
222
+ ```ts
223
+ const bp = useBreakpoint() // "xs" | "sm" | "md" | "lg" | "xl" | undefined
224
+ ```
225
+
226
+ #### useColorScheme
227
+
228
+ Returns the user's preferred color scheme. Pairs with rocketstyle's `mode`.
229
+
230
+ ```ts
231
+ const scheme = useColorScheme() // "light" | "dark"
232
+ ```
233
+
234
+ #### useReducedMotion
235
+
236
+ Returns `true` when the user prefers reduced motion.
237
+
238
+ ```ts
239
+ const reduced = useReducedMotion()
240
+ const duration = reduced ? 0 : 300
241
+ ```
242
+
243
+ ### Theme & Styling
244
+
245
+ #### useThemeValue
246
+
247
+ Deep-reads a value from the current theme by dot-separated path.
248
+
249
+ ```ts
250
+ const primary = useThemeValue<string>('colors.primary')
251
+ const columns = useThemeValue<number>('grid.columns')
252
+ ```
253
+
254
+ #### useRootSize
255
+
256
+ Returns `rootSize` from the theme with px/rem conversion utilities.
257
+
258
+ ```ts
259
+ const { rootSize, pxToRem, remToPx } = useRootSize()
260
+ pxToRem(32) // "2rem"
261
+ remToPx(2) // 32
262
+ ```
263
+
264
+ #### useSpacing
265
+
266
+ Returns a spacing function based on the theme's root size.
267
+
268
+ ```ts
269
+ const spacing = useSpacing()
270
+ spacing(1) // "8px"
271
+ spacing(2) // "16px"
272
+ spacing(0.5) // "4px"
273
+ ```
274
+
275
+ ### Composition
276
+
277
+ #### useMergedRef
278
+
279
+ Merges multiple refs (callback or object) into a single stable callback ref.
280
+
281
+ ```tsx
282
+ const Component = forwardRef((props, ref) => {
283
+ const localRef = useRef(null)
284
+ const merged = useMergedRef(ref, localRef)
285
+ return <div ref={merged} />
286
+ })
61
287
  ```
62
288
 
63
- **Parameters:**
289
+ ### Viewport
64
290
 
65
- | Param | Type | Default | Description |
66
- | ----- | ---- | ------- | ----------- |
67
- | params.throttleDelay | `number` | `200` | Milliseconds between resize handler calls |
68
- | params.onChange | `(sizes) => void` | — | Callback fired on each resize |
69
- | initialValues.width | `number` | `0` | Initial width (useful for SSR) |
70
- | initialValues.height | `number` | `0` | Initial height (useful for SSR) |
291
+ #### useWindowResize
71
292
 
72
- **Returns:** `{ width: number, height: number }`
293
+ Tracks viewport dimensions with throttled updates.
294
+
295
+ ```ts
296
+ const { width, height } = useWindowResize({ throttleDelay: 300 })
297
+ ```
73
298
 
74
299
  ## Peer Dependencies
75
300
 
package/lib/index.d.ts CHANGED
@@ -1,3 +1,116 @@
1
+ import { DependencyList, EffectCallback, Ref, useLayoutEffect } from "react";
2
+
3
+ //#region src/useBreakpoint.d.ts
4
+ type UseBreakpoint = () => string | undefined;
5
+ /**
6
+ * Returns the name of the currently active breakpoint from the
7
+ * unistyle/core theme context (e.g. `"xs"`, `"md"`, `"lg"`).
8
+ *
9
+ * Reads `theme.breakpoints` from the nearest `Provider` and
10
+ * subscribes to viewport changes via `matchMedia`.
11
+ *
12
+ * Returns `undefined` when no Provider or breakpoints are available.
13
+ */
14
+ declare const useBreakpoint: UseBreakpoint;
15
+ //#endregion
16
+ //#region src/useClickOutside.d.ts
17
+ type UseClickOutside = (ref: {
18
+ current: Element | null;
19
+ }, handler: (event: Event) => void) => void;
20
+ /**
21
+ * Calls `handler` when a click (mousedown or touchstart) occurs
22
+ * outside the element referenced by `ref`.
23
+ */
24
+ declare const useClickOutside: UseClickOutside;
25
+ //#endregion
26
+ //#region src/useColorScheme.d.ts
27
+ type UseColorScheme = () => 'light' | 'dark';
28
+ /**
29
+ * Returns the user's preferred color scheme (`"light"` or `"dark"`).
30
+ * Reacts to OS-level preference changes in real time.
31
+ * Pairs with rocketstyle's `mode` system.
32
+ */
33
+ declare const useColorScheme: UseColorScheme;
34
+ //#endregion
35
+ //#region src/useControllableState.d.ts
36
+ type UseControllableStateOptions<T> = {
37
+ value?: T;
38
+ defaultValue: T;
39
+ onChange?: (value: T) => void;
40
+ };
41
+ type UseControllableState = <T>(options: UseControllableStateOptions<T>) => [T, (next: T | ((prev: T) => T)) => void];
42
+ /**
43
+ * Unified controlled/uncontrolled state pattern.
44
+ * When `value` is provided the component is controlled; otherwise
45
+ * internal state is used with `defaultValue` as the initial value.
46
+ * The `onChange` callback fires in both modes.
47
+ */
48
+ declare const useControllableState: UseControllableState;
49
+ //#endregion
50
+ //#region src/useDebouncedCallback.d.ts
51
+ type DebouncedFn<T extends (...args: any[]) => any> = {
52
+ (...args: Parameters<T>): void;
53
+ cancel: () => void;
54
+ flush: () => void;
55
+ };
56
+ type UseDebouncedCallback = <T extends (...args: any[]) => any>(callback: T, delay: number) => DebouncedFn<T>;
57
+ /**
58
+ * Returns a stable debounced version of the callback.
59
+ * The returned function has `.cancel()` and `.flush()` methods.
60
+ * Always calls the latest callback (no stale closures).
61
+ * Cleans up on unmount.
62
+ */
63
+ declare const useDebouncedCallback: UseDebouncedCallback;
64
+ //#endregion
65
+ //#region src/useDebouncedValue.d.ts
66
+ type UseDebouncedValue = <T>(value: T, delay: number) => T;
67
+ /**
68
+ * Returns a debounced version of the value that only updates
69
+ * after `delay` ms of inactivity.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * const [search, setSearch] = useState('')
74
+ * const debouncedSearch = useDebouncedValue(search, 300)
75
+ * ```
76
+ */
77
+ declare const useDebouncedValue: UseDebouncedValue;
78
+ //#endregion
79
+ //#region src/useElementSize.d.ts
80
+ type Size = {
81
+ width: number;
82
+ height: number;
83
+ };
84
+ type UseElementSize = () => [(node: Element | null) => void, Size];
85
+ /**
86
+ * Tracks an element's `width` and `height` via `ResizeObserver`.
87
+ * Returns `[ref, { width, height }]` — pass `ref` as a callback ref.
88
+ */
89
+ declare const useElementSize: UseElementSize;
90
+ //#endregion
91
+ //#region src/useFocus.d.ts
92
+ type UseFocus = (initialValue?: boolean) => {
93
+ focused: boolean;
94
+ onFocus: () => void;
95
+ onBlur: () => void;
96
+ };
97
+ /**
98
+ * Simple focus-state hook that returns a boolean plus stable
99
+ * `onFocus`/`onBlur` handlers ready to spread onto an element.
100
+ */
101
+ declare const useFocus: UseFocus;
102
+ //#endregion
103
+ //#region src/useFocusTrap.d.ts
104
+ type UseFocusTrap = (ref: {
105
+ current: HTMLElement | null;
106
+ }, enabled?: boolean) => void;
107
+ /**
108
+ * Traps keyboard focus within the referenced container.
109
+ * Tab and Shift+Tab cycle through focusable elements inside.
110
+ * Useful for modals, dialogs, and dropdown menus.
111
+ */
112
+ declare const useFocusTrap: UseFocusTrap;
113
+ //#endregion
1
114
  //#region src/useHover.d.ts
2
115
  type UseHover = (initialValue?: boolean) => {
3
116
  hover: boolean;
@@ -10,6 +123,187 @@ type UseHover = (initialValue?: boolean) => {
10
123
  */
11
124
  declare const useHover: UseHover;
12
125
  //#endregion
126
+ //#region src/useIntersection.d.ts
127
+ type UseIntersectionOptions = {
128
+ threshold?: number | number[];
129
+ rootMargin?: string;
130
+ root?: Element | null;
131
+ };
132
+ type UseIntersection = (options?: UseIntersectionOptions) => [(node: Element | null) => void, IntersectionObserverEntry | null];
133
+ /**
134
+ * Observes an element's intersection with the viewport (or a root element).
135
+ * Returns `[ref, entry]` — pass `ref` as a callback ref.
136
+ */
137
+ declare const useIntersection: UseIntersection;
138
+ //#endregion
139
+ //#region src/useInterval.d.ts
140
+ type UseInterval = (callback: () => void, delay: number | null) => void;
141
+ /**
142
+ * Declarative `setInterval` with auto-cleanup.
143
+ * Pass `null` as `delay` to pause the interval.
144
+ * Always calls the latest callback (no stale closures).
145
+ */
146
+ declare const useInterval: UseInterval;
147
+ //#endregion
148
+ //#region src/useIsomorphicLayoutEffect.d.ts
149
+ /**
150
+ * `useLayoutEffect` on the client, `useEffect` on the server.
151
+ * Avoids the React SSR warning about useLayoutEffect.
152
+ */
153
+ declare const useIsomorphicLayoutEffect: typeof useLayoutEffect;
154
+ type UseIsomorphicLayoutEffect = typeof useIsomorphicLayoutEffect;
155
+ //#endregion
156
+ //#region src/useKeyboard.d.ts
157
+ type UseKeyboard = (key: string, handler: (event: KeyboardEvent) => void) => void;
158
+ /**
159
+ * Listens for a specific keyboard key and calls the handler.
160
+ * Matches `event.key` (e.g. `"Escape"`, `"Enter"`, `"a"`).
161
+ * Always calls the latest handler (no stale closures).
162
+ *
163
+ * @example
164
+ * ```ts
165
+ * useKeyboard('Escape', () => setOpen(false))
166
+ * ```
167
+ */
168
+ declare const useKeyboard: UseKeyboard;
169
+ //#endregion
170
+ //#region src/useLatest.d.ts
171
+ type UseLatest = <T>(value: T) => {
172
+ readonly current: T;
173
+ };
174
+ /**
175
+ * Returns a ref that always holds the latest value.
176
+ * Useful to avoid stale closures in callbacks and effects.
177
+ */
178
+ declare const useLatest: UseLatest;
179
+ //#endregion
180
+ //#region src/useMediaQuery.d.ts
181
+ type UseMediaQuery = (query: string) => boolean;
182
+ /**
183
+ * Subscribes to a CSS media query and returns whether it currently matches.
184
+ * Uses `window.matchMedia` with an event listener for live updates.
185
+ */
186
+ declare const useMediaQuery: UseMediaQuery;
187
+ //#endregion
188
+ //#region src/useMergedRef.d.ts
189
+ type UseMergedRef = <T>(...refs: (Ref<T> | undefined)[]) => (node: T | null) => void;
190
+ /**
191
+ * Merges multiple refs (callback or object) into a single stable callback ref.
192
+ * Handles null, callback refs, and object refs with `.current`.
193
+ */
194
+ declare const useMergedRef: <T>(...refs: (Ref<T> | undefined)[]) => (node: T | null) => void;
195
+ //#endregion
196
+ //#region src/usePrevious.d.ts
197
+ type UsePrevious = <T>(value: T) => T | undefined;
198
+ /**
199
+ * Returns the value from the previous render.
200
+ * Returns `undefined` on the first render.
201
+ */
202
+ declare const usePrevious: UsePrevious;
203
+ //#endregion
204
+ //#region src/useReducedMotion.d.ts
205
+ type UseReducedMotion = () => boolean;
206
+ /**
207
+ * Returns `true` when the user prefers reduced motion.
208
+ * Use to disable or simplify animations for accessibility.
209
+ */
210
+ declare const useReducedMotion: UseReducedMotion;
211
+ //#endregion
212
+ //#region src/useRootSize.d.ts
213
+ type RootSizeResult = {
214
+ rootSize: number;
215
+ pxToRem: (px: number) => string;
216
+ remToPx: (rem: number) => number;
217
+ };
218
+ type UseRootSize = () => RootSizeResult;
219
+ /**
220
+ * Returns `rootSize` from the theme context along with
221
+ * `pxToRem` and `remToPx` conversion utilities.
222
+ *
223
+ * Defaults to `16` when no Provider is mounted.
224
+ */
225
+ declare const useRootSize: UseRootSize;
226
+ //#endregion
227
+ //#region src/useScrollLock.d.ts
228
+ type UseScrollLock = (enabled: boolean) => void;
229
+ /**
230
+ * Locks page scroll by setting `overflow: hidden` on `document.body`.
231
+ * Restores the original overflow value on disable or unmount.
232
+ */
233
+ declare const useScrollLock: UseScrollLock;
234
+ //#endregion
235
+ //#region src/useSpacing.d.ts
236
+ type UseSpacing = (base?: number) => (multiplier: number) => string;
237
+ /**
238
+ * Returns a `spacing(n)` function that computes spacing values
239
+ * based on `rootSize` from the theme.
240
+ *
241
+ * @param base - Base spacing unit in px (defaults to `rootSize / 2`, i.e. 8px)
242
+ *
243
+ * @example
244
+ * ```ts
245
+ * const spacing = useSpacing()
246
+ * spacing(1) // "8px"
247
+ * spacing(2) // "16px"
248
+ * spacing(0.5) // "4px"
249
+ * ```
250
+ */
251
+ declare const useSpacing: UseSpacing;
252
+ //#endregion
253
+ //#region src/useThemeValue.d.ts
254
+ type UseThemeValue = <T = unknown>(path: string) => T | undefined;
255
+ /**
256
+ * Deep-reads a value from the current theme by dot-separated path.
257
+ *
258
+ * @example
259
+ * ```ts
260
+ * const primary = useThemeValue<string>('colors.primary')
261
+ * const columns = useThemeValue<number>('grid.columns')
262
+ * ```
263
+ */
264
+ declare const useThemeValue: UseThemeValue;
265
+ //#endregion
266
+ //#region src/useThrottledCallback.d.ts
267
+ type ThrottledFn<T extends (...args: any[]) => any> = {
268
+ (...args: Parameters<T>): void;
269
+ cancel: () => void;
270
+ };
271
+ type UseThrottledCallback = <T extends (...args: any[]) => any>(callback: T, delay: number) => ThrottledFn<T>;
272
+ /**
273
+ * Returns a stable throttled version of the callback.
274
+ * Uses `throttle` from `@vitus-labs/core`.
275
+ * Always calls the latest callback (no stale closures).
276
+ * Cleans up on unmount.
277
+ */
278
+ declare const useThrottledCallback: UseThrottledCallback;
279
+ //#endregion
280
+ //#region src/useTimeout.d.ts
281
+ type UseTimeout = (callback: () => void, delay: number | null) => {
282
+ reset: () => void;
283
+ clear: () => void;
284
+ };
285
+ /**
286
+ * Declarative `setTimeout` with auto-cleanup.
287
+ * Pass `null` as `delay` to disable. Returns `reset` and `clear` controls.
288
+ * Always calls the latest callback (no stale closures).
289
+ */
290
+ declare const useTimeout: UseTimeout;
291
+ //#endregion
292
+ //#region src/useToggle.d.ts
293
+ type UseToggle = (initialValue?: boolean) => [boolean, () => void, () => void, () => void];
294
+ /**
295
+ * Boolean state with `toggle`, `setTrue`, and `setFalse` helpers.
296
+ * Returns `[value, toggle, setTrue, setFalse]`.
297
+ */
298
+ declare const useToggle: UseToggle;
299
+ //#endregion
300
+ //#region src/useUpdateEffect.d.ts
301
+ type UseUpdateEffect = (effect: EffectCallback, deps?: DependencyList) => void;
302
+ /**
303
+ * Like `useEffect` but skips the initial mount — only fires on updates.
304
+ */
305
+ declare const useUpdateEffect: UseUpdateEffect;
306
+ //#endregion
13
307
  //#region src/useWindowResize.d.ts
14
308
  type Sizes = {
15
309
  width: number;
@@ -27,5 +321,5 @@ type UseWindowResize = (params?: Partial<{
27
321
  */
28
322
  declare const useWindowResize: UseWindowResize;
29
323
  //#endregion
30
- export { type UseHover, type UseWindowResize, useHover, useWindowResize };
324
+ export { type UseBreakpoint, type UseClickOutside, type UseColorScheme, type UseControllableState, type UseDebouncedCallback, type UseDebouncedValue, type UseElementSize, type UseFocus, type UseFocusTrap, type UseHover, type UseIntersection, type UseInterval, type UseIsomorphicLayoutEffect, type UseKeyboard, type UseLatest, type UseMediaQuery, type UseMergedRef, type UsePrevious, type UseReducedMotion, type UseRootSize, type UseScrollLock, type UseSpacing, type UseThemeValue, type UseThrottledCallback, type UseTimeout, type UseToggle, type UseUpdateEffect, type UseWindowResize, useBreakpoint, useClickOutside, useColorScheme, useControllableState, useDebouncedCallback, useDebouncedValue, useElementSize, useFocus, useFocusTrap, useHover, useIntersection, useInterval, useIsomorphicLayoutEffect, useKeyboard, useLatest, useMediaQuery, useMergedRef, usePrevious, useReducedMotion, useRootSize, useScrollLock, useSpacing, useThemeValue, useThrottledCallback, useTimeout, useToggle, useUpdateEffect, useWindowResize };
31
325
  //# sourceMappingURL=index2.d.ts.map
package/lib/index.js CHANGED
@@ -1,6 +1,279 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
2
- import { throttle } from "@vitus-labs/core";
1
+ import { context, get, throttle } from "@vitus-labs/core";
2
+ import { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
3
 
4
+ //#region src/useBreakpoint.ts
5
+ /**
6
+ * Returns the name of the currently active breakpoint from the
7
+ * unistyle/core theme context (e.g. `"xs"`, `"md"`, `"lg"`).
8
+ *
9
+ * Reads `theme.breakpoints` from the nearest `Provider` and
10
+ * subscribes to viewport changes via `matchMedia`.
11
+ *
12
+ * Returns `undefined` when no Provider or breakpoints are available.
13
+ */
14
+ const useBreakpoint = () => {
15
+ const breakpoints = useContext(context)?.theme?.breakpoints;
16
+ const sorted = useMemo(() => {
17
+ if (!breakpoints) return [];
18
+ return Object.entries(breakpoints).sort(([, a], [, b]) => a - b);
19
+ }, [breakpoints]);
20
+ const [current, setCurrent] = useState(() => {
21
+ if (sorted.length === 0) return void 0;
22
+ if (typeof window === "undefined") return sorted[0]?.[0];
23
+ const width = window.innerWidth;
24
+ let match = sorted[0]?.[0];
25
+ for (const [name, min] of sorted) if (width >= min) match = name;
26
+ return match;
27
+ });
28
+ useEffect(() => {
29
+ if (sorted.length === 0) return void 0;
30
+ const mqls = [];
31
+ const update = () => {
32
+ const width = window.innerWidth;
33
+ let match = sorted[0]?.[0];
34
+ for (const [name, min] of sorted) if (width >= min) match = name;
35
+ setCurrent(match);
36
+ };
37
+ for (const [, min] of sorted) {
38
+ const mql = window.matchMedia(`(min-width: ${min}px)`);
39
+ const handler = () => update();
40
+ mql.addEventListener("change", handler);
41
+ mqls.push({
42
+ mql,
43
+ handler
44
+ });
45
+ }
46
+ return () => {
47
+ for (const { mql, handler } of mqls) mql.removeEventListener("change", handler);
48
+ };
49
+ }, [sorted]);
50
+ return current;
51
+ };
52
+
53
+ //#endregion
54
+ //#region src/useClickOutside.ts
55
+ /**
56
+ * Calls `handler` when a click (mousedown or touchstart) occurs
57
+ * outside the element referenced by `ref`.
58
+ */
59
+ const useClickOutside = (ref, handler) => {
60
+ const handlerRef = useRef(handler);
61
+ handlerRef.current = handler;
62
+ useEffect(() => {
63
+ const listener = (event) => {
64
+ const el = ref.current;
65
+ if (!el || el.contains(event.target)) return;
66
+ handlerRef.current(event);
67
+ };
68
+ document.addEventListener("mousedown", listener);
69
+ document.addEventListener("touchstart", listener);
70
+ return () => {
71
+ document.removeEventListener("mousedown", listener);
72
+ document.removeEventListener("touchstart", listener);
73
+ };
74
+ }, [ref]);
75
+ };
76
+
77
+ //#endregion
78
+ //#region src/useMediaQuery.ts
79
+ /**
80
+ * Subscribes to a CSS media query and returns whether it currently matches.
81
+ * Uses `window.matchMedia` with an event listener for live updates.
82
+ */
83
+ const useMediaQuery = (query) => {
84
+ const [matches, setMatches] = useState(useCallback(() => typeof window !== "undefined" ? window.matchMedia(query).matches : false, [query]));
85
+ useEffect(() => {
86
+ const mql = window.matchMedia(query);
87
+ setMatches(mql.matches);
88
+ const handler = (e) => setMatches(e.matches);
89
+ mql.addEventListener("change", handler);
90
+ return () => mql.removeEventListener("change", handler);
91
+ }, [query]);
92
+ return matches;
93
+ };
94
+
95
+ //#endregion
96
+ //#region src/useColorScheme.ts
97
+ /**
98
+ * Returns the user's preferred color scheme (`"light"` or `"dark"`).
99
+ * Reacts to OS-level preference changes in real time.
100
+ * Pairs with rocketstyle's `mode` system.
101
+ */
102
+ const useColorScheme = () => {
103
+ return useMediaQuery("(prefers-color-scheme: dark)") ? "dark" : "light";
104
+ };
105
+
106
+ //#endregion
107
+ //#region src/useControllableState.ts
108
+ /**
109
+ * Unified controlled/uncontrolled state pattern.
110
+ * When `value` is provided the component is controlled; otherwise
111
+ * internal state is used with `defaultValue` as the initial value.
112
+ * The `onChange` callback fires in both modes.
113
+ */
114
+ const useControllableState = ({ value, defaultValue, onChange }) => {
115
+ const [internal, setInternal] = useState(defaultValue);
116
+ const onChangeRef = useRef(onChange);
117
+ onChangeRef.current = onChange;
118
+ const isControlled = value !== void 0;
119
+ const current = isControlled ? value : internal;
120
+ return [current, useCallback((next) => {
121
+ const nextValue = typeof next === "function" ? next(current) : next;
122
+ if (!isControlled) setInternal(nextValue);
123
+ onChangeRef.current?.(nextValue);
124
+ }, [current, isControlled])];
125
+ };
126
+
127
+ //#endregion
128
+ //#region src/useDebouncedCallback.ts
129
+ /**
130
+ * Returns a stable debounced version of the callback.
131
+ * The returned function has `.cancel()` and `.flush()` methods.
132
+ * Always calls the latest callback (no stale closures).
133
+ * Cleans up on unmount.
134
+ */
135
+ const useDebouncedCallback = (callback, delay) => {
136
+ const callbackRef = useRef(callback);
137
+ callbackRef.current = callback;
138
+ const timerRef = useRef(null);
139
+ const lastArgsRef = useRef(null);
140
+ const cancel = useCallback(() => {
141
+ if (timerRef.current != null) {
142
+ clearTimeout(timerRef.current);
143
+ timerRef.current = null;
144
+ }
145
+ lastArgsRef.current = null;
146
+ }, []);
147
+ const flush = useCallback(() => {
148
+ if (timerRef.current != null && lastArgsRef.current != null) {
149
+ clearTimeout(timerRef.current);
150
+ timerRef.current = null;
151
+ callbackRef.current(...lastArgsRef.current);
152
+ lastArgsRef.current = null;
153
+ }
154
+ }, []);
155
+ useEffect(() => cancel, [cancel]);
156
+ const debounced = useCallback((...args) => {
157
+ lastArgsRef.current = args;
158
+ if (timerRef.current != null) clearTimeout(timerRef.current);
159
+ timerRef.current = setTimeout(() => {
160
+ timerRef.current = null;
161
+ callbackRef.current(...args);
162
+ lastArgsRef.current = null;
163
+ }, delay);
164
+ }, [delay]);
165
+ return Object.assign(debounced, {
166
+ cancel,
167
+ flush
168
+ });
169
+ };
170
+
171
+ //#endregion
172
+ //#region src/useDebouncedValue.ts
173
+ /**
174
+ * Returns a debounced version of the value that only updates
175
+ * after `delay` ms of inactivity.
176
+ *
177
+ * @example
178
+ * ```ts
179
+ * const [search, setSearch] = useState('')
180
+ * const debouncedSearch = useDebouncedValue(search, 300)
181
+ * ```
182
+ */
183
+ const useDebouncedValue = (value, delay) => {
184
+ const [debounced, setDebounced] = useState(value);
185
+ useEffect(() => {
186
+ const id = setTimeout(() => setDebounced(value), delay);
187
+ return () => clearTimeout(id);
188
+ }, [value, delay]);
189
+ return debounced;
190
+ };
191
+
192
+ //#endregion
193
+ //#region src/useElementSize.ts
194
+ /**
195
+ * Tracks an element's `width` and `height` via `ResizeObserver`.
196
+ * Returns `[ref, { width, height }]` — pass `ref` as a callback ref.
197
+ */
198
+ const useElementSize = () => {
199
+ const [size, setSize] = useState({
200
+ width: 0,
201
+ height: 0
202
+ });
203
+ const observerRef = useRef(null);
204
+ return [useCallback((node) => {
205
+ if (observerRef.current) {
206
+ observerRef.current.disconnect();
207
+ observerRef.current = null;
208
+ }
209
+ if (!node) return;
210
+ const observer = new ResizeObserver((entries) => {
211
+ const entry = entries[0];
212
+ if (!entry) return;
213
+ const { width, height } = entry.contentRect;
214
+ setSize((prev) => prev.width === width && prev.height === height ? prev : {
215
+ width,
216
+ height
217
+ });
218
+ });
219
+ observer.observe(node);
220
+ observerRef.current = observer;
221
+ const rect = node.getBoundingClientRect();
222
+ setSize({
223
+ width: rect.width,
224
+ height: rect.height
225
+ });
226
+ }, []), size];
227
+ };
228
+
229
+ //#endregion
230
+ //#region src/useFocus.ts
231
+ /**
232
+ * Simple focus-state hook that returns a boolean plus stable
233
+ * `onFocus`/`onBlur` handlers ready to spread onto an element.
234
+ */
235
+ const useFocus = (initial = false) => {
236
+ const [focused, setFocused] = useState(initial);
237
+ return {
238
+ focused,
239
+ onFocus: useCallback(() => setFocused(true), []),
240
+ onBlur: useCallback(() => setFocused(false), [])
241
+ };
242
+ };
243
+
244
+ //#endregion
245
+ //#region src/useFocusTrap.ts
246
+ const FOCUSABLE = "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
247
+ /**
248
+ * Traps keyboard focus within the referenced container.
249
+ * Tab and Shift+Tab cycle through focusable elements inside.
250
+ * Useful for modals, dialogs, and dropdown menus.
251
+ */
252
+ const useFocusTrap = (ref, enabled = true) => {
253
+ useEffect(() => {
254
+ if (!enabled) return void 0;
255
+ const handler = (e) => {
256
+ if (e.key !== "Tab" || !ref.current) return;
257
+ const focusable = ref.current.querySelectorAll(FOCUSABLE);
258
+ if (focusable.length === 0) return;
259
+ const first = focusable[0];
260
+ const last = focusable[focusable.length - 1];
261
+ if (e.shiftKey) {
262
+ if (document.activeElement === first) {
263
+ e.preventDefault();
264
+ last?.focus();
265
+ }
266
+ } else if (document.activeElement === last) {
267
+ e.preventDefault();
268
+ first?.focus();
269
+ }
270
+ };
271
+ document.addEventListener("keydown", handler);
272
+ return () => document.removeEventListener("keydown", handler);
273
+ }, [ref, enabled]);
274
+ };
275
+
276
+ //#endregion
4
277
  //#region src/useHover.ts
5
278
  /**
6
279
  * Simple hover-state hook that returns a boolean plus stable
@@ -15,6 +288,292 @@ const useHover = (initial = false) => {
15
288
  };
16
289
  };
17
290
 
291
+ //#endregion
292
+ //#region src/useIntersection.ts
293
+ /**
294
+ * Observes an element's intersection with the viewport (or a root element).
295
+ * Returns `[ref, entry]` — pass `ref` as a callback ref.
296
+ */
297
+ const useIntersection = (options = {}) => {
298
+ const { threshold = 0, rootMargin = "0px", root = null } = options;
299
+ const [entry, setEntry] = useState(null);
300
+ const observerRef = useRef(null);
301
+ return [useCallback((node) => {
302
+ if (observerRef.current) {
303
+ observerRef.current.disconnect();
304
+ observerRef.current = null;
305
+ }
306
+ if (!node) return;
307
+ const observer = new IntersectionObserver((entries) => {
308
+ setEntry(entries[0] ?? null);
309
+ }, {
310
+ threshold,
311
+ rootMargin,
312
+ root
313
+ });
314
+ observer.observe(node);
315
+ observerRef.current = observer;
316
+ }, [
317
+ threshold,
318
+ rootMargin,
319
+ root
320
+ ]), entry];
321
+ };
322
+
323
+ //#endregion
324
+ //#region src/useInterval.ts
325
+ /**
326
+ * Declarative `setInterval` with auto-cleanup.
327
+ * Pass `null` as `delay` to pause the interval.
328
+ * Always calls the latest callback (no stale closures).
329
+ */
330
+ const useInterval = (callback, delay) => {
331
+ const callbackRef = useRef(callback);
332
+ callbackRef.current = callback;
333
+ useEffect(() => {
334
+ if (delay === null) return void 0;
335
+ const id = setInterval(() => callbackRef.current(), delay);
336
+ return () => clearInterval(id);
337
+ }, [delay]);
338
+ };
339
+
340
+ //#endregion
341
+ //#region src/useIsomorphicLayoutEffect.ts
342
+ /**
343
+ * `useLayoutEffect` on the client, `useEffect` on the server.
344
+ * Avoids the React SSR warning about useLayoutEffect.
345
+ */
346
+ const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
347
+
348
+ //#endregion
349
+ //#region src/useKeyboard.ts
350
+ /**
351
+ * Listens for a specific keyboard key and calls the handler.
352
+ * Matches `event.key` (e.g. `"Escape"`, `"Enter"`, `"a"`).
353
+ * Always calls the latest handler (no stale closures).
354
+ *
355
+ * @example
356
+ * ```ts
357
+ * useKeyboard('Escape', () => setOpen(false))
358
+ * ```
359
+ */
360
+ const useKeyboard = (key, handler) => {
361
+ const handlerRef = useRef(handler);
362
+ handlerRef.current = handler;
363
+ useEffect(() => {
364
+ const listener = (e) => {
365
+ if (e.key === key) handlerRef.current(e);
366
+ };
367
+ window.addEventListener("keydown", listener);
368
+ return () => window.removeEventListener("keydown", listener);
369
+ }, [key]);
370
+ };
371
+
372
+ //#endregion
373
+ //#region src/useLatest.ts
374
+ /**
375
+ * Returns a ref that always holds the latest value.
376
+ * Useful to avoid stale closures in callbacks and effects.
377
+ */
378
+ const useLatest = (value) => {
379
+ const ref = useRef(value);
380
+ ref.current = value;
381
+ return ref;
382
+ };
383
+
384
+ //#endregion
385
+ //#region src/useMergedRef.ts
386
+ /**
387
+ * Merges multiple refs (callback or object) into a single stable callback ref.
388
+ * Handles null, callback refs, and object refs with `.current`.
389
+ */
390
+ const useMergedRef = (...refs) => {
391
+ return useCallback((node) => {
392
+ for (const ref of refs) {
393
+ if (!ref) continue;
394
+ if (typeof ref === "function") ref(node);
395
+ else ref.current = node;
396
+ }
397
+ }, refs);
398
+ };
399
+
400
+ //#endregion
401
+ //#region src/usePrevious.ts
402
+ /**
403
+ * Returns the value from the previous render.
404
+ * Returns `undefined` on the first render.
405
+ */
406
+ const usePrevious = (value) => {
407
+ const ref = useRef(void 0);
408
+ useEffect(() => {
409
+ ref.current = value;
410
+ });
411
+ return ref.current;
412
+ };
413
+
414
+ //#endregion
415
+ //#region src/useReducedMotion.ts
416
+ /**
417
+ * Returns `true` when the user prefers reduced motion.
418
+ * Use to disable or simplify animations for accessibility.
419
+ */
420
+ const useReducedMotion = () => useMediaQuery("(prefers-reduced-motion: reduce)");
421
+
422
+ //#endregion
423
+ //#region src/useRootSize.ts
424
+ /**
425
+ * Returns `rootSize` from the theme context along with
426
+ * `pxToRem` and `remToPx` conversion utilities.
427
+ *
428
+ * Defaults to `16` when no Provider is mounted.
429
+ */
430
+ const useRootSize = () => {
431
+ const rootSize = useContext(context)?.theme?.rootSize ?? 16;
432
+ return useMemo(() => ({
433
+ rootSize,
434
+ pxToRem: (px) => `${px / rootSize}rem`,
435
+ remToPx: (rem) => rem * rootSize
436
+ }), [rootSize]);
437
+ };
438
+
439
+ //#endregion
440
+ //#region src/useScrollLock.ts
441
+ /**
442
+ * Locks page scroll by setting `overflow: hidden` on `document.body`.
443
+ * Restores the original overflow value on disable or unmount.
444
+ */
445
+ const useScrollLock = (enabled) => {
446
+ useEffect(() => {
447
+ if (!enabled) return void 0;
448
+ const original = document.body.style.overflow;
449
+ document.body.style.overflow = "hidden";
450
+ return () => {
451
+ document.body.style.overflow = original;
452
+ };
453
+ }, [enabled]);
454
+ };
455
+
456
+ //#endregion
457
+ //#region src/useSpacing.ts
458
+ /**
459
+ * Returns a `spacing(n)` function that computes spacing values
460
+ * based on `rootSize` from the theme.
461
+ *
462
+ * @param base - Base spacing unit in px (defaults to `rootSize / 2`, i.e. 8px)
463
+ *
464
+ * @example
465
+ * ```ts
466
+ * const spacing = useSpacing()
467
+ * spacing(1) // "8px"
468
+ * spacing(2) // "16px"
469
+ * spacing(0.5) // "4px"
470
+ * ```
471
+ */
472
+ const useSpacing = (base) => {
473
+ const { rootSize } = useRootSize();
474
+ const unit = base ?? rootSize / 2;
475
+ return useMemo(() => (multiplier) => `${unit * multiplier}px`, [unit]);
476
+ };
477
+
478
+ //#endregion
479
+ //#region src/useThemeValue.ts
480
+ /**
481
+ * Deep-reads a value from the current theme by dot-separated path.
482
+ *
483
+ * @example
484
+ * ```ts
485
+ * const primary = useThemeValue<string>('colors.primary')
486
+ * const columns = useThemeValue<number>('grid.columns')
487
+ * ```
488
+ */
489
+ const useThemeValue = (path) => {
490
+ const theme = useContext(context)?.theme;
491
+ if (!theme) return void 0;
492
+ return get(theme, path);
493
+ };
494
+
495
+ //#endregion
496
+ //#region src/useThrottledCallback.ts
497
+ /**
498
+ * Returns a stable throttled version of the callback.
499
+ * Uses `throttle` from `@vitus-labs/core`.
500
+ * Always calls the latest callback (no stale closures).
501
+ * Cleans up on unmount.
502
+ */
503
+ const useThrottledCallback = (callback, delay) => {
504
+ const callbackRef = useRef(callback);
505
+ callbackRef.current = callback;
506
+ const throttled = useMemo(() => throttle((...args) => callbackRef.current(...args), delay), [delay]);
507
+ useEffect(() => () => throttled.cancel(), [throttled]);
508
+ return throttled;
509
+ };
510
+
511
+ //#endregion
512
+ //#region src/useTimeout.ts
513
+ /**
514
+ * Declarative `setTimeout` with auto-cleanup.
515
+ * Pass `null` as `delay` to disable. Returns `reset` and `clear` controls.
516
+ * Always calls the latest callback (no stale closures).
517
+ */
518
+ const useTimeout = (callback, delay) => {
519
+ const callbackRef = useRef(callback);
520
+ callbackRef.current = callback;
521
+ const timerRef = useRef(null);
522
+ const clear = useCallback(() => {
523
+ if (timerRef.current != null) {
524
+ clearTimeout(timerRef.current);
525
+ timerRef.current = null;
526
+ }
527
+ }, []);
528
+ const reset = useCallback(() => {
529
+ clear();
530
+ if (delay !== null) timerRef.current = setTimeout(() => {
531
+ timerRef.current = null;
532
+ callbackRef.current();
533
+ }, delay);
534
+ }, [delay, clear]);
535
+ useEffect(() => {
536
+ reset();
537
+ return clear;
538
+ }, [reset, clear]);
539
+ return {
540
+ reset,
541
+ clear
542
+ };
543
+ };
544
+
545
+ //#endregion
546
+ //#region src/useToggle.ts
547
+ /**
548
+ * Boolean state with `toggle`, `setTrue`, and `setFalse` helpers.
549
+ * Returns `[value, toggle, setTrue, setFalse]`.
550
+ */
551
+ const useToggle = (initial = false) => {
552
+ const [value, setValue] = useState(initial);
553
+ return [
554
+ value,
555
+ useCallback(() => setValue((v) => !v), []),
556
+ useCallback(() => setValue(true), []),
557
+ useCallback(() => setValue(false), [])
558
+ ];
559
+ };
560
+
561
+ //#endregion
562
+ //#region src/useUpdateEffect.ts
563
+ /**
564
+ * Like `useEffect` but skips the initial mount — only fires on updates.
565
+ */
566
+ const useUpdateEffect = (effect, deps) => {
567
+ const mounted = useRef(false);
568
+ useEffect(() => {
569
+ if (!mounted.current) {
570
+ mounted.current = true;
571
+ return;
572
+ }
573
+ return effect();
574
+ }, deps);
575
+ };
576
+
18
577
  //#endregion
19
578
  //#region src/useWindowResize.ts
20
579
  /**
@@ -53,5 +612,5 @@ const useWindowResize = ({ throttleDelay = 200, onChange } = {}, { width = 0, he
53
612
  };
54
613
 
55
614
  //#endregion
56
- export { useHover, useWindowResize };
615
+ export { useBreakpoint, useClickOutside, useColorScheme, useControllableState, useDebouncedCallback, useDebouncedValue, useElementSize, useFocus, useFocusTrap, useHover, useIntersection, useInterval, useIsomorphicLayoutEffect, useKeyboard, useLatest, useMediaQuery, useMergedRef, usePrevious, useReducedMotion, useRootSize, useScrollLock, useSpacing, useThemeValue, useThrottledCallback, useTimeout, useToggle, useUpdateEffect, useWindowResize };
57
616
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitus-labs/hooks",
3
- "version": "2.0.0-alpha.25",
3
+ "version": "2.0.0-alpha.27",
4
4
  "license": "MIT",
5
5
  "author": "Vit Bokisch <vit@bokisch.cz>",
6
6
  "maintainers": [
@@ -49,14 +49,14 @@
49
49
  "node": ">= 18"
50
50
  },
51
51
  "peerDependencies": {
52
- "@vitus-labs/core": "2.0.0-alpha.24",
52
+ "@vitus-labs/core": "2.0.0-alpha.26",
53
53
  "react": ">= 19"
54
54
  },
55
55
  "devDependencies": {
56
- "@vitus-labs/core": "2.0.0-alpha.25",
57
- "@vitus-labs/tools-rolldown": "^1.7.0",
58
- "@vitus-labs/tools-storybook": "^1.7.0",
59
- "@vitus-labs/tools-typescript": "^1.7.0"
56
+ "@vitus-labs/core": "2.0.0-alpha.27",
57
+ "@vitus-labs/tools-rolldown": "1.9.1-alpha.19",
58
+ "@vitus-labs/tools-storybook": "1.9.1-alpha.19",
59
+ "@vitus-labs/tools-typescript": "1.9.1-alpha.19"
60
60
  },
61
- "gitHead": "def2d96c33d27dd520b4d8bd1cf6edc0d556f6e0"
61
+ "gitHead": "6230c7b4eb1f8fe52bd47275cf72cdcab706cb45"
62
62
  }