Another UI overall. Shift to launchpad, removed sidebar, turned everything into components.
This commit is contained in:
164
src/App.jsx
164
src/App.jsx
@@ -1,9 +1,13 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { NewTerminal } from "./Terminal.jsx";
|
import { NewTerminal } from "./Terminal.jsx";
|
||||||
import AddHostModal from './AddHostModal.jsx';
|
import AddHostModal from "./AddHostModal.jsx";
|
||||||
import {Button, ButtonGroup} from '@mui/joy';
|
import { Button } from "@mui/joy";
|
||||||
import { CssVarsProvider } from '@mui/joy';
|
import { CssVarsProvider } from "@mui/joy";
|
||||||
import theme from './theme';
|
import theme from "./theme";
|
||||||
|
import TabList from "./TabList.jsx";
|
||||||
|
import Launchpad from "./Launchpad.jsx";
|
||||||
|
import TermixIcon from "./images/termix_icon.png";
|
||||||
|
import RocketIcon from './images/launchpad_rocket.png';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isAddHostHidden, setIsAddHostHidden] = useState(true);
|
const [isAddHostHidden, setIsAddHostHidden] = useState(true);
|
||||||
@@ -15,8 +19,24 @@ function App() {
|
|||||||
ip: "",
|
ip: "",
|
||||||
user: "",
|
user: "",
|
||||||
password: "",
|
password: "",
|
||||||
port: 22
|
port: 22,
|
||||||
});
|
});
|
||||||
|
const [isLaunchpadOpen, setIsLaunchpadOpen] = useState(false);
|
||||||
|
|
||||||
|
// Toggle Launchpad when "L" key is pressed
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
if (e.ctrlKey && e.key === "l") {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsLaunchpadOpen((prev) => !prev);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleAddHost = () => {
|
const handleAddHost = () => {
|
||||||
if (form.ip && form.user && form.password && form.port) {
|
if (form.ip && form.user && form.password && form.port) {
|
||||||
@@ -41,7 +61,7 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const closeTab = (id) => {
|
const closeTab = (id) => {
|
||||||
const newTerminals = terminals.filter(t => t.id !== id);
|
const newTerminals = terminals.filter((t) => t.id !== id);
|
||||||
setTerminals(newTerminals);
|
setTerminals(newTerminals);
|
||||||
if (activeTab === id) {
|
if (activeTab === id) {
|
||||||
setActiveTab(newTerminals[0]?.id || null);
|
setActiveTab(newTerminals[0]?.id || null);
|
||||||
@@ -51,74 +71,88 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<CssVarsProvider theme={theme}>
|
<CssVarsProvider theme={theme}>
|
||||||
<div className="flex h-screen bg-neutral-900 overflow-hidden">
|
<div className="flex h-screen bg-neutral-900 overflow-hidden">
|
||||||
{/* Sidebar */}
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
<div className="w-64 bg-neutral-800 text-white p-6 flex flex-col justify-between fixed left-0 top-0 bottom-0">
|
{/* Topbar */}
|
||||||
<div className="flex flex-col items-center">
|
<div className="bg-neutral-800 text-white p-4 flex items-center justify-between gap-4 min-h-[65px]">
|
||||||
<h2 className="text-2xl font-bold mb-8">Termix</h2>
|
{/* Left: Title */}
|
||||||
|
<div className="bg-neutral-700 flex justify-center items-center gap-2 p-3 rounded-lg h-[52px]">
|
||||||
|
<img
|
||||||
|
src={TermixIcon}
|
||||||
|
alt="Termix Icon"
|
||||||
|
className="w-[30px] h-[30px]"
|
||||||
|
/>
|
||||||
|
<h2 className="text-lg font-bold ml-[-2px]">Termix</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Middle: Tabs with scroll */}
|
||||||
|
<div className="flex-1 bg-neutral-700 rounded-lg overflow-hidden h-[52px] flex items-center">
|
||||||
|
<div
|
||||||
|
className="flex-1 overflow-x-auto overflow-y-hidden scrollbar-thin scrollbar-thumb-neutral-500 scrollbar-track-neutral-700 h-[52px] scrollbar-thumb-rounded-full scrollbar-track-rounded-full scrollbar-h-1"
|
||||||
|
style={{ whiteSpace: "nowrap" }}
|
||||||
|
>
|
||||||
|
<TabList
|
||||||
|
terminals={terminals}
|
||||||
|
activeTab={activeTab}
|
||||||
|
setActiveTab={setActiveTab}
|
||||||
|
closeTab={closeTab}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Open Launchpad Button */}
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsLaunchpadOpen(true)}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.neutral[700],
|
||||||
|
"&:hover": { backgroundColor: theme.palette.neutral[300] },
|
||||||
|
flexShrink: 0,
|
||||||
|
height: "52px",
|
||||||
|
width: "52px",
|
||||||
|
padding: 0, // To ensure no padding around the image
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={RocketIcon}
|
||||||
|
alt="L"
|
||||||
|
style={{
|
||||||
|
width: "70%",
|
||||||
|
height: "70",
|
||||||
|
objectFit: "contain",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Right: Create Host Button */}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setIsAddHostHidden(false)}
|
onClick={() => setIsAddHostHidden(false)}
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.neutral[500],
|
backgroundColor: theme.palette.neutral[700],
|
||||||
'&:hover': {
|
"&:hover": { backgroundColor: theme.palette.neutral[300] },
|
||||||
backgroundColor: theme.palette.neutral[900],
|
flexShrink: 0,
|
||||||
},
|
height: "52px",
|
||||||
|
width: "52px",
|
||||||
|
fontSize: "3.5rem",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
lineHeight: "normal",
|
||||||
|
paddingTop: "2px",
|
||||||
|
verticalAlign: "middle",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Create Host
|
+
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content Area */}
|
|
||||||
<div className="flex-1 flex flex-col ml-64 overflow-hidden">
|
|
||||||
{/* Topbar */}
|
|
||||||
<div className="bg-neutral-800 text-white p-4 flex justify-between items-center space-x-2 overflow-x-auto whitespace-nowrap min-h-[64px]">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{terminals.map((terminal, index) => (
|
|
||||||
<div key={terminal.id} className="flex items-center gap-2">
|
|
||||||
{/* Tab Button Group */}
|
|
||||||
<ButtonGroup>
|
|
||||||
<Button
|
|
||||||
onClick={() => setActiveTab(terminal.id)}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: terminal.id === activeTab ? theme.palette.neutral[500] : theme.palette.neutral[900],
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: theme.palette.neutral[300],
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{terminal.title}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => closeTab(terminal.id)}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.neutral[700],
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: theme.palette.neutral[300],
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
|
|
||||||
{/* Separator (except after the last tab) */}
|
|
||||||
{index !== terminals.length - 1 && (
|
|
||||||
<div className="w-px h-6 bg-gray-600"></div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Terminal Views */}
|
{/* Terminal Views */}
|
||||||
<div className="flex-1 relative pt-12 overflow-hidden">
|
<div className="flex-1 relative pt-12 overflow-hidden">
|
||||||
{terminals.map((terminal) => (
|
{terminals.map((terminal) => (
|
||||||
<div
|
<div
|
||||||
key={terminal.id}
|
key={terminal.id}
|
||||||
className={`absolute top-0 left-0 right-0 bottom-0 ${terminal.id === activeTab ? "block" : "hidden"}`}
|
className={`absolute top-0 left-0 right-0 bottom-0 ${
|
||||||
|
terminal.id === activeTab ? "block" : "hidden"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<NewTerminal hostConfig={terminal.hostConfig} />
|
<NewTerminal hostConfig={terminal.hostConfig} />
|
||||||
</div>
|
</div>
|
||||||
@@ -126,7 +160,6 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Add Host Modal */}
|
|
||||||
<AddHostModal
|
<AddHostModal
|
||||||
isHidden={isAddHostHidden}
|
isHidden={isAddHostHidden}
|
||||||
form={form}
|
form={form}
|
||||||
@@ -134,6 +167,11 @@ function App() {
|
|||||||
handleAddHost={handleAddHost}
|
handleAddHost={handleAddHost}
|
||||||
setIsAddHostHidden={setIsAddHostHidden}
|
setIsAddHostHidden={setIsAddHostHidden}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Launchpad Component */}
|
||||||
|
{isLaunchpadOpen && (
|
||||||
|
<Launchpad onClose={() => setIsLaunchpadOpen(false)} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CssVarsProvider>
|
</CssVarsProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
34
src/Launchpad.jsx
Normal file
34
src/Launchpad.jsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {Button} from "@mui/joy";
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
function Launchpad({ onClose }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: "20%",
|
||||||
|
left: "20%",
|
||||||
|
width: "60%",
|
||||||
|
height: "60%",
|
||||||
|
backgroundColor: "gray",
|
||||||
|
zIndex: 1000,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Launchpad</h2>
|
||||||
|
<p className="mb-4">This is your all-in-one launchpad panel.</p>
|
||||||
|
<Button onClick={onClose}>Close</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Launchpad.propTypes = {
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Launchpad;
|
||||||
60
src/TabList.jsx
Normal file
60
src/TabList.jsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import {Button, ButtonGroup} from "@mui/joy";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
function TabList({ terminals, activeTab, setActiveTab, closeTab, theme }) {
|
||||||
|
return (
|
||||||
|
<div className="inline-flex items-center h-full px-[0.5rem]">
|
||||||
|
{terminals.map((terminal, index) => (
|
||||||
|
<div
|
||||||
|
key={terminal.id}
|
||||||
|
className={index < terminals.length - 1 ? "mr-[0.5rem]" : ""}
|
||||||
|
>
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button
|
||||||
|
onClick={() => setActiveTab(terminal.id)}
|
||||||
|
sx={{
|
||||||
|
backgroundColor:
|
||||||
|
terminal.id === activeTab
|
||||||
|
? theme.palette.neutral[500]
|
||||||
|
: theme.palette.neutral[800],
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
"&:hover": { backgroundColor: theme.palette.neutral[300] },
|
||||||
|
borderTopLeftRadius: "4px",
|
||||||
|
borderBottomLeftRadius: "4px",
|
||||||
|
height: "40px",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{terminal.title}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => closeTab(terminal.id)}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.neutral[700],
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
"&:hover": { backgroundColor: theme.palette.neutral[300] },
|
||||||
|
borderTopRightRadius: "4px",
|
||||||
|
borderBottomRightRadius: "4px",
|
||||||
|
height: "40px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TabList.propTypes = {
|
||||||
|
terminals: PropTypes.array.isRequired,
|
||||||
|
activeTab: PropTypes.any,
|
||||||
|
setActiveTab: PropTypes.func.isRequired,
|
||||||
|
closeTab: PropTypes.func.isRequired,
|
||||||
|
theme: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TabList;
|
||||||
@@ -36,9 +36,8 @@ export function NewTerminal({ hostConfig }) {
|
|||||||
// Resize function
|
// Resize function
|
||||||
const resizeTerminal = () => {
|
const resizeTerminal = () => {
|
||||||
const terminalContainer = terminalRef.current;
|
const terminalContainer = terminalRef.current;
|
||||||
const sidebarWidth = 14 * 16; // Sidebar width in pixels
|
const topbarHeight = 65;
|
||||||
const topbarHeight = 64; // Topbar height in pixels
|
const availableWidth = window.innerWidth;
|
||||||
const availableWidth = window.innerWidth - sidebarWidth;
|
|
||||||
const availableHeight = window.innerHeight - topbarHeight;
|
const availableHeight = window.innerHeight - topbarHeight;
|
||||||
|
|
||||||
terminalContainer.style.width = `${availableWidth}px`;
|
terminalContainer.style.width = `${availableWidth}px`;
|
||||||
|
|||||||
BIN
src/images/launchpad_rocket.png
Normal file
BIN
src/images/launchpad_rocket.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
src/images/termix_icon.png
Normal file
BIN
src/images/termix_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Reference in New Issue
Block a user