mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-24 19:32:10 +00:00
eaeeb09ab1
* feat(web): pane-navigation controller — depth/ownership state machine (TASK-2157) Turn the collection page's split pane (PLAN-2105) into a navigable mini-browser per PLAN-2154 Architecture A. Depth + session ownership are stamped in SvelteKit page.state (never raw history.state), so they follow opaque Back/Forward, survive history.go, and reconstruct on cold-load. - New pure controller ($lib/collections/paneController.ts): planPaneDrill (same-ref guard + soft depth cap + ownership INHERITANCE), planLateralOpen (first-open mints ownership / depth-0 re-target / depth>0 stack reset), planPaneClose (three-way staged unwind). Fully unit-tested (26 cases). - navigatePaneTo(target) drill added beside openItemPane; ownership created only by first-open, inherited by drills (cold-load base = unowned). - Three-way ownership-aware staged close: OWNED -> go(-(depth+1)); UNOWNED & depth>0 -> go(-depth) then afterNavigate-latched replaceState-delete; UNOWNED & depth 0 -> replaceState-delete. - R14 fence-on-continuation baked in: controllerActionSeq + a one-shot afterNavigate latch (seq-fenced, state-rechecked), schedulePaneFollow made inert at depth>0 (schedule + fired callback) and cancelled on drill/close, an in-flight guard so a rapid gesture can't stack a second history.go. - depth+ownership preserved through every ?item=-preserving nav: updateUrlFilters, the ?graph toggle (ItemDetail), and the collection rename onNavigateAway (now replaceState, not push). - navigatePaneTo exported onto the pane ItemDetail seam for TASK-2158 and reachable now via a localStorage-gated __padPaneController test hook. Tests: 26 unit + 5 Playwright e2e (open/close/j-k, drill/back/same-ref, detach j/k inertness, cold-load close, detached-row reset). Existing pane e2e suite (13) still green. Closes TASK-2157 Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra * fix(web): rebase pane ownership on collection rename + tie latch to its popstate Codex review of the pane controller (TASK-2157): - P1 (rename ownership): a collection rename replaceState's /old?item=X -> /new?item=X, but every predecessor history entry still points at the now- dead OLD slug — carrying paneOwned=true forward made an owned close history.go back onto a 404. Ownership means "a live pre-pane entry exists to unwind to", which is false after a rename, so onNavigateAway now stamps a fresh {paneDepth:0, paneOwned:false} base on the new slug: close drops ?item= in place, staying on the valid new route. New e2e covers it. - P2 (latch): gate the afterNavigate latch on nav.type==='popstate' so only its own history.go can consume it; a competing goto/link/form leaves it armed until the go settles. - P1 (owned close discards mid-pane filter changes): documented as the plan-mandated R8 behavior — an explicit close is now identical to the browser Back that already closed the pane in PLAN-2105 (no deviation). Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra * fix(web): harden pane latch consumption + bypass draft guard on rename Second Codex round (TASK-2157): - Latch is no longer dropped by a competing history traversal: run() now RETURNS whether it reached its destination (depth collapsed to the base); the afterNavigate handler consumes the latch only when run() fires, so an unrelated browser Back/Forward during the go's in-flight window leaves the latch armed for its own popstate instead of clearing it against the wrong entry. - Collection rename now bypasses the unsaved-draft beforeNavigate guard (navigatePaneAfterRename): the server-side rename already committed and the route component is reused across the same-route pathname change (drafts survive), so a "Stay" prompt could otherwise strand the user on the dead old slug with a stale owned stamp. Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra * fix(web): bound pane latch with a fallback timer + stronger reset correlation Third Codex round (TASK-2157): - P1 lockup: the "leave the latch armed until run() reaches its destination" rule could leave paneNavInFlight() stuck forever if the arming history.go was superseded (its own popstate never lands). Add a bounded fallback timer (PANE_LATCH_FALLBACK_MS) that best-effort-fires then UNCONDITIONALLY clears the latch, so the in-flight guard can never stick. clearPaneLatch() also tears down the timer (onDestroy + on consume). - P2 reset correlation: the detached-open reset now requires the landing entry to carry ?item= (the pane base), not just depth 0 — rejecting a competing browser Back that landed on the pre-pane (no-?item=) entry. - P2 rename + browser Back: documented that Back to the old-slug predecessor is an inherent rename-in-history limitation (past entries can't be rewritten), out of the controller's reach; the imperative close is fixed. Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra * fix(web): fence every controller history.go + split rename vs move nav-away Fourth Codex round (TASK-2157): - P1 (move vs rename): onNavigateAway is fired by ItemDetail for BOTH a collection rename (/user/ws/NEWSLUG?item=X — pane preserved) AND a cross-collection item move (/user/ws/coll/slug — full-page route, no ?item=). handlePaneNavigateAway now branches on whether the target keeps the pane (?item=): rename gets the rebase-to-unowned + draft-guard bypass; a move keeps the ORIGINAL guarded push so its unsaved-draft prompt still fires (the collection page unmounts and would lose drafts). - P2 (duplicate close): the production owned-go close is a one-phase history.go(-1) that wasn't fenced, so a double-click ✕ / ESC+click could stack a second traversal and overshoot the pre-pane entry. Unify all controller traversals (owned close, cold-base close, reset) through paneHistoryGo(), which marks navigation in-flight (paneNavInFlight blocks a duplicate gesture) until the traversal's own popstate settles or a bounded fallback. New e2e asserts a double close lands exactly on the pre-pane URL. Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra