Refine glass UI capsules and hub card
All checks were successful
Build Docker Image / build (push) Successful in 4m43s

This commit is contained in:
Ruslan Bakiev
2026-02-06 16:28:00 +07:00
parent 981500ec5d
commit eb31b8299b
4 changed files with 143 additions and 116 deletions

View File

@@ -36,6 +36,25 @@
--noise: 0; --noise: 0;
} }
@layer components {
.glass-topfade {
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.4) 0%,
rgba(255, 255, 255, 0.18) 45%,
rgba(255, 255, 255, 0) 100%
);
}
.glass-soft {
@apply bg-white/10 border border-white/10 backdrop-blur-md;
}
.glass-bright {
@apply bg-white/30 border border-white/20 backdrop-blur-md;
}
}
@plugin "daisyui/theme" { @plugin "daisyui/theme" {
name: "silk"; name: "silk";
default: false; default: false;

View File

@@ -19,14 +19,14 @@
<!-- Title + distance/compass --> <!-- Title + distance/compass -->
<div class="flex items-start justify-between gap-2"> <div class="flex items-start justify-between gap-2">
<Text size="base" weight="semibold" class="truncate">{{ hub.name }}</Text> <Text size="base" weight="semibold" class="truncate">{{ hub.name }}</Text>
<div class="flex items-center gap-2 text-xs text-white/50 whitespace-nowrap"> <div class="flex items-center gap-2 text-xs text-base-content/60 whitespace-nowrap">
<Text v-if="distanceLabel" size="xs" class="text-white/50">{{ distanceLabel }}</Text> <Text v-if="distanceLabel" size="xs" class="text-base-content/60">{{ distanceLabel }}</Text>
<div v-if="bearing !== null" class="flex items-center gap-1"> <div v-if="bearing !== null" class="flex items-center gap-1">
<div class="w-6 h-6 rounded-full border border-white/10 bg-white/5 flex items-center justify-center"> <div class="w-6 h-6 rounded-full border border-base-content/10 bg-base-200/40 flex items-center justify-center">
<Icon <Icon
name="lucide:arrow-up" name="lucide:arrow-up"
size="12" size="12"
class="text-white/50" class="text-base-content/60"
:style="{ transform: `rotate(${bearing}deg)` }" :style="{ transform: `rotate(${bearing}deg)` }"
/> />
</div> </div>

View File

@@ -4,77 +4,81 @@
:class="headerClasses" :class="headerClasses"
:style="{ height: `${height}px` }" :style="{ height: `${height}px` }"
> >
<div class="absolute inset-0 pointer-events-none bg-gradient-to-b from-white/45 via-white/20 to-transparent" /> <div class="absolute inset-0 pointer-events-none glass-topfade" />
<!-- Single row: Logo + Search + Icons --> <!-- Single row: Logo + Search + Icons -->
<div class="relative z-10 flex items-center h-full px-4 lg:px-6 gap-4"> <div class="relative z-10 flex items-center h-full px-4 lg:px-6 gap-4">
<!-- Left: Logo + Nav links (top aligned) --> <!-- Left: Logo + Nav links (top aligned) -->
<div class="flex items-center gap-6 flex-shrink-0 px-5 py-3 rounded-full border border-white/15 bg-white/12 backdrop-blur-md"> <div class="flex items-center flex-shrink-0 rounded-full glass-bright divide-x divide-white/20">
<NuxtLink :to="localePath('/')" class="flex items-center gap-2"> <div class="flex items-center gap-2 px-4 py-2">
<span class="font-bold text-xl" :class="useWhiteText ? 'text-white' : 'text-base-content'">Optovia</span> <NuxtLink :to="localePath('/')" class="flex items-center gap-2">
</NuxtLink> <span class="font-bold text-xl" :class="useWhiteText ? 'text-white' : 'text-base-content'">Optovia</span>
</NuxtLink>
</div>
<!-- Service nav links --> <!-- Service nav links -->
<nav v-if="showModeToggle" class="flex items-center gap-1"> <div v-if="showModeToggle" class="flex items-center px-3 py-2">
<button <nav class="flex items-center gap-1">
class="px-3 py-1 text-sm font-medium rounded-lg transition-colors" <button
:class="showActiveMode && catalogMode === 'explore' && !isClientArea
? (useWhiteText ? 'bg-white/20 text-white' : 'bg-base-300 text-base-content')
: (useWhiteText ? 'text-white/70 hover:text-white hover:bg-white/10' : 'text-base-content/70 hover:text-base-content hover:bg-base-200')"
@click="$emit('set-catalog-mode', 'explore')"
>
{{ $t('catalog.modes.explore') }}
</button>
<button
class="px-3 py-1 text-sm font-medium rounded-lg transition-colors"
:class="showActiveMode && catalogMode === 'quote' && !isClientArea
? (useWhiteText ? 'bg-white/20 text-white' : 'bg-base-300 text-base-content')
: (useWhiteText ? 'text-white/70 hover:text-white hover:bg-white/10' : 'text-base-content/70 hover:text-base-content hover:bg-base-200')"
@click="$emit('set-catalog-mode', 'quote')"
>
{{ $t('catalog.modes.quote') }}
</button>
<!-- Role switcher: Я клиент + dropdown -->
<div v-if="loggedIn" class="flex items-center">
<NuxtLink
:to="localePath(currentRole === 'SELLER' ? '/clientarea/offers' : '/clientarea/orders')"
class="px-3 py-1 text-sm font-medium rounded-lg transition-colors" class="px-3 py-1 text-sm font-medium rounded-lg transition-colors"
:class="isClientArea :class="showActiveMode && catalogMode === 'explore' && !isClientArea
? (useWhiteText ? 'bg-white/20 text-white' : 'bg-base-300 text-base-content') ? (useWhiteText ? 'bg-white/20 text-white' : 'bg-base-300 text-base-content')
: (useWhiteText ? 'text-white/70 hover:text-white hover:bg-white/10' : 'text-base-content/70 hover:text-base-content hover:bg-base-200')" : (useWhiteText ? 'text-white/70 hover:text-white hover:bg-white/10' : 'text-base-content/70 hover:text-base-content hover:bg-base-200')"
@click="$emit('set-catalog-mode', 'explore')"
> >
{{ currentRole === 'SELLER' ? $t('cabinetNav.roles.seller') : $t('cabinetNav.roles.client') }} {{ $t('catalog.modes.explore') }}
</NuxtLink> </button>
<button
<!-- Dropdown для переключения роли (если есть обе роли) --> class="px-3 py-1 text-sm font-medium rounded-lg transition-colors"
<div v-if="hasMultipleRoles" class="dropdown dropdown-end"> :class="showActiveMode && catalogMode === 'quote' && !isClientArea
<button ? (useWhiteText ? 'bg-white/20 text-white' : 'bg-base-300 text-base-content')
tabindex="0" : (useWhiteText ? 'text-white/70 hover:text-white hover:bg-white/10' : 'text-base-content/70 hover:text-base-content hover:bg-base-200')"
class="p-1 ml-0.5 transition-colors" @click="$emit('set-catalog-mode', 'quote')"
:class="useWhiteText ? 'text-white/50 hover:text-white' : 'text-base-content/50 hover:text-base-content'" >
{{ $t('catalog.modes.quote') }}
</button>
<!-- Role switcher: Я клиент + dropdown -->
<div v-if="loggedIn" class="flex items-center">
<NuxtLink
:to="localePath(currentRole === 'SELLER' ? '/clientarea/offers' : '/clientarea/orders')"
class="px-3 py-1 text-sm font-medium rounded-lg transition-colors"
:class="isClientArea
? (useWhiteText ? 'bg-white/20 text-white' : 'bg-base-300 text-base-content')
: (useWhiteText ? 'text-white/70 hover:text-white hover:bg-white/10' : 'text-base-content/70 hover:text-base-content hover:bg-base-200')"
> >
<Icon name="lucide:chevron-down" size="14" /> {{ currentRole === 'SELLER' ? $t('cabinetNav.roles.seller') : $t('cabinetNav.roles.client') }}
</button> </NuxtLink>
<ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-50 w-48 p-2 shadow-lg border border-base-300">
<li> <!-- Dropdown для переключения роли (если есть обе роли) -->
<a <div v-if="hasMultipleRoles" class="dropdown dropdown-end">
:class="{ active: currentRole === 'BUYER' }" <button
@click="$emit('switch-role', 'BUYER')" tabindex="0"
> class="p-1 ml-0.5 transition-colors"
{{ $t('cabinetNav.roles.client') }} :class="useWhiteText ? 'text-white/50 hover:text-white' : 'text-base-content/50 hover:text-base-content'"
</a> >
</li> <Icon name="lucide:chevron-down" size="14" />
<li> </button>
<a <ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-50 w-48 p-2 shadow-lg border border-base-300">
:class="{ active: currentRole === 'SELLER' }" <li>
@click="$emit('switch-role', 'SELLER')" <a
> :class="{ active: currentRole === 'BUYER' }"
{{ $t('cabinetNav.roles.seller') }} @click="$emit('switch-role', 'BUYER')"
</a> >
</li> {{ $t('cabinetNav.roles.client') }}
</ul> </a>
</li>
<li>
<a
:class="{ active: currentRole === 'SELLER' }"
@click="$emit('switch-role', 'SELLER')"
>
{{ $t('cabinetNav.roles.seller') }}
</a>
</li>
</ul>
</div>
</div> </div>
</div> </nav>
</nav> </div>
</div> </div>
<!-- Center: Search input OR Client Area tabs (vertically centered) --> <!-- Center: Search input OR Client Area tabs (vertically centered) -->
@@ -84,7 +88,7 @@
<!-- Client Area tabs --> <!-- Client Area tabs -->
<template v-if="isClientArea"> <template v-if="isClientArea">
<div class="flex items-center gap-1 rounded-full border border-white/35 bg-white/70 backdrop-blur-md p-1"> <div class="flex items-center gap-1 rounded-full glass-bright p-1">
<!-- BUYER tabs --> <!-- BUYER tabs -->
<template v-if="currentRole !== 'SELLER'"> <template v-if="currentRole !== 'SELLER'">
<NuxtLink <NuxtLink
@@ -118,7 +122,7 @@
<!-- Quote mode: Simple segmented input with search inside (white glass) --> <!-- Quote mode: Simple segmented input with search inside (white glass) -->
<template v-else-if="catalogMode === 'quote'"> <template v-else-if="catalogMode === 'quote'">
<div class="flex items-center w-full rounded-full border border-white/45 bg-white/55 backdrop-blur-md divide-x divide-base-300/30"> <div class="flex items-center w-full rounded-full glass-bright divide-x divide-white/20 overflow-hidden">
<!-- Product segment --> <!-- Product segment -->
<button <button
class="flex-1 px-4 py-2 text-left hover:bg-base-200/50 rounded-l-full transition-colors min-w-0" class="flex-1 px-4 py-2 text-left hover:bg-base-200/50 rounded-l-full transition-colors min-w-0"
@@ -167,7 +171,7 @@
<template v-else> <template v-else>
<!-- Big pill input --> <!-- Big pill input -->
<div <div
class="flex items-center gap-3 w-full px-5 py-3 rounded-full border border-white/45 bg-white/55 backdrop-blur-md focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20 transition-all cursor-text" class="flex items-center gap-3 w-full px-5 py-3 rounded-full glass-bright focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20 transition-all cursor-text"
@click="focusInput" @click="focusInput"
> >
<Icon name="lucide:search" size="22" class="text-primary flex-shrink-0" /> <Icon name="lucide:search" size="22" class="text-primary flex-shrink-0" />
@@ -212,51 +216,55 @@
</div> </div>
<!-- Right: AI + Globe + Team + User (top aligned like logo) --> <!-- Right: AI + Globe + Team + User (top aligned like logo) -->
<div class="flex items-center gap-1 flex-shrink-0 px-4 py-2 rounded-full border border-white/15 bg-white/12 backdrop-blur-md"> <div class="flex items-center flex-shrink-0 rounded-full glass-bright divide-x divide-white/20">
<!-- AI Assistant button --> <div class="flex items-center px-2 py-2">
<NuxtLink <!-- AI Assistant button -->
:to="localePath('/clientarea/ai')" <NuxtLink
class="w-8 h-8 rounded-full flex items-center justify-center transition-colors" :to="localePath('/clientarea/ai')"
:class="useWhiteText ? 'text-white/70 hover:text-white hover:bg-white/10' : 'text-base-content/70 hover:text-base-content hover:bg-base-200'"
>
<Icon name="lucide:bot" size="18" />
</NuxtLink>
<!-- Globe (language/currency) dropdown -->
<div class="dropdown dropdown-end">
<button
tabindex="0"
class="w-8 h-8 rounded-full flex items-center justify-center transition-colors" class="w-8 h-8 rounded-full flex items-center justify-center transition-colors"
:class="useWhiteText ? 'text-white/70 hover:text-white hover:bg-white/10' : 'text-base-content/70 hover:text-base-content hover:bg-base-200'" :class="useWhiteText ? 'text-white/70 hover:text-white hover:bg-white/10' : 'text-base-content/70 hover:text-base-content hover:bg-base-200'"
> >
<Icon name="lucide:globe" size="18" /> <Icon name="lucide:bot" size="18" />
</button> </NuxtLink>
<div tabindex="0" class="dropdown-content bg-base-100 rounded-box z-50 w-52 p-4 shadow-lg border border-base-300"> </div>
<div class="font-semibold mb-2">{{ $t('common.language') }}</div>
<div class="flex gap-2 mb-4"> <div class="flex items-center px-2 py-2">
<NuxtLink <!-- Globe (language/currency) dropdown -->
v-for="loc in locales" <div class="dropdown dropdown-end">
:key="loc.code"
:to="switchLocalePath(loc.code)"
class="btn btn-sm"
:class="locale === loc.code ? 'btn-primary' : 'btn-ghost'"
>
{{ loc.code.toUpperCase() }}
</NuxtLink>
</div>
<div class="font-semibold mb-2">{{ $t('common.theme') }}</div>
<button <button
class="btn btn-sm btn-ghost w-full justify-start" tabindex="0"
@click="$emit('toggle-theme')" class="w-8 h-8 rounded-full flex items-center justify-center transition-colors"
:class="useWhiteText ? 'text-white/70 hover:text-white hover:bg-white/10' : 'text-base-content/70 hover:text-base-content hover:bg-base-200'"
> >
<Icon :name="theme === 'night' ? 'lucide:sun' : 'lucide:moon'" size="16" /> <Icon name="lucide:globe" size="18" />
{{ theme === 'night' ? $t('common.theme_light') : $t('common.theme_dark') }}
</button> </button>
<div tabindex="0" class="dropdown-content bg-base-100 rounded-box z-50 w-52 p-4 shadow-lg border border-base-300">
<div class="font-semibold mb-2">{{ $t('common.language') }}</div>
<div class="flex gap-2 mb-4">
<NuxtLink
v-for="loc in locales"
:key="loc.code"
:to="switchLocalePath(loc.code)"
class="btn btn-sm"
:class="locale === loc.code ? 'btn-primary' : 'btn-ghost'"
>
{{ loc.code.toUpperCase() }}
</NuxtLink>
</div>
<div class="font-semibold mb-2">{{ $t('common.theme') }}</div>
<button
class="btn btn-sm btn-ghost w-full justify-start"
@click="$emit('toggle-theme')"
>
<Icon :name="theme === 'night' ? 'lucide:sun' : 'lucide:moon'" size="16" />
{{ theme === 'night' ? $t('common.theme_light') : $t('common.theme_dark') }}
</button>
</div>
</div> </div>
</div> </div>
<!-- Team dropdown --> <!-- Team dropdown -->
<template v-if="loggedIn && userData?.teams?.length"> <div v-if="loggedIn && userData?.teams?.length" class="flex items-center px-2 py-2">
<div class="dropdown dropdown-end"> <div class="dropdown dropdown-end">
<button <button
tabindex="0" tabindex="0"
@@ -288,10 +296,10 @@
</li> </li>
</ul> </ul>
</div> </div>
</template> </div>
<!-- User menu --> <!-- User menu -->
<template v-if="sessionChecked"> <div v-if="sessionChecked" class="flex items-center px-2 py-2">
<template v-if="loggedIn"> <template v-if="loggedIn">
<div class="dropdown dropdown-end"> <div class="dropdown dropdown-end">
<div <div
@@ -341,7 +349,7 @@
{{ $t('auth.login') }} {{ $t('auth.login') }}
</button> </button>
</template> </template>
</template> </div>
</div> </div>
</div> </div>
@@ -482,12 +490,12 @@ const getTokenIcon = (type: string) => {
// Header background classes // Header background classes
const headerClasses = computed(() => { const headerClasses = computed(() => {
if (props.isHomePage) { if (props.isHomePage) {
return 'bg-transparent border-b border-white/10 backdrop-blur-lg' return 'bg-transparent backdrop-blur-xl'
} }
if (props.isCollapsed) { if (props.isCollapsed) {
return 'bg-transparent border-b border-white/10 backdrop-blur-lg' return 'bg-transparent backdrop-blur-xl'
} }
return 'bg-transparent border-b border-white/10 backdrop-blur-lg' return 'bg-transparent backdrop-blur-xl'
}) })
// Use white text on dark backgrounds (collapsed or home page with animation) // Use white text on dark backgrounds (collapsed or home page with animation)

View File

@@ -35,7 +35,7 @@
<!-- View mode loading indicator --> <!-- View mode loading indicator -->
<div <div
v-if="clusterLoading" v-if="clusterLoading"
class="absolute top-[116px] left-1/2 -translate-x-1/2 z-30 flex items-center gap-2 bg-black/50 backdrop-blur-md rounded-full px-4 py-2 border border-white/20" class="absolute top-[116px] left-1/2 -translate-x-1/2 z-30 flex items-center gap-2 glass-soft rounded-full px-4 py-2"
> >
<span class="loading loading-spinner loading-sm text-white" /> <span class="loading loading-spinner loading-sm text-white" />
<span class="text-white text-sm">{{ $t('common.loading') }}</span> <span class="text-white text-sm">{{ $t('common.loading') }}</span>
@@ -44,7 +44,7 @@
<!-- List button (LEFT, opens panel) - hide when panel is open --> <!-- List button (LEFT, opens panel) - hide when panel is open -->
<button <button
v-if="!isPanelOpen" v-if="!isPanelOpen"
class="absolute top-[116px] left-4 z-20 hidden lg:flex items-center gap-2 bg-black/30 backdrop-blur-md rounded-lg px-3 py-1.5 border border-white/10 text-white text-sm hover:bg-black/40 transition-colors" class="absolute top-[116px] left-4 z-20 hidden lg:flex items-center gap-2 glass-soft rounded-full px-3 py-1.5 text-white text-sm hover:bg-white/15 transition-colors"
@click="openPanel" @click="openPanel"
> >
<Icon name="lucide:menu" size="16" /> <Icon name="lucide:menu" size="16" />
@@ -54,7 +54,7 @@
<!-- Filter by bounds checkbox (LEFT, next to panel when open) - only in selection mode --> <!-- Filter by bounds checkbox (LEFT, next to panel when open) - only in selection mode -->
<label <label
v-if="selectMode !== null" v-if="selectMode !== null"
class="absolute top-[116px] left-[420px] z-20 hidden lg:flex items-center gap-2 bg-black/30 backdrop-blur-md rounded-lg px-3 py-1.5 border border-white/10 cursor-pointer text-white text-sm hover:bg-black/40 transition-colors" class="absolute top-[116px] left-[420px] z-20 hidden lg:flex items-center gap-2 glass-soft rounded-full px-3 py-1.5 cursor-pointer text-white text-sm hover:bg-white/15 transition-colors"
> >
<input <input
type="checkbox" type="checkbox"
@@ -69,7 +69,7 @@
<!-- View toggle (top RIGHT overlay, below header) - hide in info mode or when hideViewToggle --> <!-- 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"> <div v-if="!isInfoMode && !hideViewToggle" class="absolute top-[116px] right-4 z-20 hidden lg:flex items-center gap-2">
<!-- View mode toggle --> <!-- View mode toggle -->
<div class="flex gap-1 bg-black/30 backdrop-blur-md rounded-lg p-1 border border-white/10"> <div class="flex gap-1 glass-bright rounded-full p-1">
<button <button
class="flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors" class="flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors"
:class="mapViewMode === 'offers' ? 'bg-white/20 text-white' : 'text-white/70 hover:text-white hover:bg-white/10'" :class="mapViewMode === 'offers' ? 'bg-white/20 text-white' : 'text-white/70 hover:text-white hover:bg-white/10'"
@@ -110,7 +110,7 @@
class="absolute top-[116px] left-4 bottom-4 z-30 max-w-[calc(100vw-2rem)] hidden lg:block" class="absolute top-[116px] left-4 bottom-4 z-30 max-w-[calc(100vw-2rem)] hidden lg:block"
:class="panelWidth" :class="panelWidth"
> >
<div class="bg-black/50 backdrop-blur-md rounded-xl shadow-lg border border-white/10 h-full flex flex-col text-white"> <div class="glass-soft rounded-2xl shadow-lg h-full flex flex-col text-white">
<slot name="panel" /> <slot name="panel" />
</div> </div>
</div> </div>
@@ -122,7 +122,7 @@
<div class="flex justify-between px-4 mb-2"> <div class="flex justify-between px-4 mb-2">
<!-- List button (mobile) --> <!-- List button (mobile) -->
<button <button
class="flex items-center gap-2 bg-black/30 backdrop-blur-md rounded-lg px-3 py-2 border border-white/10 text-white text-sm" class="flex items-center gap-2 glass-soft rounded-full px-3 py-2 text-white text-sm"
@click="openPanel" @click="openPanel"
> >
<Icon name="lucide:menu" size="16" /> <Icon name="lucide:menu" size="16" />
@@ -130,7 +130,7 @@
</button> </button>
<!-- Mobile view toggle - hide in info mode or when hideViewToggle --> <!-- Mobile view toggle - hide in info mode or when hideViewToggle -->
<div v-if="!isInfoMode && !hideViewToggle" class="flex gap-1 bg-black/30 backdrop-blur-md rounded-lg p-1 border border-white/10"> <div v-if="!isInfoMode && !hideViewToggle" class="flex gap-1 glass-bright rounded-full p-1">
<button <button
class="flex items-center justify-center w-8 h-8 rounded-md transition-colors" class="flex items-center justify-center w-8 h-8 rounded-md transition-colors"
:class="mapViewMode === 'offers' ? 'bg-white/20' : 'hover:bg-white/10'" :class="mapViewMode === 'offers' ? 'bg-white/20' : 'hover:bg-white/10'"
@@ -165,7 +165,7 @@
<Transition name="slide-up"> <Transition name="slide-up">
<div <div
v-if="isPanelOpen" v-if="isPanelOpen"
class="bg-black/50 backdrop-blur-md rounded-t-xl shadow-lg border border-white/10 transition-all duration-300 text-white h-[60vh]" class="glass-soft rounded-t-2xl shadow-lg transition-all duration-300 text-white h-[60vh]"
> >
<!-- Drag handle / close --> <!-- Drag handle / close -->
<div <div