feat(app): sidebar favorites, drag-reorder, and delete-with-confirm
- FAVORITES section at the top collects pinned workspaces (removed from their group listing); a star toggle on each row pins/unpins via setWorkspaceMeta. - Drag-to-reorder within a section using raw pointer events (HTML5 DnD is unreliable in the macOS WKWebView), with a drop-line indicator; on drop the section's `order` is reassigned sequentially and persisted. Cross-section drops are ignored (group membership unchanged). - Trash icon on row hover opens a ConfirmDelete modal that shows the live terminal count and warns before terminating them, then calls close_workspace and re-points the active workspace. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+22
-2
@@ -4,10 +4,11 @@ import { Sidebar } from "./Sidebar";
|
||||
import { TopBar } from "./TopBar";
|
||||
import { CenterToolbar } from "./CenterToolbar";
|
||||
import { Wizard } from "./Wizard";
|
||||
import { ConfirmDelete } from "./ConfirmDelete";
|
||||
import { EventCenter } from "./EventCenter";
|
||||
import { maybeNotify } from "./notify";
|
||||
import { COLORS } from "./theme";
|
||||
import { getStatusFull, applyPreset, onDaemonEvent, onDaemonRawEvent, setWorkspaceMeta, focusSurface, getEventLog, markEventsRead, getHealth } from "./socketBridge";
|
||||
import { getStatusFull, applyPreset, onDaemonEvent, onDaemonRawEvent, setWorkspaceMeta, focusSurface, getEventLog, markEventsRead, getHealth, closeWorkspaceCmd } from "./socketBridge";
|
||||
import type { EventRecord, DaemonHealth } from "./socketBridge";
|
||||
import { leafIds } from "./layoutTypes";
|
||||
import type { Group, WorkspaceView, SurfaceState } from "./layoutTypes";
|
||||
@@ -29,6 +30,7 @@ export function App() {
|
||||
const [states, setStates] = useState<Record<string, SurfaceState>>({});
|
||||
const [events, setEvents] = useState<EventRecord[]>([]);
|
||||
const [wizard, setWizard] = useState(false);
|
||||
const [deleteTarget, setDeleteTarget] = useState<WorkspaceView | null>(null);
|
||||
const [eventsOpen, setEventsOpen] = useState(() => loadFlag("spacesh.eventsOpen", true));
|
||||
const [sidebarOpen, setSidebarOpen] = useState(() => loadFlag("spacesh.sidebarOpen", true));
|
||||
const [health, setHealth] = useState<DaemonHealth | null>(null);
|
||||
@@ -137,7 +139,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} />
|
||||
<div style={{ flex: 1, display: "flex", minHeight: 0 }}>
|
||||
{sidebarOpen && <Sidebar groups={groups} workspaces={workspaces} activeId={activeId} onSelect={selectWorkspace} onNew={() => setWizard(true)} health={health} connected={connected} />}
|
||||
{sidebarOpen && <Sidebar 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); } }} />
|
||||
@@ -157,6 +159,24 @@ export function App() {
|
||||
)}
|
||||
</div>
|
||||
{wizard && <Wizard onDone={(id) => { setWizard(false); setActiveId(id); void refresh(); }} onCancel={() => setWizard(false)} />}
|
||||
{deleteTarget && (
|
||||
<ConfirmDelete
|
||||
name={deleteTarget.name}
|
||||
activeCount={Object.values(deleteTarget.surfaces).filter((s) => s.running).length}
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
onConfirm={() => {
|
||||
const tgt = deleteTarget;
|
||||
setDeleteTarget(null);
|
||||
void closeWorkspaceCmd(tgt.id).then(() => {
|
||||
if (activeId === tgt.id) {
|
||||
const next = workspaces.find((w) => w.id !== tgt.id);
|
||||
setActiveId(next ? next.id : null);
|
||||
}
|
||||
void refresh();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user