feat(app): GUI backlog — splitter, drag-reorder, fit, persist, modal focus

- LayoutEngine: fix splitter resize (track pointer 1:1 via delta-from-start)
  and add panel drag-to-reorder using raw pointer events with drop indicators
- TerminalView: auto-fit xterm to container via FitAddon + ResizeObserver
- App/TopBar: toggleable sidebar; persist sidebar/events collapse in
  localStorage; bell icon opens the activity log
- Wizard: new-workspace modal now grabs focus and handles keyboard
- deps: add @xterm/addon-fit

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 08:01:15 +07:00
parent 6a3875670a
commit 58c75c71ae
7 changed files with 287 additions and 126 deletions
+16 -3
View File
@@ -13,6 +13,15 @@ import type { EventRecord, DaemonHealth } from "./socketBridge";
import { leafIds } from "./layoutTypes";
import type { Group, WorkspaceView, SurfaceState } from "./layoutTypes";
/** Read a boolean UI flag from localStorage, falling back to `def`. */
function loadFlag(key: string, def: boolean): boolean {
try { const v = localStorage.getItem(key); return v === null ? def : v === "1"; }
catch { return def; }
}
function saveFlag(key: string, value: boolean): void {
try { localStorage.setItem(key, value ? "1" : "0"); } catch { /* ignore */ }
}
export function App() {
const [groups, setGroups] = useState<Group[]>([]);
const [workspaces, setWorkspaces] = useState<WorkspaceView[]>([]);
@@ -21,7 +30,8 @@ export function App() {
const [states, setStates] = useState<Record<string, SurfaceState>>({});
const [events, setEvents] = useState<EventRecord[]>([]);
const [wizard, setWizard] = useState(false);
const [eventsOpen, setEventsOpen] = useState(true);
const [eventsOpen, setEventsOpen] = useState(() => loadFlag("spacesh.eventsOpen", true));
const [sidebarOpen, setSidebarOpen] = useState(() => loadFlag("spacesh.sidebarOpen", true));
const [health, setHealth] = useState<DaemonHealth | null>(null);
const [connected, setConnected] = useState(false);
const [focusedId, setFocusedId] = useState<string | null>(null);
@@ -104,6 +114,9 @@ export function App() {
return () => window.removeEventListener("keydown", onKey);
}, []);
useEffect(() => { saveFlag("spacesh.eventsOpen", eventsOpen); }, [eventsOpen]);
useEffect(() => { saveFlag("spacesh.sidebarOpen", sidebarOpen); }, [sidebarOpen]);
const unread = useMemo(() => events.filter((e) => !e.read).length, [events]);
const active = workspaces.find((w) => w.id === activeId) ?? null;
const leaves = active ? leafIds(active.layout) : [];
@@ -117,9 +130,9 @@ export function App() {
return (
<div style={{ display: "flex", flexDirection: "column", height: "100vh", background: COLORS.bgApp }}>
<TopBar active={active} eventsOpen={eventsOpen} onToggleEvents={() => setEventsOpen((v) => !v)} unread={unread} />
<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 }}>
<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)} 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={() => setSearchOpen(true)} />