Deletes the vanilla shell and mounts App.tsx on #root. main.ts and the 23 views it drove are gone; App.tsx composes the ported components — sidebar, multi-view, command bar, spawn modal, help and restart overlays, daemon link, drawer — around the catalog and layout stores. The pure parts of four deleted modules moved next to the components that still use them: the spawn-form model and spawnSimilar into components/spawn, the transcript's markdown/format helpers and the diff into components/structured, the help overlay's grouping into components/dialogs. Document-level event names now live in uiEvents.ts. Deduplicated two pairs the migration left side by side: the sidebar uses dialogs/KillDialog and common/ContextMenu. It also listens for the command bar's kill/rename events, which nothing had picked up since catalog.ts went away. styles.css stays as the token and base layer, with `#app` renamed to `#root` and the pane-body rules re-keyed off the classes the React panes actually carry. Closes #253
93 lines
3.3 KiB
TypeScript
93 lines
3.3 KiB
TypeScript
// React port of ../../drawer.ts (#35): the mobile off-canvas sidebar. On
|
|
// desktop the `drawer-open` class is inert — CSS gates the drawer styling to
|
|
// narrow viewports (styles.css, `.app-shell`).
|
|
//
|
|
// Anything that puts a new view in the content area must close the drawer, or
|
|
// it renders behind the sidebar and the tap reads as a dead button — this
|
|
// listens for the same TOGGLE_DRAWER_EVENT/OPEN_SPAWN_EVENT/SPAWN_SIMILAR_EVENT
|
|
// the rest of the shell already raises, so it needs no wiring from callers.
|
|
|
|
import type { ReactNode } from "react";
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
import { OPEN_SPAWN_EVENT, SPAWN_SIMILAR_EVENT, TOGGLE_DRAWER_EVENT } from "../../uiEvents.js";
|
|
|
|
export interface DrawerProps {
|
|
sidebar: ReactNode;
|
|
children: ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export function Drawer({ sidebar, children, className }: DrawerProps) {
|
|
const [open, setOpen] = useState(false);
|
|
const sidebarRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const onToggle = () => setOpen((o) => !o);
|
|
const onClose = () => setOpen(false);
|
|
document.addEventListener(TOGGLE_DRAWER_EVENT, onToggle);
|
|
document.addEventListener(OPEN_SPAWN_EVENT, onClose);
|
|
document.addEventListener(SPAWN_SIMILAR_EVENT, onClose);
|
|
return () => {
|
|
document.removeEventListener(TOGGLE_DRAWER_EVENT, onToggle);
|
|
document.removeEventListener(OPEN_SPAWN_EVENT, onClose);
|
|
document.removeEventListener(SPAWN_SIMILAR_EVENT, onClose);
|
|
};
|
|
}, []);
|
|
|
|
// Horizontal swipes drive the drawer (right opens, left closes) rather than
|
|
// the browser's back/forward navigation. Only a clearly horizontal gesture
|
|
// past a threshold counts, so vertical terminal scrolling (which owns its
|
|
// own touch handler) is untouched.
|
|
useEffect(() => {
|
|
const mobileMq = window.matchMedia("(max-width: 859px)");
|
|
let sx = 0;
|
|
let sy = 0;
|
|
let tracking = false;
|
|
|
|
const onTouchStart = (e: TouchEvent) => {
|
|
tracking = mobileMq.matches && e.touches.length === 1;
|
|
if (tracking) {
|
|
sx = e.touches[0].clientX;
|
|
sy = e.touches[0].clientY;
|
|
}
|
|
};
|
|
const onTouchEnd = (e: TouchEvent) => {
|
|
if (!tracking) return;
|
|
tracking = false;
|
|
const t = e.changedTouches[0];
|
|
const dx = t.clientX - sx;
|
|
const dy = t.clientY - sy;
|
|
if (Math.abs(dx) < 60 || Math.abs(dx) < Math.abs(dy) * 1.5) return;
|
|
setOpen((o) => {
|
|
if (dx > 0 && !o) return true;
|
|
if (dx < 0 && o) return false;
|
|
return o;
|
|
});
|
|
};
|
|
|
|
document.addEventListener("touchstart", onTouchStart, { passive: true });
|
|
document.addEventListener("touchend", onTouchEnd, { passive: true });
|
|
return () => {
|
|
document.removeEventListener("touchstart", onTouchStart);
|
|
document.removeEventListener("touchend", onTouchEnd);
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<div className={`app-shell flex h-full min-h-0 ${open ? "drawer-open" : ""} ${className ?? ""}`}>
|
|
<div
|
|
ref={sidebarRef}
|
|
className="sidebar flex h-full min-h-0 flex-col bg-surface"
|
|
onClick={(e) => {
|
|
if ((e.target as HTMLElement).closest(".tree-leaf")) setOpen(false);
|
|
}}
|
|
>
|
|
{sidebar}
|
|
</div>
|
|
<section className="content min-h-0 flex-1">{children}</section>
|
|
<div className="mv-backdrop" onClick={() => setOpen(false)} />
|
|
</div>
|
|
);
|
|
}
|