The header carried only the brand and a session tally; app actions had already moved to the sidebar footer. The mobile ☰ it hosted moves to the pane header (replacing the redundant back arrow) and to the welcome state, so the session drawer stays reachable without a swipe. The DEV `?kit` link moves to the sidebar footer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
84 lines
2.8 KiB
TypeScript
84 lines
2.8 KiB
TypeScript
// Mobile off-canvas sidebar (issue #35); on desktop the `drawer-open` class is
|
|
// inert (CSS gates the drawer styling).
|
|
//
|
|
// 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. "Spawn
|
|
// similar" sits inside a session row but stops propagation, so it never reaches
|
|
// the row's own close — both spawn paths arrive here via document events.
|
|
|
|
import { OPEN_SPAWN_EVENT, SPAWN_SIMILAR_EVENT } from "./catalog.js";
|
|
|
|
/** Toggling the drawer from outside the shell (the pane header's ☰ on mobile),
|
|
* without threading the controller through every view. */
|
|
export const TOGGLE_DRAWER_EVENT = "maestro-toggle-drawer";
|
|
|
|
export function requestToggleDrawer(): void {
|
|
document.dispatchEvent(new CustomEvent(TOGGLE_DRAWER_EVENT));
|
|
}
|
|
|
|
export interface Drawer {
|
|
open(): void;
|
|
close(): void;
|
|
toggle(): void;
|
|
isOpen(): boolean;
|
|
}
|
|
|
|
export function installDrawer(
|
|
shellEl: HTMLElement,
|
|
sidebarEl: HTMLElement,
|
|
backdropEl: HTMLElement,
|
|
): Drawer {
|
|
const drawer: Drawer = {
|
|
open: () => shellEl.classList.add("drawer-open"),
|
|
close: () => shellEl.classList.remove("drawer-open"),
|
|
toggle: () => shellEl.classList.toggle("drawer-open"),
|
|
isOpen: () => shellEl.classList.contains("drawer-open"),
|
|
};
|
|
|
|
backdropEl.addEventListener("click", () => drawer.close());
|
|
sidebarEl.addEventListener("click", (e) => {
|
|
if ((e.target as HTMLElement).closest(".tree-leaf")) drawer.close();
|
|
});
|
|
document.addEventListener(TOGGLE_DRAWER_EVENT, () => drawer.toggle());
|
|
document.addEventListener(OPEN_SPAWN_EVENT, () => drawer.close());
|
|
document.addEventListener(SPAWN_SIMILAR_EVENT, () => drawer.close());
|
|
|
|
return drawer;
|
|
}
|
|
|
|
/** 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. */
|
|
export function installDrawerSwipe(drawer: Drawer): void {
|
|
const mobileMq = window.matchMedia("(max-width: 859px)");
|
|
let sx = 0;
|
|
let sy = 0;
|
|
let tracking = false;
|
|
document.addEventListener(
|
|
"touchstart",
|
|
(e) => {
|
|
tracking = mobileMq.matches && e.touches.length === 1;
|
|
if (tracking) {
|
|
sx = e.touches[0].clientX;
|
|
sy = e.touches[0].clientY;
|
|
}
|
|
},
|
|
{ passive: true },
|
|
);
|
|
document.addEventListener(
|
|
"touchend",
|
|
(e) => {
|
|
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;
|
|
if (dx > 0 && !drawer.isOpen()) drawer.open();
|
|
else if (dx < 0 && drawer.isOpen()) drawer.close();
|
|
},
|
|
{ passive: true },
|
|
);
|
|
}
|