Files
HAOVP/frontend/video-frontend-web/src/components/layout/AppHeader.vue
T

1142 lines
29 KiB
Vue

<template>
<header class="app-header">
<template v-if="isHome">
<div
class="home-header"
:class="{ compact: isCompactHomeHeader }"
>
<div
class="home-banner"
@mousemove="handleBannerMouseMove"
@mouseleave="resetBannerMotion"
>
<div class="banner-backdrop" />
<div class="animated-banner">
<div
v-for="layer in bannerLayers"
:key="layer.key"
class="layer"
:style="getLayerStyle(layer)"
>
<img
:src="layer.src"
alt=""
>
</div>
</div>
<div class="page-shell banner-shell">
<div class="banner-nav-row">
<nav class="banner-nav">
<button
v-for="entry in homeNavEntries"
:key="entry.label"
class="banner-link"
@click="entry.action()"
>
<van-icon
:name="entry.icon"
size="16"
/>
<span>{{ entry.label }}</span>
</button>
</nav>
<div class="banner-search-wrap">
<form
class="search-bar"
@submit.prevent="handleSearch"
>
<div class="search-content">
<van-icon
name="search"
size="20"
class="search-icon"
/>
<input
v-model="searchKeyword"
class="search-input"
type="text"
maxlength="100"
placeholder="搜索视频、用户、关键词"
>
</div>
<button
type="submit"
class="search-btn"
>
<van-icon
name="search"
size="22"
/>
</button>
</form>
</div>
<div class="banner-actions">
<button
class="avatar-entry"
@click="goToProfileOrLogin"
>
<van-image
:src="displayAvatar"
round
width="36"
height="36"
/>
</button>
<button
class="banner-action"
@click="router.push('/notifications')"
>
<van-badge
:content="messageStore.unreadNotificationCount || ''"
:show-zero="false"
>
<van-icon
name="bell"
size="18"
/>
</van-badge>
<span>消息</span>
</button>
<button
class="banner-action"
@click="router.push('/my/favorites')"
>
<van-icon
name="star-o"
size="18"
/>
<span>收藏</span>
</button>
<button
class="banner-action"
@click="router.push('/my/history')"
>
<van-icon
name="clock-o"
size="18"
/>
<span>历史</span>
</button>
<button
class="banner-action"
@click="router.push(userStore.isLoggedIn ? '/my/videos' : '/login')"
>
<van-icon
name="video-o"
size="18"
/>
<span>创作中心</span>
</button>
<button
class="upload-entry"
@click="router.push(userStore.isLoggedIn ? '/upload' : '/login')"
>
<van-icon
name="upgrade"
size="16"
/>
<span>投稿</span>
</button>
</div>
</div>
</div>
</div>
<div class="home-sticky-bar">
<div class="page-shell sticky-shell">
<div
class="sticky-brand"
@click="router.push('/')"
>
<div class="brand-mark">
<span class="brand-eye left" />
<span class="brand-eye right" />
</div>
<div class="brand-copy">
<strong>HAOVP</strong>
<span>在线视频平台</span>
</div>
</div>
<nav class="sticky-nav">
<button
v-for="entry in stickyEntries"
:key="entry.label"
class="sticky-link"
:class="{ active: entry.active }"
@click="entry.action()"
>
{{ entry.label }}
</button>
</nav>
<div class="sticky-search-wrap">
<form
class="search-bar compact-search"
@submit.prevent="handleSearch"
>
<div class="search-content">
<input
v-model="searchKeyword"
class="search-input"
type="text"
maxlength="100"
placeholder="搜索视频、用户、关键词"
>
</div>
<button
type="submit"
class="search-btn"
>
<van-icon
name="search"
size="20"
/>
</button>
</form>
</div>
<div class="sticky-actions">
<button
class="avatar-entry compact"
@click="goToProfileOrLogin"
>
<van-image
:src="displayAvatar"
round
width="32"
height="32"
/>
</button>
<button
class="icon-action"
@click="router.push('/notifications')"
>
<van-badge
:content="messageStore.unreadNotificationCount || ''"
:show-zero="false"
>
<van-icon
name="bell"
size="18"
/>
</van-badge>
</button>
<button
class="icon-action"
@click="router.push('/my/favorites')"
>
<van-icon
name="star-o"
size="18"
/>
</button>
<button
class="icon-action"
@click="router.push('/my/history')"
>
<van-icon
name="clock-o"
size="18"
/>
</button>
<button
class="upload-entry compact"
@click="router.push(userStore.isLoggedIn ? '/upload' : '/login')"
>
投稿
</button>
</div>
</div>
</div>
</div>
</template>
<template v-else>
<div class="topbar">
<div class="page-shell topbar-shell">
<nav class="top-nav">
<button
v-for="entry in stickyEntries"
:key="entry.label"
class="top-link"
:class="{ active: entry.active }"
@click="entry.action()"
>
{{ entry.label }}
</button>
</nav>
<div class="top-actions">
<template v-if="!userStore.isLoggedIn">
<button
class="top-link"
@click="router.push('/login')"
>
登录
</button>
<button
class="top-link"
@click="router.push('/register')"
>
注册
</button>
</template>
<template v-else>
<button
class="top-link"
@click="router.push('/my/favorites')"
>
收藏
</button>
<button
class="top-link"
@click="router.push('/my/history')"
>
历史
</button>
<van-popover
v-model:show="showMessagePopover"
trigger="click"
placement="bottom-end"
>
<div class="popover-menu">
<button
class="popover-item"
@click="goToMessages"
>
<span>私信</span>
<van-badge
v-if="messageStore.unreadMessageCount"
:content="messageStore.unreadMessageCount"
/>
</button>
<button
class="popover-item"
@click="goToNotifications"
>
<span>通知</span>
<van-badge
v-if="messageStore.unreadNotificationCount"
:content="messageStore.unreadNotificationCount"
/>
</button>
</div>
<template #reference>
<button class="top-icon">
<van-badge
:content="messageStore.totalUnreadCount || ''"
:show-zero="false"
>
<van-icon
name="bell"
size="18"
/>
</van-badge>
</button>
</template>
</van-popover>
</template>
<button
class="upload-entry compact"
@click="router.push(userStore.isLoggedIn ? '/upload' : '/login')"
>
投稿
</button>
</div>
</div>
</div>
<div class="mainbar">
<div class="page-shell mainbar-shell">
<div
class="sticky-brand"
@click="router.push('/')"
>
<div class="brand-mark">
<span class="brand-eye left" />
<span class="brand-eye right" />
</div>
<div class="brand-copy">
<strong>HAOVP</strong>
<span>在线视频平台</span>
</div>
</div>
<div class="sticky-search-wrap">
<form
class="search-bar compact-search"
@submit.prevent="handleSearch"
>
<div class="search-content">
<input
v-model="searchKeyword"
class="search-input"
type="text"
maxlength="100"
placeholder="搜索视频、用户、关键词"
>
</div>
<button
type="submit"
class="search-btn"
>
<van-icon
name="search"
size="20"
/>
</button>
</form>
</div>
<div class="user-area">
<template v-if="!userStore.isLoggedIn">
<button
class="login-btn"
@click="router.push('/login')"
>
登录
</button>
</template>
<template v-else>
<button
class="user-chip"
@click="goToProfileOrLogin"
>
<van-image
:src="displayAvatar"
round
width="36"
height="36"
/>
<div class="user-copy">
<strong>{{ displayNickname }}</strong>
<span>{{ displaySignature }}</span>
</div>
</button>
</template>
</div>
</div>
</div>
</template>
</header>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useMessageStore, useUserStore } from '@/stores'
import { resolveAvatarUrl } from '@/utils/media'
type BannerLayer = {
key: string
src: string
depthX: number
baseX?: number
baseY?: number
scale?: number
brightness?: number
opacity?: number
}
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const messageStore = useMessageStore()
const searchKeyword = ref('')
const showMessagePopover = ref(false)
const bannerMotionX = ref(0)
const homeScrollY = ref(0)
const homeHeaderCompact = ref(false)
const defaultAvatar =
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI1MCIgZmlsbD0iI2YzZjRmNiIvPjxjaXJjbGUgY3g9IjUwIiBjeT0iMzUiIHI9IjE1IiBmaWxsPSIjYmRjM2NmIi8+PGVsbGlwc2UgY3g9IjUwIiBjeT0iNzUiIHJ4PSIyNSIgcnk9IjIwIiBmaWxsPSIjYmRjM2NmIi8+PC9zdmc+'
const isHome = computed(() => route.path === '/')
const isCompactHomeHeader = computed(() => homeHeaderCompact.value)
const displayAvatar = computed(() => resolveAvatarUrl(userStore.userInfo?.avatar, defaultAvatar))
const displayNickname = computed(() => userStore.userInfo?.nickname || userStore.authUser?.username || '未登录用户')
const displaySignature = computed(() => userStore.userInfo?.signature || '完善资料后展示个性签名')
const bannerLayers: BannerLayer[] = [
{ key: 'l1', src: '/home-banner/48bee957f95b7bc5f0e1b7fc14b25a4cca26901a.png', depthX: 0, scale: 1 },
{ key: 'l2', src: '/home-banner/eca4ed09885bef57072526002c8900467a07dc3f.png', depthX: 7, baseX: -9.29, scale: 1 },
{ key: 'l3', src: '/home-banner/e52d6aecd75db0e310c73185304fd927834ac716.png', depthX: 28, baseX: 92.9, scale: 1 },
{ key: 'l4', src: '/home-banner/62aee277e1d935580f66d296f349bd358cd956a0.png', depthX: 0, scale: 1 },
{ key: 'l5', src: '/home-banner/b3ee9e71fbcc0520a1ac32e2b3a2d0dc6086bb9b.png', depthX: 72, baseX: 557.4, scale: 1 },
{ key: 'l6', src: '/home-banner/85e5279a1d53d4cb46033e1619d78628415f98c4.png', depthX: 0, scale: 1 },
{ key: 'l7', src: '/home-banner/0095303b06f64e05ba354655c0a6c83fb618f8e7.png', depthX: 0, scale: 1 },
{ key: 'l8', src: '/home-banner/0415c092aa2c61c1ca25ba69b8c77130f1ab37c3.png', depthX: 0, scale: 1 },
{ key: 'l9', src: '/home-banner/752ba46c080dca88401aac470f134ac0e6444705.png', depthX: 6, baseX: -8.26, baseY: 2.06, scale: 1 },
{ key: 'l10', src: '/home-banner/1fb1501cd66d326f296e78a4561f7cc89c48d719.png', depthX: 0, scale: 1 },
{ key: 'l11', src: '/home-banner/34c3621fc6fed4e16eab30d0448b35554711bfeb.png', depthX: 0, scale: 1 },
{ key: 'l12', src: '/home-banner/0546ed600f8dcfcf4c2eb3212ed1e12178abdb86.png', depthX: 0, scale: 1 },
{ key: 'l13', src: '/home-banner/d81527e08e5031badd870819fa801f6fd41fc021.png', depthX: 0, scale: 1 },
{ key: 'l14', src: '/home-banner/fa622f4b21c7fb66f84ebf87d36faaaa993363f0.png', depthX: 8, baseX: 9.29, scale: 1 },
{ key: 'l15', src: '/home-banner/0cfe34374720653a8f67d68af761d239eca15da7.png', depthX: 0, scale: 1 },
{ key: 'l16', src: '/home-banner/9afdb972babc0fed529e241176bad847a8094774.png', depthX: 0, scale: 1 },
{ key: 'l17', src: '/home-banner/0b9b00f62fd5760dc64fa16c564591957c3ace05.png', depthX: 0, scale: 1 },
{ key: 'l18', src: '/home-banner/9316d140598d5dc7e86d72e4a99ed903953d2e76.png', depthX: 0, scale: 1 },
{ key: 'l19', src: '/home-banner/82a15b3fda5ce240bac44b8d0b793b13c3bafc4e.png', depthX: 0, scale: 1 },
{ key: 'l20', src: '/home-banner/83e19c7274daec8f0e7d97cd6ad888e3e43b8083.png', depthX: 0, scale: 1 },
{ key: 'l21', src: '/home-banner/4e2110157727360d88357840903ed05b6b94c453.png', depthX: 0, scale: 1 },
{ key: 'l22', src: '/home-banner/3505e4538c81fce91fa5e6dacd4703d8277df021.png', depthX: 10, baseX: 23.23, scale: 1 },
{ key: 'l23', src: '/home-banner/8f51a0828e322ca0576f449ceb45cca453735506.png', depthX: 0, scale: 1 },
{ key: 'l24', src: '/home-banner/3d4a9667931a9a20011a384b7d699c3b286b82ab.png', depthX: 0, scale: 1.12 },
{ key: 'l25', src: '/home-banner/3c711c23ae57db96468a23e2beca49ce66f12c73.png', depthX: 0, scale: 1 },
{ key: 'l26', src: '/home-banner/e3607ba671317e2b1e4990a9225cbf9981a7a414.png', depthX: 0, scale: 1 },
{ key: 'l27', src: '/home-banner/2a4e5f40421d467e841b24eed228910bf965d4a1.png', depthX: 48, baseX: 167.23, scale: 1 },
{ key: 'l28', src: '/home-banner/446466499bd9615bc950ec4d8ec63647defc05b6.png', depthX: 5, baseY: 10.12, scale: 1.02 },
{ key: 'l29', src: '/home-banner/f8720df769d23c497096f57b73528265fd6b554a.png', depthX: 7, baseX: -14.86, baseY: 9.91, scale: 1.02 },
{ key: 'l30', src: '/home-banner/6970b12a96d85dd28a0aeb803eb3428dfec771bb.png', depthX: 8, baseX: -14.86, baseY: 9.91, scale: 1.02 },
{ key: 'l31', src: '/home-banner/83615a69b1ecab29163a54912260fd624a886906.png', depthX: 12, baseX: 25.81, baseY: 10.32, scale: 1.02 },
{ key: 'l32', src: '/home-banner/f14b431627327dda93f34b13392a3a773be5e788.png', depthX: 0, baseY: -11.35, scale: 1.14 }
]
const syncSearchKeyword = () => {
searchKeyword.value = typeof route.query.keyword === 'string' ? route.query.keyword : ''
}
const stickyEntries = computed(() => [
{ label: '首页', active: route.path === '/', action: () => router.push('/') },
{ label: '分类', active: route.path.startsWith('/category'), action: () => router.push('/category') },
{ label: '搜索', active: route.path.startsWith('/search'), action: () => router.push('/search') },
{
label: '创作中心',
active: route.path.startsWith('/my/videos') || route.path.startsWith('/upload'),
action: () => router.push(userStore.isLoggedIn ? '/my/videos' : '/login')
}
])
const homeNavEntries = computed(() => [
{ label: '首页', icon: 'tv-o', action: () => router.push('/') },
{ label: '分类', icon: 'apps-o', action: () => router.push('/category') }
])
const getLayerStyle = (layer: BannerLayer) => {
const translateX = (layer.baseX || 0) + bannerMotionX.value * layer.depthX
const translateY = layer.baseY || 0
return {
transform: `translate3d(${translateX}px, ${translateY}px, 0) scale(${layer.scale || 1})`,
filter: `brightness(${layer.brightness || 1})`,
opacity: layer.opacity || 1
}
}
const updateHomeScroll = () => {
if (!isHome.value) {
homeScrollY.value = 0
homeHeaderCompact.value = false
return
}
homeScrollY.value = window.scrollY || 0
// Use hysteresis to avoid flashing when scroll position hovers near the threshold.
if (!homeHeaderCompact.value && homeScrollY.value > 88) {
homeHeaderCompact.value = true
} else if (homeHeaderCompact.value && homeScrollY.value < 28) {
homeHeaderCompact.value = false
}
}
const handleBannerMouseMove = (event: MouseEvent) => {
const currentTarget = event.currentTarget as HTMLElement | null
if (!currentTarget) return
const rect = currentTarget.getBoundingClientRect()
const x = (event.clientX - rect.left) / rect.width
bannerMotionX.value = (x - 0.5) * -1
}
const resetBannerMotion = () => {
bannerMotionX.value = 0
}
const handleSearch = () => {
const keyword = searchKeyword.value.trim()
if (!keyword) return
router.push({ path: '/search', query: { keyword } })
}
const goToMessages = () => {
showMessagePopover.value = false
router.push('/messages')
}
const goToNotifications = () => {
showMessagePopover.value = false
router.push('/notifications')
}
const goToProfileOrLogin = () => {
const id = userStore.userId || userStore.userInfo?.id || userStore.authUser?.id
if (!id) {
router.push('/login')
return
}
router.push(`/user/${id}`)
}
onMounted(async () => {
syncSearchKeyword()
updateHomeScroll()
window.addEventListener('scroll', updateHomeScroll, { passive: true })
if (!userStore.isLoggedIn) return
try {
if (!userStore.authUser) {
await userStore.fetchAuthUser()
}
if (!userStore.userInfo) {
await userStore.fetchUserInfo()
}
} catch {
// keep usable
}
messageStore.fetchAllUnreadCount()
})
onBeforeUnmount(() => {
window.removeEventListener('scroll', updateHomeScroll)
})
watch(() => route.fullPath, () => {
syncSearchKeyword()
updateHomeScroll()
})
</script>
<style scoped lang="scss">
.app-header {
position: fixed;
inset: 0 0 auto;
z-index: 120;
}
.home-header {
position: relative;
height: var(--home-banner-height);
&.compact {
.home-banner {
opacity: 0;
transform: translateY(-16px);
pointer-events: none;
}
.home-sticky-bar {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
}
}
.home-banner {
position: absolute;
inset: 0;
overflow: hidden;
background: linear-gradient(180deg, #6e6788 0%, #8d7296 100%);
transition: opacity 0.18s ease, transform 0.18s ease;
}
.banner-backdrop,
.animated-banner {
position: absolute;
inset: 0;
}
.banner-backdrop {
background:
linear-gradient(180deg, rgba(19, 17, 32, 0.3), rgba(19, 17, 32, 0.08) 36%, rgba(19, 17, 32, 0.24)),
radial-gradient(circle at center, rgba(255, 255, 255, 0.04), transparent 46%);
}
.animated-banner {
pointer-events: none;
}
.layer {
position: absolute;
inset: 0;
transition: transform 0.12s ease-out;
will-change: transform;
img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
max-width: none;
object-fit: cover;
user-select: none;
pointer-events: none;
}
}
.banner-shell {
position: relative;
z-index: 2;
height: 100%;
}
.banner-nav-row {
padding-top: 10px;
display: grid;
grid-template-columns: minmax(0, 1fr) 560px minmax(520px, auto);
align-items: center;
gap: 18px;
}
.banner-nav,
.banner-actions {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: nowrap;
}
.banner-link,
.banner-action {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 10px;
border-radius: 8px;
color: #fff;
font-size: 13px;
font-weight: 600;
background: transparent;
white-space: nowrap;
flex-shrink: 0;
}
.banner-search-wrap,
.sticky-search-wrap {
width: 100%;
}
.banner-search-wrap {
justify-self: center;
}
.search-bar {
width: 100%;
height: 48px;
display: grid;
grid-template-columns: 1fr 52px;
border-radius: 8px;
overflow: hidden;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.12);
}
.compact-search {
height: 40px;
background: #f1f2f3;
box-shadow: inset 0 0 0 1px #e3e5e7;
}
.search-content {
display: flex;
align-items: center;
gap: 8px;
padding: 0 16px;
}
.search-icon {
color: #9499a0;
}
.search-input {
width: 100%;
border: none;
background: transparent;
outline: none;
color: #18191c;
font-size: 16px;
}
.search-input::placeholder {
color: #c0c4cc;
}
.search-btn {
display: inline-flex;
align-items: center;
justify-content: center;
color: #18191c;
}
.banner-actions {
justify-self: end;
justify-content: flex-end;
min-width: 520px;
overflow: hidden;
}
.avatar-entry {
width: 52px;
height: 52px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.08);
}
.avatar-entry.compact {
width: 40px;
height: 40px;
background: transparent;
}
.upload-entry {
height: 40px;
padding: 0 18px;
border-radius: 10px;
background: var(--pink-color);
color: #fff;
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 700;
}
.upload-entry.compact {
height: 34px;
padding-inline: 14px;
border-radius: 8px;
font-size: 12px;
}
.home-sticky-bar {
position: absolute;
inset: 0 0 auto;
height: 64px;
opacity: 0;
transform: translateY(-8px);
pointer-events: none;
background: rgba(255, 255, 255, 0.96);
border-bottom: 1px solid #e3e5e7;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
transition: opacity 0.18s ease, transform 0.18s ease;
}
.sticky-shell {
min-height: 64px;
display: grid;
grid-template-columns: auto auto minmax(320px, 560px) auto;
align-items: center;
gap: 16px;
}
.sticky-nav,
.sticky-actions {
display: flex;
align-items: center;
gap: 4px;
}
.sticky-link,
.icon-action {
display: inline-flex;
align-items: center;
justify-content: center;
color: #18191c;
font-size: 13px;
padding: 6px 8px;
border-radius: 8px;
&.active,
&:hover {
background: #f6f7f8;
}
}
.icon-action {
width: 32px;
height: 32px;
padding: 0;
}
.sticky-brand {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
}
.brand-mark {
width: 62px;
height: 36px;
border-radius: 18px;
position: relative;
background: linear-gradient(135deg, #23ade5, #fb7299);
}
.brand-mark::before,
.brand-mark::after {
content: '';
position: absolute;
top: -8px;
width: 3px;
height: 14px;
background: #2c2c2c;
border-radius: 999px;
}
.brand-mark::before {
left: 16px;
}
.brand-mark::after {
right: 16px;
}
.brand-eye {
position: absolute;
top: 14px;
width: 8px;
height: 8px;
border-radius: 999px;
background: #fff;
}
.brand-eye.left {
left: 18px;
}
.brand-eye.right {
right: 18px;
}
.brand-copy {
display: grid;
gap: 2px;
}
.brand-copy strong {
font-size: 22px;
color: #18191c;
}
.brand-copy span {
font-size: 11px;
color: #9499a0;
}
.topbar {
height: var(--header-top-height);
color: #fff;
background:
linear-gradient(180deg, rgba(6, 16, 31, 0.7), rgba(6, 16, 31, 0.34)),
radial-gradient(circle at top center, rgba(255, 255, 255, 0.18), transparent 42%);
backdrop-filter: blur(12px);
}
.topbar-shell,
.mainbar-shell {
display: flex;
align-items: center;
justify-content: space-between;
}
.topbar-shell {
min-height: var(--header-top-height);
gap: 16px;
}
.top-nav,
.top-actions {
display: flex;
align-items: center;
gap: 4px;
}
.top-link,
.top-icon {
color: rgba(255, 255, 255, 0.92);
font-size: 13px;
line-height: 1;
padding: 10px 12px;
border-radius: 999px;
transition: background 0.2s ease, color 0.2s ease;
}
.top-link:hover,
.top-link.active,
.top-icon:hover {
background: rgba(255, 255, 255, 0.14);
color: #fff;
}
.top-icon {
width: 38px;
height: 38px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
}
.mainbar {
height: var(--header-main-height);
background: #fff;
border-bottom: 1px solid var(--border-color);
}
.mainbar-shell {
min-height: var(--header-main-height);
gap: 20px;
}
.user-area {
display: flex;
align-items: center;
justify-content: flex-end;
min-width: 220px;
}
.login-btn {
height: 38px;
padding: 0 20px;
border-radius: 8px;
color: #fff;
background: var(--pink-color);
}
.user-chip {
max-width: 260px;
display: flex;
align-items: center;
gap: 10px;
padding: 6px 10px 6px 6px;
border-radius: 999px;
background: #f6f7f8;
}
.user-copy {
min-width: 0;
text-align: left;
display: grid;
}
.user-copy strong,
.user-copy span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-copy strong {
font-size: 14px;
color: #18191c;
}
.user-copy span {
font-size: 12px;
color: #9499a0;
}
.popover-menu {
min-width: 180px;
padding: 8px;
}
.popover-item {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border-radius: 8px;
text-align: left;
}
.popover-item:hover {
background: #f6f7f8;
}
@media (max-width: 1280px) {
.banner-nav-row,
.sticky-shell {
grid-template-columns: 1fr;
}
.banner-actions,
.sticky-actions {
justify-self: stretch;
flex-wrap: wrap;
}
}
@media (max-width: 900px) {
.top-nav,
.brand-copy span,
.user-copy span,
.sticky-nav {
display: none;
}
.mainbar-shell {
gap: 12px;
}
.user-area {
min-width: auto;
}
}
@media (max-width: 768px) {
.home-header {
height: 132px;
}
.home-header.compact {
height: 56px;
}
.banner-nav,
.banner-actions {
gap: 6px;
flex-wrap: wrap;
}
.banner-link span,
.banner-action span {
display: none;
}
.topbar {
display: none;
}
.brand-copy strong {
font-size: 20px;
}
.brand-mark {
width: 50px;
height: 30px;
}
.user-chip {
padding-right: 6px;
}
.user-copy {
display: none;
}
}
</style>