feat(maestro): web UI persistent sidebar + content split w/ background connection pool (NAP-338)
ci / rust (push) Successful in 5m24s
ci / rust (push) Successful in 5m24s
Replace the full-screen route-swap SPA with a persistent two-pane layout: - Global top bar (brand + Spawn + theme) above a sidebar (search + status + session tree, highlights the active session) and a content pane (welcome or the attached terminal). Selecting a row swaps only the content pane; the sidebar and its store subscription persist. - New `connections.ts` ConnectionPool: attach connections (terminal + WS) are decoupled from the UI — navigating away parks a session (keeps it streaming) rather than tearing it down, so switching back is instant with scrollback intact. LRU-bounded (MAX_LIVE=6); explicit `⏻` disconnect distinct from nav. - `attach.ts` → mountable `createAttachConnection`; `catalog.ts` exposes `renderTopbar` + `renderSidebar`; `main.ts` is the shell + hash router. - Responsive: desktop side-by-side; mobile single-pane (list ↔ terminal). Single session viewed at a time (split panes deferred). Typecheck + production build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -319,6 +319,92 @@ textarea {
|
||||
font-size: var(--fs-15);
|
||||
}
|
||||
|
||||
/* ---------- app shell: sidebar + content ---------- */
|
||||
|
||||
/* Global app header: brand + app-level actions (spawn, theme toggle), spanning
|
||||
the full width above the split. */
|
||||
.app-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-3);
|
||||
flex: 0 0 auto;
|
||||
padding: var(--sp-3) var(--sp-4);
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: color-mix(in oklab, var(--surface) 82%, transparent);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.app-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Desktop: persistent two-pane split. Sidebar holds the session list; the
|
||||
content pane shows the welcome state or the attached terminal. */
|
||||
@media (min-width: 860px) {
|
||||
.app-shell {
|
||||
display: grid;
|
||||
grid-template-columns: clamp(280px, 26vw, 360px) minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: color-mix(in oklab, var(--surface) 26%, var(--bg));
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Sidebar's own slim header: just the session search (app-level actions live
|
||||
in the global top bar). */
|
||||
.sidebar-search {
|
||||
flex: 0 0 auto;
|
||||
padding: var(--sp-3) var(--sp-4);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.sidebar-search .search {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.content-welcome {
|
||||
margin: auto;
|
||||
max-width: 48ch;
|
||||
}
|
||||
|
||||
/* Off-screen home for parked-but-connected terminals. Kept in the DOM (not
|
||||
display:none) so the live WS keeps buffering into each terminal; on
|
||||
re-mount we fit + the scrollback is already there, so switching is instant. */
|
||||
.conn-cache {
|
||||
position: fixed;
|
||||
left: -99999px;
|
||||
top: 0;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ---------- catalog ---------- */
|
||||
|
||||
.catalog {
|
||||
@@ -604,6 +690,20 @@ textarea {
|
||||
box-shadow: inset 0 0 0 1px var(--accent-ring);
|
||||
}
|
||||
|
||||
/* The session currently mounted in the content pane. */
|
||||
.tree-leaf.is-active {
|
||||
background: var(--accent-tint);
|
||||
border-left-color: var(--accent);
|
||||
}
|
||||
|
||||
.tree-leaf.is-active .leaf-name {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.tree-leaf.is-active::after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tree-leaf[data-status="dead"] {
|
||||
color: var(--muted);
|
||||
}
|
||||
@@ -800,6 +900,24 @@ textarea {
|
||||
height: var(--control-h);
|
||||
}
|
||||
|
||||
/* Desktop keeps the session list permanently in view, so the in-header "back"
|
||||
arrow is redundant — the sidebar is the nav. Mobile (single-pane) keeps it. */
|
||||
@media (min-width: 860px) {
|
||||
.attach-header .back {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.attach-header .disconnect {
|
||||
color: var(--faint);
|
||||
}
|
||||
|
||||
.attach-header .disconnect:hover {
|
||||
color: var(--danger);
|
||||
background: var(--danger-tint);
|
||||
border-color: color-mix(in oklab, var(--danger) 40%, transparent);
|
||||
}
|
||||
|
||||
.attach-title {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--fs-13);
|
||||
@@ -1253,6 +1371,22 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile: no room for a split, so show one pane at a time. The catalog route
|
||||
shows the session list full-width; opening a session swaps to the terminal
|
||||
(its header's ← returns to the list). The connection still lives in the pool,
|
||||
so returning to it is instant. */
|
||||
@media (max-width: 859px) {
|
||||
.app-shell[data-route="catalog"] > .content {
|
||||
display: none;
|
||||
}
|
||||
.app-shell:not([data-route="catalog"]) > .sidebar {
|
||||
display: none;
|
||||
}
|
||||
.sidebar {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- reduced motion ---------- */
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
Reference in New Issue
Block a user