librelad 9a87e3f894 ui(services): keep Advanced toggle thumb white and contain it in a chip wrapper
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>
2026-05-28 01:39:04 +01:00

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);
}