From 03c02a1f183bbc6885d2552beca0caa0d675c65d Mon Sep 17 00:00:00 2001 From: driver Date: Wed, 30 Sep 2026 03:25:12 +0000 Subject: [PATCH] Deep Research: the header leaves while the gallery is scrolled; the Deeper Research line is small The Driver, reviewing on a phone: on /deep-research the site header stayed while every other page lets it scroll away, and the "Want Deeper Research?" line under the research map was far too big. /deep-research keeps the verified layout at rest: the page does not scroll, the header shows, and the gallery fills the rest of the screen, its bottom menu button included. The gallery scrolls inside its own main pane, so a finger or a wheel over it never moves the page. Once that pane is scrolled down a little, gets rg-header-hidden: the header hides and the gallery takes the whole screen (100svh). Back at the pane's top, the header returns. One scroller, so nothing fights its momentum. A capture-phase scroll listener on the gallery's shadow root sees the pane; only a scroller at least half the gallery's height counts. Measured with a real finger at 390: the map (703 px) and the open sheet (211 px) never resize, and the header hides once and returns once. /research: the line is 1.2rem, the size of the homepage's "Explore our research maps" link. --- app/pages/deep-research/index.vue | 46 +++++++++++++++++++++++++++++-- app/pages/research/index.vue | 12 ++++++-- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/app/pages/deep-research/index.vue b/app/pages/deep-research/index.vue index c6a2f1fe..d3e7d506 100644 --- a/app/pages/deep-research/index.vue +++ b/app/pages/deep-research/index.vue @@ -17,6 +17,8 @@ import { mapboxTokenIfChosen } from '~/utils/mapProvider' // ONE scrollbar: no footer here, and the page itself never scrolls; the gallery's own content does. +// The header still leaves as on every other page: it hides while the gallery is scrolled down +// (see watchPaneForHeader) and the gallery then takes the whole screen. definePageMeta({ hideFooter: true }) const { t, locale } = useI18n() @@ -36,6 +38,7 @@ useHead({ const host = ref(null) let gallery: HTMLElement | null = null let resizeObs: ResizeObserver | null = null +let unwatchPane: (() => void) | null = null // The gallery fills the viewport below the header. The header's height differs between desktop // and phone (and with the language), so it is measured, not assumed. @@ -45,6 +48,31 @@ function fitBelowHeader() { document.documentElement.style.setProperty('--rg-header-h', h + 'px') } +// The gallery scrolls inside its own main pane, so a finger (or a wheel) over it scrolls that +// pane and never the page. The header leaves the way it does on every other page: once the pane +// is scrolled down a little, gets rg-header-hidden (the header hides and the gallery takes +// the full screen); back at the pane's top, the header returns. One scroller, so nothing fights +// the pane's own momentum. Only a scroller at least half the gallery's height counts (its main +// pane), not a small list such as a legend sheet's. +function watchPaneForHeader(el: HTMLElement, root: ShadowRoot) { + const onScroll = (e: Event) => { + const pane = e.target + if (!(pane instanceof HTMLElement) || pane.clientHeight < el.clientHeight / 2) return + const cls = document.documentElement.classList + if (pane.scrollTop > 8) cls.add('rg-header-hidden') + else if (pane.scrollTop <= 0) cls.remove('rg-header-hidden') + } + // Scroll events don't bubble, but they do pass the shadow root in the capture phase. + root.addEventListener('scroll', onScroll, { capture: true, passive: true }) + unwatchPane = () => root.removeEventListener('scroll', onScroll, true) +} + +// The gallery attaches its shadow root once its script has defined the element. +function whenShadowRoot(el: HTMLElement, done: (root: ShadowRoot) => void, tries = 50) { + if (el.shadowRoot) done(el.shadowRoot) + else if (tries > 0) setTimeout(() => whenShadowRoot(el, done, tries - 1), 100) +} + onMounted(() => { fitBelowHeader() // The gallery is sized to the viewport below the header; a sub-pixel header height must not add a @@ -76,6 +104,10 @@ onMounted(() => { gallery.setAttribute('provider', provider) gallery.setAttribute('profile-config', JSON.stringify(galleryConfig)) host.value?.appendChild(gallery) + customElements.whenDefined('doxa-maps-spa').then(() => { + const el = gallery + if (el) whenShadowRoot(el, root => watchPaneForHeader(el, root)) + }) }) watch(locale, (lang) => { @@ -83,9 +115,10 @@ watch(locale, (lang) => { }) onBeforeUnmount(() => { - document.documentElement.classList.remove('rg-no-page-scroll') - document.documentElement.classList.remove('rg-gallery-mounted') + document.documentElement.classList.remove('rg-no-page-scroll', 'rg-gallery-mounted', 'rg-header-hidden') resizeObs?.disconnect() + unwatchPane?.() + unwatchPane = null gallery?.remove() gallery = null }) @@ -131,6 +164,15 @@ html.rg-no-page-scroll body { overflow: hidden; } +/* While the gallery is scrolled down, the header is away and the gallery takes the whole screen, + as the header leaves on every other page. Back at the gallery's top, the header returns. */ +html.rg-header-hidden #masthead { + display: none; +} +html.rg-header-hidden .research-gallery-page { + height: 100svh; +} + /* The site header is z-index 1500 (blocks/_header.scss); the gallery's info dialog is a full-viewport layer at z-index 1000, full-screen on a phone, so the header covered the dialog's title and close button there. While the gallery is shown, the header sits just under the dialog: 999 still keeps it, diff --git a/app/pages/research/index.vue b/app/pages/research/index.vue index 4624dcf5..19e8e342 100644 --- a/app/pages/research/index.vue +++ b/app/pages/research/index.vue @@ -51,8 +51,9 @@ useTextHighlight() :profile-config="researchProfileConfig" class="rounded-xlg" /> - -

+ +

{{ t('Want Deeper Research?') }} + +