feat: refine cloud console UI

This commit is contained in:
Philipp
2026-06-12 15:11:55 +02:00
parent 452d1fb8b3
commit d8a12a41c8
4 changed files with 515 additions and 244 deletions
+9 -6
View File
@@ -18,9 +18,9 @@ export function VMCreate({ token, projectID, tenantID, onCreated }: Props) {
const [name, setName] = useState(""); const [name, setName] = useState("");
const [templateID, setTemplateID] = useState(""); const [templateID, setTemplateID] = useState("");
const [node, setNode] = useState(""); const [node, setNode] = useState("");
const [vcpu, setVCPU] = useState(2); const [vcpu, setVCPU] = useState(4);
const [ramMB, setRAMMB] = useState(2048); const [ramMB, setRAMMB] = useState(6144);
const [diskGB, setDiskGB] = useState(10); const [diskGB, setDiskGB] = useState(30);
const [sshKeyID, setSSHKeyID] = useState(""); const [sshKeyID, setSSHKeyID] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
@@ -92,14 +92,17 @@ export function VMCreate({ token, projectID, tenantID, onCreated }: Props) {
if (!open) { if (!open) {
return ( return (
<div className="create-launch">
<span>Server aus einem Template bereitstellen</span>
<button className="primary-button" onClick={() => setOpen(true)}> <button className="primary-button" onClick={() => setOpen(true)}>
Neue VM erstellen Server erstellen
</button> </button>
</div>
); );
} }
return ( return (
<div className="panel"> <div className="provision-box">
<div className="panel-header"> <div className="panel-header">
<div> <div>
<p className="eyebrow">Provisioning</p> <p className="eyebrow">Provisioning</p>
@@ -110,7 +113,7 @@ export function VMCreate({ token, projectID, tenantID, onCreated }: Props) {
</button> </button>
</div> </div>
{error ? <p className="form-error">{error}</p> : null} {error ? <p className="form-error">{error}</p> : null}
<div className="metric-list"> <div className="provision-grid">
<label> <label>
Name Name
<input <input
+34 -27
View File
@@ -48,59 +48,66 @@ export function VMList({ token, projectID }: Props) {
return <p className="metric-text">Keine VMs in diesem Projekt.</p>; return <p className="metric-text">Keine VMs in diesem Projekt.</p>;
return ( return (
<div className="vm-grid"> <div className="table-wrap">
<table className="resource-table">
<thead>
<tr>
<th>Status</th>
<th>Name</th>
<th>Node</th>
<th>Ressourcen</th>
<th>VMID</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{vms.map((vm) => ( {vms.map((vm) => (
<div className="vm-card" key={vm.id}> <tr key={vm.id}>
<div className="vm-card-header"> <td>
<strong>{vm.name}</strong>
<span className={`pill ${vm.status === "running" ? "ok" : ""}`}> <span className={`pill ${vm.status === "running" ? "ok" : ""}`}>
{vm.status} {vm.status}
</span> </span>
</div> </td>
<div className="vm-card-body"> <td>
<div className="metric"> <strong className="resource-name">{vm.name}</strong>
<span>Node</span> </td>
<strong>{vm.node}</strong> <td>{vm.node}</td>
</div> <td>
<div className="metric"> {vm.vcpu} vCPU · {vm.ram_mb} MB · {vm.disk_gb} GB
<span>vCPU / RAM / Disk</span> </td>
<strong> <td>{vm.proxmox_vmid}</td>
{vm.vcpu} / {vm.ram_mb} MB / {vm.disk_gb} GB <td>
</strong> <div className="resource-actions">
</div>
<div className="metric">
<span>Proxmox VMID</span>
<strong>{vm.proxmox_vmid}</strong>
</div>
</div>
<div className="vm-card-actions">
<button <button
className="primary-button" className="ghost-button compact"
onClick={() => handlePower(vm.id, "start")} onClick={() => handlePower(vm.id, "start")}
> >
Start Start
</button> </button>
<button <button
className="ghost-button" className="ghost-button compact"
onClick={() => handlePower(vm.id, "stop")} onClick={() => handlePower(vm.id, "stop")}
> >
Stop Stop
</button> </button>
<button <button
className="ghost-button" className="ghost-button compact"
onClick={() => handlePower(vm.id, "reboot")} onClick={() => handlePower(vm.id, "reboot")}
> >
Reboot Reboot
</button> </button>
<button <button
className="ghost-button danger" className="ghost-button compact danger"
onClick={() => handleDelete(vm.id)} onClick={() => handleDelete(vm.id)}
> >
Löschen Löschen
</button> </button>
</div> </div>
</div> </td>
</tr>
))} ))}
</tbody>
</table>
</div> </div>
); );
} }
+46 -11
View File
@@ -27,10 +27,10 @@ const supabase =
: undefined; : undefined;
const navItems: Array<{ key: ViewKey; label: string }> = [ const navItems: Array<{ key: ViewKey; label: string }> = [
{ key: "overview", label: "Uebersicht" }, { key: "overview", label: "Projekt" },
{ key: "vms", label: "VMs" }, { key: "vms", label: "Server" },
{ key: "ssh", label: "SSH-Keys" }, { key: "ssh", label: "SSH-Keys" },
{ key: "audit", label: "Audit" }, { key: "audit", label: "Audit Logs" },
{ key: "console", label: "Konsole" }, { key: "console", label: "Konsole" },
]; ];
@@ -121,6 +121,7 @@ function App() {
} }
const connected = Boolean(session && backendPrincipal); const connected = Boolean(session && backendPrincipal);
const activeTitle = pageTitle(activeView);
if (loadingSession) { if (loadingSession) {
return ( return (
@@ -152,8 +153,19 @@ function App() {
return ( return (
<main className="app-frame"> <main className="app-frame">
<aside className="sidebar"> <aside className="sidebar">
<div className="sidebar-brand">
<div className="brand-mark">P</div> <div className="brand-mark">P</div>
<div className="brand-copy">
<strong>ProxUI</strong>
<span>Cloud Console</span>
</div>
</div>
<div className="project-chip">
<span>Projekt</span>
<strong>{selectedProject?.name ?? "Nicht gewählt"}</strong>
</div>
<nav className="main-nav" aria-label="Hauptnavigation"> <nav className="main-nav" aria-label="Hauptnavigation">
<span className="nav-section-label">Resources</span>
{navItems.map((item) => ( {navItems.map((item) => (
<button <button
className={ className={
@@ -173,7 +185,7 @@ function App() {
<header className="topbar"> <header className="topbar">
<div> <div>
<p className="eyebrow">ProxUI</p> <p className="eyebrow">ProxUI</p>
<h1>Proxmox Multi-Tenant Console</h1> <h1>{activeTitle}</h1>
</div> </div>
<div className="session-box"> <div className="session-box">
<span className={connected ? "status-dot ok" : "status-dot"} /> <span className={connected ? "status-dot ok" : "status-dot"} />
@@ -209,7 +221,9 @@ function App() {
activeView={activeView} activeView={activeView}
token={session.access_token} token={session.access_token}
tenantID={selectedTenant?.id ?? ""} tenantID={selectedTenant?.id ?? ""}
tenantName={selectedTenant?.name ?? ""}
projectID={selectedProject?.id ?? ""} projectID={selectedProject?.id ?? ""}
projectName={selectedProject?.name ?? ""}
/> />
</section> </section>
</section> </section>
@@ -295,12 +309,16 @@ function ViewPanel({
activeView, activeView,
token, token,
tenantID, tenantID,
tenantName,
projectID, projectID,
projectName,
}: { }: {
activeView: ViewKey; activeView: ViewKey;
token: string; token: string;
tenantID: string; tenantID: string;
tenantName: string;
projectID: string; projectID: string;
projectName: string;
}) { }) {
const [vmRefreshKey, setVMRefreshKey] = useState(0); const [vmRefreshKey, setVMRefreshKey] = useState(0);
@@ -315,7 +333,12 @@ function ViewPanel({
return ( return (
<section className="panel view-panel"> <section className="panel view-panel">
{activeView === "overview" && ( {activeView === "overview" && (
<Overview tenantID={tenantID} projectID={projectID} /> <Overview
tenantID={tenantID}
tenantName={tenantName}
projectID={projectID}
projectName={projectName}
/>
)} )}
{activeView === "vms" && ( {activeView === "vms" && (
<VMView <VMView
@@ -339,27 +362,31 @@ function ViewPanel({
function Overview({ function Overview({
tenantID, tenantID,
tenantName,
projectID, projectID,
projectName,
}: { }: {
tenantID: string; tenantID: string;
tenantName: string;
projectID: string; projectID: string;
projectName: string;
}) { }) {
return ( return (
<> <>
<div className="panel-header"> <div className="panel-header">
<div> <div>
<p className="eyebrow">Status</p> <p className="eyebrow">Projekt</p>
<h2>Arbeitsbereich</h2> <h2>Arbeitsbereich</h2>
</div> </div>
</div> </div>
<div className="placeholder-table"> <div className="placeholder-table">
<div className="placeholder-row"> <div className="placeholder-row">
<span>Tenant</span> <span>Mandant</span>
<strong>{tenantID}</strong> <strong>{tenantName || tenantID}</strong>
</div> </div>
<div className="placeholder-row"> <div className="placeholder-row">
<span>Projekt</span> <span>Projekt</span>
<strong>{projectID || "Kein Projekt ausgewählt"}</strong> <strong>{projectName || projectID || "Kein Projekt ausgewählt"}</strong>
</div> </div>
</div> </div>
</> </>
@@ -385,8 +412,8 @@ function VMView({
<> <>
<div className="panel-header"> <div className="panel-header">
<div> <div>
<p className="eyebrow">VMs</p> <p className="eyebrow">Compute</p>
<h2>Virtuelle Maschinen</h2> <h2>Server</h2>
</div> </div>
</div> </div>
<VMCreate <VMCreate
@@ -400,6 +427,14 @@ function VMView({
); );
} }
function pageTitle(view: ViewKey) {
if (view === "vms") return "Server";
if (view === "ssh") return "SSH Keys";
if (view === "audit") return "Audit Logs";
if (view === "console") return "Konsole";
return "Projektübersicht";
}
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
+395 -169
View File
@@ -1,9 +1,11 @@
:root { :root {
color: #172026; color: #e8edf0;
background: #f4f7fb; background: #0b0d0e;
font-family: font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif; sans-serif;
font-synthesis: none;
text-rendering: optimizeLegibility;
} }
* { * {
@@ -12,10 +14,17 @@
body { body {
margin: 0; margin: 0;
background:
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px),
#0b0d0e;
background-size: 32px 32px;
} }
button, button,
input { input,
select,
textarea {
font: inherit; font: inherit;
} }
@@ -34,26 +43,27 @@ button {
.loader { .loader {
width: 32px; width: 32px;
height: 32px; height: 32px;
border: 3px solid #cfdae7; border: 3px solid #263135;
border-top-color: #147d64; border-top-color: #ff583d;
border-radius: 999px; border-radius: 999px;
animation: spin 900ms linear infinite; animation: spin 900ms linear infinite;
} }
.auth-panel { .auth-panel {
width: min(420px, 100%); width: min(420px, 100%);
border: 1px solid #d6dee8; border: 1px solid #283236;
border-radius: 8px; border-radius: 8px;
background: #ffffff; background: #111517;
padding: 28px; padding: 28px;
box-shadow: 0 18px 50px rgba(23, 32, 38, 0.08); box-shadow: 0 24px 80px rgba(0, 0, 0, 0.34);
} }
.auth-panel h1, .auth-panel h1,
.topbar h1, .topbar h1,
.panel h2 { .panel h2,
.provision-box h2 {
margin: 0; margin: 0;
line-height: 1.15; line-height: 1.12;
letter-spacing: 0; letter-spacing: 0;
} }
@@ -67,21 +77,43 @@ button {
margin-top: 24px; margin-top: 24px;
} }
.auth-form label { .auth-form label,
display: grid; .metric-list label,
gap: 8px; .context-selects label {
color: #4d5c68; color: #9aa6aa;
font-size: 14px; font-size: 13px;
font-weight: 700; font-weight: 700;
} }
.auth-form input { .auth-form label,
width: 100%; .metric-list label {
border: 1px solid #c7d2de; display: grid;
gap: 7px;
}
.auth-form input,
.metric-list input,
.metric-list select,
.ssh-form input,
.ssh-form textarea,
.context-selects select {
min-height: 38px;
border: 1px solid #313c41;
border-radius: 6px; border-radius: 6px;
color: #172026; color: #e8edf0;
background: #ffffff; background: #0d1113;
padding: 11px 12px; padding: 8px 10px;
outline: none;
}
.auth-form input:focus,
.metric-list input:focus,
.metric-list select:focus,
.ssh-form input:focus,
.ssh-form textarea:focus,
.context-selects select:focus {
border-color: #ff583d;
box-shadow: 0 0 0 3px rgba(255, 88, 61, 0.16);
} }
.auth-actions, .auth-actions,
@@ -101,13 +133,17 @@ button {
min-height: 38px; min-height: 38px;
border-radius: 6px; border-radius: 6px;
padding: 0 14px; padding: 0 14px;
font-weight: 700; font-weight: 800;
} }
.primary-button { .primary-button {
border: 1px solid #147d64; border: 1px solid #ff583d;
color: #ffffff; color: #140d0b;
background: #147d64; background: #ff583d;
}
.primary-button:hover {
background: #ff6b53;
} }
.primary-button:disabled { .primary-button:disabled {
@@ -116,14 +152,28 @@ button {
} }
.ghost-button { .ghost-button {
border: 1px solid #c7d2de; border: 1px solid #313c41;
color: #23313a; color: #d5dcdf;
background: #ffffff; background: #121719;
}
.ghost-button:hover {
border-color: #4a575c;
background: #171d20;
}
.ghost-button.danger {
border-color: #793a31;
color: #ff8b78;
}
.ghost-button.danger:hover {
background: #221312;
} }
.form-error { .form-error {
margin: 0; margin: 0;
color: #a13c21; color: #ff8b78;
font-size: 14px; font-size: 14px;
line-height: 1.45; line-height: 1.45;
} }
@@ -131,82 +181,142 @@ button {
.app-frame { .app-frame {
min-height: 100vh; min-height: 100vh;
display: grid; display: grid;
grid-template-columns: 224px minmax(0, 1fr); grid-template-columns: 236px minmax(0, 1fr);
} }
.sidebar { .sidebar {
border-right: 1px solid #dbe3ec; border-right: 1px solid #20282b;
background: #ffffff; background: rgba(12, 15, 16, 0.94);
padding: 20px 14px; padding: 22px 14px;
}
.sidebar-brand {
display: flex;
align-items: center;
gap: 10px;
} }
.brand-mark { .brand-mark {
width: 40px; width: 42px;
height: 40px; height: 42px;
display: grid; display: grid;
place-items: center; place-items: center;
border: 1px solid #2e3b3f;
border-radius: 8px; border-radius: 8px;
color: #ffffff; color: #ff583d;
background: #147d64; background: #101416;
font-weight: 800; font-weight: 900;
box-shadow: inset 0 0 0 1px rgba(255, 88, 61, 0.1);
}
.brand-copy {
display: grid;
gap: 2px;
}
.brand-copy strong {
color: #f3f6f7;
font-size: 15px;
}
.brand-copy span,
.project-chip span,
.nav-section-label {
color: #6f7b80;
font-size: 11px;
font-weight: 900;
letter-spacing: 0;
text-transform: uppercase;
}
.project-chip {
display: grid;
gap: 4px;
border: 1px solid #242e32;
border-radius: 8px;
background: #0d1113;
margin-top: 20px;
padding: 10px;
}
.project-chip strong {
overflow: hidden;
color: #e8edf0;
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
} }
.main-nav { .main-nav {
display: grid; display: grid;
gap: 6px; gap: 5px;
margin-top: 28px; margin-top: 24px;
}
.nav-section-label {
display: block;
padding: 0 10px 6px;
} }
.nav-item { .nav-item {
width: 100%; width: 100%;
border: 0; border: 1px solid transparent;
border-radius: 6px; border-radius: 6px;
color: #40515d; color: #9aa6aa;
background: transparent; background: transparent;
padding: 10px 12px; padding: 11px 12px;
text-align: left; text-align: left;
font-weight: 750;
}
.nav-item:hover {
color: #e8edf0;
background: #121719;
} }
.nav-item.active { .nav-item.active {
color: #0f2c26; border-color: #314047;
background: #e5f4ef; color: #ffffff;
font-weight: 800; background: #182024;
box-shadow: inset 3px 0 0 #ff583d;
} }
.workspace { .workspace {
min-width: 0; min-width: 0;
padding: 28px; padding: 24px 32px 40px;
} }
.topbar { .topbar {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
gap: 18px; gap: 18px;
align-items: flex-start; align-items: center;
margin-bottom: 24px; margin-bottom: 18px;
} }
.topbar h1 { .topbar h1 {
font-size: 28px; max-width: 780px;
font-size: 26px;
color: #f3f6f7;
} }
.eyebrow { .eyebrow {
margin: 0 0 8px; margin: 0 0 8px;
color: #147d64; color: #ff583d;
font-size: 12px; font-size: 12px;
font-weight: 800; font-weight: 900;
letter-spacing: 0; letter-spacing: 0;
text-transform: uppercase; text-transform: uppercase;
} }
.session-box { .session-box {
max-width: 520px; max-width: 520px;
min-width: 280px; min-width: 292px;
justify-content: flex-end; justify-content: flex-end;
gap: 12px; gap: 12px;
border: 1px solid #d6dee8; border: 1px solid #283236;
border-radius: 8px; border-radius: 8px;
background: #ffffff; background: #111517;
padding: 10px; padding: 10px;
} }
@@ -223,11 +333,12 @@ button {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
color: #f3f6f7;
font-size: 14px; font-size: 14px;
} }
.session-box span { .session-box span {
color: #60717d; color: #8e9aa0;
font-size: 13px; font-size: 13px;
} }
@@ -236,17 +347,21 @@ button {
height: 10px; height: 10px;
flex: 0 0 auto; flex: 0 0 auto;
border-radius: 999px; border-radius: 999px;
background: #d99b33; background: #c9952e;
} }
.status-dot.ok { .status-dot.ok {
background: #147d64; background: #28c28b;
} }
.content-grid { .context-bar,
display: grid; .panel,
grid-template-columns: 1fr; .provision-box,
gap: 18px; .vm-card {
border: 1px solid #283236;
border-radius: 8px;
background: rgba(17, 21, 23, 0.96);
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.2);
} }
.context-bar { .context-bar {
@@ -254,9 +369,6 @@ button {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 14px; gap: 14px;
border: 1px solid #d6dee8;
border-radius: 8px;
background: #ffffff;
padding: 12px 16px; padding: 12px 16px;
margin-bottom: 18px; margin-bottom: 18px;
} }
@@ -265,35 +377,210 @@ button {
display: flex; display: flex;
gap: 14px; gap: 14px;
align-items: center; align-items: center;
flex-wrap: wrap;
} }
.context-selects label { .context-selects label {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
color: #4d5c68; }
.content-grid {
display: grid;
grid-template-columns: 1fr;
gap: 18px;
}
.panel {
min-width: 0;
padding: 20px;
}
.view-panel {
min-height: 168px;
}
.panel-header {
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
}
.panel h2,
.provision-box h2 {
color: #f3f6f7;
font-size: 21px;
}
.metric-list,
.placeholder-table {
display: grid;
gap: 10px;
}
.metric,
.placeholder-row {
display: flex;
justify-content: space-between;
gap: 16px;
border: 1px solid #222b2f;
border-radius: 6px;
background: #0d1113;
padding: 12px;
}
.metric span,
.placeholder-row span,
.metric-text {
color: #8e9aa0;
}
.metric strong,
.placeholder-row strong {
min-width: 0;
overflow-wrap: anywhere;
color: #edf2f3;
text-align: right;
}
.pill {
border: 1px solid #7d5a20;
border-radius: 999px;
color: #e5b559;
background: #1d170d;
padding: 5px 10px;
font-size: 12px;
font-weight: 900;
}
.pill.ok {
border-color: #2a795f;
color: #52d7a7;
background: #0d201a;
}
.provision-box {
display: grid;
gap: 14px;
margin: 14px 0 16px;
padding: 16px;
}
.create-launch {
display: flex;
justify-content: space-between;
align-items: center;
gap: 14px;
border: 1px solid #242e32;
border-radius: 8px;
background: #0d1113;
margin-bottom: 14px;
padding: 12px;
}
.create-launch span {
color: #9aa6aa;
font-size: 14px;
}
.provision-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.provision-grid label {
display: grid;
gap: 7px;
color: #9aa6aa;
font-size: 13px; font-size: 13px;
font-weight: 700; font-weight: 700;
} }
.context-selects select { .provision-grid input,
border: 1px solid #c7d2de; .provision-grid select {
min-height: 38px;
border: 1px solid #313c41;
border-radius: 6px; border-radius: 6px;
padding: 7px 10px; color: #e8edf0;
font: inherit; background: #0d1113;
background: #ffffff; padding: 8px 10px;
outline: none;
}
.provision-grid input:focus,
.provision-grid select:focus {
border-color: #ff583d;
box-shadow: 0 0 0 3px rgba(255, 88, 61, 0.16);
} }
.vm-grid { .vm-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 14px; gap: 14px;
margin-top: 14px; margin-top: 14px;
} }
.vm-card { .table-wrap {
border: 1px solid #d6dee8; overflow-x: auto;
border: 1px solid #242e32;
border-radius: 8px; border-radius: 8px;
background: #ffffff; background: #0d1113;
}
.resource-table {
width: 100%;
min-width: 820px;
border-collapse: collapse;
}
.resource-table th,
.resource-table td {
border-bottom: 1px solid #20282b;
padding: 12px;
text-align: left;
vertical-align: middle;
}
.resource-table th {
color: #7b878c;
font-size: 11px;
font-weight: 900;
letter-spacing: 0;
text-transform: uppercase;
}
.resource-table td {
color: #c9d2d6;
font-size: 14px;
}
.resource-table tbody tr:hover {
background: #111719;
}
.resource-table tbody tr:last-child td {
border-bottom: 0;
}
.resource-name {
color: #f3f6f7;
}
.resource-actions {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.compact {
min-height: 32px;
padding: 0 10px;
font-size: 13px;
}
.vm-card {
padding: 16px; padding: 16px;
} }
@@ -301,12 +588,21 @@ button {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 10px; gap: 12px;
margin-bottom: 12px;
}
.vm-card-header strong {
min-width: 0;
overflow: hidden;
color: #f3f6f7;
text-overflow: ellipsis;
white-space: nowrap;
} }
.vm-card-body { .vm-card-body {
display: grid; display: grid;
gap: 6px; gap: 8px;
margin-bottom: 12px; margin-bottom: 12px;
} }
@@ -322,103 +618,18 @@ button {
margin-bottom: 14px; margin-bottom: 14px;
} }
.ssh-form input,
.ssh-form textarea {
border: 1px solid #c7d2de;
border-radius: 6px;
padding: 8px 10px;
font: inherit;
}
.metric-text { .metric-text {
color: #60717d;
font-size: 14px; font-size: 14px;
} }
.metric-list label { code {
display: grid; display: block;
gap: 4px;
color: #4d5c68;
font-size: 13px;
font-weight: 700;
}
.metric-list input {
border: 1px solid #c7d2de;
border-radius: 6px;
padding: 8px 10px;
font: inherit;
}
.ghost-button.danger {
border-color: #da8a7a;
color: #a13c21;
}
.ghost-button.danger:hover {
background: #fdf3f0;
}
.panel {
min-width: 0;
border: 1px solid #d6dee8;
border-radius: 8px;
background: #ffffff;
padding: 22px;
}
.panel-header {
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
}
.panel h2 {
font-size: 21px;
}
.metric-list,
.placeholder-table {
display: grid;
gap: 10px;
}
.metric,
.placeholder-row {
display: flex;
justify-content: space-between;
gap: 16px;
border: 1px solid #edf1f5;
border-radius: 6px;
padding: 12px;
}
.metric span,
.placeholder-row span {
color: #60717d;
}
.metric strong,
.placeholder-row strong {
min-width: 0;
overflow-wrap: anywhere; overflow-wrap: anywhere;
text-align: right; border: 1px solid #283236;
} border-radius: 6px;
color: #d7f7ec;
.pill { background: #090c0d;
border: 1px solid #d7a44d; padding: 10px;
border-radius: 999px;
color: #8a5c0b;
background: #fff8e8;
padding: 5px 10px;
font-size: 12px;
font-weight: 800;
}
.pill.ok {
border-color: #6fb79f;
color: #126b55;
background: #e8f6f1;
} }
@keyframes spin { @keyframes spin {
@@ -434,13 +645,18 @@ button {
.sidebar { .sidebar {
border-right: 0; border-right: 0;
border-bottom: 1px solid #dbe3ec; border-bottom: 1px solid #20282b;
} }
.main-nav { .main-nav {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
.project-chip,
.nav-section-label {
display: none;
}
.topbar, .topbar,
.content-grid { .content-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -470,6 +686,16 @@ button {
flex-direction: column; flex-direction: column;
} }
.create-launch,
.context-bar {
align-items: flex-start;
flex-direction: column;
}
.provision-grid {
grid-template-columns: 1fr;
}
.metric strong, .metric strong,
.placeholder-row strong { .placeholder-row strong {
text-align: left; text-align: left;