From 353b5d8087f81bed5f4eb947db460c1ba63b70f8 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 28 Jun 2026 21:41:23 -0400 Subject: [PATCH] =?UTF-8?q?feat(explore):=20=E2=86=90=20/=20=E2=86=92=20ke?= =?UTF-8?q?yboard=20navigation=20through=20the=20walk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Arrow keys walk the Explore breadcrumb trail: ← steps back, → goes forward to an already-visited item or — with no forward history — jumps to a RANDOM neighbour to keep the rabbit-hole going (operator-asked). The trail gains a cursor (browser back/forward semantics): stepping back no longer trims the forward branch, so → can return to it; a genuinely new walk off a back-step truncates the stale branch then appends. The crumb-bar "current" highlight follows the cursor, not the tip. Arrows are ignored while typing a tag, but still navigate when the tag input is focused-but-empty (it auto-focuses after every walk, so otherwise arrow-nav would dead-end after one step). Modifier-key combos pass through untouched. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa --- frontend/src/stores/explore.js | 45 +++++++++++++++++++++++++----- frontend/src/views/ExploreView.vue | 31 +++++++++++++++----- 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/frontend/src/stores/explore.js b/frontend/src/stores/explore.js index dd7dfaf..29f7cc0 100644 --- a/frontend/src/stores/explore.js +++ b/frontend/src/stores/explore.js @@ -19,6 +19,10 @@ export const useExploreStore = defineStore('explore', () => { const anchor = ref(null) // /api/gallery/image/ payload const neighbors = ref([]) // [{id, thumbnail_url, ...}] const breadcrumb = ref([]) // [{id, thumbnail_url}] walked path + // Index of the current anchor within breadcrumb — browser-style back/forward. + // The trail keeps its forward branch when you step back (so ← / → can move + // through visited items); a NEW walk off a back-step truncates that branch. + const cursor = ref(-1) const loading = ref(false) const error = ref(null) @@ -54,13 +58,39 @@ export const useExploreStore = defineStore('explore', () => { } } - // Forward walk appends; navigating to an id already in the trail (a - // breadcrumb click, or a loop back) TRIMS to it — so the route stays the - // single source of truth and the crumb bar never grows stale branches. + // The route is the source of truth; this reconciles the trail + cursor to it. + // Revisiting an id already on the path (← / →, a crumb click, or a loop) just + // MOVES the cursor there — the forward branch is preserved so → can return to + // it. A genuinely new image off a back-step truncates the stale forward branch + // (browser semantics), then appends and points the cursor at the new tip. function _reconcileTrail (id, thumbnailUrl) { const idx = breadcrumb.value.findIndex((c) => c.id === id) - if (idx >= 0) breadcrumb.value = breadcrumb.value.slice(0, idx + 1) - else breadcrumb.value = [...breadcrumb.value, { id, thumbnail_url: thumbnailUrl }] + if (idx >= 0) { + cursor.value = idx + return + } + const base = cursor.value < breadcrumb.value.length - 1 + ? breadcrumb.value.slice(0, cursor.value + 1) + : breadcrumb.value + breadcrumb.value = [...base, { id, thumbnail_url: thumbnailUrl }] + cursor.value = breadcrumb.value.length - 1 + } + + // ← target: the previous crumb (null at the start of the trail). + function backTarget () { + return cursor.value > 0 ? breadcrumb.value[cursor.value - 1].id : null + } + + // → target: the next already-visited crumb if we'd stepped back, else a + // RANDOM neighbour to keep the rabbit-hole going. Null if neither exists. + function forwardTarget () { + if (cursor.value >= 0 && cursor.value < breadcrumb.value.length - 1) { + return breadcrumb.value[cursor.value + 1].id + } + if (neighbors.value.length) { + return neighbors.value[Math.floor(Math.random() * neighbors.value.length)].id + } + return null } function reset () { @@ -68,6 +98,7 @@ export const useExploreStore = defineStore('explore', () => { anchor.value = null neighbors.value = [] breadcrumb.value = [] + cursor.value = -1 error.value = null loading.value = false } @@ -145,8 +176,8 @@ export const useExploreStore = defineStore('explore', () => { function close () {} return { - anchor, neighbors, breadcrumb, loading, error, NEIGHBOR_LIMIT, - anchorOn, reset, + anchor, neighbors, breadcrumb, cursor, loading, error, NEIGHBOR_LIMIT, + anchorOn, reset, backTarget, forwardTarget, // host surface current, currentImageId, reloadTags, addExistingTag, removeTag, createAndAdd, close, diff --git a/frontend/src/views/ExploreView.vue b/frontend/src/views/ExploreView.vue index 13d3057..67ea039 100644 --- a/frontend/src/views/ExploreView.vue +++ b/frontend/src/views/ExploreView.vue @@ -22,9 +22,9 @@