Merge sidebar-rail: collapsed icon rail

This commit is contained in:
2026-06-15 13:41:05 +07:00
2 changed files with 34 additions and 2 deletions
+1 -1
View File
@@ -164,7 +164,7 @@ export function App() {
<div style={{ display: "flex", flexDirection: "column", height: "100vh", background: COLORS.bgApp }}>
<TopBar active={active} eventsOpen={eventsOpen} onToggleEvents={() => setEventsOpen((v) => !v)} onShowEvents={() => setEventsOpen(true)} sidebarOpen={sidebarOpen} onToggleSidebar={() => setSidebarOpen((v) => !v)} unread={unread} onOpenSettings={() => { if (config) setSettingsOpen(true); }} />
<div style={{ flex: 1, display: "flex", minHeight: 0 }}>
{sidebarOpen && <Sidebar groups={groups} workspaces={workspaces} activeId={activeId} onSelect={selectWorkspace} onNew={() => setWizard(true)} onDelete={setDeleteTarget} health={health} connected={connected} />}
<Sidebar railMode={!sidebarOpen} groups={groups} workspaces={workspaces} activeId={activeId} onSelect={selectWorkspace} onNew={() => setWizard(true)} onDelete={setDeleteTarget} health={health} connected={connected} />
<div style={{ flex: 1, display: "flex", flexDirection: "column", minWidth: 0 }}>
{active && (
<CenterToolbar selected="" onSelect={(p) => { if (active) void applyPreset(active.id, p, []); }} onOpenSearch={() => { if (effectiveFocus) { setSearchSurfaceId(effectiveFocus); setSearchNonce((n) => n + 1); } }} />
+33 -1
View File
@@ -26,8 +26,9 @@ function aggregate(w: WorkspaceView): SurfaceState | "stopped" {
interface DropAt { section: string; index: number }
export function Sidebar({
groups, workspaces, activeId, onSelect, onNew, onDelete, health, connected,
railMode, groups, workspaces, activeId, onSelect, onNew, onDelete, health, connected,
}: {
railMode: boolean;
groups: Group[];
workspaces: WorkspaceView[];
activeId: string | null;
@@ -183,6 +184,37 @@ export function Sidebar({
const section = (key: string, items: WorkspaceView[]) => items.map((w, i) => row(w, key, items, i));
// Collapsed: a narrow rail of status rings so terminal activity stays visible.
if (railMode) {
const rail = [
...pinned,
...groups.slice().sort((a, b) => a.order - b.order).flatMap((g) => byGroup(g.id)),
...ungrouped,
];
return (
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: 48, flex: "0 0 48px", background: COLORS.bgSidebar, height: "100%", padding: "10px 0", boxSizing: "border-box", borderRight: `1px solid ${COLORS.borderSubtle}`, gap: 8 }}>
<button onClick={onNew} title="New workspace"
style={{ display: "flex", alignItems: "center", justifyContent: "center", width: 30, height: 30, borderRadius: 8, background: COLORS.bgElevated, border: `1px solid ${COLORS.borderStrong}`, color: COLORS.textPrimary, cursor: "pointer" }}>
<Plus size={15} />
</button>
<div style={{ flex: 1, overflowY: "auto", display: "flex", flexDirection: "column", alignItems: "center", gap: 6, minHeight: 0 }}>
{rail.map((w) => {
const isActive = w.id === activeId;
return (
<button key={w.id} onClick={() => onSelect(w.id)} title={w.name}
style={{ position: "relative", display: "flex", alignItems: "center", justifyContent: "center", width: 32, height: 32, borderRadius: 8, cursor: "pointer", background: isActive ? COLORS.bgElevated : "transparent", border: `1px solid ${isActive ? COLORS.borderStrong : "transparent"}` }}>
<span style={{ width: 12, height: 12, borderRadius: "50%", border: `2px solid ${STATE_COLOR[aggregate(w)]}`, boxSizing: "border-box" }} />
{w.unread && <span style={{ position: "absolute", top: 3, right: 3, width: 7, height: 7, borderRadius: "50%", background: COLORS.accent }} />}
</button>
);
})}
</div>
<span title={connected ? "spaceshd · live" : "spaceshd · offline"}
style={{ width: 8, height: 8, borderRadius: "50%", background: connected ? COLORS.stDone : COLORS.textMuted, flex: "0 0 8px" }} />
</div>
);
}
return (
<div style={{ display: "flex", flexDirection: "column", width: 248, flex: "0 0 248px", background: COLORS.bgSidebar, height: "100%", padding: 14, boxSizing: "border-box" }}>
<button onClick={onNew}