Ports catalog.ts's tree renderer to components under src/components/sidebar/, following docs/SIDEBAR.md: two-line cards behind a 5px activity rail, adaptive meta line (duration, detail, program, diff stat), collapsible folders, j/k walk, inline rename and a right-click menu. The nav walk reads a pure model (model.ts) rather than the DOM, so the rule that an expanded folder header is skipped and a collapsed one is not is testable without a render. Rail motion needs @keyframes, so it stays in sidebar.css rather than the Tailwind config. Closes #244
65 lines
1.7 KiB
TypeScript
65 lines
1.7 KiB
TypeScript
// Right-click menu on a session card (docs/SIDEBAR.md): rename, spawn similar,
|
|
// kill. Rendered into <body> so a scrolling sidebar cannot clip it.
|
|
|
|
import { useEffect, useRef } from "react";
|
|
import { createPortal } from "react-dom";
|
|
|
|
export interface ContextMenuItem {
|
|
label: string;
|
|
danger?: boolean;
|
|
onSelect(): void;
|
|
}
|
|
|
|
export interface ContextMenuProps {
|
|
x: number;
|
|
y: number;
|
|
items: ContextMenuItem[];
|
|
onClose(): void;
|
|
}
|
|
|
|
export function ContextMenu({ x, y, items, onClose }: ContextMenuProps) {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") onClose();
|
|
};
|
|
const onDown = (e: Event) => {
|
|
if (!ref.current?.contains(e.target as Node)) onClose();
|
|
};
|
|
document.addEventListener("keydown", onKey);
|
|
document.addEventListener("pointerdown", onDown, true);
|
|
return () => {
|
|
document.removeEventListener("keydown", onKey);
|
|
document.removeEventListener("pointerdown", onDown, true);
|
|
};
|
|
}, [onClose]);
|
|
|
|
return createPortal(
|
|
<div
|
|
ref={ref}
|
|
role="menu"
|
|
className="context-menu fixed z-modal min-w-[160px] rounded-r-2 border border-border-strong bg-surface py-sp-1 shadow-2"
|
|
style={{ left: x, top: y }}
|
|
>
|
|
{items.map((item) => (
|
|
<button
|
|
key={item.label}
|
|
type="button"
|
|
role="menuitem"
|
|
className={`block w-full px-sp-3 py-sp-1 text-left text-fs-12 hover:bg-surface-2 ${
|
|
item.danger ? "text-danger" : "text-ink"
|
|
}`}
|
|
onClick={() => {
|
|
onClose();
|
|
item.onSelect();
|
|
}}
|
|
>
|
|
{item.label}
|
|
</button>
|
|
))}
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|