mirror of
https://github.com/jamesstringer90/appsandbox.git
synced 2026-10-21 10:40:25 -05:00
Expose available GPUs in the UI and API, preserve the selected device, and fall back to Default GPU if it disappears.
560 lines
14 KiB
CSS
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;
|
|
}
|