feat(ui): align web shell and landing with logistics style
This commit is contained in:
474
app/components/AppHeader.vue
Normal file
474
app/components/AppHeader.vue
Normal file
@@ -0,0 +1,474 @@
|
||||
<script setup lang="ts">
|
||||
interface MenuLink {
|
||||
label: string
|
||||
to: string
|
||||
icon: 'home' | 'catalog' | 'orders' | 'profile'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
hideSearchCapsule?: boolean
|
||||
}>(), {
|
||||
hideSearchCapsule: false,
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const localePath = useLocalePath()
|
||||
const switchLocalePath = useSwitchLocalePath()
|
||||
const { locale, locales } = useI18n()
|
||||
const { signIn, signOut, loggedIn, user } = useAuth()
|
||||
|
||||
const { productLabel, hubLabel, supplierLabel, quantity, canSearch } = useCatalogSearch()
|
||||
|
||||
const isMenuOpen = ref(false)
|
||||
|
||||
const isAuthenticated = computed(() => loggedIn.value || Boolean(user.value?.id))
|
||||
const profileLabel = computed(() => {
|
||||
return user.value?.id || (locale.value === 'en' ? 'Profile' : 'Профиль')
|
||||
})
|
||||
|
||||
const isHomePage = computed(() => route.path === localePath('/'))
|
||||
const isCatalogPage = computed(() => route.path.startsWith(localePath('/catalog')))
|
||||
const isClientAreaPage = computed(() => route.path.startsWith(localePath('/clientarea')))
|
||||
const isAuthPage = computed(() => {
|
||||
return route.path === localePath('/auth')
|
||||
|| route.path === localePath('/sign-in')
|
||||
|| route.path === localePath('/callback')
|
||||
})
|
||||
|
||||
const showSearchCapsule = computed(() => {
|
||||
if (props.hideSearchCapsule || isAuthPage.value) return false
|
||||
return isHomePage.value || isCatalogPage.value
|
||||
})
|
||||
|
||||
const headerBackdropClass = computed(() => {
|
||||
if (isHomePage.value) return 'header-glass-backdrop--landing'
|
||||
return 'header-glass-backdrop--default'
|
||||
})
|
||||
|
||||
const menuLinks = computed<MenuLink[]>(() => {
|
||||
const links: MenuLink[] = [
|
||||
{ label: locale.value === 'en' ? 'Home' : 'Главная', to: '/', icon: 'home' },
|
||||
{ label: locale.value === 'en' ? 'Catalog' : 'Каталог', to: '/catalog', icon: 'catalog' },
|
||||
{ label: locale.value === 'en' ? 'Orders' : 'Заказы', to: '/clientarea/orders', icon: 'orders' },
|
||||
]
|
||||
|
||||
if (isAuthenticated.value) {
|
||||
links.push({ label: locale.value === 'en' ? 'Profile' : 'Профиль', to: '/clientarea/profile', icon: 'profile' })
|
||||
}
|
||||
|
||||
return links
|
||||
})
|
||||
|
||||
const searchProductLabel = computed(() => productLabel.value || (locale.value === 'en' ? 'Product' : 'Товар'))
|
||||
const searchDestinationLabel = computed(() => {
|
||||
return hubLabel.value || supplierLabel.value || (locale.value === 'en' ? 'Destination' : 'Точка назначения')
|
||||
})
|
||||
const searchQuantityLabel = computed(() => {
|
||||
return quantity.value ? `${quantity.value} t` : (locale.value === 'en' ? 'Volume' : 'Объем')
|
||||
})
|
||||
|
||||
function isBasePathActive(path: string, exact = false) {
|
||||
const localized = localePath(path)
|
||||
if (exact) return route.path === localized
|
||||
return route.path === localized || route.path.startsWith(`${localized}/`)
|
||||
}
|
||||
|
||||
async function openStep(step: 'product' | 'destination' | 'quantity') {
|
||||
await navigateTo(localePath(`/catalog/${step}`))
|
||||
}
|
||||
|
||||
async function submitHeaderSearch() {
|
||||
if (canSearch.value) {
|
||||
await navigateTo(localePath('/catalog/results'))
|
||||
return
|
||||
}
|
||||
await navigateTo(localePath('/catalog/product'))
|
||||
}
|
||||
|
||||
async function handleAuthAction() {
|
||||
if (isAuthenticated.value) {
|
||||
await signOut()
|
||||
return
|
||||
}
|
||||
await signIn()
|
||||
}
|
||||
|
||||
watch(() => route.fullPath, () => {
|
||||
isMenuOpen.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="header-glass fixed inset-x-0 top-0 z-50 border-0">
|
||||
<div class="header-glass-backdrop" :class="headerBackdropClass" aria-hidden="true" />
|
||||
|
||||
<div class="relative z-10 mx-auto max-w-[2200px] px-4 py-4">
|
||||
<div class="grid items-center gap-2 lg:grid-cols-[auto_1fr_auto]">
|
||||
<div class="pill-glass relative z-20 hidden h-16 items-center rounded-full px-2.5 py-2.5 lg:flex">
|
||||
<NuxtLink :to="localePath('/')" class="btn h-11 min-h-0 rounded-full border-0 bg-transparent px-4 shadow-none text-sm font-black tracking-wide">
|
||||
optovia
|
||||
</NuxtLink>
|
||||
|
||||
<div class="h-6 w-px shrink-0 bg-base-300/85" />
|
||||
|
||||
<div class="flex items-center rounded-full bg-white/55 p-1">
|
||||
<NuxtLink
|
||||
v-for="lang in locales"
|
||||
:key="lang.code"
|
||||
:to="switchLocalePath(lang.code)"
|
||||
class="rounded-full px-3 py-1 text-xs font-semibold transition-colors"
|
||||
:class="locale === lang.code ? 'bg-white text-[#2f2418]' : 'text-[#6f6250] hover:text-[#2f2418]'"
|
||||
>
|
||||
{{ lang.code.toUpperCase() }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="h-6 w-px shrink-0 bg-base-300/85" />
|
||||
|
||||
<button
|
||||
class="btn h-11 w-11 min-h-0 min-w-0 rounded-full border-0 bg-transparent p-0 shadow-none text-base-content/70"
|
||||
:aria-label="locale === 'en' ? 'Menu' : 'Меню'"
|
||||
@click="isMenuOpen = !isMenuOpen"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-5 w-5" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<path d="M4 7h16M4 12h16M4 17h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="hidden lg:block" aria-hidden="true" />
|
||||
|
||||
<div class="lg:hidden">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary h-10 min-h-0 w-full rounded-full text-sm font-semibold"
|
||||
@click="openStep('product')"
|
||||
>
|
||||
{{ locale === 'en' ? 'Start Search' : 'Начать поиск' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pill-glass relative z-20 hidden h-16 items-center rounded-full px-2.5 py-2.5 lg:flex">
|
||||
<NuxtLink :to="localePath('/catalog')" class="btn h-11 min-h-0 rounded-full border-0 bg-transparent px-4 shadow-none">
|
||||
{{ locale === 'en' ? 'Catalog' : 'Каталог' }}
|
||||
</NuxtLink>
|
||||
|
||||
<div class="h-6 w-px shrink-0 bg-base-300/85" />
|
||||
|
||||
<NuxtLink :to="localePath('/clientarea/orders')" class="btn h-11 min-h-0 rounded-full border-0 bg-transparent px-4 shadow-none">
|
||||
{{ locale === 'en' ? 'Orders' : 'Заказы' }}
|
||||
</NuxtLink>
|
||||
|
||||
<div class="h-6 w-px shrink-0 bg-base-300/85" />
|
||||
|
||||
<NuxtLink
|
||||
v-if="isAuthenticated"
|
||||
:to="localePath('/clientarea/profile')"
|
||||
class="btn h-11 min-h-0 gap-2 rounded-full border-0 bg-transparent pl-2 pr-4 shadow-none"
|
||||
>
|
||||
<UserAvatar :seed="profileLabel" :label="profileLabel" :size="34" />
|
||||
{{ profileLabel }}
|
||||
</NuxtLink>
|
||||
<button
|
||||
v-else
|
||||
class="btn h-11 min-h-0 rounded-full border-0 bg-transparent px-4 shadow-none"
|
||||
@click="handleAuthAction"
|
||||
>
|
||||
{{ locale === 'en' ? 'Log in' : 'Войти' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showSearchCapsule" class="pointer-events-none absolute inset-x-0 z-10 hidden justify-center lg:flex" style="top: 16px;">
|
||||
<div class="pill-glass h-16 min-w-0 w-full max-w-[1120px] rounded-full px-2.5 py-2.5 pointer-events-auto shadow-2xl">
|
||||
<form class="flex min-w-0 flex-wrap items-center gap-2 rounded-full" @submit.prevent="submitHeaderSearch">
|
||||
<label class="search-arch input flex h-11 min-h-0 min-w-[170px] flex-1 items-center gap-3 rounded-full shadow-none">
|
||||
<span class="shrink-0 text-base leading-none">🛒</span>
|
||||
<input
|
||||
:value="searchProductLabel"
|
||||
type="text"
|
||||
class="w-full cursor-pointer"
|
||||
:placeholder="locale === 'en' ? 'Product' : 'Товар'"
|
||||
readonly
|
||||
@focus.prevent="openStep('product')"
|
||||
@click.prevent="openStep('product')"
|
||||
>
|
||||
</label>
|
||||
|
||||
<label class="search-arch input flex h-11 min-h-0 min-w-[170px] flex-1 items-center gap-3 rounded-full shadow-none">
|
||||
<span class="shrink-0 text-base leading-none">📍</span>
|
||||
<input
|
||||
:value="searchDestinationLabel"
|
||||
type="text"
|
||||
class="w-full cursor-pointer"
|
||||
:placeholder="locale === 'en' ? 'Destination' : 'Назначение'"
|
||||
readonly
|
||||
@focus.prevent="openStep('destination')"
|
||||
@click.prevent="openStep('destination')"
|
||||
>
|
||||
</label>
|
||||
|
||||
<label class="search-arch input flex h-11 min-h-0 min-w-[190px] flex-[1.4] items-center gap-3 rounded-full shadow-none">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-4 w-4 shrink-0 text-base-content/60" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
||||
<path d="m3.3 7 8.7 5 8.7-5" />
|
||||
<path d="M12 22V12" />
|
||||
</svg>
|
||||
<input
|
||||
:value="searchQuantityLabel"
|
||||
type="text"
|
||||
class="w-full cursor-pointer"
|
||||
:placeholder="locale === 'en' ? 'Volume' : 'Объем'"
|
||||
readonly
|
||||
@focus.prevent="openStep('quantity')"
|
||||
@click.prevent="openStep('quantity')"
|
||||
>
|
||||
</label>
|
||||
|
||||
<button type="submit" class="btn btn-secondary h-11 min-h-0 rounded-full px-5">
|
||||
{{ locale === 'en' ? 'Find' : 'Найти' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav v-if="!isAuthPage" class="dock-glass fixed inset-x-0 bottom-0 z-50 flex items-center justify-around px-2 py-2 lg:hidden">
|
||||
<NuxtLink :to="localePath('/')" class="dock-item" :class="isBasePathActive('/', true) ? 'dock-item--active' : ''" aria-label="Home">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-5 w-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||
<polyline points="9 22 9 12 15 12 15 22" />
|
||||
</svg>
|
||||
<span class="dock-label">{{ locale === 'en' ? 'Home' : 'Домой' }}</span>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink :to="localePath('/catalog')" class="dock-item" :class="isBasePathActive('/catalog') ? 'dock-item--active' : ''" aria-label="Catalog">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-5 w-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="7" height="7" rx="1.5" />
|
||||
<rect x="14" y="3" width="7" height="7" rx="1.5" />
|
||||
<rect x="3" y="14" width="7" height="7" rx="1.5" />
|
||||
<rect x="14" y="14" width="7" height="7" rx="1.5" />
|
||||
</svg>
|
||||
<span class="dock-label">{{ locale === 'en' ? 'Catalog' : 'Каталог' }}</span>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink :to="localePath('/clientarea/orders')" class="dock-item" :class="isBasePathActive('/clientarea/orders') ? 'dock-item--active' : ''" aria-label="Orders">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-5 w-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 7h11v10H3z" />
|
||||
<path d="M14 10h4l3 3v4h-7z" />
|
||||
<circle cx="7.5" cy="18" r="1.5" />
|
||||
<circle cx="18.5" cy="18" r="1.5" />
|
||||
</svg>
|
||||
<span class="dock-label">{{ locale === 'en' ? 'Orders' : 'Заказы' }}</span>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink
|
||||
v-if="isAuthenticated"
|
||||
:to="localePath('/clientarea/profile')"
|
||||
class="dock-item"
|
||||
:class="isBasePathActive('/clientarea/profile') ? 'dock-item--active' : ''"
|
||||
aria-label="Profile"
|
||||
>
|
||||
<UserAvatar :seed="profileLabel" :label="profileLabel" :size="24" />
|
||||
<span class="dock-label">{{ locale === 'en' ? 'Profile' : 'Профиль' }}</span>
|
||||
</NuxtLink>
|
||||
|
||||
<button v-else class="dock-item" aria-label="Auth" @click="handleAuthAction">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-5 w-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
<span class="dock-label">{{ locale === 'en' ? 'Log in' : 'Войти' }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<Teleport to="body">
|
||||
<Transition name="menu-overlay">
|
||||
<div v-if="isMenuOpen" class="fixed inset-0 z-[100] flex flex-col overflow-y-auto bg-[#0b0b0d]">
|
||||
<div class="flex justify-end px-5 pt-5">
|
||||
<button class="flex h-12 w-12 items-center justify-center rounded-full bg-white/10 text-white transition hover:bg-white/20" @click="isMenuOpen = false">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-6 w-6" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<path d="M18 6 6 18M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="mx-auto flex w-full max-w-md flex-1 flex-col justify-center gap-0 px-6 py-8">
|
||||
<NuxtLink
|
||||
v-for="(link, i) in menuLinks"
|
||||
:key="link.to"
|
||||
:to="localePath(link.to)"
|
||||
class="menu-item group flex items-center gap-4 rounded-2xl px-5 py-5 transition-all duration-200 hover:bg-white/8 hover:pl-7"
|
||||
:class="isBasePathActive(link.to) ? 'bg-white/8' : ''"
|
||||
:style="{ animationDelay: `${i * 60}ms` }"
|
||||
@click="isMenuOpen = false"
|
||||
>
|
||||
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-amber-400 to-orange-500 shadow-lg transition-transform duration-200 group-hover:scale-110">
|
||||
<svg v-if="link.icon === 'home'" viewBox="0 0 24 24" fill="none" class="h-5 w-5 text-white" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||
<polyline points="9 22 9 12 15 12 15 22" />
|
||||
</svg>
|
||||
<svg v-else-if="link.icon === 'catalog'" viewBox="0 0 24 24" fill="none" class="h-5 w-5 text-white" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="7" height="7" rx="1.5" />
|
||||
<rect x="14" y="3" width="7" height="7" rx="1.5" />
|
||||
<rect x="3" y="14" width="7" height="7" rx="1.5" />
|
||||
<rect x="14" y="14" width="7" height="7" rx="1.5" />
|
||||
</svg>
|
||||
<svg v-else-if="link.icon === 'orders'" viewBox="0 0 24 24" fill="none" class="h-5 w-5 text-white" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 7h11v10H3z" />
|
||||
<path d="M14 10h4l3 3v4h-7z" />
|
||||
<circle cx="7.5" cy="18" r="1.5" />
|
||||
<circle cx="18.5" cy="18" r="1.5" />
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" fill="none" class="h-5 w-5 text-white" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0">
|
||||
<p class="text-xl font-semibold text-white">{{ link.label }}</p>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
|
||||
<button
|
||||
class="menu-item mt-6 flex items-center gap-3 rounded-2xl px-5 py-4 text-left text-white/80 transition hover:bg-white/8"
|
||||
@click="handleAuthAction"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-5 w-5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||
<path d="M16 17l5-5-5-5" />
|
||||
<path d="M21 12H9" />
|
||||
</svg>
|
||||
<span class="text-base font-medium">
|
||||
{{ isAuthenticated ? (locale === 'en' ? 'Log out' : 'Выйти') : (locale === 'en' ? 'Log in' : 'Войти') }}
|
||||
</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.header-glass {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.header-glass-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
height: 220%;
|
||||
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
||||
backdrop-filter: blur(16px) saturate(180%);
|
||||
-webkit-mask-image: linear-gradient(to bottom, black 0%, black 28%, rgba(0,0,0,0.35) 52%, rgba(0,0,0,0.08) 74%, transparent 100%);
|
||||
mask-image: linear-gradient(to bottom, black 0%, black 28%, rgba(0,0,0,0.35) 52%, rgba(0,0,0,0.08) 74%, transparent 100%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.header-glass-backdrop--default {
|
||||
background: linear-gradient(180deg, rgba(243, 238, 230, 0.92) 0%, rgba(243, 238, 230, 0.72) 38%, rgba(243, 238, 230, 0) 100%);
|
||||
}
|
||||
|
||||
.header-glass-backdrop--landing {
|
||||
background: linear-gradient(180deg, rgba(11, 26, 47, 0.78) 0%, rgba(11, 26, 47, 0.38) 36%, rgba(11, 26, 47, 0) 100%);
|
||||
}
|
||||
|
||||
.pill-glass {
|
||||
position: relative;
|
||||
background: rgba(243, 238, 230, 0.94);
|
||||
border: 0;
|
||||
box-shadow: 0 14px 34px rgba(62, 47, 26, 0.12);
|
||||
}
|
||||
|
||||
.pill-glass::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-arch {
|
||||
background: #ffffff;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
padding-left: 0.7rem;
|
||||
padding-right: 0.7rem;
|
||||
}
|
||||
|
||||
.search-arch span {
|
||||
color: rgba(55, 65, 81, 0.72);
|
||||
}
|
||||
|
||||
.search-arch input::placeholder {
|
||||
color: rgba(107, 114, 128, 0.62);
|
||||
}
|
||||
|
||||
.menu-overlay-enter-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.menu-overlay-enter-active .menu-item {
|
||||
animation: menu-item-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
|
||||
.menu-overlay-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.menu-overlay-leave-active .menu-item {
|
||||
animation: menu-item-out 0.2s ease both;
|
||||
}
|
||||
|
||||
.menu-overlay-enter-from,
|
||||
.menu-overlay-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes menu-item-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes menu-item-out {
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
}
|
||||
|
||||
.dock-glass {
|
||||
background: #e8ddca;
|
||||
border-top: 1px solid #d5c7b0;
|
||||
box-shadow: 0 -4px 14px rgba(60, 47, 29, 0.12);
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
|
||||
.dock-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 12px;
|
||||
color: rgba(30, 23, 14, 0.65);
|
||||
transition: color 0.15s ease;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.dock-item--active {
|
||||
color: #2f2416;
|
||||
}
|
||||
|
||||
.dock-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -35,17 +35,22 @@
|
||||
<!-- List button (LEFT, opens panel) - hide when panel is open -->
|
||||
<button
|
||||
v-if="!isPanelOpen"
|
||||
class="absolute top-[116px] left-4 z-20 hidden lg:flex items-center gap-2 map-chip rounded-full px-3 py-1.5 text-base-content text-sm hover:bg-base-200 transition-colors"
|
||||
class="absolute bottom-4 left-4 top-[108px] z-20 hidden w-14 lg:flex flex-col items-center justify-between rounded-full border border-[#d5c7b0] bg-[#f3eee6] px-2 py-3 text-[#2f2418] shadow-[0_20px_40px_rgba(38,29,18,0.18)] transition-colors hover:bg-[#eee6dc]"
|
||||
@click="openPanel"
|
||||
>
|
||||
<Icon name="lucide:menu" size="16" />
|
||||
<span>{{ $t('catalog.list') }}</span>
|
||||
<span class="flex h-10 w-10 items-center justify-center rounded-full bg-white text-[#5f4b33]">
|
||||
<Icon name="lucide:panel-left-open" size="16" />
|
||||
</span>
|
||||
<span class="-rotate-90 whitespace-nowrap text-[11px] font-bold uppercase tracking-[0.14em] text-[#6f6353]">
|
||||
{{ $t('catalog.list') }}
|
||||
</span>
|
||||
<span class="h-10 w-10" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<!-- Filter by bounds checkbox (LEFT, next to panel when open) - only in selection mode -->
|
||||
<label
|
||||
v-if="selectMode !== null"
|
||||
class="absolute top-[116px] z-20 hidden lg:flex items-center gap-2 map-chip rounded-full px-3 py-1.5 cursor-pointer text-base-content text-sm hover:bg-base-200 transition-colors"
|
||||
class="absolute top-[108px] z-20 hidden lg:flex items-center gap-2 rounded-full border border-[#d5c7b0] bg-[#f3eee6] px-3 py-1.5 cursor-pointer text-[#2f2418] text-sm shadow-[0_12px_26px_rgba(38,29,18,0.12)] transition-colors hover:bg-[#eee6dc]"
|
||||
:style="boundsFilterStyle"
|
||||
>
|
||||
<input
|
||||
@@ -61,11 +66,11 @@
|
||||
<!-- View toggle (top RIGHT overlay, below header) - hide in info mode or when hideViewToggle -->
|
||||
<div v-if="!isInfoMode && !hideViewToggle" class="absolute top-[116px] right-4 z-20 hidden lg:flex items-center gap-2">
|
||||
<!-- View mode toggle -->
|
||||
<div class="flex gap-1 map-chip rounded-full p-1">
|
||||
<div class="flex gap-1 rounded-full border border-[#d5c7b0] bg-[#f3eee6] p-1 shadow-[0_12px_26px_rgba(38,29,18,0.12)]">
|
||||
<button
|
||||
v-if="showOffersToggle"
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-bold transition-colors"
|
||||
:class="mapViewMode === 'offers' ? 'bg-base-300 text-base-content' : 'text-base-content/70 hover:text-base-content hover:bg-base-200'"
|
||||
:class="mapViewMode === 'offers' ? 'bg-[#2f2416] text-[#fff8ef]' : 'text-[#6b5b48] hover:text-[#2f2418] hover:bg-[#ece3d3]'"
|
||||
@click="setMapViewMode('offers')"
|
||||
>
|
||||
<span class="w-5 h-5 rounded-full flex items-center justify-center" style="background-color: #f97316">
|
||||
@@ -76,7 +81,7 @@
|
||||
<button
|
||||
v-if="showHubsToggle"
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-bold transition-colors"
|
||||
:class="mapViewMode === 'hubs' ? 'bg-base-300 text-base-content' : 'text-base-content/70 hover:text-base-content hover:bg-base-200'"
|
||||
:class="mapViewMode === 'hubs' ? 'bg-[#2f2416] text-[#fff8ef]' : 'text-[#6b5b48] hover:text-[#2f2418] hover:bg-[#ece3d3]'"
|
||||
@click="setMapViewMode('hubs')"
|
||||
>
|
||||
<span class="w-5 h-5 rounded-full flex items-center justify-center" style="background-color: #22c55e">
|
||||
@@ -87,7 +92,7 @@
|
||||
<button
|
||||
v-if="showSuppliersToggle"
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-bold transition-colors"
|
||||
:class="mapViewMode === 'suppliers' ? 'bg-base-300 text-base-content' : 'text-base-content/70 hover:text-base-content hover:bg-base-200'"
|
||||
:class="mapViewMode === 'suppliers' ? 'bg-[#2f2416] text-[#fff8ef]' : 'text-[#6b5b48] hover:text-[#2f2418] hover:bg-[#ece3d3]'"
|
||||
@click="setMapViewMode('suppliers')"
|
||||
>
|
||||
<span class="w-5 h-5 rounded-full flex items-center justify-center" style="background-color: #3b82f6">
|
||||
@@ -102,10 +107,10 @@
|
||||
<Transition name="slide-left">
|
||||
<div
|
||||
v-if="isPanelOpen"
|
||||
class="absolute top-[116px] left-0 bottom-0 z-30 max-w-[calc(100vw-1rem)] hidden lg:block"
|
||||
class="absolute top-[108px] left-4 bottom-4 z-30 max-w-[calc(100vw-1rem)] hidden lg:block"
|
||||
:class="panelWidth"
|
||||
>
|
||||
<div class="bg-base-100 text-base-content border border-base-300 border-l-0 rounded-none rounded-tr-[1.1rem] rounded-bl-[1.1rem] shadow-2xl h-full flex flex-col">
|
||||
<div class="h-full flex flex-col overflow-hidden rounded-[28px] border-0 bg-[#f3eee6] text-[#2f2418] shadow-[0_28px_70px_rgba(38,29,18,0.18)]">
|
||||
<slot name="panel" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -117,7 +122,7 @@
|
||||
<div class="flex justify-between px-4 mb-2">
|
||||
<!-- List button (mobile) -->
|
||||
<button
|
||||
class="flex items-center gap-2 map-chip rounded-full px-3 py-2 text-base-content text-sm font-medium"
|
||||
class="flex items-center gap-2 rounded-full border border-[#d5c7b0] bg-[#f3eee6] px-3 py-2 text-[#2f2418] text-sm font-medium shadow-[0_10px_24px_rgba(38,29,18,0.12)]"
|
||||
@click="openPanel"
|
||||
>
|
||||
<Icon name="lucide:menu" size="16" />
|
||||
@@ -125,11 +130,11 @@
|
||||
</button>
|
||||
|
||||
<!-- Mobile view toggle - hide in info mode or when hideViewToggle -->
|
||||
<div v-if="!isInfoMode && !hideViewToggle" class="flex gap-1 map-chip rounded-full p-1">
|
||||
<div v-if="!isInfoMode && !hideViewToggle" class="flex gap-1 rounded-full border border-[#d5c7b0] bg-[#f3eee6] p-1 shadow-[0_10px_24px_rgba(38,29,18,0.12)]">
|
||||
<button
|
||||
v-if="showOffersToggle"
|
||||
class="flex items-center justify-center w-8 h-8 rounded-full transition-colors"
|
||||
:class="mapViewMode === 'offers' ? 'bg-base-300' : 'hover:bg-base-200'"
|
||||
:class="mapViewMode === 'offers' ? 'bg-[#2f2416]' : 'hover:bg-[#ece3d3]'"
|
||||
@click="setMapViewMode('offers')"
|
||||
>
|
||||
<span class="w-5 h-5 rounded-full flex items-center justify-center" style="background-color: #f97316">
|
||||
@@ -139,7 +144,7 @@
|
||||
<button
|
||||
v-if="showHubsToggle"
|
||||
class="flex items-center justify-center w-8 h-8 rounded-full transition-colors"
|
||||
:class="mapViewMode === 'hubs' ? 'bg-base-300' : 'hover:bg-base-200'"
|
||||
:class="mapViewMode === 'hubs' ? 'bg-[#2f2416]' : 'hover:bg-[#ece3d3]'"
|
||||
@click="setMapViewMode('hubs')"
|
||||
>
|
||||
<span class="w-5 h-5 rounded-full flex items-center justify-center" style="background-color: #22c55e">
|
||||
@@ -149,7 +154,7 @@
|
||||
<button
|
||||
v-if="showSuppliersToggle"
|
||||
class="flex items-center justify-center w-8 h-8 rounded-full transition-colors"
|
||||
:class="mapViewMode === 'suppliers' ? 'bg-base-300' : 'hover:bg-base-200'"
|
||||
:class="mapViewMode === 'suppliers' ? 'bg-[#2f2416]' : 'hover:bg-[#ece3d3]'"
|
||||
@click="setMapViewMode('suppliers')"
|
||||
>
|
||||
<span class="w-5 h-5 rounded-full flex items-center justify-center" style="background-color: #3b82f6">
|
||||
@@ -163,7 +168,7 @@
|
||||
<Transition name="slide-up">
|
||||
<div
|
||||
v-if="isPanelOpen"
|
||||
class="bg-base-100 text-base-content rounded-t-3xl border-t border-base-300 shadow-[0_-8px_40px_rgba(0,0,0,0.12)] transition-all duration-300 h-[60vh]"
|
||||
class="rounded-t-3xl border-t border-[#d5c7b0] bg-[#f3eee6] text-[#2f2418] shadow-[0_-8px_40px_rgba(0,0,0,0.12)] transition-all duration-300 h-[60vh]"
|
||||
>
|
||||
<!-- Drag handle / close -->
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user