react-robot-vacuum 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/README.md ADDED
@@ -0,0 +1,201 @@
1
+ # 🤖 React Robot Vacuum
2
+
3
+ An animated React component featuring an autonomous robot vacuum that cleans your page by collecting dirt particles. Built with TypeScript, React 19, and CSS Modules.
4
+
5
+ ![Robot Vacuum Demo](https://via.placeholder.com/800x400?text=Robot+Vacuum+Demo)
6
+
7
+ ## ✨ Features
8
+
9
+ - 🎯 **Autonomous Navigation** - Robot intelligently navigates to dirt particles
10
+ - 🔄 **Smooth Animations** - Realistic acceleration, rotation, and movement
11
+ - ⏸️ **Window Focus Detection** - Automatically pauses when tab is inactive
12
+ - 🎮 **Imperative Control** - Start, stop, and reset via ref
13
+ - 📢 **Lifecycle Callbacks** - Track cleaning progress with events
14
+ - ⚙️ **Fully Customizable** - Props for size, position, speed, and more
15
+ - 💪 **TypeScript** - Full type safety with exported interfaces
16
+ - 🎨 **CSS Modules** - Scoped styling, no conflicts
17
+ - ⚡ **React 17/18/19** - Compatible with all modern React versions
18
+
19
+ ## 📦 Installation
20
+
21
+ ```bash
22
+ npm install react-robot-vacuum
23
+ ```
24
+
25
+ ```bash
26
+ yarn add react-robot-vacuum
27
+ ```
28
+
29
+ ```bash
30
+ pnpm add react-robot-vacuum
31
+ ```
32
+
33
+ ## 🚀 Quick Start
34
+
35
+ ```tsx
36
+ import { RobotVacuum } from "react-robot-vacuum";
37
+
38
+ function App() {
39
+ return <RobotVacuum />;
40
+ }
41
+ ```
42
+
43
+ That's it! The robot will automatically start cleaning your page.
44
+
45
+ ## 📖 API Reference
46
+
47
+ ### Props
48
+
49
+ | Prop | Type | Default | Description |
50
+ |------|------|---------|-------------|
51
+ | `numberOfDirtBits` | `number` | `5` | Number of dirt particles to spawn |
52
+ | `autoStart` | `boolean` | `true` | Whether cleaning starts automatically |
53
+ | `minSpeed` | `number` | `0.5` | Minimum movement speed in seconds |
54
+ | `speedFactor` | `number` | `100` | Factor for calculating speed based on distance |
55
+ | `rotationDuration` | `number` | `0.6` | Duration of rotation animation in seconds |
56
+ | `onCleaningStart` | `() => void` | `undefined` | Callback when cleaning starts |
57
+ | `onCleaningComplete` | `() => void` | `undefined` | Callback when robot returns to dock |
58
+ | `onDirtCollected` | `(collected: number, total: number) => void` | `undefined` | Callback fired each time dirt is collected |
59
+
60
+ ### Ref Methods
61
+
62
+ Use a ref to control the robot imperatively:
63
+
64
+ ```tsx
65
+ import { useRef } from "react";
66
+ import { RobotVacuum, RobotVacuumRef } from "react-robot-vacuum";
67
+
68
+ function App() {
69
+ const robotRef = useRef<RobotVacuumRef>(null);
70
+
71
+ return (
72
+ <>
73
+ <RobotVacuum ref={robotRef} autoStart={false} />
74
+ <button onClick={() => robotRef.current?.startCleaning()}>
75
+ Start Cleaning
76
+ </button>
77
+ <button onClick={() => robotRef.current?.reset()}>
78
+ Reset
79
+ </button>
80
+ </>
81
+ );
82
+ }
83
+ ```
84
+
85
+ #### Methods
86
+
87
+ - **`startCleaning()`** - Manually start the cleaning process
88
+ - **`reset()`** - Reset robot to dock and generate new dirt
89
+
90
+ ### Types
91
+
92
+ ```tsx
93
+ export interface RobotVacuumRef {
94
+ startCleaning: () => void;
95
+ reset: () => void;
96
+ }
97
+
98
+ export interface RobotVacuumProps {
99
+ readonly numberOfDirtBits?: number;
100
+ readonly minSpeed?: number;
101
+ readonly speedFactor?: number;
102
+ readonly rotationDuration?: number;
103
+ readonly autoStart?: boolean;
104
+ readonly onCleaningStart?: () => void;
105
+ readonly onCleaningComplete?: () => void;
106
+ readonly onDirtCollected?: (collected: number, total: number) => void;
107
+ }
108
+ ```
109
+
110
+ ## 🎨 Examples
111
+
112
+ ### Manual Control with Callbacks
113
+
114
+ ```tsx
115
+ import { useRef, useState } from "react";
116
+ import { RobotVacuum, RobotVacuumRef } from "react-robot-vacuum";
117
+
118
+ function App() {
119
+ const robotRef = useRef<RobotVacuumRef>(null);
120
+ const [progress, setProgress] = useState("0/0");
121
+ const [status, setStatus] = useState("Ready");
122
+
123
+ return (
124
+ <div>
125
+ <RobotVacuum
126
+ ref={robotRef}
127
+ autoStart={false}
128
+ numberOfDirtBits={10}
129
+ onCleaningStart={() => setStatus("Cleaning...")}
130
+ onDirtCollected={(collected, total) => {
131
+ setProgress(`${collected}/${total}`);
132
+ }}
133
+ onCleaningComplete={() => setStatus("Complete!")}
134
+ />
135
+
136
+ <div style={{ position: "fixed", top: 20, right: 20 }}>
137
+ <p>Status: {status}</p>
138
+ <p>Progress: {progress}</p>
139
+ <button onClick={() => robotRef.current?.startCleaning()}>
140
+ Start
141
+ </button>
142
+ <button onClick={() => robotRef.current?.reset()}>
143
+ Reset
144
+ </button>
145
+ </div>
146
+ </div>
147
+ );
148
+ }
149
+ ```
150
+
151
+ ### Slow and Methodical Cleaning
152
+
153
+ ```tsx
154
+ import { RobotVacuum } from "react-robot-vacuum";
155
+
156
+ function App() {
157
+ return (
158
+ <RobotVacuum
159
+ minSpeed={1.5}
160
+ speedFactor={50}
161
+ rotationDuration={1.2}
162
+ />
163
+ );
164
+ }
165
+ ```
166
+
167
+ ## 🎮 Live Demo
168
+
169
+ Try it on [CodeSandbox](https://codesandbox.io/s/react-robot-vacuum-demo)
170
+
171
+ ## 🛠️ Development
172
+
173
+ ```bash
174
+ # Install dependencies
175
+ npm install
176
+
177
+ # Run dev server
178
+ npm run vite:dev
179
+
180
+ # Build for production
181
+ npm run build
182
+
183
+ # Lint
184
+ npm run lint
185
+ ```
186
+
187
+ ## 📄 License
188
+
189
+ MIT © [Your Name]
190
+
191
+ ## 🤝 Contributing
192
+
193
+ Contributions, issues, and feature requests are welcome!
194
+
195
+ ## ⭐ Show your support
196
+
197
+ Give a ⭐️ if this project helped you!
198
+
199
+ ---
200
+
201
+ Made with ❤️ and TypeScript
@@ -0,0 +1,151 @@
1
+ .container {
2
+ position: absolute;
3
+ width: 100%;
4
+ height: 100%;
5
+ overflow: hidden;
6
+ top: 0;
7
+ left: 0;
8
+ }
9
+
10
+ .robot {
11
+ visibility: hidden; /* Hide the robot by default */
12
+ position: absolute;
13
+ z-index: 100;
14
+ width: 25px;
15
+ height: 25px;
16
+ background-color: #333;
17
+ border-radius: 50%;
18
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
19
+ }
20
+
21
+ /* Robot center button */
22
+ .robot::before {
23
+ content: "";
24
+ position: absolute;
25
+ top: 50%;
26
+ left: 50%;
27
+ transform: translate(-50%, -50%);
28
+ width: 7px;
29
+ height: 7px;
30
+ background-color: #666;
31
+ border-radius: 50%;
32
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.3);
33
+ }
34
+
35
+ /* State styles for the robot */
36
+ .idle::before {
37
+ background-color: #c0c0c0;
38
+ box-shadow: 0 0 10px rgba(255, 255, 255, 0.8); /* Glowing effect */
39
+ animation: glow 1s infinite alternate;
40
+ }
41
+
42
+ .cleaning::before {
43
+ background-color: #00ff00;
44
+ animation: flash 0.5s infinite alternate;
45
+ }
46
+
47
+ .returning::before {
48
+ background-color: orange;
49
+ animation: flash 0.5s infinite alternate;
50
+ }
51
+
52
+ /* Keyframe for glowing effect */
53
+ @keyframes glow {
54
+ 0% {
55
+ box-shadow: 0 0 10px rgba(255, 255, 255, 0.8);
56
+ }
57
+ 100% {
58
+ box-shadow: 0 0 20px rgba(255, 255, 255, 1);
59
+ }
60
+ }
61
+
62
+ /* Keyframe for flashing effect */
63
+ @keyframes flash {
64
+ 0% {
65
+ opacity: 1;
66
+ }
67
+ 100% {
68
+ opacity: 0.5;
69
+ }
70
+ }
71
+
72
+ /* Robot wheels */
73
+ .wheel {
74
+ position: absolute;
75
+ width: 2px;
76
+ height: 7px;
77
+ background-color: #444;
78
+ border-radius: 1px;
79
+ top: calc(50% - 4px);
80
+ }
81
+
82
+ .wheel.left {
83
+ left: -1px;
84
+ }
85
+
86
+ .wheel.right {
87
+ right: -1px;
88
+ }
89
+
90
+ /* Robot sensors */
91
+ .sensor {
92
+ position: absolute;
93
+ width: 4px;
94
+ height: 4px;
95
+ background-color: #444;
96
+ border-radius: 50%;
97
+ top: 3px;
98
+ }
99
+
100
+ .sensor.left {
101
+ left: 3px;
102
+ }
103
+
104
+ .sensor.right {
105
+ right: 3px;
106
+ }
107
+
108
+ /* Robot front panel */
109
+ .front-panel {
110
+ position: absolute;
111
+ top: 15px;
112
+ left: 50%;
113
+ transform: translateX(-50%);
114
+ width: 13px;
115
+ height: 7px;
116
+ background-color: #666;
117
+ border-radius: 7px 7px 0 0;
118
+ }
119
+
120
+ .dirt {
121
+ position: absolute;
122
+ z-index: 100;
123
+ width: 5px;
124
+ height: 5px;
125
+ background-color: #fff; /* Light gray color */
126
+ border: 1px solid black;
127
+ }
128
+ .dock {
129
+ position: absolute;
130
+ top: 5px;
131
+ left: 25px;
132
+ transform: translateX(-50%);
133
+ width: 20px;
134
+ height: 20px;
135
+ background-color: #333; /* Dark color for the dock */
136
+ border-radius: 10px; /* Rounded corners */
137
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* Subtle shadow */
138
+ justify-content: center;
139
+ align-items: center;
140
+ }
141
+
142
+ .dock::before {
143
+ content: "";
144
+ position: absolute;
145
+ left: -5px;
146
+ top: -5px; /* Adjust as needed */
147
+ width: 30px; /* Adjust as needed */
148
+ height: 10px; /* Adjust as needed */
149
+ background-color: #333; /* Same color as the dock */
150
+ border-radius: 5px 5px 0 0; /* Rounded top corners */
151
+ }
package/dist/index.css ADDED
@@ -0,0 +1,129 @@
1
+ /* src/RobotVacuum/RobotVacuum.module.css */
2
+ .container {
3
+ position: absolute;
4
+ width: 100%;
5
+ height: 100%;
6
+ overflow: hidden;
7
+ top: 0;
8
+ left: 0;
9
+ }
10
+ .robot {
11
+ visibility: hidden;
12
+ position: absolute;
13
+ z-index: 100;
14
+ width: 25px;
15
+ height: 25px;
16
+ background-color: #333;
17
+ border-radius: 50%;
18
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
19
+ }
20
+ .robot::before {
21
+ content: "";
22
+ position: absolute;
23
+ top: 50%;
24
+ left: 50%;
25
+ transform: translate(-50%, -50%);
26
+ width: 7px;
27
+ height: 7px;
28
+ background-color: #666;
29
+ border-radius: 50%;
30
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.3);
31
+ }
32
+ .idle::before {
33
+ background-color: #c0c0c0;
34
+ box-shadow: 0 0 10px rgba(255, 255, 255, 0.8);
35
+ animation: glow 1s infinite alternate;
36
+ }
37
+ .cleaning::before {
38
+ background-color: #00ff00;
39
+ animation: flash 0.5s infinite alternate;
40
+ }
41
+ .returning::before {
42
+ background-color: orange;
43
+ animation: flash 0.5s infinite alternate;
44
+ }
45
+ @keyframes glow {
46
+ 0% {
47
+ box-shadow: 0 0 10px rgba(255, 255, 255, 0.8);
48
+ }
49
+ 100% {
50
+ box-shadow: 0 0 20px rgba(255, 255, 255, 1);
51
+ }
52
+ }
53
+ @keyframes flash {
54
+ 0% {
55
+ opacity: 1;
56
+ }
57
+ 100% {
58
+ opacity: 0.5;
59
+ }
60
+ }
61
+ .wheel {
62
+ position: absolute;
63
+ width: 2px;
64
+ height: 7px;
65
+ background-color: #444;
66
+ border-radius: 1px;
67
+ top: calc(50% - 4px);
68
+ }
69
+ .wheel.left {
70
+ left: -1px;
71
+ }
72
+ .wheel.right {
73
+ right: -1px;
74
+ }
75
+ .sensor {
76
+ position: absolute;
77
+ width: 4px;
78
+ height: 4px;
79
+ background-color: #444;
80
+ border-radius: 50%;
81
+ top: 3px;
82
+ }
83
+ .sensor.left {
84
+ left: 3px;
85
+ }
86
+ .sensor.right {
87
+ right: 3px;
88
+ }
89
+ .front-panel {
90
+ position: absolute;
91
+ top: 15px;
92
+ left: 50%;
93
+ transform: translateX(-50%);
94
+ width: 13px;
95
+ height: 7px;
96
+ background-color: #666;
97
+ border-radius: 7px 7px 0 0;
98
+ }
99
+ .dirt {
100
+ position: absolute;
101
+ z-index: 100;
102
+ width: 5px;
103
+ height: 5px;
104
+ background-color: #fff;
105
+ border: 1px solid black;
106
+ }
107
+ .dock {
108
+ position: absolute;
109
+ top: 5px;
110
+ left: 25px;
111
+ transform: translateX(-50%);
112
+ width: 20px;
113
+ height: 20px;
114
+ background-color: #333;
115
+ border-radius: 10px;
116
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
117
+ justify-content: center;
118
+ align-items: center;
119
+ }
120
+ .dock::before {
121
+ content: "";
122
+ position: absolute;
123
+ left: -5px;
124
+ top: -5px;
125
+ width: 30px;
126
+ height: 10px;
127
+ background-color: #333;
128
+ border-radius: 5px 5px 0 0;
129
+ }
@@ -0,0 +1,45 @@
1
+ import * as react from 'react';
2
+
3
+ /**
4
+ * Methods exposed via ref for imperative control
5
+ */
6
+ interface RobotVacuumRef {
7
+ startCleaning: () => void;
8
+ reset: () => void;
9
+ }
10
+ /**
11
+ * Props for RobotVacuum component
12
+ */
13
+ interface RobotVacuumProps {
14
+ readonly numberOfDirtBits?: number;
15
+ readonly minSpeed?: number;
16
+ readonly speedFactor?: number;
17
+ readonly rotationDuration?: number;
18
+ readonly autoStart?: boolean;
19
+ readonly onCleaningStart?: () => void;
20
+ readonly onCleaningComplete?: () => void;
21
+ readonly onDirtCollected?: (collected: number, total: number) => void;
22
+ }
23
+ /**
24
+ * Robotv2 - Advanced robot vacuum component for React 19
25
+ *
26
+ * Features:
27
+ * - Full TypeScript support with immutable types
28
+ * - Creates an overlay on the page with random dirt particles
29
+ * - Robot navigates to each dirt particle and collects it
30
+ * - Smooth acceleration and rotation animations
31
+ * - Automatic return to dock when finished
32
+ * - Imperative control via ref (startCleaning, reset)
33
+ *
34
+ * @param numberOfDirtBits - Number of dirt particles to spawn (default: 5)
35
+ * @param minSpeed - Minimum movement speed in seconds (default: 0.5)
36
+ * @param speedFactor - Factor for calculating speed based on distance (default: 100)
37
+ * @param rotationDuration - Duration of rotation animation in seconds (default: 0.6)
38
+ * @param autoStart - Whether to automatically start cleaning on mount (default: true)
39
+ * @param onCleaningStart - Callback fired when cleaning starts
40
+ * @param onCleaningComplete - Callback fired when robot returns to dock
41
+ * @param onDirtCollected - Callback fired when dirt is collected
42
+ */
43
+ declare const Robotv2: react.ForwardRefExoticComponent<RobotVacuumProps & react.RefAttributes<RobotVacuumRef>>;
44
+
45
+ export { Robotv2 as RobotVacuum, type RobotVacuumProps, type RobotVacuumRef };
@@ -0,0 +1,45 @@
1
+ import * as react from 'react';
2
+
3
+ /**
4
+ * Methods exposed via ref for imperative control
5
+ */
6
+ interface RobotVacuumRef {
7
+ startCleaning: () => void;
8
+ reset: () => void;
9
+ }
10
+ /**
11
+ * Props for RobotVacuum component
12
+ */
13
+ interface RobotVacuumProps {
14
+ readonly numberOfDirtBits?: number;
15
+ readonly minSpeed?: number;
16
+ readonly speedFactor?: number;
17
+ readonly rotationDuration?: number;
18
+ readonly autoStart?: boolean;
19
+ readonly onCleaningStart?: () => void;
20
+ readonly onCleaningComplete?: () => void;
21
+ readonly onDirtCollected?: (collected: number, total: number) => void;
22
+ }
23
+ /**
24
+ * Robotv2 - Advanced robot vacuum component for React 19
25
+ *
26
+ * Features:
27
+ * - Full TypeScript support with immutable types
28
+ * - Creates an overlay on the page with random dirt particles
29
+ * - Robot navigates to each dirt particle and collects it
30
+ * - Smooth acceleration and rotation animations
31
+ * - Automatic return to dock when finished
32
+ * - Imperative control via ref (startCleaning, reset)
33
+ *
34
+ * @param numberOfDirtBits - Number of dirt particles to spawn (default: 5)
35
+ * @param minSpeed - Minimum movement speed in seconds (default: 0.5)
36
+ * @param speedFactor - Factor for calculating speed based on distance (default: 100)
37
+ * @param rotationDuration - Duration of rotation animation in seconds (default: 0.6)
38
+ * @param autoStart - Whether to automatically start cleaning on mount (default: true)
39
+ * @param onCleaningStart - Callback fired when cleaning starts
40
+ * @param onCleaningComplete - Callback fired when robot returns to dock
41
+ * @param onDirtCollected - Callback fired when dirt is collected
42
+ */
43
+ declare const Robotv2: react.ForwardRefExoticComponent<RobotVacuumProps & react.RefAttributes<RobotVacuumRef>>;
44
+
45
+ export { Robotv2 as RobotVacuum, type RobotVacuumProps, type RobotVacuumRef };
package/dist/index.js ADDED
@@ -0,0 +1,297 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.tsx
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ RobotVacuum: () => RobotVacuum_default2
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/RobotVacuum/RobotVacuum.tsx
28
+ var import_react = require("react");
29
+
30
+ // src/RobotVacuum/RobotVacuum.module.css
31
+ var RobotVacuum_default = {};
32
+
33
+ // src/RobotVacuum/RobotVacuum.tsx
34
+ var import_jsx_runtime = require("react/jsx-runtime");
35
+ var Robotv2 = (0, import_react.forwardRef)(
36
+ ({
37
+ numberOfDirtBits = 5,
38
+ minSpeed = 0.5,
39
+ speedFactor = 100,
40
+ rotationDuration = 0.6,
41
+ autoStart = true,
42
+ onCleaningStart,
43
+ onCleaningComplete,
44
+ onDirtCollected
45
+ }, ref) => {
46
+ const [dirtPositions, setDirtPositions] = (0, import_react.useState)(
47
+ []
48
+ );
49
+ const [robotState, setRobotState] = (0, import_react.useState)("idle");
50
+ const [isDocumentVisible, setIsDocumentVisible] = (0, import_react.useState)(true);
51
+ const robotRef = (0, import_react.useRef)(null);
52
+ const backgroundRef = (0, import_react.useRef)(null);
53
+ const cleaningAbortedRef = (0, import_react.useRef)(false);
54
+ const ROBOT_SIZE = 25;
55
+ const ROBOT_HALF_SIZE = ROBOT_SIZE / 2;
56
+ const DOCKED_POSITION = {
57
+ x: 12,
58
+ y: 5
59
+ };
60
+ const initializeRobotPosition = async () => {
61
+ const robotElement = robotRef.current;
62
+ if (!robotElement) return;
63
+ robotElement.style.visibility = "visible";
64
+ robotElement.style.left = `${DOCKED_POSITION.x}px`;
65
+ robotElement.style.top = `${DOCKED_POSITION.y}px`;
66
+ robotElement.style.transform = "rotate(0deg)";
67
+ };
68
+ const getRandomPosition = (width, height) => ({
69
+ x: Math.floor(Math.random() * width),
70
+ y: Math.floor(Math.random() * height),
71
+ collected: false
72
+ });
73
+ const isPositionEmpty = (x, y) => {
74
+ const backgroundElement = backgroundRef.current;
75
+ if (!backgroundElement) return false;
76
+ const elementAtPosition = document.elementFromPoint(x, y);
77
+ return elementAtPosition === backgroundElement || elementAtPosition !== null && backgroundElement.contains(elementAtPosition);
78
+ };
79
+ const createRandomDirt = () => {
80
+ const backgroundElement = backgroundRef.current;
81
+ if (!backgroundElement) return;
82
+ const { width, height } = backgroundElement.getBoundingClientRect();
83
+ const newDirt = [];
84
+ let attempts = 0;
85
+ const maxAttempts = 1e3;
86
+ while (newDirt.length < numberOfDirtBits && attempts < maxAttempts) {
87
+ const position = getRandomPosition(width, height);
88
+ if (isPositionEmpty(position.x, position.y)) {
89
+ newDirt.push(position);
90
+ }
91
+ attempts++;
92
+ }
93
+ newDirt.sort((a, b) => a.y - b.y || a.x - b.x);
94
+ setDirtPositions(Object.freeze(newDirt));
95
+ };
96
+ const getRobotPosition = () => {
97
+ const robotElement = robotRef.current;
98
+ if (!robotElement) return null;
99
+ const rect = robotElement.getBoundingClientRect();
100
+ const transform = robotElement.style.transform;
101
+ const rotationMatch = transform.match(/rotate\(([^)]+)deg\)/);
102
+ const rotation = rotationMatch ? parseFloat(rotationMatch[1]) : 0;
103
+ return {
104
+ x: rect.left + rect.width / 2,
105
+ y: rect.top + rect.height / 2,
106
+ rotation
107
+ };
108
+ };
109
+ const calculateShortestRotation = (targetAngle) => {
110
+ const currentPos = getRobotPosition();
111
+ if (!currentPos) return targetAngle;
112
+ let current = currentPos.rotation % 360;
113
+ let target = targetAngle % 360;
114
+ if (current < 0) current += 360;
115
+ if (target < 0) target += 360;
116
+ let diff = target - current;
117
+ if (diff > 180) {
118
+ diff -= 360;
119
+ } else if (diff < -180) {
120
+ diff += 360;
121
+ }
122
+ return current + diff;
123
+ };
124
+ const rotateRobot = async (targetAngle) => {
125
+ const robotElement = robotRef.current;
126
+ if (!robotElement) return;
127
+ const shortestAngle = calculateShortestRotation(targetAngle);
128
+ robotElement.style.transition = `transform ${rotationDuration}s ease-in-out`;
129
+ robotElement.style.transform = `rotate(${shortestAngle}deg)`;
130
+ await new Promise(
131
+ (resolve) => setTimeout(resolve, rotationDuration * 1e3)
132
+ );
133
+ };
134
+ const calculateMovementTime = (distance) => {
135
+ return Math.max(Math.sqrt(distance / speedFactor), minSpeed);
136
+ };
137
+ const moveRobotToPosition = async (targetX, targetY) => {
138
+ const robotElement = robotRef.current;
139
+ if (!robotElement) return;
140
+ const currentPos = getRobotPosition();
141
+ if (!currentPos) return;
142
+ if (Math.abs(currentPos.x - targetX) > 1) {
143
+ const deltaX = targetX - currentPos.x;
144
+ const angle = deltaX > 0 ? 90 : -90;
145
+ await rotateRobot(angle);
146
+ const distanceX = Math.abs(deltaX);
147
+ const movementTime = calculateMovementTime(distanceX);
148
+ robotElement.style.transition = `left ${movementTime}s ease-in-out`;
149
+ robotElement.style.left = `${targetX - ROBOT_HALF_SIZE}px`;
150
+ await new Promise(
151
+ (resolve) => setTimeout(resolve, movementTime * 1e3)
152
+ );
153
+ }
154
+ if (Math.abs(currentPos.y - targetY) > 1) {
155
+ const deltaY = targetY - currentPos.y;
156
+ const angle = deltaY > 0 ? 180 : 0;
157
+ await rotateRobot(angle);
158
+ const distanceY = Math.abs(deltaY);
159
+ const movementTime = calculateMovementTime(distanceY);
160
+ robotElement.style.transition = `top ${movementTime}s ease-in-out`;
161
+ robotElement.style.top = `${targetY - ROBOT_HALF_SIZE}px`;
162
+ await new Promise(
163
+ (resolve) => setTimeout(resolve, movementTime * 1e3)
164
+ );
165
+ }
166
+ };
167
+ const cleanAndReturn = async () => {
168
+ if (dirtPositions.length === 0) return;
169
+ onCleaningStart?.();
170
+ cleaningAbortedRef.current = false;
171
+ const uncollectedDirt = [...dirtPositions];
172
+ let collectedCount = 0;
173
+ for (let i = 0; i < uncollectedDirt.length; i++) {
174
+ if (cleaningAbortedRef.current) {
175
+ setRobotState("idle");
176
+ return;
177
+ }
178
+ const dirt = uncollectedDirt[i];
179
+ if (dirt.collected) continue;
180
+ setRobotState("cleaning");
181
+ const currentPos2 = getRobotPosition();
182
+ if (currentPos2) {
183
+ await moveRobotToPosition(dirt.x, dirt.y);
184
+ if (cleaningAbortedRef.current) {
185
+ setRobotState("idle");
186
+ return;
187
+ }
188
+ setDirtPositions(
189
+ (prev) => Object.freeze(
190
+ prev.map((d, idx) => idx === i ? { ...d, collected: true } : d)
191
+ )
192
+ );
193
+ collectedCount++;
194
+ onDirtCollected?.(collectedCount, uncollectedDirt.length);
195
+ }
196
+ await new Promise((resolve) => setTimeout(resolve, 200));
197
+ }
198
+ if (cleaningAbortedRef.current) {
199
+ setRobotState("idle");
200
+ return;
201
+ }
202
+ setRobotState("returning");
203
+ const currentPos = getRobotPosition();
204
+ if (currentPos) {
205
+ const dockCenterX = DOCKED_POSITION.x + ROBOT_HALF_SIZE;
206
+ const dockCenterY = DOCKED_POSITION.y + ROBOT_HALF_SIZE;
207
+ await moveRobotToPosition(dockCenterX, dockCenterY);
208
+ }
209
+ setRobotState("idle");
210
+ onCleaningComplete?.();
211
+ };
212
+ (0, import_react.useImperativeHandle)(ref, () => ({
213
+ startCleaning: () => {
214
+ if (robotState === "idle" && isDocumentVisible) {
215
+ cleanAndReturn();
216
+ }
217
+ },
218
+ reset: () => {
219
+ cleaningAbortedRef.current = true;
220
+ setRobotState("idle");
221
+ initializeRobotPosition();
222
+ createRandomDirt();
223
+ }
224
+ }));
225
+ (0, import_react.useEffect)(() => {
226
+ const handleVisibilityChange = () => {
227
+ const isVisible = !document.hidden;
228
+ setIsDocumentVisible(isVisible);
229
+ if (!isVisible) {
230
+ cleaningAbortedRef.current = true;
231
+ }
232
+ };
233
+ document.addEventListener("visibilitychange", handleVisibilityChange);
234
+ return () => {
235
+ document.removeEventListener(
236
+ "visibilitychange",
237
+ handleVisibilityChange
238
+ );
239
+ };
240
+ }, []);
241
+ (0, import_react.useEffect)(() => {
242
+ initializeRobotPosition();
243
+ createRandomDirt();
244
+ }, []);
245
+ (0, import_react.useEffect)(() => {
246
+ if (autoStart && dirtPositions.length > 0 && robotState === "idle" && isDocumentVisible) {
247
+ cleanAndReturn();
248
+ }
249
+ }, [dirtPositions, robotState, isDocumentVisible, autoStart]);
250
+ (0, import_react.useEffect)(() => {
251
+ const robotElement = robotRef.current;
252
+ if (!robotElement) return;
253
+ robotElement.classList.remove(
254
+ RobotVacuum_default.idle,
255
+ RobotVacuum_default.cleaning,
256
+ RobotVacuum_default.returning
257
+ );
258
+ robotElement.classList.add(RobotVacuum_default[robotState]);
259
+ }, [robotState]);
260
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { ref: backgroundRef, className: RobotVacuum_default.container, children: [
261
+ dirtPositions.map(
262
+ (dirt, index) => !dirt.collected ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
263
+ "div",
264
+ {
265
+ className: RobotVacuum_default.dirt,
266
+ style: { left: `${dirt.x}px`, top: `${dirt.y}px` },
267
+ role: "presentation",
268
+ "aria-label": "dirt particle"
269
+ },
270
+ `dirt-${index}`
271
+ ) : null
272
+ ),
273
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: RobotVacuum_default.dock, role: "presentation", "aria-label": "dock" }),
274
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
275
+ "div",
276
+ {
277
+ ref: robotRef,
278
+ className: RobotVacuum_default.robot,
279
+ role: "presentation",
280
+ "aria-label": "robot vacuum",
281
+ children: [
282
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `${RobotVacuum_default.wheel} ${RobotVacuum_default.left}` }),
283
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `${RobotVacuum_default.wheel} ${RobotVacuum_default.right}` }),
284
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `${RobotVacuum_default.sensor} ${RobotVacuum_default.left}` }),
285
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `${RobotVacuum_default.sensor} ${RobotVacuum_default.right}` })
286
+ ]
287
+ }
288
+ )
289
+ ] });
290
+ }
291
+ );
292
+ Robotv2.displayName = "Robotv2";
293
+ var RobotVacuum_default2 = Robotv2;
294
+ // Annotate the CommonJS export names for ESM import in node:
295
+ 0 && (module.exports = {
296
+ RobotVacuum
297
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,276 @@
1
+ // src/RobotVacuum/RobotVacuum.tsx
2
+ import {
3
+ forwardRef,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ useRef,
7
+ useState
8
+ } from "react";
9
+
10
+ // src/RobotVacuum/RobotVacuum.module.css
11
+ var RobotVacuum_default = {};
12
+
13
+ // src/RobotVacuum/RobotVacuum.tsx
14
+ import { jsx, jsxs } from "react/jsx-runtime";
15
+ var Robotv2 = forwardRef(
16
+ ({
17
+ numberOfDirtBits = 5,
18
+ minSpeed = 0.5,
19
+ speedFactor = 100,
20
+ rotationDuration = 0.6,
21
+ autoStart = true,
22
+ onCleaningStart,
23
+ onCleaningComplete,
24
+ onDirtCollected
25
+ }, ref) => {
26
+ const [dirtPositions, setDirtPositions] = useState(
27
+ []
28
+ );
29
+ const [robotState, setRobotState] = useState("idle");
30
+ const [isDocumentVisible, setIsDocumentVisible] = useState(true);
31
+ const robotRef = useRef(null);
32
+ const backgroundRef = useRef(null);
33
+ const cleaningAbortedRef = useRef(false);
34
+ const ROBOT_SIZE = 25;
35
+ const ROBOT_HALF_SIZE = ROBOT_SIZE / 2;
36
+ const DOCKED_POSITION = {
37
+ x: 12,
38
+ y: 5
39
+ };
40
+ const initializeRobotPosition = async () => {
41
+ const robotElement = robotRef.current;
42
+ if (!robotElement) return;
43
+ robotElement.style.visibility = "visible";
44
+ robotElement.style.left = `${DOCKED_POSITION.x}px`;
45
+ robotElement.style.top = `${DOCKED_POSITION.y}px`;
46
+ robotElement.style.transform = "rotate(0deg)";
47
+ };
48
+ const getRandomPosition = (width, height) => ({
49
+ x: Math.floor(Math.random() * width),
50
+ y: Math.floor(Math.random() * height),
51
+ collected: false
52
+ });
53
+ const isPositionEmpty = (x, y) => {
54
+ const backgroundElement = backgroundRef.current;
55
+ if (!backgroundElement) return false;
56
+ const elementAtPosition = document.elementFromPoint(x, y);
57
+ return elementAtPosition === backgroundElement || elementAtPosition !== null && backgroundElement.contains(elementAtPosition);
58
+ };
59
+ const createRandomDirt = () => {
60
+ const backgroundElement = backgroundRef.current;
61
+ if (!backgroundElement) return;
62
+ const { width, height } = backgroundElement.getBoundingClientRect();
63
+ const newDirt = [];
64
+ let attempts = 0;
65
+ const maxAttempts = 1e3;
66
+ while (newDirt.length < numberOfDirtBits && attempts < maxAttempts) {
67
+ const position = getRandomPosition(width, height);
68
+ if (isPositionEmpty(position.x, position.y)) {
69
+ newDirt.push(position);
70
+ }
71
+ attempts++;
72
+ }
73
+ newDirt.sort((a, b) => a.y - b.y || a.x - b.x);
74
+ setDirtPositions(Object.freeze(newDirt));
75
+ };
76
+ const getRobotPosition = () => {
77
+ const robotElement = robotRef.current;
78
+ if (!robotElement) return null;
79
+ const rect = robotElement.getBoundingClientRect();
80
+ const transform = robotElement.style.transform;
81
+ const rotationMatch = transform.match(/rotate\(([^)]+)deg\)/);
82
+ const rotation = rotationMatch ? parseFloat(rotationMatch[1]) : 0;
83
+ return {
84
+ x: rect.left + rect.width / 2,
85
+ y: rect.top + rect.height / 2,
86
+ rotation
87
+ };
88
+ };
89
+ const calculateShortestRotation = (targetAngle) => {
90
+ const currentPos = getRobotPosition();
91
+ if (!currentPos) return targetAngle;
92
+ let current = currentPos.rotation % 360;
93
+ let target = targetAngle % 360;
94
+ if (current < 0) current += 360;
95
+ if (target < 0) target += 360;
96
+ let diff = target - current;
97
+ if (diff > 180) {
98
+ diff -= 360;
99
+ } else if (diff < -180) {
100
+ diff += 360;
101
+ }
102
+ return current + diff;
103
+ };
104
+ const rotateRobot = async (targetAngle) => {
105
+ const robotElement = robotRef.current;
106
+ if (!robotElement) return;
107
+ const shortestAngle = calculateShortestRotation(targetAngle);
108
+ robotElement.style.transition = `transform ${rotationDuration}s ease-in-out`;
109
+ robotElement.style.transform = `rotate(${shortestAngle}deg)`;
110
+ await new Promise(
111
+ (resolve) => setTimeout(resolve, rotationDuration * 1e3)
112
+ );
113
+ };
114
+ const calculateMovementTime = (distance) => {
115
+ return Math.max(Math.sqrt(distance / speedFactor), minSpeed);
116
+ };
117
+ const moveRobotToPosition = async (targetX, targetY) => {
118
+ const robotElement = robotRef.current;
119
+ if (!robotElement) return;
120
+ const currentPos = getRobotPosition();
121
+ if (!currentPos) return;
122
+ if (Math.abs(currentPos.x - targetX) > 1) {
123
+ const deltaX = targetX - currentPos.x;
124
+ const angle = deltaX > 0 ? 90 : -90;
125
+ await rotateRobot(angle);
126
+ const distanceX = Math.abs(deltaX);
127
+ const movementTime = calculateMovementTime(distanceX);
128
+ robotElement.style.transition = `left ${movementTime}s ease-in-out`;
129
+ robotElement.style.left = `${targetX - ROBOT_HALF_SIZE}px`;
130
+ await new Promise(
131
+ (resolve) => setTimeout(resolve, movementTime * 1e3)
132
+ );
133
+ }
134
+ if (Math.abs(currentPos.y - targetY) > 1) {
135
+ const deltaY = targetY - currentPos.y;
136
+ const angle = deltaY > 0 ? 180 : 0;
137
+ await rotateRobot(angle);
138
+ const distanceY = Math.abs(deltaY);
139
+ const movementTime = calculateMovementTime(distanceY);
140
+ robotElement.style.transition = `top ${movementTime}s ease-in-out`;
141
+ robotElement.style.top = `${targetY - ROBOT_HALF_SIZE}px`;
142
+ await new Promise(
143
+ (resolve) => setTimeout(resolve, movementTime * 1e3)
144
+ );
145
+ }
146
+ };
147
+ const cleanAndReturn = async () => {
148
+ if (dirtPositions.length === 0) return;
149
+ onCleaningStart?.();
150
+ cleaningAbortedRef.current = false;
151
+ const uncollectedDirt = [...dirtPositions];
152
+ let collectedCount = 0;
153
+ for (let i = 0; i < uncollectedDirt.length; i++) {
154
+ if (cleaningAbortedRef.current) {
155
+ setRobotState("idle");
156
+ return;
157
+ }
158
+ const dirt = uncollectedDirt[i];
159
+ if (dirt.collected) continue;
160
+ setRobotState("cleaning");
161
+ const currentPos2 = getRobotPosition();
162
+ if (currentPos2) {
163
+ await moveRobotToPosition(dirt.x, dirt.y);
164
+ if (cleaningAbortedRef.current) {
165
+ setRobotState("idle");
166
+ return;
167
+ }
168
+ setDirtPositions(
169
+ (prev) => Object.freeze(
170
+ prev.map((d, idx) => idx === i ? { ...d, collected: true } : d)
171
+ )
172
+ );
173
+ collectedCount++;
174
+ onDirtCollected?.(collectedCount, uncollectedDirt.length);
175
+ }
176
+ await new Promise((resolve) => setTimeout(resolve, 200));
177
+ }
178
+ if (cleaningAbortedRef.current) {
179
+ setRobotState("idle");
180
+ return;
181
+ }
182
+ setRobotState("returning");
183
+ const currentPos = getRobotPosition();
184
+ if (currentPos) {
185
+ const dockCenterX = DOCKED_POSITION.x + ROBOT_HALF_SIZE;
186
+ const dockCenterY = DOCKED_POSITION.y + ROBOT_HALF_SIZE;
187
+ await moveRobotToPosition(dockCenterX, dockCenterY);
188
+ }
189
+ setRobotState("idle");
190
+ onCleaningComplete?.();
191
+ };
192
+ useImperativeHandle(ref, () => ({
193
+ startCleaning: () => {
194
+ if (robotState === "idle" && isDocumentVisible) {
195
+ cleanAndReturn();
196
+ }
197
+ },
198
+ reset: () => {
199
+ cleaningAbortedRef.current = true;
200
+ setRobotState("idle");
201
+ initializeRobotPosition();
202
+ createRandomDirt();
203
+ }
204
+ }));
205
+ useEffect(() => {
206
+ const handleVisibilityChange = () => {
207
+ const isVisible = !document.hidden;
208
+ setIsDocumentVisible(isVisible);
209
+ if (!isVisible) {
210
+ cleaningAbortedRef.current = true;
211
+ }
212
+ };
213
+ document.addEventListener("visibilitychange", handleVisibilityChange);
214
+ return () => {
215
+ document.removeEventListener(
216
+ "visibilitychange",
217
+ handleVisibilityChange
218
+ );
219
+ };
220
+ }, []);
221
+ useEffect(() => {
222
+ initializeRobotPosition();
223
+ createRandomDirt();
224
+ }, []);
225
+ useEffect(() => {
226
+ if (autoStart && dirtPositions.length > 0 && robotState === "idle" && isDocumentVisible) {
227
+ cleanAndReturn();
228
+ }
229
+ }, [dirtPositions, robotState, isDocumentVisible, autoStart]);
230
+ useEffect(() => {
231
+ const robotElement = robotRef.current;
232
+ if (!robotElement) return;
233
+ robotElement.classList.remove(
234
+ RobotVacuum_default.idle,
235
+ RobotVacuum_default.cleaning,
236
+ RobotVacuum_default.returning
237
+ );
238
+ robotElement.classList.add(RobotVacuum_default[robotState]);
239
+ }, [robotState]);
240
+ return /* @__PURE__ */ jsxs("div", { ref: backgroundRef, className: RobotVacuum_default.container, children: [
241
+ dirtPositions.map(
242
+ (dirt, index) => !dirt.collected ? /* @__PURE__ */ jsx(
243
+ "div",
244
+ {
245
+ className: RobotVacuum_default.dirt,
246
+ style: { left: `${dirt.x}px`, top: `${dirt.y}px` },
247
+ role: "presentation",
248
+ "aria-label": "dirt particle"
249
+ },
250
+ `dirt-${index}`
251
+ ) : null
252
+ ),
253
+ /* @__PURE__ */ jsx("div", { className: RobotVacuum_default.dock, role: "presentation", "aria-label": "dock" }),
254
+ /* @__PURE__ */ jsxs(
255
+ "div",
256
+ {
257
+ ref: robotRef,
258
+ className: RobotVacuum_default.robot,
259
+ role: "presentation",
260
+ "aria-label": "robot vacuum",
261
+ children: [
262
+ /* @__PURE__ */ jsx("div", { className: `${RobotVacuum_default.wheel} ${RobotVacuum_default.left}` }),
263
+ /* @__PURE__ */ jsx("div", { className: `${RobotVacuum_default.wheel} ${RobotVacuum_default.right}` }),
264
+ /* @__PURE__ */ jsx("div", { className: `${RobotVacuum_default.sensor} ${RobotVacuum_default.left}` }),
265
+ /* @__PURE__ */ jsx("div", { className: `${RobotVacuum_default.sensor} ${RobotVacuum_default.right}` })
266
+ ]
267
+ }
268
+ )
269
+ ] });
270
+ }
271
+ );
272
+ Robotv2.displayName = "Robotv2";
273
+ var RobotVacuum_default2 = Robotv2;
274
+ export {
275
+ RobotVacuum_default2 as RobotVacuum
276
+ };
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "react-robot-vacuum",
3
+ "version": "1.0.0",
4
+ "description": "An animated React component featuring a robot vacuum that autonomously cleans your page by collecting dirt particles",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./styles.css": "./dist/RobotVacuum.module.css"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsup src/index.tsx --format cjs,esm --dts --external react --external react-dom && cp src/RobotVacuum/RobotVacuum.module.css dist/",
22
+ "dev": "tsup src/index.tsx --format cjs,esm --dts --watch",
23
+ "vite:dev": "vite",
24
+ "vite:build": "vite build",
25
+ "lint": "eslint src/**/*.{ts,tsx}",
26
+ "lint:fix": "eslint src/**/*.{ts,tsx} --fix",
27
+ "prepublishOnly": "npm run build",
28
+ "test": "echo \"Error: no test specified\" && exit 1"
29
+ },
30
+ "keywords": [
31
+ "react",
32
+ "component",
33
+ "robot",
34
+ "vacuum",
35
+ "animation",
36
+ "interactive",
37
+ "typescript",
38
+ "react19",
39
+ "css-modules"
40
+ ],
41
+ "author": "ZhanmuTW",
42
+ "license": "MIT",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/zhanmu-tw/react-robot-vacuum.git"
46
+ },
47
+ "bugs": {
48
+ "url": "https://github.com/zhanmu-tw/react-robot-vacuum/issues"
49
+ },
50
+ "homepage": "https://github.com/zhanmu-tw/react-robot-vacuum#readme",
51
+ "peerDependencies": {
52
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
53
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "@eslint/js": "^9.39.2",
57
+ "@types/eslint__js": "^8.42.3",
58
+ "@types/react": "^18.2.0",
59
+ "@types/react-dom": "^18.2.0",
60
+ "eslint": "^9.39.2",
61
+ "eslint-plugin-react": "^7.37.5",
62
+ "eslint-plugin-react-hooks": "^7.0.1",
63
+ "react": "^18.2.0",
64
+ "react-dom": "^18.2.0",
65
+ "tsup": "^8.0.0",
66
+ "typescript": "^5.3.0",
67
+ "typescript-eslint": "^8.51.0",
68
+ "vite": "^7.3.0"
69
+ }
70
+ }