feat(kanban): sidebar edge zone now toggles (open + close)

The 32px left drag zone now also closes the sidebar when it is open.
Symmetric behaviour: dwell ≥400ms while dragging closes if open / opens
if closed.

To prevent a drag that starts with the pointer already inside the
sidebar (e.g. dragging a sidebar card itself) from immediately auto-
closing, the close action requires the pointer to have left the strip
at least once after the drag started. So:
- closed + drag-to-edge -> opens (unchanged).
- open + drag a card out, then move the card back to the edge -> closes.
- open + drag a sidebar card directly to the board -> nothing happens.

After a successful toggle the dwell flag resets, so the user must leave
the strip and re-enter to trigger another action.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 14:03:06 +02:00
parent 257858a1f3
commit 76d85959f1
+24 -8
View File
@@ -193,6 +193,12 @@ export function App() {
}
let timer: number | null = null;
let inside = false;
// Para evitar que un drag iniciado dentro del sidebar abierto dispare un
// cierre inmediato, exigimos que el puntero haya salido de la franja al
// menos una vez tras empezar el drag. Asi: abrir = entrar a la franja
// tras empezar fuera (que ya pasaba); cerrar = salir de la franja y
// volver a entrar.
let hasLeftStrip = false;
const clear = () => {
if (timer !== null) {
window.clearTimeout(timer);
@@ -203,16 +209,26 @@ export function App() {
const nowInside = ev.clientX <= DRAG_EDGE_WIDTH;
if (nowInside === inside) return;
inside = nowInside;
setEdgeArmed(nowInside);
if (nowInside) {
if (navOpenRef.current) return; // already open, nothing to do
clear();
timer = window.setTimeout(() => {
setNavOpen(true);
}, DRAG_EDGE_HOVER_MS);
} else {
// El brillo visible solo cuando "armable": fuera-y-luego-dentro (open
// siempre) o dentro con sidebar cerrado (open trigger).
const armable = nowInside && (!navOpenRef.current || hasLeftStrip);
setEdgeArmed(armable);
if (!nowInside) {
hasLeftStrip = true;
clear();
return;
}
// nowInside = true
if (!armable) return;
clear();
const willOpen = !navOpenRef.current;
timer = window.setTimeout(() => {
setNavOpen(willOpen);
// Tras toggle, resetea el flag para no encadenar otra accion sin
// que el usuario salga + vuelva.
hasLeftStrip = false;
setEdgeArmed(false);
}, DRAG_EDGE_HOVER_MS);
};
document.addEventListener("mousemove", onMove);
return () => {