Explore: focus-everywhere + provenance in rail; tag-gaps cleanup #134

Merged
bvandeusen merged 3 commits from dev into main 2026-06-26 21:40:57 -04:00
2 changed files with 28 additions and 12 deletions
Showing only changes of commit c8a8e23050 - Show all commits
+16 -4
View File
@@ -30,7 +30,15 @@
@accepted="focusTagInput"
/>
<v-dialog v-model="renameDialog" max-width="420">
<!-- @after-leave: when either dialog finishes closing (apply OR cancel),
hand focus back to the tag input. Fires after Vuetify's own
focus-return to the activator, so ours wins; keeps the keyboard flow on
the input (operator-asked 2026-06-26). Mobile guard preserved via
focusTagInput → TagAutocomplete. -->
<v-dialog
v-model="renameDialog" max-width="420"
@after-leave="focusTagInput"
>
<TagRenameDialog
v-if="renameTarget"
:tag="renameTarget"
@@ -41,6 +49,7 @@
<v-dialog
v-model="fandomDialog" max-width="460"
@after-enter="fandomSetRef?.focusSearch?.()"
@after-leave="focusTagInput"
>
<FandomSetDialog
ref="fandomSetRef"
@@ -92,19 +101,22 @@ async function onNavigate(tag) {
function focusTagInput() { tagInputRef.value?.focus?.() }
defineExpose({ focusTagInput })
// Every tag mutation hands focus back to the input so the operator can keep
// typing the next tag without re-clicking — matches the accept-suggestion flow
// (operator-asked 2026-06-26; the Explore workspace leans on this hard).
async function onRemove(tagId) {
errorMsg.value = null
try { await host.removeTag(tagId) }
try { await host.removeTag(tagId); focusTagInput() }
catch (e) { errorMsg.value = e.message }
}
async function onPickExisting(hit) {
errorMsg.value = null
try { await host.addExistingTag(hit.id) }
try { await host.addExistingTag(hit.id); focusTagInput() }
catch (e) { errorMsg.value = e.message }
}
async function onPickNew(payload) {
errorMsg.value = null
try { await host.createAndAdd(payload) }
try { await host.createAndAdd(payload); focusTagInput() }
catch (e) { errorMsg.value = e.message }
}
// A suggestion picked from the autocomplete dropdown runs the SAME path as the
+12 -8
View File
@@ -130,14 +130,18 @@ const seeding = ref(false)
const seedError = ref(null)
const tagPanelRef = ref(null)
// Auto-focus the tag input whenever the focused image changes (seed + every
// walk) so tagging needs no extra click — the whole point of the workspace
// (operator-asked 2026-06-26). Reuses TagPanel/TagAutocomplete's focus, which
// keeps its own mobile guard (no soft-keyboard pop on touch).
watch(
() => store.currentImageId,
(id) => { if (id != null) nextTick(() => tagPanelRef.value?.focusTagInput?.()) },
)
// Auto-focus the tag input after any action so tagging needs no extra click —
// the whole point of the workspace (operator-asked 2026-06-26). nextTick waits
// for the post-navigation re-render, then rAF lands the focus AFTER paint so a
// neighbour/breadcrumb click that re-renders the grid can't steal it back.
// Reuses TagPanel/TagAutocomplete's focus, which keeps its mobile guard (no
// soft-keyboard pop on touch).
function refocusTag () {
nextTick(() => requestAnimationFrame(() => tagPanelRef.value?.focusTagInput?.()))
}
// Every focused-image change (seed + every walk: neighbour click, breadcrumb,
// Random image).
watch(() => store.currentImageId, (id) => { if (id != null) refocusTag() })
// The route is the source of truth. With an anchor, walk it; without one (the
// bare /explore nav entry) seed a RANDOM image so the tab kick-starts a rabbit