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 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
+ [![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**
@@ -0,0 +1,2 @@
1
+ import { TerminalCommand } from '../hooks/useTerminal';
2
+ export declare const baseCommands: TerminalCommand[];
@@ -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,8 @@
1
+ import React from 'react';
2
+ import './Terminal.css';
3
+ export type TerminalProps = {
4
+ isOpen: boolean;
5
+ onClose: () => void;
6
+ };
7
+ declare const Terminal: React.FC<TerminalProps>;
8
+ export default Terminal;
@@ -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 '../Terminal.css';
2
+ type Props = {
3
+ onClose: () => void;
4
+ };
5
+ export declare function CloseButton({ onClose }: Props): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -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 '../Terminal.css';
2
+ type Props = {
3
+ onClick: () => void;
4
+ isMaximized: boolean;
5
+ };
6
+ export declare function MaximizeButton({ onClick, isMaximized }: Props): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -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
+ }
@@ -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
@@ -0,0 +1,4 @@
1
+ export { default as Terminal } from './components/Terminal';
2
+ export { TerminalProvider, useTerminalContext } from './context/TerminalProvider';
3
+ export { useTerminal } from './hooks/useTerminal';
4
+ export { baseCommands } from './commands/baseCommands';
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
+ }