Project Initialisation
added Project files
This commit is contained in:
317
web/app.js
Normal file
317
web/app.js
Normal file
@@ -0,0 +1,317 @@
|
||||
const state = {
|
||||
config: null,
|
||||
selected: 0,
|
||||
view: "servers",
|
||||
query: "",
|
||||
capabilities: null,
|
||||
};
|
||||
|
||||
const els = {
|
||||
navItems: document.querySelectorAll(".nav-item"),
|
||||
views: {
|
||||
servers: document.querySelector("#serversView"),
|
||||
commands: document.querySelector("#commandsView"),
|
||||
settings: document.querySelector("#settingsView"),
|
||||
},
|
||||
viewTitle: document.querySelector("#viewTitle"),
|
||||
terminalStatus: document.querySelector("#terminalStatus"),
|
||||
searchInput: document.querySelector("#searchInput"),
|
||||
serverGrid: document.querySelector("#serverGrid"),
|
||||
serverForm: document.querySelector("#serverForm"),
|
||||
formTitle: document.querySelector("#formTitle"),
|
||||
deleteButton: document.querySelector("#deleteButton"),
|
||||
connectButton: document.querySelector("#connectButton"),
|
||||
addButton: document.querySelector("#addButton"),
|
||||
reloadButton: document.querySelector("#reloadButton"),
|
||||
sshButton: document.querySelector("#sshButton"),
|
||||
sshCommand: document.querySelector("#sshCommand"),
|
||||
commandsList: document.querySelector("#commandsList"),
|
||||
settingsForm: document.querySelector("#settingsForm"),
|
||||
toast: document.querySelector("#toast"),
|
||||
};
|
||||
|
||||
async function loadConfig() {
|
||||
const [configResponse, capabilitiesResponse] = await Promise.all([
|
||||
fetch("/api/config"),
|
||||
fetch("/api/capabilities"),
|
||||
]);
|
||||
|
||||
const response = configResponse;
|
||||
if (!response.ok) throw new Error("Config konnte nicht geladen werden");
|
||||
state.config = await response.json();
|
||||
state.capabilities = capabilitiesResponse.ok ? await capabilitiesResponse.json() : null;
|
||||
state.selected = Math.min(state.selected, Math.max(0, state.config.servers.length - 1));
|
||||
render();
|
||||
}
|
||||
|
||||
async function saveConfig(message = "Gespeichert") {
|
||||
const response = await fetch("/api/config", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(state.config),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Config konnte nicht gespeichert werden");
|
||||
state.config = await response.json();
|
||||
showToast(message);
|
||||
render();
|
||||
}
|
||||
|
||||
function render() {
|
||||
renderNavigation();
|
||||
renderServers();
|
||||
renderServerForm();
|
||||
renderCommands();
|
||||
renderSettings();
|
||||
}
|
||||
|
||||
function renderNavigation() {
|
||||
els.navItems.forEach((item) => {
|
||||
item.classList.toggle("active", item.dataset.view === state.view);
|
||||
});
|
||||
|
||||
Object.entries(els.views).forEach(([name, node]) => {
|
||||
node.classList.toggle("active", name === state.view);
|
||||
});
|
||||
|
||||
const titles = {
|
||||
servers: "Server",
|
||||
commands: "Quick Commands",
|
||||
settings: "Settings",
|
||||
};
|
||||
els.viewTitle.textContent = titles[state.view];
|
||||
|
||||
if (state.capabilities?.terminal_available) {
|
||||
els.terminalStatus.textContent = `Terminal: ${state.capabilities.terminal}`;
|
||||
} else {
|
||||
els.terminalStatus.textContent = "Kein unterstützter Terminal-Emulator gefunden";
|
||||
}
|
||||
}
|
||||
|
||||
function filteredServers() {
|
||||
const query = state.query.trim().toLowerCase();
|
||||
const servers = state.config?.servers ?? [];
|
||||
if (!query) return servers.map((server, index) => ({ server, index }));
|
||||
|
||||
return servers
|
||||
.map((server, index) => ({ server, index }))
|
||||
.filter(({ server }) => {
|
||||
return [server.name, server.host, server.user, server.group, server.auth]
|
||||
.join(" ")
|
||||
.toLowerCase()
|
||||
.includes(query);
|
||||
});
|
||||
}
|
||||
|
||||
function renderServers() {
|
||||
const items = filteredServers();
|
||||
els.serverGrid.innerHTML = "";
|
||||
|
||||
if (items.length === 0) {
|
||||
els.serverGrid.innerHTML = `<p class="server-meta">Keine Server gefunden.</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const { server, index } of items) {
|
||||
const card = document.createElement("article");
|
||||
card.className = "server-card";
|
||||
card.classList.toggle("active", index === state.selected);
|
||||
card.innerHTML = `
|
||||
<h3>${escapeHTML(server.name || "Unbenannt")}</h3>
|
||||
<p class="server-meta">${escapeHTML(server.user || "")}@${escapeHTML(server.host || "")}:${server.port || 22}</p>
|
||||
<div class="pill-row">
|
||||
<span class="pill">${escapeHTML(server.group || "Keine Gruppe")}</span>
|
||||
<span class="pill">${escapeHTML(server.auth || "key")}</span>
|
||||
${server.kitty_fix ? `<span class="pill">kitty</span>` : ""}
|
||||
</div>
|
||||
`;
|
||||
card.addEventListener("click", () => {
|
||||
state.selected = index;
|
||||
els.sshCommand.hidden = true;
|
||||
render();
|
||||
});
|
||||
els.serverGrid.append(card);
|
||||
}
|
||||
}
|
||||
|
||||
function renderServerForm() {
|
||||
const server = state.config?.servers?.[state.selected];
|
||||
const disabled = !server;
|
||||
els.serverForm.querySelectorAll("input, select, button").forEach((input) => {
|
||||
input.disabled = disabled;
|
||||
});
|
||||
els.connectButton.disabled = disabled || !state.capabilities?.terminal_available;
|
||||
|
||||
if (!server) {
|
||||
els.formTitle.textContent = "Kein Server ausgewählt";
|
||||
els.serverForm.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
els.formTitle.textContent = server.name ? `${server.name} bearbeiten` : "Server bearbeiten";
|
||||
setFormValue("name", server.name);
|
||||
setFormValue("host", server.host);
|
||||
setFormValue("user", server.user);
|
||||
setFormValue("port", server.port || 22);
|
||||
setFormValue("group", server.group);
|
||||
setFormValue("auth", server.auth || "key");
|
||||
setFormValue("key", server.key);
|
||||
setFormValue("password_id", server.password_id);
|
||||
els.serverForm.elements.kitty_fix.checked = Boolean(server.kitty_fix);
|
||||
}
|
||||
|
||||
function renderCommands() {
|
||||
const commands = state.config?.quick_commands ?? [];
|
||||
els.commandsList.innerHTML = "";
|
||||
|
||||
if (commands.length === 0) {
|
||||
els.commandsList.innerHTML = `<p class="server-meta">Keine Quick Commands konfiguriert.</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const command of commands) {
|
||||
const item = document.createElement("article");
|
||||
item.className = "command-item";
|
||||
item.innerHTML = `
|
||||
<h3>${escapeHTML(command.name)}</h3>
|
||||
<code>${escapeHTML(command.command)}</code>
|
||||
`;
|
||||
els.commandsList.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
function renderSettings() {
|
||||
const settings = state.config?.settings;
|
||||
if (!settings) return;
|
||||
|
||||
els.settingsForm.elements.theme.value = settings.theme || "neon-green";
|
||||
els.settingsForm.elements.term.value = settings.terminal?.term || "xterm-256color";
|
||||
els.settingsForm.elements.enable_kitty_fix.checked = Boolean(settings.terminal?.enable_kitty_fix);
|
||||
}
|
||||
|
||||
function addServer() {
|
||||
state.config.servers.push({
|
||||
name: "Neuer Server",
|
||||
host: "",
|
||||
user: "root",
|
||||
port: 22,
|
||||
group: "Homelab",
|
||||
auth: "key",
|
||||
key: "",
|
||||
password_id: "",
|
||||
kitty_fix: true,
|
||||
});
|
||||
state.selected = state.config.servers.length - 1;
|
||||
state.view = "servers";
|
||||
render();
|
||||
}
|
||||
|
||||
function collectServerForm() {
|
||||
const form = els.serverForm.elements;
|
||||
return {
|
||||
name: form.name.value.trim(),
|
||||
host: form.host.value.trim(),
|
||||
user: form.user.value.trim(),
|
||||
port: Number(form.port.value) || 22,
|
||||
group: form.group.value.trim(),
|
||||
auth: form.auth.value,
|
||||
key: form.key.value.trim(),
|
||||
password_id: form.password_id.value.trim(),
|
||||
kitty_fix: form.kitty_fix.checked,
|
||||
};
|
||||
}
|
||||
|
||||
function setFormValue(name, value) {
|
||||
els.serverForm.elements[name].value = value ?? "";
|
||||
}
|
||||
|
||||
async function showSSHCommand() {
|
||||
const response = await fetch(`/api/ssh-command/${state.selected}`, { method: "POST" });
|
||||
if (!response.ok) throw new Error("SSH Befehl konnte nicht erzeugt werden");
|
||||
|
||||
const data = await response.json();
|
||||
els.sshCommand.textContent = data.command;
|
||||
els.sshCommand.hidden = false;
|
||||
}
|
||||
|
||||
async function connectSSH() {
|
||||
const response = await fetch(`/api/connect/${state.selected}`, { method: "POST" });
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || "SSH Verbindung konnte nicht gestartet werden");
|
||||
}
|
||||
|
||||
els.sshCommand.textContent = data.command;
|
||||
els.sshCommand.hidden = false;
|
||||
showToast("SSH Terminal gestartet");
|
||||
}
|
||||
|
||||
function showToast(message) {
|
||||
els.toast.textContent = message;
|
||||
els.toast.hidden = false;
|
||||
window.setTimeout(() => {
|
||||
els.toast.hidden = true;
|
||||
}, 2200);
|
||||
}
|
||||
|
||||
function escapeHTML(value) {
|
||||
return String(value ?? "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
els.navItems.forEach((item) => {
|
||||
item.addEventListener("click", () => {
|
||||
state.view = item.dataset.view;
|
||||
render();
|
||||
});
|
||||
});
|
||||
|
||||
els.searchInput.addEventListener("input", (event) => {
|
||||
state.query = event.target.value;
|
||||
renderServers();
|
||||
});
|
||||
|
||||
els.addButton.addEventListener("click", addServer);
|
||||
els.reloadButton.addEventListener("click", () => loadConfig().then(() => showToast("Neu geladen")).catch((error) => showToast(error.message)));
|
||||
els.sshButton.addEventListener("click", () => showSSHCommand().catch((error) => showToast(error.message)));
|
||||
els.connectButton.addEventListener("click", () => connectSSH().catch((error) => showToast(error.message)));
|
||||
|
||||
els.deleteButton.addEventListener("click", async () => {
|
||||
if (!state.config.servers[state.selected]) return;
|
||||
state.config.servers.splice(state.selected, 1);
|
||||
state.selected = Math.max(0, state.selected - 1);
|
||||
await saveConfig("Server gelöscht");
|
||||
});
|
||||
|
||||
els.serverForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const server = collectServerForm();
|
||||
|
||||
if (!server.name || !server.host || !server.user) {
|
||||
showToast("Name, Host und User sind Pflichtfelder");
|
||||
return;
|
||||
}
|
||||
|
||||
state.config.servers[state.selected] = server;
|
||||
await saveConfig("Server gespeichert");
|
||||
});
|
||||
|
||||
els.settingsForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const form = els.settingsForm.elements;
|
||||
state.config.settings = {
|
||||
theme: form.theme.value.trim() || "neon-green",
|
||||
terminal: {
|
||||
term: form.term.value.trim() || "xterm-256color",
|
||||
enable_kitty_fix: form.enable_kitty_fix.checked,
|
||||
},
|
||||
};
|
||||
await saveConfig("Settings gespeichert");
|
||||
});
|
||||
|
||||
loadConfig().catch((error) => showToast(error.message));
|
||||
105
web/index.html
Normal file
105
web/index.html
Normal file
@@ -0,0 +1,105 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>PulseGate GUI</title>
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">
|
||||
<div class="mark">PG</div>
|
||||
<div>
|
||||
<h1>PulseGate</h1>
|
||||
<p>SSH Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="search">
|
||||
<span>Suche</span>
|
||||
<input id="searchInput" type="search" placeholder="Server, Host, Gruppe">
|
||||
</label>
|
||||
|
||||
<nav class="nav">
|
||||
<button class="nav-item active" data-view="servers">Server</button>
|
||||
<button class="nav-item" data-view="commands">Commands</button>
|
||||
<button class="nav-item" data-view="settings">Settings</button>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<section class="workspace">
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<p class="eyebrow" id="configPath">Lokale Konfiguration</p>
|
||||
<h2 id="viewTitle">Server</h2>
|
||||
<p class="status-line" id="terminalStatus">Terminal wird geprüft...</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button id="reloadButton" class="ghost">Neu laden</button>
|
||||
<button id="addButton">Server hinzufügen</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section id="serversView" class="view active">
|
||||
<div id="serverGrid" class="server-grid"></div>
|
||||
<form id="serverForm" class="editor">
|
||||
<div class="editor-header">
|
||||
<h3 id="formTitle">Server bearbeiten</h3>
|
||||
<button type="button" id="deleteButton" class="danger">Löschen</button>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<label>Name<input name="name" required></label>
|
||||
<label>Host<input name="host" required></label>
|
||||
<label>User<input name="user" required></label>
|
||||
<label>Port<input name="port" type="number" min="1" max="65535" value="22"></label>
|
||||
<label>Group<input name="group"></label>
|
||||
<label>Auth
|
||||
<select name="auth">
|
||||
<option value="key">key</option>
|
||||
<option value="password">password</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Key Path<input name="key" placeholder="~/.ssh/id_ed25519"></label>
|
||||
<label>Password ID<input name="password_id"></label>
|
||||
</div>
|
||||
|
||||
<label class="check">
|
||||
<input name="kitty_fix" type="checkbox">
|
||||
<span>Kitty Fix für diesen Server nutzen</span>
|
||||
</label>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" id="connectButton">Verbinden</button>
|
||||
<button type="button" id="sshButton" class="ghost">SSH Befehl</button>
|
||||
<button type="submit">Speichern</button>
|
||||
</div>
|
||||
|
||||
<pre id="sshCommand" class="command-box" hidden></pre>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section id="commandsView" class="view">
|
||||
<div id="commandsList" class="command-list"></div>
|
||||
</section>
|
||||
|
||||
<section id="settingsView" class="view">
|
||||
<form id="settingsForm" class="settings-panel">
|
||||
<label>Theme<input name="theme"></label>
|
||||
<label>TERM Override<input name="term"></label>
|
||||
<label class="check">
|
||||
<input name="enable_kitty_fix" type="checkbox">
|
||||
<span>Kitty Fix global aktivieren</span>
|
||||
</label>
|
||||
<button type="submit">Settings speichern</button>
|
||||
</form>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="toast" class="toast" hidden></div>
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
346
web/styles.css
Normal file
346
web/styles.css
Normal file
@@ -0,0 +1,346 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #07110d;
|
||||
--panel: #0d1b15;
|
||||
--panel-2: #13251e;
|
||||
--text: #d7ffe9;
|
||||
--muted: #8aa99b;
|
||||
--line: #1f4b38;
|
||||
--accent: #00ff99;
|
||||
--accent-2: #33ccff;
|
||||
--danger: #ff6666;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 6px;
|
||||
background: var(--accent);
|
||||
color: #001a10;
|
||||
padding: 10px 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.ghost {
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
border-color: var(--line);
|
||||
}
|
||||
|
||||
button.danger {
|
||||
background: transparent;
|
||||
color: var(--danger);
|
||||
border-color: rgba(255, 102, 102, 0.45);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background: #09150f;
|
||||
color: var(--text);
|
||||
padding: 10px 11px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.shell {
|
||||
display: grid;
|
||||
grid-template-columns: 280px minmax(0, 1fr);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right: 1px solid var(--line);
|
||||
background: #06100c;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 13px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.mark {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 6px;
|
||||
background: var(--accent);
|
||||
color: #001a10;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.brand h1,
|
||||
.brand p,
|
||||
.topbar h2,
|
||||
.topbar p,
|
||||
.editor h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.brand h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.brand p,
|
||||
.eyebrow {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.status-line {
|
||||
color: var(--accent-2);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.search {
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
border-color: transparent;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
border-color: var(--line);
|
||||
background: var(--panel);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.workspace {
|
||||
min-width: 0;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.topbar h2 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.actions,
|
||||
.form-actions,
|
||||
.editor-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.view.active {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
#serversView.active {
|
||||
grid-template-columns: minmax(280px, 430px) minmax(360px, 1fr);
|
||||
}
|
||||
|
||||
.server-grid {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.server-card,
|
||||
.editor,
|
||||
.settings-panel,
|
||||
.command-item {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.server-card {
|
||||
padding: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.server-card.active {
|
||||
border-color: var(--accent);
|
||||
background: var(--panel-2);
|
||||
}
|
||||
|
||||
.server-card h3 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.server-meta {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.pill-row {
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
margin-top: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pill {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
padding: 3px 8px;
|
||||
color: var(--accent-2);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.editor,
|
||||
.settings-panel {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.check input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
justify-content: flex-end;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.command-box {
|
||||
margin: 16px 0 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
background: #050b08;
|
||||
color: var(--accent);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.command-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.command-item {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.command-item h3 {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.command-item code {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.settings-panel {
|
||||
max-width: 520px;
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--panel-2);
|
||||
color: var(--text);
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
#serversView.active {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.topbar,
|
||||
.actions {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user