From add1c1ad144e65d9530698d0f76e23deb8078752 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 5 Jun 2026 08:35:22 -0400 Subject: [PATCH] =?UTF-8?q?fix(modal):=20mobile=20=E2=80=94=20no=20tag=20a?= =?UTF-8?q?utofocus=20+=20sticky=20image=20over=20scrolling=20panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator-flagged 2026-06-05 (mobile): - TagAutocomplete no longer autofocuses on the ≤900px stacked layout — focusing popped the soft keyboard, shrinking the viewport and shoving the pinned image + nav/close controls out of view. matchMedia gate (safe on plain HTTP); desktop autofocus unchanged. - ImageViewer stacked layout: the body now scrolls and the media pane is sticky (height:55vh, top:0), so the image + prev/next/close + integrity badge stay pinned while the metadata panel scrolls beneath. Replaces the old fixed image + 40vh internally-scrolled panel that could push the controls off. Prev/next re-centered over the image band; opaque obsidian bg so the scrolling panel doesn't bleed through. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/components/modal/ImageViewer.vue | 31 ++++++++++++++++--- .../src/components/modal/TagAutocomplete.vue | 11 ++++++- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/modal/ImageViewer.vue b/frontend/src/components/modal/ImageViewer.vue index 1529bbc..5bf9c79 100644 --- a/frontend/src/components/modal/ImageViewer.vue +++ b/frontend/src/components/modal/ImageViewer.vue @@ -203,14 +203,37 @@ function nextFrame() { } @media (max-width: 900px) { - .fc-viewer__body { flex-direction: column; } - /* Side panel drops below the image — the next arrow uses the full width. */ - .fc-viewer__nav--next { right: 16px; } + /* Stacked layout (operator-flagged 2026-06-05): pin the image pane and + its controls at the top while the metadata panel scrolls beneath it. + Previously the image + a 40vh panel shared one fixed viewport and the + controls could be pushed out of view; now the body scrolls and the + media is sticky, so the image + prev/next/close (and the integrity + badge) stay visible no matter how far the panel is scrolled. */ + .fc-viewer__body { + flex-direction: column; + overflow-y: auto; + } + .fc-viewer__media { + flex: none; + height: 55vh; + position: sticky; + top: 0; + z-index: 1; + /* Opaque obsidian so the scrolling panel never bleeds through the + haze behind the pinned image. */ + background: rgb(20, 23, 26); + } .fc-viewer__side { width: 100%; - max-height: 40vh; + max-height: none; border-left: none; border-top: 1px solid rgb(var(--v-theme-surface-light)); } + /* Re-center the prev/next arrows over the 55vh image band (their base + top:50% would land on the scrolling panel); next uses the full width + now that the panel is below, not beside. Close + integrity badge keep + their top:16px/72px — already within the pinned band. */ + .fc-viewer__nav { top: 27.5vh; } + .fc-viewer__nav--next { right: 16px; } } diff --git a/frontend/src/components/modal/TagAutocomplete.vue b/frontend/src/components/modal/TagAutocomplete.vue index 5131688..ec6a258 100644 --- a/frontend/src/components/modal/TagAutocomplete.vue +++ b/frontend/src/components/modal/TagAutocomplete.vue @@ -65,8 +65,17 @@ const store = useTagStore() // Vuetify's v-text-field exposes .focus() on the component instance; // nextTick waits for the modal's mount to finish so the inner // element exists. +// +// EXCEPTION — not on mobile (operator-flagged 2026-06-05): focusing the +// field pops the soft keyboard, which shrinks the visual viewport and +// shoves the pinned image + its nav/close controls out of view. 900px is +// the modal's stacked-layout breakpoint (ImageViewer.vue). matchMedia is +// safe on plain HTTP (not a secure-context API). const inputRef = ref(null) -onMounted(() => { nextTick(() => inputRef.value?.focus?.()) }) +onMounted(() => { + if (window.matchMedia?.('(max-width: 900px)')?.matches) return + nextTick(() => inputRef.value?.focus?.()) +}) // Single text input; no kind dropdown. Client-side mirror of the // backend's parse_kind_prefix lives below — kept in sync with