feat(explore): ← / → keyboard navigation through the walk
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
This commit is contained in:
@@ -22,9 +22,9 @@
|
||||
<button
|
||||
v-for="(c, i) in store.breadcrumb" :key="c.id"
|
||||
class="fc-ex__crumb"
|
||||
:class="{ 'fc-ex__crumb--current': i === store.breadcrumb.length - 1 }"
|
||||
:class="{ 'fc-ex__crumb--current': i === store.cursor }"
|
||||
type="button"
|
||||
:aria-current="i === store.breadcrumb.length - 1 ? 'page' : undefined"
|
||||
:aria-current="i === store.cursor ? 'page' : undefined"
|
||||
:title="`Step ${i + 1}`"
|
||||
@click="goTo(c.id)"
|
||||
>
|
||||
@@ -191,13 +191,30 @@ function goTo (id) {
|
||||
|
||||
function openInViewer (id) { modal.open(id) }
|
||||
|
||||
// Modal-parity focus: T or "/" jumps to the tag input (the same shortcut the
|
||||
// image modal binds), unless the caret is already in a text field.
|
||||
// Keyboard: T or "/" jumps to the tag input (modal-parity); ←/→ walk the
|
||||
// breadcrumb trail — ← steps back, → goes forward to an already-visited item
|
||||
// or, with no forward history, jumps to a random neighbour (keep rabbit-holing).
|
||||
function onKeyDown (ev) {
|
||||
if ((ev.key === 't' || ev.key === '/') && !isTextEntry(ev.target)) {
|
||||
const input = document.querySelector('.fc-tag-autocomplete input')
|
||||
if (input) { ev.preventDefault(); input.focus() }
|
||||
if (ev.metaKey || ev.ctrlKey || ev.altKey) return
|
||||
const inText = isTextEntry(ev.target)
|
||||
if (ev.key === 't' || ev.key === '/') {
|
||||
if (!inText) {
|
||||
const input = document.querySelector('.fc-tag-autocomplete input')
|
||||
if (input) { ev.preventDefault(); input.focus() }
|
||||
}
|
||||
return
|
||||
}
|
||||
if (ev.key !== 'ArrowLeft' && ev.key !== 'ArrowRight') return
|
||||
// The tag input auto-focuses after every walk, so also allow navigation while
|
||||
// it's focused-but-EMPTY (the caret has nowhere to go) — otherwise arrow-nav
|
||||
// would dead-end after one step. Once a tag is being typed, arrows move the
|
||||
// caret instead.
|
||||
const t = ev.target
|
||||
const inEmptyTagInput =
|
||||
inText && t.value === '' && t.closest?.('.fc-tag-autocomplete')
|
||||
if (inText && !inEmptyTagInput) return
|
||||
const id = ev.key === 'ArrowLeft' ? store.backTarget() : store.forwardTarget()
|
||||
if (id != null) { ev.preventDefault(); goTo(id) }
|
||||
}
|
||||
onMounted(() => document.addEventListener('keydown', onKeyDown))
|
||||
onUnmounted(() => document.removeEventListener('keydown', onKeyDown))
|
||||
|
||||
Reference in New Issue
Block a user