fix(modal): mobile — no tag autofocus + sticky image over scrolling panel
CI / lint (push) Successful in 3s
CI / backend-lint-and-test (push) Successful in 11s
CI / frontend-build (push) Successful in 24s
CI / integration (push) Successful in 2m58s

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 08:35:22 -04:00
parent 593f65c9cc
commit add1c1ad14
2 changed files with 37 additions and 5 deletions
+27 -4
View File
@@ -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; }
}
</style>
@@ -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 <input>
// 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