Stop the profile header from flickering on scroll.

Use a sticky avatar pin instead of changing hero height while the white sheet moves.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-10 21:46:55 +03:30
co-authored by Cursor
parent 20a8e2eec2
commit b3b80d8f9d
@@ -99,13 +99,6 @@ const unreadLabel = computed(() =>
notificationStore.unreadCount > 9 ? '9+' : String(notificationStore.unreadCount),
)
const samplePrice = computed(() => sampleProduct.value.basePrice ?? SAMPLE_PRODUCT.basePrice ?? 0)
const collapse = ref(0)
const COLLAPSE_DISTANCE = 160
function onProfileScroll(event: Event) {
const top = (event.target as HTMLElement).scrollTop
collapse.value = Math.min(1, Math.max(0, top / COLLAPSE_DISTANCE))
}
onMounted(async () => {
loading.value = true
@@ -134,7 +127,9 @@ onMounted(async () => {
<template>
<div class="page profile-page">
<header class="hero" :class="{ 'is-collapsed': collapse > 0.85 }" :style="{ '--c': String(collapse) }">
<div class="page__scroll page__scroll--with-nav profile-scroll">
<header class="hero">
<div class="hero__pin">
<div class="hero__top">
<button
type="button"
@@ -161,13 +156,13 @@ onMounted(async () => {
</span>
</button>
</div>
<UserAvatar
class="hero__avatar"
:src="auth.user?.avatarUrl"
:initials="displayName.charAt(0)"
:size="84"
/>
</div>
<div class="hero__meta">
<h2>{{ displayName }}</h2>
<p>کد واسطه: {{ code }}</p>
@@ -183,10 +178,7 @@ onMounted(async () => {
</div>
</header>
<div
class="page__scroll page__scroll--with-nav profile-scroll"
@scroll.passive="onProfileScroll"
>
<div class="profile-sheet">
<PageSkeleton v-if="loading" variant="profile" />
<template v-else>
<section class="panel">
@@ -334,6 +326,7 @@ onMounted(async () => {
</template>
</div>
</div>
</div>
</template>
<style scoped>
@@ -342,30 +335,39 @@ onMounted(async () => {
}
.hero {
--c: 0;
flex-shrink: 0;
color: #fff;
padding: 0 0 28px;
text-align: center;
background: linear-gradient(
145deg,
var(--color-primary) 0%,
color-mix(in srgb, var(--color-primary) 78%, #0a1f18) 100%
);
}
.hero__pin {
position: sticky;
top: 0;
z-index: 3;
padding: 12px var(--page-padding) 10px;
background: linear-gradient(
145deg,
var(--color-primary) 0%,
color-mix(in srgb, var(--color-primary) 78%, #0a1f18) 100%
);
color: #fff;
padding: 12px var(--page-padding) calc(44px - var(--c) * 34px);
text-align: center;
}
.hero__top {
display: grid;
grid-template-columns: 40px 1fr 40px;
align-items: center;
margin-bottom: calc(16px * (1 - var(--c)));
margin-bottom: 12px;
}
.hero__top h1 {
margin: 0;
font-size: 1rem;
font-weight: 700;
opacity: calc(1 - var(--c));
}
.icon-btn {
@@ -397,23 +399,13 @@ onMounted(async () => {
}
.hero__avatar {
margin: 0 auto calc(12px * (1 - var(--c)));
margin: 0 auto;
border: 3px solid rgba(255, 255, 255, 0.35);
color: #fff;
transform: scale(calc(1 - var(--c) * 0.18));
transform-origin: center top;
}
.hero__meta {
overflow: hidden;
max-height: calc((1 - var(--c)) * 130px);
opacity: calc(1 - var(--c));
transform: translateY(calc(var(--c) * -12px));
pointer-events: none;
}
.hero.is-collapsed .hero__meta {
visibility: hidden;
padding: 10px var(--page-padding) 0;
}
.hero h2 {
@@ -447,11 +439,16 @@ onMounted(async () => {
}
.profile-scroll {
position: relative;
z-index: 1;
flex: 1;
min-height: 0;
margin-top: -24px;
background: transparent;
padding: 0;
}
.profile-sheet {
position: relative;
z-index: 1;
margin-top: -20px;
padding: 20px var(--page-padding) 24px;
display: flex;
flex-direction: column;
@@ -461,7 +458,7 @@ onMounted(async () => {
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.06);
}
.profile-scroll > * {
.profile-sheet > * {
flex-shrink: 0;
}