variable header size
This commit is contained in:
@@ -1,20 +1,96 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { RouterLink, useRouter, useRoute } from 'vue-router'
|
||||
import { useFeeds, logout as logoutSession } from '@/composables/useFeeds'
|
||||
import Modal from './modal/AddUrl.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { sync, showModal, viewMode, toggleViewMode, layout, toggleLayout, markAllRead, feeds } = useFeeds()
|
||||
const { sync, showModal, viewMode, toggleViewMode, layout, toggleLayout, markAllRead, feeds, setupIntersectionObserver, consumeScrollCorrection } = useFeeds()
|
||||
|
||||
const headerRef = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
// Measured once, on mount, from the header's full (expanded) size. Never
|
||||
// re-measured when `compact` toggles: this only drives #app's initial
|
||||
// padding-top / RssFeeds' scroll-margin-top, both of which only matter at
|
||||
// scroll position 0 — where the header is always expanded anyway. Updating
|
||||
// it mid-scroll would shift page layout and jump the scroll position.
|
||||
const h = headerRef.value?.getBoundingClientRect().height ?? 0
|
||||
document.documentElement.style.setProperty('--app-nav-height', `${h}px`)
|
||||
})
|
||||
|
||||
// Shrinks the header once scrolled past COMPACT_ENTER, restores it below
|
||||
// COMPACT_EXIT. The gap between the two (rather than one toggle point)
|
||||
// avoids flicker when the scroll position hovers near the boundary.
|
||||
const COMPACT_ENTER = 64
|
||||
const COMPACT_EXIT = 32
|
||||
// List-view reading removes each article from the DOM once it's marked
|
||||
// read, which collapses the remaining content back toward the top of the
|
||||
// (now shorter) page — useFeeds.js corrects scrollY afterwards to keep the
|
||||
// next article anchored under the header. That correction isn't a one-off
|
||||
// flash: scrollY can sit low for as long as the user happens to pause
|
||||
// between scroll gestures, so a short debounce alone doesn't cover it.
|
||||
// consumeScrollCorrection() lets AppNav tell that "the page moved because
|
||||
// content was removed" apart from a real user scroll and ignore it
|
||||
// entirely. EXPAND_DEBOUNCE_MS stays as a second, smaller layer for plain
|
||||
// scroll jitter (momentum bounce, trackpad micro-scrolls) unrelated to
|
||||
// article removal.
|
||||
const EXPAND_DEBOUNCE_MS = 150
|
||||
const compact = ref(false)
|
||||
let ticking = false
|
||||
let expandTimer = null
|
||||
|
||||
function handleScroll() {
|
||||
if (ticking) return
|
||||
ticking = true
|
||||
requestAnimationFrame(() => {
|
||||
const y = window.scrollY
|
||||
const wasCorrection = consumeScrollCorrection()
|
||||
if (!compact.value && y > COMPACT_ENTER) {
|
||||
compact.value = true
|
||||
if (expandTimer) {
|
||||
clearTimeout(expandTimer)
|
||||
expandTimer = null
|
||||
}
|
||||
} else if (compact.value && y < COMPACT_EXIT && !wasCorrection) {
|
||||
if (!expandTimer) {
|
||||
expandTimer = setTimeout(() => {
|
||||
expandTimer = null
|
||||
if (window.scrollY < COMPACT_EXIT) {
|
||||
compact.value = false
|
||||
}
|
||||
}, EXPAND_DEBOUNCE_MS)
|
||||
}
|
||||
} else if (expandTimer && !wasCorrection) {
|
||||
clearTimeout(expandTimer)
|
||||
expandTimer = null
|
||||
}
|
||||
ticking = false
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
if (expandTimer) {
|
||||
clearTimeout(expandTimer)
|
||||
expandTimer = null
|
||||
}
|
||||
})
|
||||
|
||||
// The read-tracking IntersectionObserver (useFeeds.js) bakes the header's
|
||||
// current height into its rootMargin, so it needs re-syncing once the
|
||||
// compact/expanded transition finishes. Deferred past the CSS transition
|
||||
// duration (0.2s) so the header's rendered height has actually settled to
|
||||
// its target value before it's measured.
|
||||
watch(compact, () => {
|
||||
setTimeout(setupIntersectionObserver, 220)
|
||||
})
|
||||
|
||||
const onFeedsPage = computed(() => route.path === '/feeds')
|
||||
|
||||
const unreadCount = computed(() => feeds.value.filter(f => !f.read).length)
|
||||
@@ -62,7 +138,7 @@ function handleToggleLayout() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header ref="headerRef" class="app-nav">
|
||||
<header ref="headerRef" class="app-nav" :class="{ 'app-nav--compact': compact }">
|
||||
<div class="app-nav__wrapper">
|
||||
<span class="app-nav__title">RSS Reader<span v-if="unreadCount" class="app-nav__unread"> ({{ unreadCount }})</span></span>
|
||||
<button
|
||||
@@ -133,11 +209,21 @@ function handleToggleLayout() {
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
transition: padding 0.2s ease;
|
||||
}
|
||||
|
||||
.app-nav--compact .app-nav__wrapper {
|
||||
padding: 0.375rem 1rem;
|
||||
}
|
||||
|
||||
.app-nav__title {
|
||||
font-weight: bold;
|
||||
font-size: clamp(1.1rem, 4vw, 1.4rem);
|
||||
transition: font-size 0.2s ease;
|
||||
}
|
||||
|
||||
.app-nav--compact .app-nav__title {
|
||||
font-size: clamp(0.95rem, 3.5vw, 1.1rem);
|
||||
}
|
||||
|
||||
.app-nav__unread {
|
||||
@@ -231,5 +317,9 @@ function handleToggleLayout() {
|
||||
.app-nav__wrapper {
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
.app-nav--compact .app-nav__wrapper {
|
||||
padding: 0.5rem 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user