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 CHANGED
@@ -1,146 +1,150 @@
1
- # 🐱 MaoMao Terminal
2
-
3
- > A dynamic, context-aware, and draggable terminal component for React applications.
4
-
5
- [![npm version](https://img.shields.io/npm/v/maomao-terminal.svg)](https://www.npmjs.com/package/maomao-terminal)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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**
1
+ # 🐱 MaoMao Terminal v1.0.2
2
+
3
+ > A dynamic, context-aware, and draggable terminal component for React applications.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/maomao-terminal.svg)](https://www.npmjs.com/package/maomao-terminal)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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.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
- 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;
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 { useState, useRef, useEffect } from 'react';
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 [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();
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
- 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: {
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: { 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 }))] })) }));
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,6 @@
1
+ export declare function useKeys(): {
2
+ input: string;
3
+ onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
4
+ onChange: import("react").Dispatch<import("react").SetStateAction<string>>;
5
+ history: string[];
6
+ };
@@ -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
+ }
@@ -9,6 +9,8 @@ export declare function useStatus(): {
9
9
  };
10
10
  isMinimized: boolean;
11
11
  isMaximized: boolean;
12
+ isDragging: boolean;
13
+ isResizing: boolean;
12
14
  drag: (e: React.MouseEvent) => void;
13
15
  resize: (e: React.MouseEvent) => void;
14
16
  minimize: () => void;
@@ -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.1",
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
+ }