maomao-terminal 1.0.1 → 1.0.3
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 +150 -146
- package/dist/commands/baseCommands.js +17 -17
- package/dist/components/Terminal.css +183 -185
- package/dist/components/Terminal.js +14 -53
- package/dist/hooks/useDraggable.d.ts +14 -0
- package/dist/hooks/useDraggable.js +54 -0
- package/dist/hooks/useKeys.d.ts +6 -0
- package/dist/hooks/useKeys.js +61 -0
- package/dist/hooks/useStatus.d.ts +2 -0
- package/dist/hooks/useStatus.js +10 -1
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -1,146 +1,150 @@
|
|
|
1
|
-
# 🐱 MaoMao Terminal
|
|
2
|
-
|
|
3
|
-
> A dynamic, context-aware, and draggable terminal component for React applications.
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/maomao-terminal)
|
|
6
|
-
[](https://opensource.org/licenses/MIT)
|
|
7
|
-
|
|
8
|
-
**MaoMao Terminal** is not just a UI toy; it's a powerful debugging and interaction tool. It allows you to inject commands dynamically from any component in your application tree, making it context-aware.
|
|
9
|
-
|
|
10
|
-
## ✨ Features
|
|
11
|
-
|
|
12
|
-
- **Context-Aware**: Register commands from any component using the `useTerminalContext` hook.
|
|
13
|
-
- **Draggable & Resizable**: Fully interactive window management (minimize, maximize, drag, resize).
|
|
14
|
-
- **Built-in Utilities**: Comes with `inspect`, `location`, `storage`, `time`, and more.
|
|
15
|
-
- **Command History**: Navigate through previous commands with Up/Down arrows.
|
|
16
|
-
- **Animations**: Smooth transitions powered by `framer-motion`.
|
|
17
|
-
- **TypeScript**: Fully typed for excellent developer experience.
|
|
18
|
-
|
|
19
|
-
## ✅ Compatibility
|
|
20
|
-
|
|
21
|
-
| Technology | Support | Note |
|
|
22
|
-
|------------|---------|------|
|
|
23
|
-
| **React** | v16.8+ | Requires Hooks support (`useState`, `useEffect`) |
|
|
24
|
-
| **Next.js**| v13+ | Fully aligned with App Router (`'use client'`) & Pages Router |
|
|
25
|
-
| **Vite** | All versions | Works out of the box |
|
|
26
|
-
|
|
27
|
-
## 📦 Installation
|
|
28
|
-
|
|
29
|
-
This library depends on `react`, `react-dom`, and `framer-motion`.
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
# npm
|
|
33
|
-
npm install maomao-terminal framer-motion
|
|
34
|
-
|
|
35
|
-
# yarn
|
|
36
|
-
yarn add maomao-terminal framer-motion
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## 🚀 Quick Start
|
|
40
|
-
|
|
41
|
-
### 1. Wrap your application with `TerminalProvider`
|
|
42
|
-
|
|
43
|
-
This provider manages the state of global and dynamic commands.
|
|
44
|
-
|
|
45
|
-
```tsx
|
|
46
|
-
import React, { useState } from 'react';
|
|
47
|
-
import { TerminalProvider, Terminal } from 'maomao-terminal';
|
|
48
|
-
// Import default styles (required)
|
|
49
|
-
import 'maomao-terminal/dist/components/Terminal.css';
|
|
50
|
-
|
|
51
|
-
function App() {
|
|
52
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
53
|
-
|
|
54
|
-
return (
|
|
55
|
-
<TerminalProvider>
|
|
56
|
-
<div className="app-container">
|
|
57
|
-
<button onClick={() => setIsOpen(true)}>Open Terminal</button>
|
|
58
|
-
|
|
59
|
-
<Terminal
|
|
60
|
-
isOpen={isOpen}
|
|
61
|
-
onClose={() => setIsOpen(false)}
|
|
62
|
-
/>
|
|
63
|
-
|
|
64
|
-
{/* Your app content */}
|
|
65
|
-
</div>
|
|
66
|
-
</TerminalProvider>
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export default App;
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## 💡 Advanced Usage: Dynamic Commands
|
|
74
|
-
|
|
75
|
-
The real power of MaoMao Terminal lies in its ability to "learn" commands from the components currently rendered on the screen.
|
|
76
|
-
|
|
77
|
-
Use `useTerminalContext` to register commands that are only available when a specific component is mounted.
|
|
78
|
-
|
|
79
|
-
```tsx
|
|
80
|
-
import { useEffect } from 'react';
|
|
81
|
-
import { useTerminalContext } from 'maomao-terminal';
|
|
82
|
-
|
|
83
|
-
const UserProfile = ({ userId }) => {
|
|
84
|
-
const {
|
|
85
|
-
|
|
86
|
-
useEffect(() => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
|
113
|
-
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
1
|
+
# 🐱 MaoMao Terminal v1.0.2
|
|
2
|
+
|
|
3
|
+
> A dynamic, context-aware, and draggable terminal component for React applications.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/maomao-terminal)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
**MaoMao Terminal** is not just a UI toy; it's a powerful debugging and interaction tool. It allows you to inject commands dynamically from any component in your application tree, making it context-aware.
|
|
9
|
+
|
|
10
|
+
## ✨ Features
|
|
11
|
+
|
|
12
|
+
- **Context-Aware**: Register commands from any component using the `useTerminalContext` hook.
|
|
13
|
+
- **Draggable & Resizable**: Fully interactive window management (minimize, maximize, drag, resize).
|
|
14
|
+
- **Built-in Utilities**: Comes with `inspect`, `location`, `storage`, `time`, and more.
|
|
15
|
+
- **Command History**: Navigate through previous commands with Up/Down arrows.
|
|
16
|
+
- **Animations**: Smooth transitions powered by `framer-motion`.
|
|
17
|
+
- **TypeScript**: Fully typed for excellent developer experience.
|
|
18
|
+
|
|
19
|
+
## ✅ Compatibility
|
|
20
|
+
|
|
21
|
+
| Technology | Support | Note |
|
|
22
|
+
|------------|---------|------|
|
|
23
|
+
| **React** | v16.8+ | Requires Hooks support (`useState`, `useEffect`) |
|
|
24
|
+
| **Next.js**| v13+ | Fully aligned with App Router (`'use client'`) & Pages Router |
|
|
25
|
+
| **Vite** | All versions | Works out of the box |
|
|
26
|
+
|
|
27
|
+
## 📦 Installation
|
|
28
|
+
|
|
29
|
+
This library depends on `react`, `react-dom`, and `framer-motion`.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# npm
|
|
33
|
+
npm install maomao-terminal framer-motion
|
|
34
|
+
|
|
35
|
+
# yarn
|
|
36
|
+
yarn add maomao-terminal framer-motion
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 🚀 Quick Start
|
|
40
|
+
|
|
41
|
+
### 1. Wrap your application with `TerminalProvider`
|
|
42
|
+
|
|
43
|
+
This provider manages the state of global and dynamic commands.
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import React, { useState } from 'react';
|
|
47
|
+
import { TerminalProvider, Terminal } from 'maomao-terminal';
|
|
48
|
+
// Import default styles (required)
|
|
49
|
+
import 'maomao-terminal/dist/components/Terminal.css';
|
|
50
|
+
|
|
51
|
+
function App() {
|
|
52
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<TerminalProvider>
|
|
56
|
+
<div className="app-container">
|
|
57
|
+
<button onClick={() => setIsOpen(true)}>Open Terminal</button>
|
|
58
|
+
|
|
59
|
+
<Terminal
|
|
60
|
+
isOpen={isOpen}
|
|
61
|
+
onClose={() => setIsOpen(false)}
|
|
62
|
+
/>
|
|
63
|
+
|
|
64
|
+
{/* Your app content */}
|
|
65
|
+
</div>
|
|
66
|
+
</TerminalProvider>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default App;
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 💡 Advanced Usage: Dynamic Commands
|
|
74
|
+
|
|
75
|
+
The real power of MaoMao Terminal lies in its ability to "learn" commands from the components currently rendered on the screen.
|
|
76
|
+
|
|
77
|
+
Use `useTerminalContext` to register commands that are only available when a specific component is mounted.
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import { useEffect } from 'react';
|
|
81
|
+
import { useTerminalContext } from 'maomao-terminal';
|
|
82
|
+
|
|
83
|
+
const UserProfile = ({ userId }) => {
|
|
84
|
+
const { registerDynamicCommands, unregisterDynamicCommands } = useTerminalContext();
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
const commands = [
|
|
88
|
+
{
|
|
89
|
+
command: 'getUser',
|
|
90
|
+
response: async (args) => {
|
|
91
|
+
// You can clear data, fetch API, or log info
|
|
92
|
+
return `Current User ID: ${userId}`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
// Register a command specific to this component
|
|
98
|
+
registerDynamicCommands(commands);
|
|
99
|
+
|
|
100
|
+
// Cleanup when component unmounts
|
|
101
|
+
return () => unregisterDynamicCommands(commands);
|
|
102
|
+
}, [userId, registerDynamicCommands, unregisterDynamicCommands]);
|
|
103
|
+
|
|
104
|
+
return <div>User Profile Component</div>;
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Now, when `UserProfile` is on screen, you can type `getUser` in the terminal!
|
|
109
|
+
|
|
110
|
+
## 🛠 Built-in Commands
|
|
111
|
+
|
|
112
|
+
| Command | Description | Args |
|
|
113
|
+
|---------|-------------|------|
|
|
114
|
+
| `help` | Lists available commands | |
|
|
115
|
+
| `clear` | Clears the terminal output | |
|
|
116
|
+
| `echo` | Repeats your input | `[text]` |
|
|
117
|
+
| `inspect` | Inspects global window properties | |
|
|
118
|
+
| `location` | Shows current URL | |
|
|
119
|
+
| `storage` | Inspects or clears localStorage | `[clear]` |
|
|
120
|
+
| `viewport` | Shows window dimensions | |
|
|
121
|
+
| `time` | Shows current local time | |
|
|
122
|
+
| `about` | Library information | |
|
|
123
|
+
|
|
124
|
+
## 🎨 Customization
|
|
125
|
+
|
|
126
|
+
The terminal comes with a default dark theme. You can override styles by targeting the CSS classes defined in `Terminal.css`.
|
|
127
|
+
|
|
128
|
+
Top-level classes:
|
|
129
|
+
- `.terminal-container`: The main window.
|
|
130
|
+
- `.terminal-header`: The drag handle and title bar.
|
|
131
|
+
- `.terminal-body`: The content area.
|
|
132
|
+
- `.terminal-input`: The command input field.
|
|
133
|
+
|
|
134
|
+
## 🤝 Contributing
|
|
135
|
+
|
|
136
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
137
|
+
|
|
138
|
+
1. Fork the repository
|
|
139
|
+
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
|
140
|
+
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
|
141
|
+
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
|
142
|
+
5. Open a Pull Request
|
|
143
|
+
|
|
144
|
+
## 📄 License
|
|
145
|
+
|
|
146
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
Created with ❤️ by **Juan Manuel Camacho Sanchez**
|
|
@@ -62,23 +62,23 @@ export const baseCommands = [
|
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
command: 'about',
|
|
65
|
-
response: () => `
|
|
66
|
-
══════════════════════════════════════════════════════
|
|
67
|
-
🐱 MaoMao Terminal
|
|
68
|
-
Empowering Front-End Developers
|
|
69
|
-
══════════════════════════════════════════════════════
|
|
70
|
-
|
|
71
|
-
v1.0.
|
|
72
|
-
|
|
73
|
-
A dynamic, context-aware terminal overlay for React.
|
|
74
|
-
Seamlessly inject commands from your components and
|
|
75
|
-
supercharge your debugging workflow.
|
|
76
|
-
|
|
77
|
-
Created by Juan Manuel Camacho Sanchez
|
|
78
|
-
|
|
79
|
-
猫
|
|
80
|
-
|
|
81
|
-
══════════════════════════════════════════════════════
|
|
65
|
+
response: () => `
|
|
66
|
+
══════════════════════════════════════════════════════
|
|
67
|
+
🐱 MaoMao Terminal
|
|
68
|
+
Empowering Front-End Developers
|
|
69
|
+
══════════════════════════════════════════════════════
|
|
70
|
+
|
|
71
|
+
v1.0.2
|
|
72
|
+
|
|
73
|
+
A dynamic, context-aware terminal overlay for React.
|
|
74
|
+
Seamlessly inject commands from your components and
|
|
75
|
+
supercharge your debugging workflow.
|
|
76
|
+
|
|
77
|
+
Created by Juan Manuel Camacho Sanchez
|
|
78
|
+
|
|
79
|
+
猫
|
|
80
|
+
|
|
81
|
+
══════════════════════════════════════════════════════
|
|
82
82
|
`,
|
|
83
83
|
},
|
|
84
84
|
];
|
|
@@ -1,186 +1,184 @@
|
|
|
1
|
-
/* ================================
|
|
2
|
-
|
|
3
|
-
================================ */
|
|
4
|
-
.terminal-container {
|
|
5
|
-
position: fixed;
|
|
6
|
-
background: rgba(10, 10, 10, 0.5);
|
|
7
|
-
color: #
|
|
8
|
-
border-radius: 8px;
|
|
9
|
-
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.6);
|
|
10
|
-
backdrop-filter: blur(12px);
|
|
11
|
-
-webkit-backdrop-filter: blur(12px);
|
|
12
|
-
display: flex;
|
|
13
|
-
flex-direction: column;
|
|
14
|
-
white-space: pre-wrap;
|
|
15
|
-
z-index: 50;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
border
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
font-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
border-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
.terminal-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
185
|
-
background-color: #52525b;
|
|
1
|
+
/* ================================
|
|
2
|
+
CONTAINER
|
|
3
|
+
================================ */
|
|
4
|
+
.terminal-container {
|
|
5
|
+
position: fixed;
|
|
6
|
+
background: rgba(10, 10, 10, 0.5);
|
|
7
|
+
color: #65fdb9;
|
|
8
|
+
border-radius: 8px;
|
|
9
|
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.6);
|
|
10
|
+
backdrop-filter: blur(12px);
|
|
11
|
+
-webkit-backdrop-filter: blur(12px);
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
white-space: pre-wrap;
|
|
15
|
+
z-index: 50;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
transition: box-shadow 0.2s;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.terminal-container:focus-within {
|
|
21
|
+
box-shadow: 0 15px 50px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(34, 197, 94, 0.3);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.terminal-container.maximized {
|
|
25
|
+
border-radius: 0;
|
|
26
|
+
border: none;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* ================================
|
|
30
|
+
HEADER
|
|
31
|
+
================================ */
|
|
32
|
+
.terminal-header {
|
|
33
|
+
background: #18181b;
|
|
34
|
+
padding: 0 12px;
|
|
35
|
+
cursor: grab;
|
|
36
|
+
user-select: none;
|
|
37
|
+
display: flex;
|
|
38
|
+
justify-content: space-between;
|
|
39
|
+
align-items: center;
|
|
40
|
+
border-bottom: 1px solid #333;
|
|
41
|
+
height: 42px;
|
|
42
|
+
flex-shrink: 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.terminal-header:active {
|
|
46
|
+
cursor: grabbing;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.terminal-title {
|
|
50
|
+
font-weight: 600;
|
|
51
|
+
font-size: 0.85rem;
|
|
52
|
+
color: #a1a1aa;
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 8px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.terminal-controls {
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
gap: 6px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.terminal-btn {
|
|
65
|
+
background: transparent;
|
|
66
|
+
border: none;
|
|
67
|
+
color: #71717a;
|
|
68
|
+
/* Zinc 500 */
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
padding: 6px;
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
border-radius: 4px;
|
|
75
|
+
transition: all 0.2s;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.terminal-btn:hover {
|
|
79
|
+
background: rgba(255, 255, 255, 0.1);
|
|
80
|
+
color: #e4e4e7;
|
|
81
|
+
/* Zinc 200 */
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.terminal-btn.close:hover {
|
|
85
|
+
background: #ef4444;
|
|
86
|
+
color: white;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ================================
|
|
90
|
+
BODY
|
|
91
|
+
================================ */
|
|
92
|
+
.terminal-body {
|
|
93
|
+
flex: 1;
|
|
94
|
+
padding: 12px;
|
|
95
|
+
overflow-y: auto;
|
|
96
|
+
background: rgba(0, 0, 0, 0.4);
|
|
97
|
+
position: relative;
|
|
98
|
+
font-size: 0.95rem;
|
|
99
|
+
line-height: 1.6;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.terminal-line {
|
|
103
|
+
margin-bottom: 4px;
|
|
104
|
+
word-break: break-all;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* ================================
|
|
108
|
+
INPUT
|
|
109
|
+
================================ */
|
|
110
|
+
.terminal-input-wrapper {
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
border-top: 1px solid #333;
|
|
114
|
+
background: rgba(0, 0, 0, 0.8);
|
|
115
|
+
padding: 8px 12px;
|
|
116
|
+
min-height: 44px;
|
|
117
|
+
flex-shrink: 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.terminal-prompt {
|
|
121
|
+
color: #65fdb9;
|
|
122
|
+
font-weight: bold;
|
|
123
|
+
margin-right: 8px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.terminal-input {
|
|
127
|
+
flex: 1;
|
|
128
|
+
background: transparent;
|
|
129
|
+
color: white;
|
|
130
|
+
border: none;
|
|
131
|
+
outline: none;
|
|
132
|
+
font-family: inherit;
|
|
133
|
+
font-size: inherit;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* ================================
|
|
137
|
+
RESIZE HANDLE
|
|
138
|
+
================================ */
|
|
139
|
+
.terminal-resize-handle {
|
|
140
|
+
width: 16px;
|
|
141
|
+
height: 16px;
|
|
142
|
+
position: absolute;
|
|
143
|
+
bottom: 0;
|
|
144
|
+
right: 0;
|
|
145
|
+
cursor: se-resize;
|
|
146
|
+
z-index: 60;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.terminal-resize-handle::after {
|
|
150
|
+
content: '';
|
|
151
|
+
position: absolute;
|
|
152
|
+
bottom: 3px;
|
|
153
|
+
right: 3px;
|
|
154
|
+
width: 6px;
|
|
155
|
+
height: 6px;
|
|
156
|
+
border-bottom: 2px solid #555;
|
|
157
|
+
border-right: 2px solid #555;
|
|
158
|
+
border-radius: 0 0 2px 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.terminal-resize-handle:hover::after {
|
|
162
|
+
border-color: #65fdb9;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* ================================
|
|
166
|
+
CUSTOM SCROLLBAR
|
|
167
|
+
================================ */
|
|
168
|
+
.terminal-scrollbar::-webkit-scrollbar {
|
|
169
|
+
width: 10px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.terminal-scrollbar::-webkit-scrollbar-track {
|
|
173
|
+
background: #18181b;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.terminal-scrollbar::-webkit-scrollbar-thumb {
|
|
177
|
+
background-color: #3f3f46;
|
|
178
|
+
border: 2px solid #18181b;
|
|
179
|
+
border-radius: 6px;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.terminal-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
183
|
+
background-color: #52525b;
|
|
186
184
|
}
|
|
@@ -1,70 +1,23 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { useRef, useEffect } from 'react';
|
|
4
4
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
5
|
-
import { useTerminal } from '../hooks/useTerminal';
|
|
6
5
|
import { useStatus } from '../hooks/useStatus';
|
|
7
6
|
import { CloseButton } from './icons/Close';
|
|
8
7
|
import { MaximizeButton } from './icons/Maximize';
|
|
9
8
|
import { MinimizeButton } from './icons/Minimize';
|
|
10
9
|
import './Terminal.css';
|
|
10
|
+
import { useKeys } from '../hooks/useKeys';
|
|
11
11
|
const Terminal = ({ isOpen, onClose }) => {
|
|
12
12
|
const terminalRef = useRef(null);
|
|
13
13
|
const scrollRef = useRef(null);
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const { history, commandHistory, executeCommand } = useTerminal();
|
|
17
|
-
const { size, position, drag, resize, isMaximized, isMinimized, minimize, maximize, restore } = useStatus();
|
|
14
|
+
const { history, input, onChange, onKeyDown } = useKeys();
|
|
15
|
+
const { size, position, drag, resize, isMaximized, isMinimized, isDragging, isResizing, minimize, maximize, restore } = useStatus();
|
|
18
16
|
useEffect(() => {
|
|
19
17
|
if (scrollRef.current)
|
|
20
18
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
21
19
|
}, [history]);
|
|
22
|
-
|
|
23
|
-
switch (e.key) {
|
|
24
|
-
case 'Enter':
|
|
25
|
-
enter(e);
|
|
26
|
-
break;
|
|
27
|
-
case 'ArrowUp':
|
|
28
|
-
arrowUp(e);
|
|
29
|
-
break;
|
|
30
|
-
case 'ArrowDown':
|
|
31
|
-
arrowDown(e);
|
|
32
|
-
break;
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
const enter = (e) => {
|
|
36
|
-
e.preventDefault();
|
|
37
|
-
if (!input.trim())
|
|
38
|
-
return;
|
|
39
|
-
executeCommand(input);
|
|
40
|
-
setInput('');
|
|
41
|
-
setHistoryPointer(null);
|
|
42
|
-
};
|
|
43
|
-
const arrowUp = (e) => {
|
|
44
|
-
e.preventDefault();
|
|
45
|
-
if (commandHistory.length === 0)
|
|
46
|
-
return;
|
|
47
|
-
const newPointer = historyPointer === null
|
|
48
|
-
? commandHistory.length - 1
|
|
49
|
-
: Math.max(0, historyPointer - 1);
|
|
50
|
-
setHistoryPointer(newPointer);
|
|
51
|
-
setInput(commandHistory[newPointer]);
|
|
52
|
-
};
|
|
53
|
-
const arrowDown = (e) => {
|
|
54
|
-
e.preventDefault();
|
|
55
|
-
if (historyPointer === null)
|
|
56
|
-
return;
|
|
57
|
-
const newPointer = historyPointer + 1;
|
|
58
|
-
if (newPointer >= commandHistory.length) {
|
|
59
|
-
setHistoryPointer(null);
|
|
60
|
-
setInput('');
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
setHistoryPointer(newPointer);
|
|
64
|
-
setInput(commandHistory[newPointer]);
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
return (_jsx(AnimatePresence, { children: isOpen && (_jsxs(motion.div, { ref: terminalRef, className: "terminal-container", initial: { opacity: 0, scale: 0.9 }, animate: {
|
|
20
|
+
return (_jsx(AnimatePresence, { children: isOpen && (_jsxs(motion.div, { ref: terminalRef, className: "terminal-container font-mono", initial: { opacity: 0, scale: 0.9 }, animate: {
|
|
68
21
|
opacity: 1,
|
|
69
22
|
scale: 1,
|
|
70
23
|
width: isMaximized ? window.innerWidth : size.width,
|
|
@@ -72,6 +25,14 @@ const Terminal = ({ isOpen, onClose }) => {
|
|
|
72
25
|
left: isMaximized ? 0 : position.x,
|
|
73
26
|
top: isMaximized ? 0 : position.y,
|
|
74
27
|
borderRadius: isMaximized ? 0 : 8
|
|
75
|
-
}, exit: { opacity: 0, scale: 0.9 }, transition: {
|
|
28
|
+
}, exit: { opacity: 0, scale: 0.9 }, transition: {
|
|
29
|
+
type: 'spring',
|
|
30
|
+
stiffness: 400,
|
|
31
|
+
damping: 35,
|
|
32
|
+
width: isResizing ? { duration: 0 } : { type: 'spring', stiffness: 400, damping: 35 },
|
|
33
|
+
height: isResizing ? { duration: 0 } : { type: 'spring', stiffness: 400, damping: 35 },
|
|
34
|
+
left: isDragging ? { duration: 0 } : { type: 'spring', stiffness: 400, damping: 35 },
|
|
35
|
+
top: isDragging ? { duration: 0 } : { type: 'spring', stiffness: 400, damping: 35 }
|
|
36
|
+
}, children: [_jsxs("div", { className: "terminal-header", onMouseDown: drag, children: [_jsxs("span", { className: "terminal-title", children: [_jsx("span", { className: "w-3 h-3 rounded-full bg-red-500 block" }), _jsx("span", { className: "w-3 h-3 rounded-full bg-yellow-500 block" }), _jsx("span", { className: "w-3 h-3 rounded-full bg-green-500 block" }), _jsx("span", { style: { marginLeft: 8 }, children: "MaoMaoTerminal:~" })] }), _jsxs("div", { className: "terminal-controls", children: [_jsx(MinimizeButton, { onClick: minimize, isMinimized: isMinimized }), _jsx(MaximizeButton, { onClick: isMaximized ? restore : maximize, isMaximized: isMaximized }), _jsx(CloseButton, { onClose: onClose })] })] }), _jsx("div", { ref: scrollRef, className: "terminal-body terminal-scrollbar", children: history.map((line, i) => (_jsxs("div", { className: "terminal-line", children: [_jsx("span", { className: "terminal-prompt", children: ">" }), line] }, i))) }), _jsxs("div", { className: "terminal-input-wrapper", children: [_jsx("span", { className: "terminal-prompt", children: "$" }), _jsx("input", { className: "terminal-input", autoFocus: true, placeholder: "Type a command...", value: input, onChange: e => onChange(e.target.value), onKeyDown: onKeyDown })] }), !isMaximized && !isMinimized && (_jsx("div", { className: "terminal-resize-handle", onMouseDown: resize }))] })) }));
|
|
76
37
|
};
|
|
77
38
|
export default Terminal;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type Position = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
};
|
|
5
|
+
type Size = {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function useDraggable(size: Size): {
|
|
10
|
+
position: Position;
|
|
11
|
+
isDragging: boolean;
|
|
12
|
+
startDrag: (e: React.MouseEvent) => void;
|
|
13
|
+
};
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
export function useDraggable(size) {
|
|
3
|
+
const [position, setPosition] = useState({ x: 100, y: 100 });
|
|
4
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
5
|
+
const positionRef = useRef(position);
|
|
6
|
+
const offsetRef = useRef({ x: 0, y: 0 });
|
|
7
|
+
const frameRef = useRef(null);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
positionRef.current = position;
|
|
10
|
+
}, [position]);
|
|
11
|
+
const clamp = (v, min, max) => Math.min(Math.max(v, min), max);
|
|
12
|
+
const startDrag = useCallback((e) => {
|
|
13
|
+
e.preventDefault();
|
|
14
|
+
setIsDragging(true);
|
|
15
|
+
offsetRef.current = {
|
|
16
|
+
x: e.clientX - positionRef.current.x,
|
|
17
|
+
y: e.clientY - positionRef.current.y
|
|
18
|
+
};
|
|
19
|
+
}, []);
|
|
20
|
+
const onMouseMove = useCallback((e) => {
|
|
21
|
+
const next = {
|
|
22
|
+
x: clamp(e.clientX - offsetRef.current.x, 0, window.innerWidth - size.width),
|
|
23
|
+
y: clamp(e.clientY - offsetRef.current.y, 0, window.innerHeight - size.height)
|
|
24
|
+
};
|
|
25
|
+
positionRef.current = next;
|
|
26
|
+
if (frameRef.current === null) {
|
|
27
|
+
frameRef.current = requestAnimationFrame(() => {
|
|
28
|
+
setPosition(positionRef.current);
|
|
29
|
+
frameRef.current = null;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}, [size]);
|
|
33
|
+
const stopDrag = useCallback(() => {
|
|
34
|
+
setIsDragging(false);
|
|
35
|
+
if (frameRef.current)
|
|
36
|
+
cancelAnimationFrame(frameRef.current);
|
|
37
|
+
frameRef.current = null;
|
|
38
|
+
}, []);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!isDragging)
|
|
41
|
+
return;
|
|
42
|
+
window.addEventListener('mousemove', onMouseMove);
|
|
43
|
+
window.addEventListener('mouseup', stopDrag);
|
|
44
|
+
return () => {
|
|
45
|
+
window.removeEventListener('mousemove', onMouseMove);
|
|
46
|
+
window.removeEventListener('mouseup', stopDrag);
|
|
47
|
+
};
|
|
48
|
+
}, [isDragging, onMouseMove, stopDrag]);
|
|
49
|
+
return {
|
|
50
|
+
position,
|
|
51
|
+
isDragging,
|
|
52
|
+
startDrag
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import { useTerminal } from "./useTerminal";
|
|
3
|
+
export function useKeys() {
|
|
4
|
+
const [input, setInput] = useState("");
|
|
5
|
+
const [historyPointer, setHistoryPointer] = useState(null);
|
|
6
|
+
const { history, commandHistory, executeCommand } = useTerminal();
|
|
7
|
+
const handleEnter = useCallback((e) => {
|
|
8
|
+
e.preventDefault();
|
|
9
|
+
const value = input.trim();
|
|
10
|
+
if (!value)
|
|
11
|
+
return;
|
|
12
|
+
executeCommand(value);
|
|
13
|
+
setInput("");
|
|
14
|
+
setHistoryPointer(null);
|
|
15
|
+
}, [input, executeCommand]);
|
|
16
|
+
const handleArrowUp = useCallback((e) => {
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
if (commandHistory.length === 0)
|
|
19
|
+
return;
|
|
20
|
+
setHistoryPointer(prev => {
|
|
21
|
+
const next = prev === null
|
|
22
|
+
? commandHistory.length - 1
|
|
23
|
+
: Math.max(0, prev - 1);
|
|
24
|
+
setInput(commandHistory[next]);
|
|
25
|
+
return next;
|
|
26
|
+
});
|
|
27
|
+
}, [commandHistory]);
|
|
28
|
+
const handleArrowDown = useCallback((e) => {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
setHistoryPointer(prev => {
|
|
31
|
+
if (prev === null)
|
|
32
|
+
return null;
|
|
33
|
+
const next = prev + 1;
|
|
34
|
+
if (next >= commandHistory.length) {
|
|
35
|
+
setInput("");
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
setInput(commandHistory[next]);
|
|
39
|
+
return next;
|
|
40
|
+
});
|
|
41
|
+
}, [commandHistory]);
|
|
42
|
+
const handleKeyDown = useCallback((e) => {
|
|
43
|
+
switch (e.key) {
|
|
44
|
+
case "Enter":
|
|
45
|
+
handleEnter(e);
|
|
46
|
+
break;
|
|
47
|
+
case "ArrowUp":
|
|
48
|
+
handleArrowUp(e);
|
|
49
|
+
break;
|
|
50
|
+
case "ArrowDown":
|
|
51
|
+
handleArrowDown(e);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}, [handleEnter, handleArrowUp, handleArrowDown]);
|
|
55
|
+
return {
|
|
56
|
+
input,
|
|
57
|
+
onKeyDown: handleKeyDown,
|
|
58
|
+
onChange: setInput,
|
|
59
|
+
history
|
|
60
|
+
};
|
|
61
|
+
}
|
package/dist/hooks/useStatus.js
CHANGED
|
@@ -6,12 +6,15 @@ export function useStatus() {
|
|
|
6
6
|
const [position, setPosition] = useState({ x: 100, y: 100 });
|
|
7
7
|
const [isMinimized, setIsMinimized] = useState(false);
|
|
8
8
|
const [isMaximized, setIsMaximized] = useState(false);
|
|
9
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
10
|
+
const [isResizing, setIsResizing] = useState(false);
|
|
9
11
|
const isDraggingRef = useRef(false);
|
|
10
12
|
const isResizingRef = useRef(false);
|
|
11
13
|
const dragStart = useRef({ x: 0, y: 0 });
|
|
12
14
|
const resizeStart = useRef({ x: 0, y: 0, width: 0, height: 0 });
|
|
13
15
|
const lastFloatingSize = useRef({ width: 500, height: 300 });
|
|
14
16
|
const lastFloatingPosition = useRef({ x: 100, y: 100 });
|
|
17
|
+
const preventSelect = (e) => e.preventDefault();
|
|
15
18
|
useEffect(() => {
|
|
16
19
|
if (!isMaximized && !isMinimized) {
|
|
17
20
|
lastFloatingSize.current = size;
|
|
@@ -37,6 +40,8 @@ export function useStatus() {
|
|
|
37
40
|
const end = useCallback(() => {
|
|
38
41
|
isDraggingRef.current = false;
|
|
39
42
|
isResizingRef.current = false;
|
|
43
|
+
setIsDragging(false);
|
|
44
|
+
setIsResizing(false);
|
|
40
45
|
}, []);
|
|
41
46
|
useEffect(() => {
|
|
42
47
|
const handleMove = (e) => {
|
|
@@ -72,17 +77,21 @@ export function useStatus() {
|
|
|
72
77
|
const drag = (e) => {
|
|
73
78
|
if (isMaximized)
|
|
74
79
|
return;
|
|
80
|
+
document.addEventListener('selectstart', preventSelect);
|
|
75
81
|
isDraggingRef.current = true;
|
|
82
|
+
setIsDragging(true);
|
|
76
83
|
dragStart.current = { x: e.clientX - position.x, y: e.clientY - position.y };
|
|
84
|
+
document.removeEventListener('selectstart', preventSelect);
|
|
77
85
|
};
|
|
78
86
|
const resize = (e) => {
|
|
79
87
|
if (isMaximized || isMinimized)
|
|
80
88
|
return;
|
|
81
89
|
isResizingRef.current = true;
|
|
90
|
+
setIsResizing(true);
|
|
82
91
|
resizeStart.current = { x: e.clientX, y: e.clientY, width: size.width, height: size.height };
|
|
83
92
|
};
|
|
84
93
|
return {
|
|
85
|
-
size, position, isMinimized, isMaximized,
|
|
94
|
+
size, position, isMinimized, isMaximized, isDragging, isResizing,
|
|
86
95
|
drag, resize, minimize, maximize, restore
|
|
87
96
|
};
|
|
88
97
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "maomao-terminal",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "A React-style terminal component library",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -22,19 +22,21 @@
|
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
24
|
"build": "tsc && shx mkdir -p dist/components && shx cp src/components/*.css dist/components/",
|
|
25
|
-
"watch": "tsc --watch",
|
|
25
|
+
"watch": "concurrently \"tsc --watch\" \"cpx \\\"src/components/*.css\\\" dist/components/ --watch\"",
|
|
26
26
|
"prepublishOnly": "npm run build"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"framer-motion": "^10.0.0 || ^11.0.0 || ^12.0.0",
|
|
30
|
-
"react": "^18.0.0",
|
|
31
|
-
"react-dom": "^18.0.0"
|
|
30
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
31
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/react": "^18.0.0",
|
|
35
35
|
"@types/react-dom": "^18.0.0",
|
|
36
|
+
"concurrently": "^9.2.1",
|
|
37
|
+
"cpx": "^1.5.0",
|
|
36
38
|
"framer-motion": "^12.23.24",
|
|
37
39
|
"shx": "^0.4.0",
|
|
38
40
|
"typescript": "^5.0.0"
|
|
39
41
|
}
|
|
40
|
-
}
|
|
42
|
+
}
|