feat(maestro): multi-view web UI — splits, tabs, drag-dock, highlights
ci / rust (push) Successful in 5m46s
ci / rust (push) Successful in 5m46s
Render the content pane as tabs of nested split trees, each session leaf a live attach mounted through the multi-mount pool: - #30 arbitrarily-nested H/V splits with mouse-resizable dividers; per-pane ⏻/▥/▤/✕ tools; closing a pane collapses its parent split. - #31 tabs, each holding its own tree; add / switch / close. - #32 drag-to-dock: drag a pane title or a sidebar session onto a pane for a 5-zone dock (edges split, centre swaps/replaces); a sidebar drag moves an already-open session. - #33 focused pane at full strength, others dim; sidebar shows focused (loud) vs open-elsewhere (quiet) with a stable per-session colour mapping rows to panes. Opening a session fills the focused pane (single-pane parity by default); multi-pane is opt-in via split / drag / tabs. Panes auto-close when their session ends (guarded against eviction and intentional close). The steady "connected" label is hidden as noise. Closes #30, #31, #32, #33. Part of epic #37. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1899,3 +1899,321 @@ textarea {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
multi-view (#30 splits, #31 tabs, #33 highlights)
|
||||
Tabs of nested split trees; each session leaf hosts a live .attach. The
|
||||
focused pane reads at full strength, the rest recede — and the sidebar row
|
||||
for the focused session matches it (loud), open-elsewhere rows stay quiet.
|
||||
========================================================================= */
|
||||
.mv {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* tab bar */
|
||||
.mv-tabs {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 2px;
|
||||
height: var(--bar-h);
|
||||
padding: 0 var(--sp-2);
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
flex: none;
|
||||
}
|
||||
.mv-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
padding: 0 var(--sp-3);
|
||||
align-self: center;
|
||||
height: calc(var(--bar-h) - 8px);
|
||||
border-radius: var(--r-1);
|
||||
font: 500 var(--fs-12) / 1 var(--font-mono);
|
||||
color: var(--muted);
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mv-tab:hover {
|
||||
background: var(--surface-2);
|
||||
color: var(--ink);
|
||||
}
|
||||
.mv-tab.is-active {
|
||||
background: var(--surface-3);
|
||||
color: var(--ink);
|
||||
border-color: var(--border);
|
||||
}
|
||||
.mv-tab-close {
|
||||
opacity: 0.5;
|
||||
font-size: var(--fs-13);
|
||||
line-height: 1;
|
||||
}
|
||||
.mv-tab-close:hover {
|
||||
opacity: 1;
|
||||
color: var(--danger);
|
||||
}
|
||||
.mv-tab-add {
|
||||
align-self: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: var(--r-1);
|
||||
color: var(--muted);
|
||||
font-size: var(--fs-15);
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mv-tab-add:hover {
|
||||
background: var(--surface-2);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* canvas + welcome */
|
||||
.mv-canvas {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
}
|
||||
.mv-canvas > .mv-split,
|
||||
.mv-canvas > .mv-pane {
|
||||
position: absolute;
|
||||
inset: var(--sp-2);
|
||||
}
|
||||
.mv-canvas > .content-welcome {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
margin: auto;
|
||||
height: max-content;
|
||||
}
|
||||
|
||||
/* splits + dividers */
|
||||
.mv-split {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
.mv-split.row {
|
||||
flex-direction: row;
|
||||
}
|
||||
.mv-split.col {
|
||||
flex-direction: column;
|
||||
}
|
||||
.mv-split > .mv-slot {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
}
|
||||
.mv-split > .mv-slot > * {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
.mv-divider {
|
||||
flex: none;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.mv-split.row > .mv-divider {
|
||||
width: 6px;
|
||||
cursor: col-resize;
|
||||
}
|
||||
.mv-split.col > .mv-divider {
|
||||
height: 6px;
|
||||
cursor: row-resize;
|
||||
}
|
||||
.mv-divider::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
margin: auto;
|
||||
background: var(--border);
|
||||
}
|
||||
.mv-split.row > .mv-divider::after {
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
}
|
||||
.mv-split.col > .mv-divider::after {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
.mv-divider:hover::after,
|
||||
.mv-divider.is-dragging::after {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
/* a single pane = wrapper around a live .attach (or an empty placeholder) */
|
||||
.mv-pane {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-2);
|
||||
overflow: hidden;
|
||||
background: var(--term-bg);
|
||||
}
|
||||
.mv-pane-host {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
}
|
||||
.mv-pane-host > .attach {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
/* per-session colour strip — maps a pane to its sidebar row */
|
||||
.mv-pane::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0 0 auto 0;
|
||||
height: 2px;
|
||||
background: var(--mv-color, transparent);
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* focused pane: accent ring + glow, full strength */
|
||||
.mv-pane.is-focused {
|
||||
border-color: var(--accent);
|
||||
box-shadow: inset 0 0 0 1px var(--accent), 0 0 22px -8px var(--accent-ring);
|
||||
}
|
||||
/* unfocused panes recede so the eye finds the live one */
|
||||
.mv-pane:not(.is-focused) {
|
||||
border-color: color-mix(in oklab, var(--border) 70%, transparent);
|
||||
}
|
||||
.mv-pane:not(.is-focused) .attach-body {
|
||||
opacity: 0.45;
|
||||
}
|
||||
.mv-pane:not(.is-focused) .attach-header {
|
||||
opacity: 0.7;
|
||||
}
|
||||
/* per-pane back button is meaningless in multi-view (use Close pane instead) */
|
||||
.mv-pane .attach-header .back {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mv-pane-empty {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: var(--sp-4);
|
||||
color: var(--faint);
|
||||
font: var(--fs-12) / 1.5 var(--font-mono);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* pane tools cluster (disconnect / split / close), top-right corner. The attach
|
||||
header's own disconnect button is hidden — ⏻ lives here as the leftmost tool. */
|
||||
.mv-pane-tools {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 4px;
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity var(--dur-fast) var(--ease-out);
|
||||
}
|
||||
.mv-pane .attach-header .disconnect {
|
||||
display: none;
|
||||
}
|
||||
.mv-tool-off:hover {
|
||||
background: var(--danger-tint);
|
||||
color: var(--danger);
|
||||
}
|
||||
.mv-pane:hover .mv-pane-tools {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
/* The tools share the header's right corner with the connection-state label, so
|
||||
hide that label while hovering (tools showing) — no overlap. */
|
||||
.mv-pane:hover .attach-state {
|
||||
visibility: hidden;
|
||||
}
|
||||
/* "connected" is noise — a streaming terminal already shows it's connected. Only
|
||||
the transient/meaningful states surface (connecting/error); a closed session
|
||||
auto-closes its pane, so "closed" is rarely seen. */
|
||||
.attach-state[data-state="open"] {
|
||||
display: none;
|
||||
}
|
||||
.mv-tool {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: var(--r-1);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface-2);
|
||||
color: var(--muted);
|
||||
font-size: var(--fs-12);
|
||||
cursor: pointer;
|
||||
}
|
||||
.mv-tool:hover {
|
||||
background: var(--surface-3);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* drag-to-dock (issue #32): a cursor-following ghost + the target pane's drop
|
||||
zone highlight. */
|
||||
.mv-drag-ghost {
|
||||
position: fixed;
|
||||
z-index: var(--z-toast);
|
||||
pointer-events: none;
|
||||
padding: 2px 10px;
|
||||
border-radius: var(--r-2);
|
||||
background: var(--surface-3);
|
||||
border: 1px solid var(--accent);
|
||||
color: var(--ink);
|
||||
font: 600 var(--fs-12) / 1.4 var(--font-mono);
|
||||
box-shadow: var(--shadow-2);
|
||||
}
|
||||
.mv-dock-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.mv-dock-zone {
|
||||
background: color-mix(in oklab, var(--accent) 30%, transparent);
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: var(--r-1);
|
||||
}
|
||||
|
||||
/* sidebar two-tier highlight + colour dot (issue #33).
|
||||
.is-active = focused pane (loud, from existing rule); .is-open = open
|
||||
elsewhere (quiet). The dot uses the session's colour so a row maps to a pane. */
|
||||
.tree-leaf.is-open {
|
||||
background: color-mix(in oklab, var(--accent) 6%, transparent);
|
||||
}
|
||||
.tree-leaf.is-open::after,
|
||||
.tree-leaf.is-active::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 9px;
|
||||
top: 50%;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-top: -3.5px;
|
||||
border-radius: 50%;
|
||||
background: var(--mv-color, var(--accent));
|
||||
pointer-events: none;
|
||||
}
|
||||
.tree-leaf.is-open::after {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.tree-leaf.is-active::after {
|
||||
box-shadow: 0 0 8px var(--mv-color, var(--accent-ring));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user