Polish container overview layout for filled height without empty voids.
Release rolling / release (push) Has been cancelled

Split overview into a stretching Identity/Runtime/Network band and a
content-height Ports/Mounts/Timeline strip with timeline, mount rows,
and clearer empty states so vertical space is used without sparse panels.
This commit is contained in:
Raven Scott
2026-07-13 17:41:19 -04:00
parent c85324867e
commit ac754d6f5a
3 changed files with 347 additions and 114 deletions
+62 -31
View File
@@ -3945,31 +3945,71 @@ function populateOverviewTab(config, container) {
: kv('fa-project-diagram', 'Nets', muted('None')),
].join('');
const emptyState = (icon, text) =>
`<div class="detail-overview-empty"><i class="fas ${icon}"></i><span>${esc(text)}</span></div>`;
const portsBody =
portShow.length > 0
? `<div class="detail-chip-row">${portShow.map((p) => chip(p)).join('')}${
portExtra ? chip(`+${portExtra} more`) : ''
}</div>`
: `<div class="text-muted small">No published ports</div>`;
? `<div class="detail-chip-row detail-chip-row--flow">${portShow
.map((p) => chip(p))
.join('')}${portExtra ? chip(`+${portExtra} more`) : ''}</div>`
: emptyState('fa-plug', 'No published ports');
const storageBody =
mountShow.length > 0
? `<div class="detail-chip-row detail-chip-row--stack">${mountShow
? `<div class="detail-chip-row detail-chip-row--flow">${mountShow
.map((m) => {
const src = m.Source || m.Name || '?';
const dst = m.Destination || '?';
const shortSrc =
src.length > 28 ? '' + src.slice(-27) : src;
const label = `${shortSrc}${dst}`;
const full = `${src}${dst} (${m.Type || 'bind'}${m.RW === false || m.Mode === 'ro' ? ', ro' : ''})`;
return chip(label, full);
const ro = m.RW === false || m.Mode === 'ro';
const full = `${src}${dst} (${m.Type || 'bind'}${ro ? ', ro' : ''})`;
return `<span class="detail-mount-item" title="${esc(full)}">
<span class="detail-mount-src">${esc(src)}</span>
<i class="fas fa-arrow-right detail-mount-arrow"></i>
<span class="detail-mount-dst">${esc(dst)}</span>
${ro ? '<span class="detail-mount-ro">ro</span>' : ''}
</span>`;
})
.join('')}${mountExtra ? chip(`+${mountExtra} more`) : ''}</div>`
: `<div class="text-muted small">No mounts</div>`;
.join('')}${
mountExtra
? `<span class="detail-chip">+${mountExtra} more</span>`
: ''
}</div>`
: emptyState('fa-hdd', 'No mounts');
const timelineBody = `
<div class="detail-timeline">
<div class="detail-timeline-item">
<div class="detail-timeline-dot"></div>
<div class="detail-timeline-meta">
<span class="detail-timeline-label">Created</span>
<span class="detail-timeline-value">${esc(created)}</span>
${createdRelative ? `<span class="relative-time">${esc(createdRelative)}</span>` : ''}
</div>
</div>
<div class="detail-timeline-item">
<div class="detail-timeline-dot detail-timeline-dot--active"></div>
<div class="detail-timeline-meta">
<span class="detail-timeline-label">Started</span>
<span class="detail-timeline-value">${esc(started)}</span>
${startedRelative ? `<span class="relative-time">${esc(startedRelative)}</span>` : ''}
</div>
</div>
${
composeWorkdir
? `<div class="detail-timeline-item">
<div class="detail-timeline-dot"></div>
<div class="detail-timeline-meta">
<span class="detail-timeline-label">Compose</span>
<span class="detail-timeline-value detail-kv-value--wrap" title="${esc(composeWorkdir)}">${esc(composeWorkdir)}</span>
</div>
</div>`
: ''
}
</div>`;
const commandLine = [entryStr, cmdStr].filter(Boolean).join(' ').trim();
const commandDisplay =
commandLine.length > 140 ? commandLine.slice(0, 137) + '…' : commandLine;
const commandDisplay = commandLine;
content.innerHTML = `
<div class="detail-section-card detail-section-card--flat detail-overview-card">
@@ -3988,33 +4028,24 @@ function populateOverviewTab(config, container) {
<div class="detail-metric-row">${metricChips}</div>
<div class="detail-overview-panels">
<div class="detail-overview-primary">
${section('Identity', 'fa-id-card', identityBody)}
${section('Runtime', 'fa-cog', runtimeBody)}
${section('Network', 'fa-network-wired', networkBody)}
<section class="detail-overview-section">
</div>
<div class="detail-overview-secondary">
<section class="detail-overview-section detail-overview-section--soft">
<h4 class="detail-overview-section-title"><i class="fas fa-plug"></i> Ports</h4>
${portsBody}
</section>
<section class="detail-overview-section">
<section class="detail-overview-section detail-overview-section--soft">
<h4 class="detail-overview-section-title"><i class="fas fa-hdd"></i> Mounts</h4>
${storageBody}
</section>
<section class="detail-overview-section">
<section class="detail-overview-section detail-overview-section--soft">
<h4 class="detail-overview-section-title"><i class="fas fa-clock"></i> Timeline</h4>
<div class="detail-kv-grid detail-kv-grid--compact">
${kv(
'fa-calendar-plus',
'Created',
`${esc(created)}${createdRelative ? ` <span class="relative-time">${esc(createdRelative)}</span>` : ''}`
)}
${kv(
'fa-play-circle',
'Started',
`${esc(started)}${startedRelative ? ` <span class="relative-time">${esc(startedRelative)}</span>` : ''}`
)}
${composeWorkdir ? kv('fa-folder-open', 'Compose', `<span class="detail-kv-value--wrap" title="${esc(composeWorkdir)}">${esc(composeWorkdir.length > 40 ? '…' + composeWorkdir.slice(-38) : composeWorkdir)}</span>`) : ''}
</div>
${timelineBody}
</section>
</div>
+5 -2
View File
@@ -879,10 +879,13 @@ body {
flex-direction: column;
}
#container-details-view #overview-pane .detail-overview-panels {
#container-details-view #overview-pane .detail-overview-primary {
flex: 1 1 auto;
min-height: 0;
grid-template-rows: 1fr 1fr;
}
#container-details-view #overview-pane .detail-overview-secondary {
flex: 0 0 auto;
}
#container-details-view .detail-fill-panel,
+280 -81
View File
@@ -3562,11 +3562,11 @@
font-size: 0.7rem;
}
/* Overview compact key/value grid — card fills tab body height */
/* Overview card — fill height with intentional primary + secondary bands */
.detail-overview-card {
display: flex;
flex-direction: column;
gap: 10px;
gap: 12px;
min-height: 0;
flex: 1 1 auto;
height: 100%;
@@ -3580,31 +3580,41 @@
display: flex;
flex-direction: column;
margin-bottom: 0;
padding: 14px 16px 12px;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.025) 0%,
transparent 40%
),
var(--bg-elevated);
border: 1px solid var(--border-color);
border-radius: 14px;
}
.detail-overview-hero {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 0;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
flex-shrink: 0;
}
.detail-overview-hero-main {
display: flex;
align-items: center;
gap: 10px;
gap: 12px;
min-width: 0;
flex: 1 1 auto;
}
.detail-overview-name {
font-size: 1.05rem;
font-weight: 650;
font-size: 1.2rem;
font-weight: 700;
letter-spacing: -0.02em;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
@@ -3624,7 +3634,7 @@
display: inline-flex;
align-items: center;
gap: 6px;
padding: 3px 8px;
padding: 4px 10px;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 600;
@@ -3645,96 +3655,136 @@
background: rgba(248, 113, 113, 0.1);
}
/* Metric strip under hero */
/* Metric strip — slightly roomier */
.detail-metric-row {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 6px;
gap: 8px;
flex-shrink: 0;
}
.detail-metric-chip {
display: flex;
flex-direction: column;
gap: 1px;
padding: 6px 8px;
border-radius: 8px;
gap: 2px;
padding: 10px 12px;
border-radius: 10px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border: 1px solid rgba(255, 255, 255, 0.06);
min-width: 0;
transition: border-color 0.15s ease;
}
.detail-metric-chip:hover {
border-color: rgba(45, 212, 191, 0.28);
}
.detail-metric-chip i {
font-size: 0.7rem;
font-size: 0.75rem;
color: var(--accent-primary);
margin-bottom: 1px;
margin-bottom: 2px;
opacity: 0.95;
}
.detail-metric-label {
font-size: 0.625rem;
font-weight: 650;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
letter-spacing: 0.05em;
color: var(--text-muted);
}
.detail-metric-value {
font-size: 0.8125rem;
font-weight: 650;
font-size: 0.9375rem;
font-weight: 700;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.2;
letter-spacing: -0.02em;
}
/* Multi-panel body — grows to fill leftover vertical space */
.detail-overview-panels {
/* Primary: Identity / Runtime / Network — absorbs leftover height */
.detail-overview-primary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-rows: 1fr 1fr;
gap: 8px 10px;
gap: 10px;
min-height: 0;
flex: 1 1 auto;
align-content: stretch;
}
/* Secondary: Ports / Mounts / Timeline — content-height band (no empty voids) */
.detail-overview-secondary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
flex: 0 0 auto;
min-height: 0;
}
.detail-overview-panels {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
min-height: 0;
}
.detail-overview-section {
min-width: 0;
min-height: 0;
height: 100%;
padding: 8px 10px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.05);
padding: 12px 14px;
border-radius: 12px;
background: rgba(0, 0, 0, 0.18);
border: 1px solid rgba(255, 255, 255, 0.06);
display: flex;
flex-direction: column;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
}
.detail-overview-section--soft {
height: auto;
min-height: 7.5rem;
background: rgba(255, 255, 255, 0.02);
}
.detail-overview-section-title {
margin: 0 0 6px;
margin: 0 0 10px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
letter-spacing: 0.06em;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 6px;
gap: 7px;
flex-shrink: 0;
}
.detail-overview-section .detail-kv-grid,
.detail-overview-section .detail-chip-row,
.detail-overview-section > .text-muted {
flex: 1 1 auto;
min-height: 0;
}
.detail-overview-section-title i {
color: var(--accent-primary);
font-size: 0.75rem;
opacity: 0.9;
font-size: 0.8rem;
opacity: 0.95;
}
/* Distribute key/values evenly through primary panel height */
.detail-overview-primary .detail-kv-grid--compact {
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
justify-content: space-evenly;
gap: 0;
}
.detail-overview-secondary .detail-chip-row,
.detail-overview-secondary .detail-timeline,
.detail-overview-secondary .detail-overview-empty {
flex: 1 1 auto;
}
.detail-kv-grid {
@@ -3751,19 +3801,22 @@
.detail-kv {
display: grid;
grid-template-columns: minmax(5.5rem, 6.5rem) minmax(0, 1fr);
gap: 8px;
align-items: baseline;
padding: 5px 0;
gap: 10px;
align-items: center;
padding: 7px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
min-width: 0;
}
.detail-kv-grid--compact .detail-kv {
/* Room for PLATFORM / HOSTNAME / WORKDIR without colliding with values */
grid-template-columns: minmax(5.75rem, 6.75rem) minmax(0, 1fr);
gap: 8px;
padding: 3px 0;
align-items: start;
grid-template-columns: minmax(4.75rem, 5.75rem) minmax(0, 1fr);
gap: 10px;
padding: 6px 0;
align-items: center;
}
.detail-overview-primary .detail-kv {
border-bottom-color: rgba(255, 255, 255, 0.045);
}
.detail-kv:last-child {
@@ -3778,7 +3831,7 @@
color: var(--text-muted);
display: inline-flex;
align-items: center;
gap: 5px;
gap: 6px;
white-space: nowrap;
overflow: hidden;
min-width: 0;
@@ -3786,23 +3839,21 @@
}
.detail-kv-grid--compact .detail-kv-label {
/* Keep label in its column; never bleed into the value */
flex-shrink: 0;
overflow: hidden;
text-overflow: ellipsis;
letter-spacing: 0.02em;
font-size: 0.625rem;
letter-spacing: 0.03em;
font-size: 0.65rem;
}
.detail-kv-grid--compact .detail-kv-label i {
flex-shrink: 0;
width: 0.85em;
width: 0.9em;
text-align: center;
}
.detail-kv-grid--compact .detail-kv-value {
min-width: 0;
overflow: hidden;
}
.detail-kv-label i {
@@ -3813,7 +3864,7 @@
.detail-kv-value {
font-size: 0.875rem;
font-weight: 500;
font-weight: 550;
color: var(--text-primary);
display: flex;
align-items: center;
@@ -3824,7 +3875,7 @@
}
.detail-kv-grid--compact .detail-kv-value {
font-size: 0.8125rem;
font-size: 0.85rem;
}
.detail-kv-value--wrap {
@@ -3835,39 +3886,184 @@
.detail-chip-row {
display: flex;
flex-wrap: wrap;
gap: 4px;
gap: 6px;
min-width: 0;
align-content: flex-start;
}
.detail-chip-row--flow {
max-height: none;
overflow: visible;
}
.detail-chip-row--stack {
max-height: 5.5rem;
overflow: hidden;
max-height: none;
overflow: visible;
}
.detail-chip {
display: inline-block;
display: inline-flex;
align-items: center;
max-width: 100%;
padding: 2px 7px;
border-radius: 6px;
font-size: 0.6875rem;
padding: 4px 9px;
border-radius: 7px;
font-size: 0.75rem;
font-weight: 550;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
color: var(--text-secondary);
background: var(--bg-primary);
border: 1px solid var(--border-color);
overflow: hidden;
text-overflow: ellipsis;
background: rgba(0, 0, 0, 0.28);
border: 1px solid rgba(255, 255, 255, 0.07);
white-space: nowrap;
}
/* Mount paths as readable rows */
.detail-mount-item {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px 8px;
width: 100%;
padding: 7px 10px;
border-radius: 8px;
background: rgba(0, 0, 0, 0.22);
border: 1px solid rgba(255, 255, 255, 0.06);
font-size: 0.75rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
color: var(--text-secondary);
min-width: 0;
}
.detail-mount-src {
color: var(--text-muted);
overflow-wrap: anywhere;
word-break: break-all;
min-width: 0;
}
.detail-mount-arrow {
font-size: 0.6rem;
color: var(--accent-primary);
opacity: 0.85;
flex-shrink: 0;
}
.detail-mount-dst {
color: var(--text-primary);
font-weight: 600;
overflow-wrap: anywhere;
word-break: break-all;
min-width: 0;
}
.detail-mount-ro {
margin-left: auto;
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--accent-warning);
background: rgba(245, 158, 11, 0.12);
border: 1px solid rgba(245, 158, 11, 0.25);
border-radius: 4px;
padding: 1px 5px;
}
.detail-overview-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 3.5rem;
padding: 12px 8px;
color: var(--text-muted);
font-size: 0.8rem;
text-align: center;
}
.detail-overview-empty i {
font-size: 1rem;
opacity: 0.45;
color: var(--text-muted);
}
/* Timeline band */
.detail-timeline {
display: flex;
flex-direction: column;
gap: 10px;
padding: 2px 0 0 4px;
}
.detail-timeline-item {
display: flex;
align-items: flex-start;
gap: 10px;
min-width: 0;
position: relative;
}
.detail-timeline-item:not(:last-child)::before {
content: '';
position: absolute;
left: 4px;
top: 12px;
bottom: -10px;
width: 1px;
background: rgba(45, 212, 191, 0.25);
}
.detail-timeline-dot {
width: 9px;
height: 9px;
margin-top: 4px;
border-radius: 50%;
background: var(--bg-tertiary);
border: 2px solid rgba(45, 212, 191, 0.45);
flex-shrink: 0;
z-index: 1;
}
.detail-timeline-dot--active {
background: var(--accent-primary);
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(45, 212, 191, 0.15);
}
.detail-timeline-meta {
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
}
.detail-timeline-label {
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
}
.detail-timeline-value {
font-size: 0.8125rem;
font-weight: 550;
color: var(--text-primary);
line-height: 1.3;
}
.detail-timeline-meta .relative-time {
font-size: 0.7rem;
}
.detail-overview-command {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 8px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
gap: 10px;
padding: 9px 12px;
border-radius: 10px;
background: rgba(0, 0, 0, 0.28);
border: 1px solid rgba(255, 255, 255, 0.07);
min-width: 0;
flex-shrink: 0;
}
@@ -3882,7 +4078,7 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.75rem;
font-size: 0.8125rem;
color: var(--accent-primary);
background: transparent;
border: none;
@@ -3890,9 +4086,12 @@
}
@media (max-width: 1100px) {
.detail-overview-panels {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-rows: repeat(3, 1fr);
.detail-overview-primary,
.detail-overview-secondary {
grid-template-columns: 1fr;
}
.detail-overview-primary {
flex: 0 1 auto;
}
.detail-metric-row {
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -3903,12 +4102,12 @@
#overview-pane .detail-tab-body {
overflow-y: auto;
}
.detail-overview-panels {
grid-template-columns: 1fr;
grid-template-rows: none;
grid-auto-rows: auto;
.detail-overview-primary {
flex: 0 0 auto;
}
.detail-overview-primary .detail-kv-grid--compact {
justify-content: flex-start;
}
.detail-metric-row {
grid-template-columns: repeat(2, minmax(0, 1fr));
}