feat: session rename + graph chrome polish (popovers dismiss, no-shift no-match, coverage reseat)
Rename (owner ask): clicking the header title opens edit mode with the whole title selected — typing replaces, arrows move inside. Enter or a click away commits (PUT /api/chats/:id); Escape and an empty commit discard. renameChat joins the ChatStore port; like saveSettings it is NOT conversation activity — updatedAt stays put so sidebar order never churns on a rename. The sidebar patch is instant: the header dispatches lh:chat-renamed and SessionsSidebar folds it into its snapshot (the 2.5s poll is only a backstop). Sidebar right-click rename is DELIBERATELY deferred: it needs a real context-menu system, which gets its own pass later (owner decision, 2026-09-17). Graph chrome: - The extraction-coverage button moves between search and import; the gear keeps the far-right corner. Coverage still renders on an EMPTY graph (that is exactly when it matters). - The no-match search note no longer shifts its siblings: the chrome row is items-start (every button is 44px tall, so top alignment pins the row while the search container grows downward). - Popovers dismiss properly: pointerdown anywhere outside the control's root (button + panel) closes it, and so does Escape. Two panels can never be open at once — the sibling chrome buttons are outside the open root, so pointerdown closes the first before the click opens the second. New shared hook components/popover-dismiss.ts, wired into both the settings gear and the coverage control. Gates: typecheck, eslint, 498/498 (+2 rename store tests), build. Live-verified: select-all/focus/Enter/Escape/empty/blur round-trip, updatedAt untouched, sidebar instant patch; chrome order + frozen positions under a no-match query; popover close via canvas, search input, Esc, mutual exclusion, toggle intact.
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import { useEffect, type RefObject } from "react";
|
||||
|
||||
/**
|
||||
* Close-on-dismissal for floating panels (graph chrome, 2026-09-17):
|
||||
* any pointerdown landing OUTSIDE the control's root closes it; Escape
|
||||
* closes it too. The root contains both the toggle button and the panel,
|
||||
* so clicks on the control's own button stay "inside" — its toggle keeps
|
||||
* working — while a click on a sibling chrome button counts as outside:
|
||||
* pointerdown closes the open panel first, then the sibling's click opens
|
||||
* its own. Two dropdowns can therefore never be open at once.
|
||||
*/
|
||||
export function usePopoverDismiss(
|
||||
rootRef: RefObject<HTMLElement | null>,
|
||||
open: boolean,
|
||||
close: () => void,
|
||||
): void {
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onPointerDown = (e: PointerEvent) => {
|
||||
if (rootRef.current && !rootRef.current.contains(e.target as Node)) close();
|
||||
};
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") close();
|
||||
};
|
||||
document.addEventListener("pointerdown", onPointerDown);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", onPointerDown);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [open, close, rootRef]);
|
||||
}
|
||||
Reference in New Issue
Block a user