maomao-terminal 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/LICENSE +21 -0
- package/README.md +146 -0
- package/dist/commands/baseCommands.d.ts +2 -0
- package/dist/commands/baseCommands.js +84 -0
- package/dist/components/Terminal.css +186 -0
- package/dist/components/Terminal.d.ts +8 -0
- package/dist/components/Terminal.js +77 -0
- package/dist/components/icons/Close.d.ts +6 -0
- package/dist/components/icons/Close.js +6 -0
- package/dist/components/icons/Maximize.d.ts +7 -0
- package/dist/components/icons/Maximize.js +7 -0
- package/dist/components/icons/Minimize.d.ts +8 -0
- package/dist/components/icons/Minimize.js +6 -0
- package/dist/context/TerminalProvider.d.ts +13 -0
- package/dist/context/TerminalProvider.js +27 -0
- package/dist/hooks/useStatus.d.ts +17 -0
- package/dist/hooks/useStatus.js +88 -0
- package/dist/hooks/useTerminal.d.ts +10 -0
- package/dist/hooks/useTerminal.js +40 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Juan Manuel Camacho Sanchez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
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 { registerCommand, unregisterCommand } = useTerminalContext();
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
// Register a command specific to this component
|
|
88
|
+
registerCommand({
|
|
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
|
+
// Cleanup when component unmounts
|
|
97
|
+
return () => unregisterCommand('getUser');
|
|
98
|
+
}, [userId, registerCommand, unregisterCommand]);
|
|
99
|
+
|
|
100
|
+
return <div>User Profile Component</div>;
|
|
101
|
+
};
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Now, when `UserProfile` is on screen, you can type `getUser` in the terminal!
|
|
105
|
+
|
|
106
|
+
## 🛠 Built-in Commands
|
|
107
|
+
|
|
108
|
+
| Command | Description | Args |
|
|
109
|
+
|---------|-------------|------|
|
|
110
|
+
| `help` | Lists available commands | |
|
|
111
|
+
| `clear` | Clears the terminal output | |
|
|
112
|
+
| `echo` | Repeats your input | `[text]` |
|
|
113
|
+
| `inspect` | Inspects global window properties | |
|
|
114
|
+
| `location` | Shows current URL | |
|
|
115
|
+
| `storage` | Inspects or clears localStorage | `[clear]` |
|
|
116
|
+
| `viewport` | Shows window dimensions | |
|
|
117
|
+
| `time` | Shows current local time | |
|
|
118
|
+
| `about` | Library information | |
|
|
119
|
+
|
|
120
|
+
## 🎨 Customization
|
|
121
|
+
|
|
122
|
+
The terminal comes with a default dark theme. You can override styles by targeting the CSS classes defined in `Terminal.css`.
|
|
123
|
+
|
|
124
|
+
Top-level classes:
|
|
125
|
+
- `.terminal-container`: The main window.
|
|
126
|
+
- `.terminal-header`: The drag handle and title bar.
|
|
127
|
+
- `.terminal-body`: The content area.
|
|
128
|
+
- `.terminal-input`: The command input field.
|
|
129
|
+
|
|
130
|
+
## 🤝 Contributing
|
|
131
|
+
|
|
132
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
133
|
+
|
|
134
|
+
1. Fork the repository
|
|
135
|
+
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
|
136
|
+
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
|
137
|
+
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
|
138
|
+
5. Open a Pull Request
|
|
139
|
+
|
|
140
|
+
## 📄 License
|
|
141
|
+
|
|
142
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
Created with ❤️ by **Juan Manuel Camacho Sanchez**
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export const baseCommands = [
|
|
2
|
+
{
|
|
3
|
+
command: 'echo',
|
|
4
|
+
response: args => args.join(' '),
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
command: 'help',
|
|
8
|
+
response: () => `\nAvailable commands:\n` +
|
|
9
|
+
` clear → Clear the console\n` +
|
|
10
|
+
` echo → Echo input\n` +
|
|
11
|
+
` help → Show this help message\n` +
|
|
12
|
+
` inspect → Show current page info\n` +
|
|
13
|
+
` location → Show current URL\n` +
|
|
14
|
+
` reload → Refresh the page\n` +
|
|
15
|
+
` storage → Show localStorage keys (or 'storage clear')\n` +
|
|
16
|
+
` userAgent → Show browser user agent\n` +
|
|
17
|
+
` viewport → Show window dimensions\n` +
|
|
18
|
+
` time → Show current local time\n`,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
command: 'inspect',
|
|
22
|
+
response: () => {
|
|
23
|
+
const keys = Object.keys(window);
|
|
24
|
+
return `Window properties (${keys.length}):\n${keys.slice(0, 20).join(', ')}${keys.length > 20 ? ', ...' : ''}`;
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
command: 'location',
|
|
29
|
+
response: () => `Current URL: ${window.location.href}`,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
command: 'userAgent',
|
|
33
|
+
response: () => `Browser user agent:\n${navigator.userAgent}`,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
command: 'time',
|
|
37
|
+
response: () => `Current time: ${new Date().toLocaleTimeString()}`,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
command: 'viewport',
|
|
41
|
+
response: () => `Viewport: ${window.innerWidth} x ${window.innerHeight} px`,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
command: 'reload',
|
|
45
|
+
response: () => {
|
|
46
|
+
window.location.reload();
|
|
47
|
+
return 'Reloading...';
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
command: 'storage',
|
|
52
|
+
response: (args) => {
|
|
53
|
+
if (args[0] === 'clear') {
|
|
54
|
+
localStorage.clear();
|
|
55
|
+
return 'LocalStorage cleared.';
|
|
56
|
+
}
|
|
57
|
+
const keys = Object.keys(localStorage);
|
|
58
|
+
if (keys.length === 0)
|
|
59
|
+
return 'LocalStorage is empty.';
|
|
60
|
+
return `LocalStorage keys (${keys.length}):\n- ${keys.join('\n- ')}`;
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
command: 'about',
|
|
65
|
+
response: () => `
|
|
66
|
+
══════════════════════════════════════════════════════
|
|
67
|
+
🐱 MaoMao Terminal
|
|
68
|
+
Empowering Front-End Developers
|
|
69
|
+
══════════════════════════════════════════════════════
|
|
70
|
+
|
|
71
|
+
v1.0.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
|
+
══════════════════════════════════════════════════════
|
|
82
|
+
`,
|
|
83
|
+
},
|
|
84
|
+
];
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/* ================================
|
|
2
|
+
CONTENEDOR PRINCIPAL
|
|
3
|
+
================================ */
|
|
4
|
+
.terminal-container {
|
|
5
|
+
position: fixed;
|
|
6
|
+
background: rgba(10, 10, 10, 0.5);
|
|
7
|
+
color: #22c55e;
|
|
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
|
+
font-family: "JetBrains Mono", "Fira Code", "Courier New", monospace;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
transition: box-shadow 0.2s;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.terminal-container:focus-within {
|
|
22
|
+
box-shadow: 0 15px 50px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(34, 197, 94, 0.3);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.terminal-container.maximized {
|
|
26
|
+
border-radius: 0;
|
|
27
|
+
border: none;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* ================================
|
|
31
|
+
HEADER
|
|
32
|
+
================================ */
|
|
33
|
+
.terminal-header {
|
|
34
|
+
background: #18181b;
|
|
35
|
+
padding: 0 12px;
|
|
36
|
+
cursor: grab;
|
|
37
|
+
user-select: none;
|
|
38
|
+
display: flex;
|
|
39
|
+
justify-content: space-between;
|
|
40
|
+
align-items: center;
|
|
41
|
+
border-bottom: 1px solid #333;
|
|
42
|
+
height: 42px;
|
|
43
|
+
flex-shrink: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.terminal-header:active {
|
|
47
|
+
cursor: grabbing;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.terminal-title {
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
font-size: 0.85rem;
|
|
53
|
+
color: #a1a1aa;
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
gap: 8px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.terminal-controls {
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: 6px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.terminal-btn {
|
|
66
|
+
background: transparent;
|
|
67
|
+
border: none;
|
|
68
|
+
color: #71717a;
|
|
69
|
+
/* Zinc 500 */
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
padding: 6px;
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
justify-content: center;
|
|
75
|
+
border-radius: 4px;
|
|
76
|
+
transition: all 0.2s;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.terminal-btn:hover {
|
|
80
|
+
background: rgba(255, 255, 255, 0.1);
|
|
81
|
+
color: #e4e4e7;
|
|
82
|
+
/* Zinc 200 */
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.terminal-btn.close:hover {
|
|
86
|
+
background: #ef4444;
|
|
87
|
+
color: white;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* ================================
|
|
91
|
+
BODY
|
|
92
|
+
================================ */
|
|
93
|
+
.terminal-body {
|
|
94
|
+
flex: 1;
|
|
95
|
+
padding: 12px;
|
|
96
|
+
overflow-y: auto;
|
|
97
|
+
background: rgba(0, 0, 0, 0.4);
|
|
98
|
+
position: relative;
|
|
99
|
+
font-size: 0.95rem;
|
|
100
|
+
line-height: 1.6;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.terminal-line {
|
|
104
|
+
margin-bottom: 4px;
|
|
105
|
+
word-break: break-all;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* ================================
|
|
109
|
+
INPUT
|
|
110
|
+
================================ */
|
|
111
|
+
.terminal-input-wrapper {
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
border-top: 1px solid #333;
|
|
115
|
+
background: rgba(0, 0, 0, 0.8);
|
|
116
|
+
padding: 8px 12px;
|
|
117
|
+
min-height: 44px;
|
|
118
|
+
flex-shrink: 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.terminal-prompt {
|
|
122
|
+
color: #eab308;
|
|
123
|
+
/* Yellow 500 */
|
|
124
|
+
font-weight: bold;
|
|
125
|
+
margin-right: 8px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.terminal-input {
|
|
129
|
+
flex: 1;
|
|
130
|
+
background: transparent;
|
|
131
|
+
color: #22c55e;
|
|
132
|
+
border: none;
|
|
133
|
+
outline: none;
|
|
134
|
+
font-family: inherit;
|
|
135
|
+
font-size: inherit;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* ================================
|
|
139
|
+
RESIZE HANDLE
|
|
140
|
+
================================ */
|
|
141
|
+
.terminal-resize-handle {
|
|
142
|
+
width: 16px;
|
|
143
|
+
height: 16px;
|
|
144
|
+
position: absolute;
|
|
145
|
+
bottom: 0;
|
|
146
|
+
right: 0;
|
|
147
|
+
cursor: se-resize;
|
|
148
|
+
z-index: 60;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.terminal-resize-handle::after {
|
|
152
|
+
content: '';
|
|
153
|
+
position: absolute;
|
|
154
|
+
bottom: 3px;
|
|
155
|
+
right: 3px;
|
|
156
|
+
width: 6px;
|
|
157
|
+
height: 6px;
|
|
158
|
+
border-bottom: 2px solid #555;
|
|
159
|
+
border-right: 2px solid #555;
|
|
160
|
+
border-radius: 0 0 2px 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.terminal-resize-handle:hover::after {
|
|
164
|
+
border-color: #22c55e;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* ================================
|
|
168
|
+
CUSTOM SCROLLBAR
|
|
169
|
+
================================ */
|
|
170
|
+
.terminal-scrollbar::-webkit-scrollbar {
|
|
171
|
+
width: 10px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.terminal-scrollbar::-webkit-scrollbar-track {
|
|
175
|
+
background: #18181b;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.terminal-scrollbar::-webkit-scrollbar-thumb {
|
|
179
|
+
background-color: #3f3f46;
|
|
180
|
+
border: 2px solid #18181b;
|
|
181
|
+
border-radius: 6px;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.terminal-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
185
|
+
background-color: #52525b;
|
|
186
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useRef, useEffect } from 'react';
|
|
4
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
5
|
+
import { useTerminal } from '../hooks/useTerminal';
|
|
6
|
+
import { useStatus } from '../hooks/useStatus';
|
|
7
|
+
import { CloseButton } from './icons/Close';
|
|
8
|
+
import { MaximizeButton } from './icons/Maximize';
|
|
9
|
+
import { MinimizeButton } from './icons/Minimize';
|
|
10
|
+
import './Terminal.css';
|
|
11
|
+
const Terminal = ({ isOpen, onClose }) => {
|
|
12
|
+
const terminalRef = useRef(null);
|
|
13
|
+
const scrollRef = useRef(null);
|
|
14
|
+
const [input, setInput] = useState('');
|
|
15
|
+
const [historyPointer, setHistoryPointer] = useState(null);
|
|
16
|
+
const { history, commandHistory, executeCommand } = useTerminal();
|
|
17
|
+
const { size, position, drag, resize, isMaximized, isMinimized, minimize, maximize, restore } = useStatus();
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (scrollRef.current)
|
|
20
|
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
21
|
+
}, [history]);
|
|
22
|
+
const handleKeyDown = (e) => {
|
|
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: {
|
|
68
|
+
opacity: 1,
|
|
69
|
+
scale: 1,
|
|
70
|
+
width: isMaximized ? window.innerWidth : size.width,
|
|
71
|
+
height: isMinimized ? 42 : isMaximized ? window.innerHeight : size.height,
|
|
72
|
+
left: isMaximized ? 0 : position.x,
|
|
73
|
+
top: isMaximized ? 0 : position.y,
|
|
74
|
+
borderRadius: isMaximized ? 0 : 8
|
|
75
|
+
}, exit: { opacity: 0, scale: 0.9 }, transition: { type: 'spring', stiffness: 400, damping: 35 }, 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: "\u279C" }), 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 => setInput(e.target.value), onKeyDown: handleKeyDown })] }), !isMaximized && !isMinimized && (_jsx("div", { className: "terminal-resize-handle", onMouseDown: resize }))] })) }));
|
|
76
|
+
};
|
|
77
|
+
export default Terminal;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import '../Terminal.css';
|
|
3
|
+
const CloseIcon = () => (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }));
|
|
4
|
+
export function CloseButton({ onClose }) {
|
|
5
|
+
return (_jsx("button", { className: "terminal-btn close", onClick: onClose, title: "Close", children: _jsx(CloseIcon, {}) }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import '../Terminal.css';
|
|
3
|
+
const MaximizeIcon = () => (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }) }));
|
|
4
|
+
const RestoreIcon = () => (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("rect", { x: "5", y: "9", width: "14", height: "14", rx: "2", ry: "2" }), _jsx("path", { d: "M9 5h10a2 2 0 0 1 2 2v10" })] }));
|
|
5
|
+
export function MaximizeButton({ onClick, isMaximized }) {
|
|
6
|
+
return (_jsx("button", { className: "terminal-btn", onClick: () => { onClick(); }, title: isMaximized ? "Restore" : "Maximize", children: isMaximized ? _jsx(RestoreIcon, {}) : _jsx(MaximizeIcon, {}) }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import '../Terminal.css';
|
|
2
|
+
type Props = {
|
|
3
|
+
onClick: () => void;
|
|
4
|
+
isMinimized: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare const MinimizeIcon: () => import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export declare function MinimizeButton({ onClick, isMinimized }: Props): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import '../Terminal.css';
|
|
3
|
+
export const MinimizeIcon = () => (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("line", { x1: "5", y1: "12", x2: "19", y2: "12" }) }));
|
|
4
|
+
export function MinimizeButton({ onClick, isMinimized }) {
|
|
5
|
+
return (_jsx("button", { className: "terminal-btn", onClick: () => { onClick(); }, disabled: isMinimized, title: isMinimized ? "Restore" : "Minimize", children: _jsx(MinimizeIcon, {}) }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { TerminalCommand } from '../hooks/useTerminal';
|
|
3
|
+
type TerminalContextType = {
|
|
4
|
+
globalCommands: TerminalCommand[];
|
|
5
|
+
dynamicCommands: TerminalCommand[];
|
|
6
|
+
registerDynamicCommands: (commands: TerminalCommand[]) => void;
|
|
7
|
+
unregisterDynamicCommands: (commands: TerminalCommand[]) => void;
|
|
8
|
+
};
|
|
9
|
+
export declare const TerminalProvider: ({ children }: {
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export declare const useTerminalContext: () => TerminalContextType;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext, useState, useCallback, useMemo } from 'react';
|
|
4
|
+
import { baseCommands } from '../commands/baseCommands';
|
|
5
|
+
const TerminalContext = createContext(undefined);
|
|
6
|
+
export const TerminalProvider = ({ children }) => {
|
|
7
|
+
const [dynamicCommands, setDynamicCommands] = useState([]);
|
|
8
|
+
const globalCommands = useMemo(() => baseCommands, []);
|
|
9
|
+
const registerDynamicCommands = useCallback((commands) => {
|
|
10
|
+
setDynamicCommands(prev => {
|
|
11
|
+
const existingCommands = new Set(prev.map(c => c.command));
|
|
12
|
+
const newCommands = commands.filter(c => !existingCommands.has(c.command));
|
|
13
|
+
return [...prev, ...newCommands];
|
|
14
|
+
});
|
|
15
|
+
}, []);
|
|
16
|
+
const unregisterDynamicCommands = useCallback((commands) => {
|
|
17
|
+
const toRemove = new Set(commands.map(c => c.command));
|
|
18
|
+
setDynamicCommands(prev => prev.filter(c => !toRemove.has(c.command)));
|
|
19
|
+
}, []);
|
|
20
|
+
return (_jsx(TerminalContext.Provider, { value: { globalCommands, dynamicCommands, registerDynamicCommands, unregisterDynamicCommands }, children: children }));
|
|
21
|
+
};
|
|
22
|
+
export const useTerminalContext = () => {
|
|
23
|
+
const context = useContext(TerminalContext);
|
|
24
|
+
if (!context)
|
|
25
|
+
throw new Error('useTerminalContext must be used within TerminalProvider');
|
|
26
|
+
return context;
|
|
27
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function useStatus(): {
|
|
2
|
+
size: {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
};
|
|
6
|
+
position: {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
};
|
|
10
|
+
isMinimized: boolean;
|
|
11
|
+
isMaximized: boolean;
|
|
12
|
+
drag: (e: React.MouseEvent) => void;
|
|
13
|
+
resize: (e: React.MouseEvent) => void;
|
|
14
|
+
minimize: () => void;
|
|
15
|
+
maximize: () => void;
|
|
16
|
+
restore: () => void;
|
|
17
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useRef, useState, useEffect, useCallback } from "react";
|
|
2
|
+
export function useStatus() {
|
|
3
|
+
const MIN_HEIGHT = 42;
|
|
4
|
+
const MIN_WIDTH = 300;
|
|
5
|
+
const [size, setSize] = useState({ width: 500, height: 300 });
|
|
6
|
+
const [position, setPosition] = useState({ x: 100, y: 100 });
|
|
7
|
+
const [isMinimized, setIsMinimized] = useState(false);
|
|
8
|
+
const [isMaximized, setIsMaximized] = useState(false);
|
|
9
|
+
const isDraggingRef = useRef(false);
|
|
10
|
+
const isResizingRef = useRef(false);
|
|
11
|
+
const dragStart = useRef({ x: 0, y: 0 });
|
|
12
|
+
const resizeStart = useRef({ x: 0, y: 0, width: 0, height: 0 });
|
|
13
|
+
const lastFloatingSize = useRef({ width: 500, height: 300 });
|
|
14
|
+
const lastFloatingPosition = useRef({ x: 100, y: 100 });
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!isMaximized && !isMinimized) {
|
|
17
|
+
lastFloatingSize.current = size;
|
|
18
|
+
lastFloatingPosition.current = position;
|
|
19
|
+
}
|
|
20
|
+
}, [size, position, isMaximized, isMinimized]);
|
|
21
|
+
const dragTo = useCallback((e) => {
|
|
22
|
+
if (!isDraggingRef.current)
|
|
23
|
+
return;
|
|
24
|
+
setPosition({
|
|
25
|
+
x: Math.min(Math.max(0, e.clientX - dragStart.current.x), window.innerWidth - size.width),
|
|
26
|
+
y: Math.min(Math.max(0, e.clientY - dragStart.current.y), window.innerHeight - size.height)
|
|
27
|
+
});
|
|
28
|
+
}, [size]);
|
|
29
|
+
const resizeTo = useCallback((e) => {
|
|
30
|
+
if (!isResizingRef.current)
|
|
31
|
+
return;
|
|
32
|
+
setSize({
|
|
33
|
+
width: Math.max(MIN_WIDTH, resizeStart.current.width + (e.clientX - resizeStart.current.x)),
|
|
34
|
+
height: Math.max(MIN_HEIGHT, resizeStart.current.height + (e.clientY - resizeStart.current.y))
|
|
35
|
+
});
|
|
36
|
+
}, []);
|
|
37
|
+
const end = useCallback(() => {
|
|
38
|
+
isDraggingRef.current = false;
|
|
39
|
+
isResizingRef.current = false;
|
|
40
|
+
}, []);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const handleMove = (e) => {
|
|
43
|
+
if (isDraggingRef.current)
|
|
44
|
+
dragTo(e);
|
|
45
|
+
if (isResizingRef.current)
|
|
46
|
+
resizeTo(e);
|
|
47
|
+
};
|
|
48
|
+
window.addEventListener('mousemove', handleMove);
|
|
49
|
+
window.addEventListener('mouseup', end);
|
|
50
|
+
return () => {
|
|
51
|
+
window.removeEventListener('mousemove', handleMove);
|
|
52
|
+
window.removeEventListener('mouseup', end);
|
|
53
|
+
};
|
|
54
|
+
}, [dragTo, resizeTo, end]);
|
|
55
|
+
const minimize = () => {
|
|
56
|
+
setIsMinimized(true);
|
|
57
|
+
setIsMaximized(false);
|
|
58
|
+
setSize(prev => (Object.assign(Object.assign({}, prev), { height: MIN_HEIGHT })));
|
|
59
|
+
};
|
|
60
|
+
const maximize = () => {
|
|
61
|
+
setIsMaximized(true);
|
|
62
|
+
setIsMinimized(false);
|
|
63
|
+
setPosition({ x: 0, y: 0 });
|
|
64
|
+
setSize({ width: window.innerWidth, height: window.innerHeight });
|
|
65
|
+
};
|
|
66
|
+
const restore = () => {
|
|
67
|
+
setSize(lastFloatingSize.current);
|
|
68
|
+
setPosition(lastFloatingPosition.current);
|
|
69
|
+
setIsMinimized(false);
|
|
70
|
+
setIsMaximized(false);
|
|
71
|
+
};
|
|
72
|
+
const drag = (e) => {
|
|
73
|
+
if (isMaximized)
|
|
74
|
+
return;
|
|
75
|
+
isDraggingRef.current = true;
|
|
76
|
+
dragStart.current = { x: e.clientX - position.x, y: e.clientY - position.y };
|
|
77
|
+
};
|
|
78
|
+
const resize = (e) => {
|
|
79
|
+
if (isMaximized || isMinimized)
|
|
80
|
+
return;
|
|
81
|
+
isResizingRef.current = true;
|
|
82
|
+
resizeStart.current = { x: e.clientX, y: e.clientY, width: size.width, height: size.height };
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
size, position, isMinimized, isMaximized,
|
|
86
|
+
drag, resize, minimize, maximize, restore
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface TerminalCommand {
|
|
2
|
+
command: string;
|
|
3
|
+
response: (args: string[]) => string | Promise<string>;
|
|
4
|
+
}
|
|
5
|
+
export declare function useTerminal(): {
|
|
6
|
+
history: string[];
|
|
7
|
+
commandHistory: string[];
|
|
8
|
+
executeCommand: (input: string) => Promise<string | undefined>;
|
|
9
|
+
clearHistory: () => void;
|
|
10
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
import { useState } from 'react';
|
|
12
|
+
import { useTerminalContext } from '../context/TerminalProvider';
|
|
13
|
+
export function useTerminal() {
|
|
14
|
+
const { globalCommands, dynamicCommands } = useTerminalContext();
|
|
15
|
+
const [history, setHistory] = useState([]);
|
|
16
|
+
const [commandHistory, setCommandHistory] = useState([]);
|
|
17
|
+
const executeCommand = (input) => __awaiter(this, void 0, void 0, function* () {
|
|
18
|
+
if (!input.trim())
|
|
19
|
+
return;
|
|
20
|
+
const parts = input.trim().split(/\s+/);
|
|
21
|
+
const cmdName = parts[0];
|
|
22
|
+
const args = parts.slice(1);
|
|
23
|
+
// Add to command history for navigation
|
|
24
|
+
setCommandHistory(prev => [...prev, input]);
|
|
25
|
+
if (cmdName === 'clear') {
|
|
26
|
+
setHistory([]);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const cmd = [...dynamicCommands, ...globalCommands].find(c => c.command === cmdName);
|
|
30
|
+
let result;
|
|
31
|
+
if (cmd)
|
|
32
|
+
result = yield cmd.response(args);
|
|
33
|
+
else
|
|
34
|
+
result = `Command not found: ${cmdName}`;
|
|
35
|
+
setHistory(prev => [...prev, `$ ${input}`, result]);
|
|
36
|
+
return result;
|
|
37
|
+
});
|
|
38
|
+
const clearHistory = () => setHistory([]);
|
|
39
|
+
return { history, commandHistory, executeCommand, clearHistory };
|
|
40
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { default as Terminal } from './components/Terminal';
|
|
2
|
+
export { TerminalProvider, useTerminalContext } from './context/TerminalProvider';
|
|
3
|
+
export { useTerminal, type TerminalCommand } from './hooks/useTerminal';
|
|
4
|
+
export { baseCommands } from './commands/baseCommands';
|
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "maomao-terminal",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A React-style terminal component library",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"author": "Juan Manuel Camacho Sanchez",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/jmcamacho7/maomao-terminal.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"react",
|
|
15
|
+
"terminal",
|
|
16
|
+
"component",
|
|
17
|
+
"ui"
|
|
18
|
+
],
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc && shx mkdir -p dist/components && shx cp src/components/*.css dist/components/",
|
|
25
|
+
"watch": "tsc --watch",
|
|
26
|
+
"prepublishOnly": "npm run build"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"framer-motion": "^10.0.0",
|
|
30
|
+
"react": "^18.0.0",
|
|
31
|
+
"react-dom": "^18.0.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/react": "^18.0.0",
|
|
35
|
+
"@types/react-dom": "^18.0.0",
|
|
36
|
+
"framer-motion": "^12.23.24",
|
|
37
|
+
"shx": "^0.4.0",
|
|
38
|
+
"typescript": "^5.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|