Skip to content

Commit 6b81d01

Browse files
committed
main 🧊 v0.3.23
1 parent a49a275 commit 6b81d01

24 files changed

Lines changed: 1412 additions & 75 deletions

File tree

‎packages/core/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@siberiacancode/reactuse",
3-
"version": "0.3.22",
3+
"version": "0.3.23",
44
"description": "The ultimate collection of react hooks",
55
"author": {
66
"name": "SIBERIA CAN CODE 🧊",
Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { useEffect, useRef, useState } from 'react';
2-
import { throttle } from '@/utils/helpers';
1+
import { useEffect, useRef } from 'react';
2+
import { useRerender } from '../useRerender/useRerender';
33
/**
44
* @name useDeviceMotion
55
* @description - Hook that work with device motion
@@ -9,66 +9,69 @@ import { throttle } from '@/utils/helpers';
99
* @browserapi DeviceMotionEvent https://developer.mozilla.org/en-US/docs/Web/API/Window/DeviceMotionEvent
1010
*
1111
* @overload
12-
* @param {number} [delay=1000] The delay in milliseconds
1312
* @param {(event: DeviceMotionEvent) => void} [callback] The callback function to be invoked
14-
* @returns {UseDeviceMotionReturn} The device motion data and interval
13+
* @returns {UseDeviceMotionReturn} Device motion controls with snapshot/watch API
1514
*
1615
* @example
17-
* const { interval, rotationRate, acceleration, accelerationIncludingGravity } = useDeviceMotion(500, (event) => console.log(event));
18-
*
19-
* @overload
20-
* @param {(event: DeviceMotionEvent) => void} [callback] The callback function to be invoked
21-
* @returns {UseDeviceMotionReturn} The device motion data and interval
22-
*
23-
* @example
24-
* const { interval, rotationRate, acceleration, accelerationIncludingGravity } = useDeviceMotion((event) => console.log(event));
16+
* const { interval, rotationRate, acceleration, accelerationIncludingGravity } = useDeviceMotion((event) => console.log(event)).watch();
2517
*
2618
* @overload
2719
* @param {UseDeviceMotionOptions} [options] Configuration options
28-
* @param {number} [options.delay] The delay in milliseconds
2920
* @param {boolean} [options.enabled] Whether to enable the hook
3021
* @param {(event: DeviceMotionEvent) => void} [options.onChange] The callback function to be invoked
31-
* @returns {UseDeviceMotionReturn} The device motion data and interval
22+
* @returns {UseDeviceMotionReturn} Device motion controls with snapshot/watch API
3223
*
3324
* @example
34-
* const { interval, rotationRate, acceleration, accelerationIncludingGravity } = useDeviceMotion();
25+
* const { interval, rotationRate, acceleration, accelerationIncludingGravity } = useDeviceMotion().watch();
3526
*/
3627
export const useDeviceMotion = (...params) => {
37-
const delay = typeof params[0] === 'number' ? params[0] : (params[0]?.delay ?? 1000);
3828
const callback = typeof params[0] === 'function' ? params[0] : params[0]?.onChange;
39-
const enabled = params[0]?.enabled ?? true;
40-
const [value, setValue] = useState({
29+
const enabled = typeof params[0] === 'object' ? (params[0]?.enabled ?? true) : true;
30+
const snapshotRef = useRef({
4131
interval: 0,
4232
rotationRate: { alpha: null, beta: null, gamma: null },
4333
acceleration: { x: null, y: null, z: null },
4434
accelerationIncludingGravity: { x: null, y: null, z: null }
4535
});
4636
const internalCallbackRef = useRef(callback);
37+
const watchingRef = useRef(false);
38+
const rerender = useRerender();
4739
internalCallbackRef.current = callback;
40+
const watch = () => {
41+
watchingRef.current = true;
42+
return snapshotRef.current;
43+
};
44+
const updateValue = (value) => {
45+
snapshotRef.current = value;
46+
if (watchingRef.current) rerender();
47+
};
4848
useEffect(() => {
4949
if (!enabled) return;
50-
const onDeviceMotion = throttle((event) => {
50+
const onDeviceMotion = (event) => {
5151
internalCallbackRef.current?.(event);
52-
setValue({
52+
updateValue({
5353
interval: event.interval,
5454
rotationRate: {
55-
...value.rotationRate,
55+
...snapshotRef.current.rotationRate,
5656
...event.rotationRate
5757
},
5858
acceleration: {
59-
...value.acceleration,
59+
...snapshotRef.current.acceleration,
6060
...event.acceleration
6161
},
6262
accelerationIncludingGravity: {
63-
...value.accelerationIncludingGravity,
63+
...snapshotRef.current.accelerationIncludingGravity,
6464
...event.accelerationIncludingGravity
6565
}
6666
});
67-
}, delay);
67+
};
6868
window.addEventListener('devicemotion', onDeviceMotion);
6969
return () => {
7070
window.removeEventListener('devicemotion', onDeviceMotion);
7171
};
72-
}, [delay, enabled]);
73-
return value;
72+
}, [enabled]);
73+
return {
74+
snapshot: snapshotRef.current,
75+
watch
76+
};
7477
};

‎packages/core/src/bundle/utils/helpers/index.js‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export * from './copy';
2-
export * from './debounce';
32
export * from './getDate';
43
export * from './getRetry';
54
export * from './isTarget';

‎packages/core/src/hooks/useDefault/useDefault.demo.tsx‎

Lines changed: 140 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,148 @@
11
import { useDefault } from '@siberiacancode/reactuse';
2+
import { ChevronDownIcon, RotateCcwIcon } from 'lucide-react';
3+
4+
interface ProjectSettings {
5+
framework: string;
6+
name: string;
7+
packageManager: string;
8+
styling: string;
9+
}
10+
11+
const DEFAULT_SETTINGS: ProjectSettings = {
12+
name: 'my-awesome-app',
13+
framework: 'next',
14+
packageManager: 'npm',
15+
styling: 'tailwind'
16+
};
17+
18+
const FRAMEWORKS = [
19+
{ value: 'next', label: 'Next.js' },
20+
{ value: 'vite', label: 'Vite' },
21+
{ value: 'remix', label: 'Remix' },
22+
{ value: 'astro', label: 'Astro' }
23+
];
24+
25+
const PACKAGE_MANAGERS = [
26+
{ value: 'npm', label: 'npm' },
27+
{ value: 'pnpm', label: 'pnpm' },
28+
{ value: 'yarn', label: 'yarn' },
29+
{ value: 'bun', label: 'bun' }
30+
];
31+
32+
const STYLING = [
33+
{ value: 'tailwind', label: 'Tailwind CSS' },
34+
{ value: 'css-modules', label: 'CSS Modules' },
35+
{ value: 'styled', label: 'styled-components' },
36+
{ value: 'vanilla', label: 'Vanilla CSS' }
37+
];
238

339
const Demo = () => {
4-
const initialUser = { name: 'Dima' };
5-
const defaultUser = { name: 'Danila' };
6-
const [user, setUser] = useDefault(initialUser, defaultUser);
40+
const [settings, setSettings] = useDefault<ProjectSettings>(DEFAULT_SETTINGS, DEFAULT_SETTINGS);
41+
42+
const update = (key: keyof ProjectSettings, value: string) =>
43+
setSettings({ ...settings, [key]: value });
44+
45+
const isDefault =
46+
settings.name === DEFAULT_SETTINGS.name &&
47+
settings.framework === DEFAULT_SETTINGS.framework &&
48+
settings.packageManager === DEFAULT_SETTINGS.packageManager &&
49+
settings.styling === DEFAULT_SETTINGS.styling;
750

851
return (
9-
<div>
10-
<p>
11-
User: <code>{user.name}</code>
12-
</p>
13-
<input value={user.name} onChange={(event) => setUser({ name: event.target.value })} />
14-
<button type='button' onClick={() => setUser(null)}>
15-
Clear user
16-
</button>
17-
</div>
52+
<section className='flex w-full max-w-md flex-col gap-4 p-4'>
53+
<div className='flex items-start justify-between gap-4'>
54+
<div className='flex flex-col gap-1'>
55+
<h3>Project settings</h3>
56+
<p className='text-muted-foreground text-sm'>Configure your project the way you like.</p>
57+
</div>
58+
59+
<button
60+
aria-label='Reset to defaults'
61+
className='size-9! p-0!'
62+
data-variant='outline'
63+
disabled={isDefault}
64+
type='button'
65+
onClick={() => setSettings(null)}
66+
>
67+
<RotateCcwIcon className='size-4' />
68+
</button>
69+
</div>
70+
71+
<div className='flex flex-col divide-y rounded-xl border'>
72+
<div className='flex items-center justify-between gap-4 px-4 py-3'>
73+
<label className='text-sm font-medium' htmlFor='project-name'>
74+
Name
75+
</label>
76+
<input
77+
className='max-w-44'
78+
id='project-name'
79+
type='text'
80+
value={settings.name}
81+
onChange={(event) => update('name', event.target.value)}
82+
/>
83+
</div>
84+
85+
<div className='flex items-center justify-between gap-4 px-4 py-3'>
86+
<label className='text-sm font-medium' htmlFor='framework'>
87+
Framework
88+
</label>
89+
<div className='relative'>
90+
<select
91+
id='framework'
92+
value={settings.framework}
93+
onChange={(event) => update('framework', event.target.value)}
94+
>
95+
{FRAMEWORKS.map((option) => (
96+
<option key={option.value} value={option.value}>
97+
{option.label}
98+
</option>
99+
))}
100+
</select>
101+
<ChevronDownIcon className='text-muted-foreground pointer-events-none absolute top-1/2 right-2 size-4 -translate-y-1/2' />
102+
</div>
103+
</div>
104+
105+
<div className='flex items-center justify-between gap-4 px-4 py-3'>
106+
<label className='text-sm font-medium' htmlFor='package-manager'>
107+
Package manager
108+
</label>
109+
<div className='relative'>
110+
<select
111+
id='package-manager'
112+
value={settings.packageManager}
113+
onChange={(event) => update('packageManager', event.target.value)}
114+
>
115+
{PACKAGE_MANAGERS.map((option) => (
116+
<option key={option.value} value={option.value}>
117+
{option.label}
118+
</option>
119+
))}
120+
</select>
121+
<ChevronDownIcon className='text-muted-foreground pointer-events-none absolute top-1/2 right-2 size-4 -translate-y-1/2' />
122+
</div>
123+
</div>
124+
125+
<div className='flex items-center justify-between gap-4 px-4 py-3'>
126+
<label className='text-sm font-medium' htmlFor='styling'>
127+
Styling
128+
</label>
129+
<div className='relative'>
130+
<select
131+
id='styling'
132+
value={settings.styling}
133+
onChange={(event) => update('styling', event.target.value)}
134+
>
135+
{STYLING.map((option) => (
136+
<option key={option.value} value={option.value}>
137+
{option.label}
138+
</option>
139+
))}
140+
</select>
141+
<ChevronDownIcon className='text-muted-foreground pointer-events-none absolute top-1/2 right-2 size-4 -translate-y-1/2' />
142+
</div>
143+
</div>
144+
</div>
145+
</section>
18146
);
19147
};
20148

‎packages/core/src/hooks/useDeviceMotion/useDeviceMotion.demo.tsx‎

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,93 @@
11
import { useDeviceMotion } from '@siberiacancode/reactuse';
2+
import { SmartphoneIcon } from 'lucide-react';
3+
4+
const CIRCLE_SIZE = 200;
5+
const BUBBLE_SIZE = 36;
6+
const MAX_OFFSET = CIRCLE_SIZE / 2 - BUBBLE_SIZE / 2 - 8;
7+
const GRAVITY = 9.8;
8+
const LEVEL_THRESHOLD = 0.5;
9+
10+
const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);
211

312
const Demo = () => {
4-
const deviceMotion = useDeviceMotion();
13+
const motion = useDeviceMotion({ delay: 100 });
14+
15+
const x = motion.accelerationIncludingGravity.x;
16+
const y = motion.accelerationIncludingGravity.y;
17+
18+
if (x === null || y === null) {
19+
return (
20+
<section className='flex w-full max-w-sm flex-col items-center p-4'>
21+
<div className='bg-muted/40 flex flex-col items-center gap-3 rounded-2xl p-8 text-center'>
22+
<SmartphoneIcon className='text-muted-foreground size-10' />
23+
<p className='text-sm font-medium'>Device motion not supported</p>
24+
<p className='text-muted-foreground text-xs'>
25+
Open this page on a mobile device to use the spirit level.
26+
</p>
27+
</div>
28+
</section>
29+
);
30+
}
31+
32+
const offsetX = clamp((-x / GRAVITY) * MAX_OFFSET, -MAX_OFFSET, MAX_OFFSET);
33+
const offsetY = clamp((y / GRAVITY) * MAX_OFFSET, -MAX_OFFSET, MAX_OFFSET);
34+
35+
const tiltX = ((-x / GRAVITY) * 90).toFixed(1);
36+
const tiltY = ((y / GRAVITY) * 90).toFixed(1);
37+
38+
const isLevel = Math.abs(x) < LEVEL_THRESHOLD && Math.abs(y) < LEVEL_THRESHOLD;
539

640
return (
7-
<pre lang='json'>
8-
<b>Device motion data:</b>
9-
<p>{JSON.stringify(deviceMotion, null, 2)}</p>
10-
</pre>
41+
<section className='flex w-full max-w-sm flex-col items-center gap-5 p-4'>
42+
<div className='flex flex-col items-center gap-1 text-center'>
43+
<h3>Spirit level</h3>
44+
<p className='text-muted-foreground text-sm'>
45+
Tilt your device — the bubble follows gravity.
46+
</p>
47+
</div>
48+
49+
<div
50+
className='border-border bg-muted/30 relative rounded-full border-2'
51+
style={{ width: CIRCLE_SIZE, height: CIRCLE_SIZE }}
52+
>
53+
<div className='bg-border absolute top-1/2 left-0 h-px w-full -translate-y-1/2' />
54+
<div className='bg-border absolute top-0 left-1/2 h-full w-px -translate-x-1/2' />
55+
56+
<div
57+
style={{
58+
width: BUBBLE_SIZE,
59+
height: BUBBLE_SIZE,
60+
marginLeft: -BUBBLE_SIZE / 2,
61+
marginTop: -BUBBLE_SIZE / 2
62+
}}
63+
className='border-foreground/40 absolute top-1/2 left-1/2 rounded-full border-2'
64+
/>
65+
66+
<div
67+
style={{
68+
width: BUBBLE_SIZE,
69+
height: BUBBLE_SIZE,
70+
marginLeft: -BUBBLE_SIZE / 2,
71+
marginTop: -BUBBLE_SIZE / 2,
72+
backgroundColor: isLevel ? 'oklch(0.65 0.15 160)' : 'oklch(0.55 0.18 250)',
73+
transform: `translate(${offsetX}px, ${offsetY}px)`,
74+
boxShadow: '0 2px 8px rgba(0,0,0,0.15)'
75+
}}
76+
className='absolute top-1/2 left-1/2 rounded-full transition-transform duration-100 ease-out'
77+
/>
78+
</div>
79+
80+
<div className='flex gap-6 text-sm'>
81+
<div className='flex flex-col items-center'>
82+
<span className='text-muted-foreground text-xs'>Tilt X</span>
83+
<code>{tiltX}°</code>
84+
</div>
85+
<div className='flex flex-col items-center'>
86+
<span className='text-muted-foreground text-xs'>Tilt Y</span>
87+
<code>{tiltY}°</code>
88+
</div>
89+
</div>
90+
</section>
1191
);
1292
};
1393

‎packages/core/src/utils/helpers/debounce.ts‎

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)