Two fixes to the .lp-ui-advanced-toggle on the Services tab header: 1. The thumb flipped from --text-primary (white-ish) to --text-on-accent (a dark navy on the default theme) when toggled on, which read as a "black circle" inside the accent track. Other toggles in the project (.eo-toggle in modal.css, .routing-toggle in routing.css) keep the thumb white in both states — only the track changes colour. Dropping the checked-state thumb fill brings this toggle in line. 2. The toggle was floating bare in the header row next to nothing, which looked out of place compared to the contained button-style controls in the same slot on Backups (Backup now / Open backup center). Wrapped it in a chip: neutral rgba(text, 0.06) bg + 0.15 border + 6×12 padding, hover bumps both alphas. Same recipe a .task-btn uses for its resting state, so the toggle visually reads as a control sitting in line with the rest of the row's actions. Signed-off-by: librelad <librelad@digitalangels.vip>
459 lines
13 KiB
CSS
459 lines
13 KiB
CSS
/*
|
|
Services tab — rows reuse the .task-item / .task-header / .task-info /
|
|
.task-actions / .task-details / .log-container pattern from the
|
|
task list so the two surfaces look identical. The only service-only
|
|
bits are the status dot inside the status pill, the port chips, and
|
|
a streaming-state hint on the log container.
|
|
*/
|
|
|
|
.services-section {
|
|
padding: 0;
|
|
}
|
|
|
|
/* Mirrors .config-title for visual parity across tabs. */
|
|
.services-title {
|
|
padding: 20px;
|
|
background: transparent;
|
|
border-bottom: 1px solid var(--border-color);
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.services-title h3 {
|
|
margin: 0 0 8px 0;
|
|
color: var(--text-primary, #fff);
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.services-title p {
|
|
margin: 0;
|
|
color: var(--text-secondary, #ccc);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.services-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* Recessed dark panel wrapping the service rows — mirrors the
|
|
.tasks-container the Tasks tab uses on the app detail page so the
|
|
two tabs share one visual idiom. rgba(bg, 0.2) reads as a sunken
|
|
pocket inside the tab-pane's glass surface. */
|
|
.services-rows {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.6rem;
|
|
padding: 16px;
|
|
margin: 16px;
|
|
background: rgba(var(--bg-rgb), 0.2);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Loading + empty + error states */
|
|
/* ------------------------------------------------------------------ */
|
|
.services-loading {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.6rem;
|
|
padding: 2rem;
|
|
color: var(--text-secondary, var(--text-muted));
|
|
}
|
|
|
|
.services-spinner {
|
|
width: 16px;
|
|
height: 16px;
|
|
border: 2px solid rgba(var(--text-rgb), 0.15);
|
|
border-top-color: var(--accent-color, var(--accent));
|
|
border-radius: 50%;
|
|
animation: services-spin 0.7s linear infinite;
|
|
}
|
|
|
|
@keyframes services-spin { to { transform: rotate(360deg); } }
|
|
|
|
.services-empty {
|
|
text-align: center;
|
|
padding: 2.5rem 1rem;
|
|
color: var(--text-secondary, var(--text-muted));
|
|
}
|
|
|
|
.services-empty-icon {
|
|
font-size: 2rem;
|
|
display: block;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.services-empty p {
|
|
margin: 0.25rem 0;
|
|
}
|
|
|
|
.services-empty-hint {
|
|
font-size: 0.85rem;
|
|
opacity: 0.75;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Status dot inside the .task-status pill */
|
|
/* ------------------------------------------------------------------ */
|
|
.service-dot {
|
|
display: inline-block;
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
margin-right: 6px;
|
|
vertical-align: middle;
|
|
background: var(--text-muted);
|
|
}
|
|
|
|
@keyframes service-pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.55; }
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Port + IP chips inside .task-info (alongside status / time) */
|
|
/* ------------------------------------------------------------------ */
|
|
.service-port {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 2px;
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 0.72rem;
|
|
background: rgba(var(--accent-rgb), 0.08);
|
|
border: 1px solid rgba(var(--accent-rgb), 0.25);
|
|
color: var(--text-secondary);
|
|
padding: 2px 7px;
|
|
border-radius: 4px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.service-port-arrow {
|
|
opacity: 0.5;
|
|
margin: 0 1px;
|
|
}
|
|
|
|
.service-port-proto {
|
|
margin-left: 4px;
|
|
font-size: 0.65rem;
|
|
opacity: 0.6;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.service-ip {
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
background: rgba(var(--text-rgb), 0.05);
|
|
border: 1px solid rgba(var(--text-rgb), 0.08);
|
|
border-radius: 4px;
|
|
padding: 1px 6px;
|
|
font-size: 0.72rem;
|
|
color: var(--text-secondary, var(--text-muted));
|
|
}
|
|
|
|
/* The Open button — flagged with .open on top of .task-btn so the
|
|
shared task-row hover styles still apply but a slightly different
|
|
accent makes it distinguishable from Restart. */
|
|
.task-btn.open {
|
|
color: var(--text-secondary);
|
|
}
|
|
.task-btn.open:hover {
|
|
background: rgba(var(--accent-rgb), 0.15);
|
|
}
|
|
|
|
.service-app-icon {
|
|
width: 30px;
|
|
height: 30px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Streaming state hint on the log container */
|
|
/* ------------------------------------------------------------------ */
|
|
.service-log-output[data-stream="connecting"]::before {
|
|
content: 'Connecting…';
|
|
color: var(--status-warning);
|
|
display: block;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.service-log-output[data-stream="disconnected"]::before {
|
|
content: '⚠ disconnected — retrying…';
|
|
color: var(--status-warning);
|
|
display: block;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.service-log-output[data-stream="closed"]::after {
|
|
content: '— stream closed —';
|
|
color: var(--text-muted);
|
|
display: block;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
/* Spinner-on-restart while the request is in flight, mirroring the
|
|
subtle “task is doing something” visual cue used by the task list. */
|
|
.task-btn.is-running {
|
|
opacity: 0.6;
|
|
cursor: wait;
|
|
}
|
|
|
|
/* ============================================================
|
|
Live container chips (CPU%, memory) — rendered inline in the
|
|
service row header alongside the existing port/IP chips.
|
|
Updated in place by the periodic stats refresh.
|
|
============================================================ */
|
|
.service-live-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 2px 9px;
|
|
font-size: 0.74rem;
|
|
font-weight: 600;
|
|
font-variant-numeric: tabular-nums;
|
|
color: rgba(var(--text-rgb), 0.85);
|
|
background: rgba(var(--accent-rgb), 0.12);
|
|
border: 1px solid rgba(var(--accent-rgb), 0.25);
|
|
border-radius: 999px;
|
|
transition: color .2s ease, background .2s ease, border-color .2s ease;
|
|
}
|
|
.service-live-chip.warn {
|
|
color: var(--status-warning);
|
|
background: rgba(var(--status-warning-rgb), 0.14);
|
|
border-color: rgba(var(--status-warning-rgb), 0.4);
|
|
}
|
|
.service-live-chip.danger {
|
|
color: var(--status-danger);
|
|
background: rgba(var(--status-danger-rgb), 0.16);
|
|
border-color: rgba(var(--status-danger-rgb), 0.5);
|
|
}
|
|
|
|
/* ============================================================
|
|
Rich container detail panel — limits, image, healthcheck,
|
|
networks, mounts. Rendered inside .task-details above the
|
|
log container so it's discoverable from the existing "Logs"
|
|
expand action. Gated behind the global Advanced UI mode so
|
|
a Beginner doesn't see a wall of technical detail.
|
|
============================================================ */
|
|
.service-rich {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
margin: 8px 0 14px;
|
|
}
|
|
body:not(.lp-ui--advanced) .service-rich { display: none; }
|
|
|
|
/* "Show logs" / "Hide logs" toggle sitting at the very bottom of the
|
|
open .task-details panel. Logs are off by default — the user opts in
|
|
per service so a row of expanded services doesn't open one SSE stream
|
|
each. Centred and given a little top breathing room so it reads as
|
|
a footer action rather than another inline button. */
|
|
.service-logs-toggle {
|
|
display: flex;
|
|
justify-content: center;
|
|
margin-top: 8px;
|
|
margin-bottom: 4px;
|
|
}
|
|
.service-logs-toggle .service-show-logs {
|
|
padding: 6px 14px;
|
|
}
|
|
.service-item.logs-shown .service-logs-toggle {
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
/* ============================================================
|
|
Beginner / Advanced toggle in the Services tab title row.
|
|
Project-wide visual; same component will be reused wherever
|
|
else surfaces grow an Advanced-only section.
|
|
============================================================ */
|
|
.services-title {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
}
|
|
.services-title-main { flex: 1; min-width: 0; }
|
|
|
|
/* Contained chip wrapper so the toggle reads as a control sitting next
|
|
to whatever action buttons share its row, instead of floating. Same
|
|
neutral bg/border recipe a .task-btn uses for its resting state. */
|
|
.lp-ui-advanced-toggle {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
flex-shrink: 0;
|
|
padding: 6px 12px;
|
|
background: rgba(var(--text-rgb), 0.06);
|
|
border: 1px solid rgba(var(--text-rgb), 0.15);
|
|
border-radius: 8px;
|
|
transition: background .15s ease, border-color .15s ease;
|
|
}
|
|
.lp-ui-advanced-toggle:hover {
|
|
background: rgba(var(--text-rgb), 0.10);
|
|
border-color: rgba(var(--text-rgb), 0.25);
|
|
}
|
|
.lp-ui-advanced-toggle input {
|
|
position: absolute;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
.lp-ui-advanced-toggle-track {
|
|
position: relative;
|
|
width: 34px;
|
|
height: 18px;
|
|
background: rgba(var(--text-rgb), 0.18);
|
|
border-radius: 999px;
|
|
transition: background .15s ease;
|
|
flex-shrink: 0;
|
|
}
|
|
/* Thumb stays white in both states — matching .eo-toggle / .routing-toggle
|
|
elsewhere. The previous checked-state fill used --text-on-accent
|
|
which resolves to a dark navy on the default theme and read as a
|
|
"black circle" inside the accent track. */
|
|
.lp-ui-advanced-toggle-thumb {
|
|
position: absolute;
|
|
top: 2px;
|
|
left: 2px;
|
|
width: 14px;
|
|
height: 14px;
|
|
background: var(--text-primary);
|
|
border-radius: 50%;
|
|
transition: transform .15s ease;
|
|
}
|
|
.lp-ui-advanced-toggle input:checked + .lp-ui-advanced-toggle-track {
|
|
background: var(--accent);
|
|
}
|
|
.lp-ui-advanced-toggle input:checked + .lp-ui-advanced-toggle-track .lp-ui-advanced-toggle-thumb {
|
|
transform: translateX(16px);
|
|
}
|
|
.lp-ui-advanced-toggle-label {
|
|
font-size: 0.78rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.02em;
|
|
color: rgba(var(--text-rgb), 0.7);
|
|
transition: color .15s ease;
|
|
}
|
|
.lp-ui-advanced-toggle:hover .lp-ui-advanced-toggle-label { color: var(--text-primary); }
|
|
.lp-ui-advanced-toggle input:focus-visible + .lp-ui-advanced-toggle-track {
|
|
outline: 2px solid rgba(var(--accent-rgb), 0.6);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.service-rich-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
gap: 10px;
|
|
}
|
|
.service-rich-cell {
|
|
padding: 10px 12px;
|
|
background: rgba(var(--text-rgb), 0.04);
|
|
border: 1px solid rgba(var(--text-rgb), 0.08);
|
|
border-radius: 8px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
min-width: 0;
|
|
}
|
|
.service-rich-cell span {
|
|
font-size: 0.66rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: rgba(var(--text-rgb), 0.45);
|
|
font-weight: 700;
|
|
}
|
|
.service-rich-cell strong {
|
|
font-size: 0.88rem;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
word-break: break-word;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.service-rich-section h4 {
|
|
font-size: 0.78rem;
|
|
font-weight: 700;
|
|
margin: 0 0 6px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: rgba(var(--text-rgb), 0.7);
|
|
}
|
|
|
|
.service-rich-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 0.8rem;
|
|
background: rgba(var(--text-rgb), 0.03);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
.service-rich-table th {
|
|
text-align: left;
|
|
font-size: 0.66rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: rgba(var(--text-rgb), 0.45);
|
|
padding: 8px 10px;
|
|
border-bottom: 1px solid rgba(var(--text-rgb), 0.08);
|
|
}
|
|
.service-rich-table td {
|
|
padding: 7px 10px;
|
|
border-bottom: 1px solid rgba(var(--text-rgb), 0.04);
|
|
color: rgba(var(--text-rgb), 0.85);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.service-rich-table tr:last-child td { border-bottom: none; }
|
|
|
|
.service-mount-type {
|
|
display: inline-block;
|
|
padding: 1px 7px;
|
|
border-radius: 4px;
|
|
font-size: 0.65rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
.service-mount-volume { background: rgba(var(--status-success-rgb), 0.2); color: var(--status-success); }
|
|
.service-mount-bind { background: rgba(var(--accent-rgb), 0.18); color: var(--accent); }
|
|
.service-mount-tmpfs { background: rgba(var(--status-warning-rgb), 0.18); color: var(--status-warning); }
|
|
.service-mount-path {
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 0.76rem;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.service-health {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 6px;
|
|
}
|
|
.service-health-pill {
|
|
padding: 2px 10px;
|
|
border-radius: 999px;
|
|
font-size: 0.7rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
.service-health-healthy { background: rgba(var(--status-success-rgb), 0.18); color: var(--status-success); }
|
|
.service-health-starting { background: rgba(var(--status-warning-rgb), 0.18); color: var(--status-warning); }
|
|
.service-health-unhealthy { background: rgba(var(--status-danger-rgb), 0.18); color: var(--status-danger); }
|
|
.service-health-unknown { background: rgba(var(--text-rgb), 0.10); color: rgba(var(--text-rgb), 0.6); }
|
|
.service-health-fail { color: var(--status-danger); font-size: 0.76rem; font-weight: 600; }
|
|
.service-health-log {
|
|
background: rgba(var(--text-rgb), 0.03);
|
|
border-radius: 6px;
|
|
margin-top: 4px;
|
|
padding: 4px 8px;
|
|
font-size: 0.76rem;
|
|
}
|
|
.service-health-log summary { cursor: pointer; color: rgba(var(--text-rgb), 0.65); }
|
|
.service-health-log pre {
|
|
margin: 6px 0 0;
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 0.76rem;
|
|
white-space: pre-wrap;
|
|
color: rgba(var(--text-rgb), 0.7);
|
|
}
|