Files
James Stringer 66ffaf6dcd Allow GPU selection when creating and editing VMs
Expose available GPUs in the UI and API, preserve the selected device, and fall back to Default GPU if it disappears.
2026-09-15 20:17:11 -04:00

560 lines
14 KiB
CSS

/* App Sandbox - Dark Theme */
:root {
--bg: #1e1e1e;
--ctrl-bg: #2d2d2d;
--ctrl-border: #3e3e3e;
--text: #e6e6e6;
--text-dim: #888;
--accent: #4ea6f5;
--accent-hover: #6db8ff;
--danger: #dc3232;
--danger-dim: #8c2828;
--success: #00c800;
--warning: #dcc800;
--section-gap: 12px;
--radius: 4px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: 'Segoe UI', sans-serif;
font-size: 13px;
padding: 8px 12px;
overflow: hidden;
user-select: none;
height: 100vh;
display: flex;
flex-direction: column;
}
/* Section headings */
h2 {
font-size: 12px;
font-weight: 600;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
padding-bottom: 4px;
border-bottom: 1px solid var(--ctrl-border);
}
/* Section header row: title on left, action button on right */
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
padding-bottom: 4px;
border-bottom: 1px solid var(--ctrl-border);
}
.section-header h2 {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
#btn-new-sandbox {
height: 26px;
padding: 2px 14px;
}
/* Collapsible sections */
h2.collapsible { cursor: pointer; }
h2.collapsible:hover { color: var(--text); }
.collapse-arrow { display: inline-block; font-size: 10px; transition: transform 0.15s; }
.collapsed .collapse-arrow { transform: rotate(-90deg); }
.collapsed .section-body, .collapsed #log-panel { display: none; }
/* Form layout */
.form-grid {
display: grid;
grid-template-columns: 90px 1fr;
gap: 4px 8px;
align-items: center;
}
.form-grid label {
font-size: 12px;
color: var(--text-dim);
text-align: right;
padding-right: 4px;
}
.form-row {
display: flex;
align-items: center;
gap: 6px;
min-height: 26px;
}
.form-row .host-info {
font-size: 11px;
color: var(--text-dim);
margin-left: 8px;
}
/* Inputs */
input[type="text"],
input[type="number"],
input[type="password"],
select {
background: var(--ctrl-bg);
color: var(--text);
border: 1px solid var(--ctrl-border);
border-radius: var(--radius);
padding: 3px 6px;
font-size: 12px;
font-family: inherit;
outline: none;
height: 24px;
}
input:focus, select:focus {
border-color: var(--accent);
}
input[type="text"] { width: 100%; }
input[type="number"] { width: 80px; }
input[type="password"] { width: 100%; }
select { min-width: 80px; }
#gpu-mode, #edit-gpu-mode { max-width: 100%; text-overflow: ellipsis; }
.input-sm { width: 80px !important; }
.input-md { width: 140px !important; }
/* Buttons */
button {
background: var(--ctrl-bg);
color: var(--text);
border: 1px solid var(--ctrl-border);
border-radius: var(--radius);
padding: 4px 12px;
font-size: 12px;
font-family: inherit;
cursor: pointer;
height: 26px;
white-space: nowrap;
}
button:hover { background: #3a3a3a; border-color: #555; }
button:active { background: #444; }
button:disabled { opacity: 0.4; cursor: default; }
button:disabled:hover { background: var(--ctrl-bg); border-color: var(--ctrl-border); }
/* Disabled checkboxes (e.g. "Deploy SSH key" until "SSH Server" is ticked).
Chromium/WebView2 dims a disabled native checkbox automatically, but WebKit
(the macOS WKWebView host) does not -- so dim it, and the whole label, here
to make the unavailable affordance obvious on both engines. */
input[type="checkbox"]:disabled { opacity: 0.4; cursor: default; }
label:has(input[type="checkbox"]:disabled) { opacity: 0.5; cursor: default; }
button.primary {
background: #2a5a8a;
border-color: #3a7aba;
}
button.primary:hover { background: #3a6a9a; }
button.primary:disabled { background: #2a3a4a; border-color: #3a4a5a; }
/* Image row with OR separator */
.image-row {
display: flex;
align-items: center;
gap: 6px;
}
.image-row input[type="text"] { flex: 1; min-width: 120px; }
.or-separator {
color: var(--text-dim);
font-size: 11px;
font-weight: 600;
padding: 0 2px;
}
.image-row select { min-width: 140px; }
/* Template custom dropdown */
.template-dropdown {
position: relative;
min-width: 180px;
}
.template-dropdown-selected {
background: var(--ctrl-bg);
color: var(--text);
border: 1px solid var(--ctrl-border);
border-radius: var(--radius);
padding: 3px 6px;
font-size: 12px;
height: 24px;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.template-dropdown.open .template-dropdown-selected {
border-color: var(--accent);
}
.template-dropdown-list {
display: none;
position: absolute;
top: 100%;
left: 0; right: 0;
background: var(--ctrl-bg);
border: 1px solid var(--accent);
border-radius: var(--radius);
margin-top: 2px;
z-index: 100;
max-height: 200px;
overflow-y: auto;
}
.template-dropdown.open .template-dropdown-list { display: block; }
.template-dropdown-item {
display: flex;
align-items: center;
padding: 4px 6px;
font-size: 12px;
cursor: pointer;
color: var(--text);
}
.template-dropdown-item:hover { background: #3a3a3a; }
.template-dropdown-item .tpl-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.template-dropdown-item .tpl-delete {
flex-shrink: 0;
margin-left: 6px;
cursor: pointer;
opacity: 0.5;
font-size: 14px;
}
.template-dropdown-item .tpl-delete:hover { opacity: 1; }
/* Inline field validation warning */
.field-warn {
font-size: 11px;
color: var(--danger);
white-space: normal;
overflow-wrap: anywhere;
flex: 1 1 0;
min-width: 0;
}
/* Password match validation */
.pass-mismatch:focus { border-color: var(--danger-dim) !important; }
.pass-match:focus { border-color: var(--success) !important; }
/* Password reveal toggle */
.toggle-pass {
cursor: pointer;
font-size: 14px;
opacity: 0.5;
user-select: none;
}
.toggle-pass:hover { opacity: 1; }
/* Credentials row */
.cred-row {
display: flex;
align-items: center;
gap: 6px;
}
.cred-row label {
font-size: 12px;
color: var(--text-dim);
white-space: nowrap;
}
.cred-row input { flex: 1; }
/* Action buttons row */
.action-buttons {
display: flex;
gap: 6px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--ctrl-border);
}
/* Section spacing */
section { margin-bottom: var(--section-gap); flex-shrink: 0; }
section:last-of-type { flex: 1; min-height: 0; display: flex; flex-direction: column; margin-bottom: 0; }
section:last-of-type.collapsed { flex: 0; }
/* VM Table */
.vm-table-wrap {
overflow-x: auto;
border: 1px solid var(--ctrl-border);
border-radius: var(--radius);
}
table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
th {
background: #252525;
color: var(--text-dim);
font-weight: 600;
font-size: 11px;
text-align: left;
padding: 4px 6px;
border-bottom: 1px solid var(--ctrl-border);
white-space: nowrap;
position: sticky;
top: 0;
}
td {
padding: 3px 6px;
border-bottom: 1px solid #2a2a2a;
white-space: nowrap;
}
td.empty-state { text-align: center; color: var(--text-dim); padding: 20px 6px; }
.empty-state-btn { height: 32px; padding: 4px 18px; font-size: 13px; }
tr:hover td { background: #252525; }
tr.selected td { background: #1a3a5a; }
/* Icon columns — fixed width, centered in both header and body */
th.icon-col, td.icon-col {
width: 28px;
text-align: center;
padding-left: 2px;
padding-right: 2px;
}
/* Icon button cells */
.icon-btn {
background: none;
border: none;
font-size: 14px;
padding: 0;
height: auto;
width: 100%;
text-align: center;
cursor: pointer;
line-height: 1;
}
.icon-btn:hover { opacity: 0.8; }
.icon-btn:disabled { opacity: 0.3; cursor: default; }
/* Icon colors by state */
.icon-btn.start { color: var(--success); }
.icon-btn.start.inactive { color: #505050; }
.icon-btn.shutdown { color: var(--warning); }
.icon-btn.shutdown.inactive { color: #505050; }
.icon-btn.stop { color: #dc3232; }
.icon-btn.stop.inactive { color: #505050; }
.icon-btn.delete { color: #c83c3c; }
.icon-btn.delete.running { color: #642828; }
.icon-btn.ssh { color: var(--accent); font-family: monospace; font-weight: bold; }
.icon-btn.ssh.inactive { color: #505050; }
.icon-btn.ssh.hidden { display: none; }
.icon-btn.edit { color: var(--text-dim); }
.icon-btn.edit:hover { color: var(--text); }
/* Agent dot */
.agent-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: #c80000;
vertical-align: middle;
}
.agent-dot.online { background: var(--success); }
.agent-dot.disabled { background: var(--text-dim); }
/* Status text */
.status-running { color: var(--success); font-weight: 600; }
.status-shutting-down { color: var(--warning); font-weight: 600; }
.status-building { color: var(--accent); font-weight: 600; }
.spinner {
display: inline-block;
width: 10px; height: 10px;
border: 2px solid var(--accent);
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-left: 5px;
vertical-align: middle;
}
@keyframes spin { to { transform: rotate(360deg); } }
.status-stopped { color: var(--text-dim); }
.snap-cell { white-space: nowrap; }
.snap-open {
display: block;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
font-size: 11px;
}
/* Log panel */
#log-panel {
background: var(--ctrl-bg);
border: 1px solid var(--ctrl-border);
border-radius: var(--radius);
padding: 6px;
flex: 1;
min-height: 60px;
overflow-y: auto;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 11px;
line-height: 1.5;
color: #ccc;
user-select: text;
cursor: text;
}
#log-panel .log-line {
white-space: pre-wrap;
word-break: break-all;
}
/* Confirmation modal */
.modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.6);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal-overlay.active { display: flex; }
.modal {
background: #2a2a2a;
border: 1px solid #555;
border-radius: 6px;
padding: 20px;
max-width: 420px;
width: 90%;
}
.modal.wide {
max-width: 640px;
width: 92%;
display: flex;
flex-direction: column;
max-height: calc(100vh - 32px);
}
.modal.wide h3, .modal.wide .modal-buttons {
flex-shrink: 0;
}
#edit-vm-overlay .modal { max-width: 480px; }
.modal-form-body {
min-height: 0;
overflow-y: auto;
color-scheme: dark;
margin-bottom: 16px;
}
.modal-form-body .form-grid { grid-template-columns: 90px minmax(0, 1fr); }
.modal-form-body .form-row, .modal-form-body .cred-row,
.modal-form-body .image-row { min-width: 0; flex-wrap: wrap; }
.modal-form-body .cred-row input { min-width: 0; }
#edit-vm-warn, #snapshot-warn { display: block; margin-top: 12px; }
#edit-vm-warn:empty, #snapshot-warn:empty { display: none; }
#snapshot-current { overflow-wrap: anywhere; }
#snapshot-tree {
border: 1px solid var(--ctrl-border);
border-radius: var(--radius);
background: var(--ctrl-bg);
padding: 6px;
}
#snapshot-tree ul { list-style: none; margin: 0; padding: 0; }
#snapshot-tree ul ul {
margin-left: 13px;
padding-left: 12px;
border-left: 1px solid var(--ctrl-border);
}
.snapshot-choice {
display: flex;
align-items: center;
gap: 8px;
padding: 6px;
width: 100%;
height: auto;
font-size: inherit;
text-align: left;
white-space: normal;
}
.snapshot-choice, .snapshot-choice:hover, .snapshot-choice:active, .snapshot-choice:disabled:hover {
background: none;
border: none;
}
.snapshot-choice[aria-pressed="true"] .snapshot-name { font-weight: 600; }
.snapshot-choice:disabled { opacity: 1; }
.snapshot-choice:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; }
.snapshot-name { min-width: 0; overflow-wrap: anywhere; }
.snapshot-meta { min-width: 0; margin-left: auto; color: var(--text-dim); font-size: 11px; text-align: right; overflow-wrap: anywhere; }
#snapshot-help { margin: 12px 0 0; }
#btn-snapshot-create { margin-right: auto; }
.disk-location-btn { flex-shrink: 0; }
#disk-location-overlay .modal { max-width: 560px; max-height: calc(100vh - 32px); overflow-y: auto; color-scheme: dark; }
#disk-location-overlay .disk-directory-field { margin-bottom: 16px; }
.disk-directory-label { display: block; color: var(--text-dim); font-size: 12px; margin-bottom: 6px; }
.disk-directory-field { min-width: 0; }
.disk-directory-row { display: flex; align-items: center; gap: 6px; }
.disk-directory-row input[type="text"] { flex: 1; min-width: 0; }
.disk-directory-row button { flex-shrink: 0; }
.disk-directory-field .field-warn { display: block; margin: 4px 0 0; }
.disk-directory-field .field-warn:empty { display: none; }
#disk-location-space { display: block; color: var(--text-dim); font-size: 11px; margin-top: 6px; }
@media (max-width: 560px) {
.modal-form-body .form-grid { grid-template-columns: minmax(0, 1fr); }
.modal-form-body .form-grid > label { text-align: left; margin-top: 5px; }
.modal.wide .modal-buttons { flex-wrap: wrap; }
.modal.wide { padding: 14px; }
.snapshot-choice { flex-wrap: wrap; }
.snapshot-meta { flex-basis: 100%; text-align: left; }
}
.modal h3 {
font-size: 14px;
margin-bottom: 12px;
overflow-wrap: anywhere;
}
.modal p {
font-size: 12px;
color: #bbb;
margin-bottom: 16px;
line-height: 1.5;
}
.modal-buttons {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.modal-buttons .danger {
background: #5a1a1a;
border-color: #8a2a2a;
color: #ff8888;
}
.modal-buttons .danger:hover { background: #6a2a2a; }
.modal-buttons .primary {
background: #1a3a5a;
border-color: #2a5a8a;
color: #88bbff;
}
.modal-buttons .primary:hover { background: #2a4a6a; }
.prereq-progress {
margin-top: 12px;
height: 6px;
background: #1a1a1a;
border-radius: 3px;
overflow: hidden;
}
.prereq-progress-bar {
height: 100%;
width: 0%;
background: #4a8adf;
border-radius: 3px;
transition: width 0.3s ease;
}
.prereq-pct {
margin-top: 6px;
font-size: 11px;
color: #888;
text-align: center;
}