diff --git a/app/composables/useScrollCollapse.ts b/app/composables/useScrollCollapse.ts index 1aa5064..0d562e5 100644 --- a/app/composables/useScrollCollapse.ts +++ b/app/composables/useScrollCollapse.ts @@ -1,55 +1,66 @@ /** - * Composable for collapsing header on scroll - * Returns isCollapsed state that becomes true when scroll > threshold - * User can manually override the collapsed state + * Composable for smooth header collapse on scroll + * Returns pixel positions for all fixed layers */ -export const useScrollCollapse = (threshold = 50) => { - const isCollapsed = ref(false) +export const useScrollCollapse = (headerHeight = 112, searchBarHeight = 48) => { const scrollY = ref(0) - const manualOverride = ref(false) + + // Layer positions in pixels + // Layers 1+2 (MainNav + SubNav): slide up as user scrolls + const headerOffset = computed(() => -Math.min(scrollY.value, headerHeight)) + + // Layer 3 (SearchBar): slides down to top:0 + const searchBarTop = computed(() => Math.max(0, headerHeight - scrollY.value)) + + // Layer 4 (Map): follows SearchBar, minimum is searchBarHeight + const mapTop = computed(() => Math.max(searchBarHeight, headerHeight + searchBarHeight - scrollY.value)) + + // Content padding-top + const contentPaddingTop = computed(() => Math.max(searchBarHeight, headerHeight + searchBarHeight - scrollY.value)) + + // Is header fully collapsed? + const isCollapsed = computed(() => scrollY.value >= headerHeight) + + // Expand - scroll to top + const expand = () => { + if (import.meta.client) { + window.scrollTo({ top: 0, behavior: 'smooth' }) + } + } + + // Collapse - scroll down to hide header + const collapse = () => { + if (import.meta.client) { + window.scrollTo({ top: headerHeight, behavior: 'smooth' }) + } + } const onScroll = () => { - if (import.meta.server) return - - scrollY.value = window.scrollY - - // Only auto-collapse if user hasn't manually expanded - if (!manualOverride.value) { - isCollapsed.value = scrollY.value > threshold + if (import.meta.client) { + scrollY.value = window.scrollY } - - // Reset manual override when scrolling back to top - if (scrollY.value <= threshold) { - manualOverride.value = false - isCollapsed.value = false - } - } - - // Manual expand (user clicked chevron down) - const expand = () => { - isCollapsed.value = false - manualOverride.value = true - } - - // Manual collapse (user clicked chevron up) - const collapse = () => { - isCollapsed.value = true - manualOverride.value = false } onMounted(() => { - window.addEventListener('scroll', onScroll, { passive: true }) - // Initial check - onScroll() + if (import.meta.client) { + window.addEventListener('scroll', onScroll, { passive: true }) + onScroll() // Initial value + } }) onUnmounted(() => { - window.removeEventListener('scroll', onScroll) + if (import.meta.client) { + window.removeEventListener('scroll', onScroll) + } }) return { - isCollapsed: readonly(isCollapsed), scrollY: readonly(scrollY), + headerOffset, + searchBarTop, + mapTop, + contentPaddingTop, + isCollapsed, expand, collapse } diff --git a/app/layouts/catalog.vue b/app/layouts/catalog.vue index e964d1d..f6030ce 100644 --- a/app/layouts/catalog.vue +++ b/app/layouts/catalog.vue @@ -1,22 +1,9 @@ @@ -123,12 +108,16 @@ const route = useRoute() const localePath = useLocalePath() const { t } = useI18n() -// Collapsible header -const { isCollapsed, expand, collapse } = useScrollCollapse(50) -const isHeaderCollapsed = computed(() => isCollapsed.value) - -const expandHeader = () => expand() -const collapseHeader = () => collapse() +// Smooth collapsible header - headerHeight = 112px (64 + 48), searchBarHeight = 48px +const { + headerOffset, + searchBarTop, + mapTop, + contentPaddingTop, + isCollapsed, + expand, + collapse +} = useScrollCollapse(112, 48) // Mobile view toggle const mobileView = ref<'list' | 'map'>('list') @@ -180,7 +169,7 @@ const isSubNavActive = (path: string) => { } // Provide collapsed state to children -provide('headerCollapsed', isHeaderCollapsed) +provide('headerCollapsed', isCollapsed) // Avatar generation const generateUserAvatar = async (seed: string) => {