feat: adopt Apple-style glassmorphism UI from logistics project
All checks were successful
Build Docker Image / build (push) Successful in 5m41s
All checks were successful
Build Docker Image / build (push) Successful in 5m41s
Three-tier glass system (glass-underlay, glass-capsule, glass-chip), pill-glass capsules with inner shine for header nav pills, two-layer header backdrop with fade mask, solid white left panel and juicy rounded-t-3xl bottom sheet for map interactions, bold/black headings throughout.
This commit is contained in:
@@ -37,24 +37,103 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.glass-topfade {
|
/* ── Three-tier glass system (Apple-style glassmorphism) ── */
|
||||||
background: linear-gradient(
|
|
||||||
180deg,
|
/* Tier 1 — lightest underlay, large panels / sidebars */
|
||||||
rgba(255, 255, 255, 0.4) 0%,
|
.glass-underlay {
|
||||||
rgba(255, 255, 255, 0.18) 45%,
|
background: rgba(255, 255, 255, 0.34);
|
||||||
rgba(255, 255, 255, 0) 100%
|
box-shadow:
|
||||||
);
|
0 16px 44px rgba(24, 20, 12, 0.11),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.4);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tier 2 — medium capsule, nav pills / search bar */
|
||||||
|
.glass-capsule {
|
||||||
|
background: rgba(255, 255, 255, 0.56);
|
||||||
|
box-shadow:
|
||||||
|
0 8px 24px rgba(24, 20, 12, 0.1),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.56);
|
||||||
|
backdrop-filter: blur(22px);
|
||||||
|
-webkit-backdrop-filter: blur(22px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tier 3 — densest chip, small tags / badges */
|
||||||
|
.glass-chip {
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.62);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Legacy aliases — keep backward compat during transition */
|
||||||
.glass-soft {
|
.glass-soft {
|
||||||
@apply bg-white/10 border border-white/10 backdrop-blur-md;
|
background: rgba(255, 255, 255, 0.34);
|
||||||
|
box-shadow:
|
||||||
|
0 16px 44px rgba(24, 20, 12, 0.11),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.4);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-bright {
|
.glass-bright {
|
||||||
@apply bg-white/30 border border-white/20 backdrop-blur-md;
|
background: rgba(255, 255, 255, 0.56);
|
||||||
|
box-shadow:
|
||||||
|
0 8px 24px rgba(24, 20, 12, 0.1),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.56);
|
||||||
|
backdrop-filter: blur(22px);
|
||||||
|
-webkit-backdrop-filter: blur(22px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Header glass: two-layer Apple-style glassmorphism ── */
|
||||||
|
|
||||||
|
.header-glass {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layer 1: frosted bar backdrop — fades to transparent at bottom */
|
||||||
|
.header-glass-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
height: 350%;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
||||||
|
backdrop-filter: blur(16px) saturate(180%);
|
||||||
|
-webkit-mask-image: linear-gradient(to bottom, black 0%, black 20%, rgba(0,0,0,0.4) 40%, rgba(0,0,0,0.1) 65%, transparent 100%);
|
||||||
|
mask-image: linear-gradient(to bottom, black 0%, black 20%, rgba(0,0,0,0.4) 40%, rgba(0,0,0,0.1) 65%, transparent 100%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layer 2: capsule pills — denser frosted glass with inner shine */
|
||||||
|
.pill-glass {
|
||||||
|
position: relative;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||||
|
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||||
|
backdrop-filter: blur(20px) saturate(180%);
|
||||||
|
box-shadow:
|
||||||
|
0 8px 32px rgba(31, 38, 135, 0.2),
|
||||||
|
inset 0 4px 20px rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inner shine highlight — liquid glass refraction */
|
||||||
|
.pill-glass::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow:
|
||||||
|
inset -10px -8px 0 -11px rgba(255, 255, 255, 1),
|
||||||
|
inset 0 -9px 0 -8px rgba(255, 255, 255, 1);
|
||||||
|
opacity: 0.6;
|
||||||
|
filter: blur(1px) brightness(115%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
@plugin "daisyui/theme" {
|
@plugin "daisyui/theme" {
|
||||||
name: "silk";
|
name: "silk";
|
||||||
default: false;
|
default: false;
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<header
|
<header
|
||||||
class="relative overflow-hidden"
|
class="relative overflow-hidden"
|
||||||
:class="headerClasses"
|
|
||||||
:style="{ height: `${height}px` }"
|
:style="{ height: `${height}px` }"
|
||||||
>
|
>
|
||||||
<div class="absolute top-0 left-0 right-0 pointer-events-none glass-topfade" :style="glassStyle" />
|
|
||||||
<!-- Single row: Logo + Search + Icons -->
|
<!-- Single row: Logo + Search + Icons -->
|
||||||
<div
|
<div
|
||||||
class="relative z-10 flex px-4 lg:px-6 gap-4"
|
class="relative z-10 flex px-3 lg:px-4 gap-2"
|
||||||
:class="isHeroLayout ? 'items-start pt-4' : 'items-center'"
|
:class="isHeroLayout ? 'items-start pt-2' : 'items-center'"
|
||||||
:style="rowStyle"
|
:style="rowStyle"
|
||||||
>
|
>
|
||||||
<!-- Left: Logo + AI button + Nav links (top aligned) -->
|
<!-- Left: Logo + AI button + Nav links (top aligned) -->
|
||||||
<div class="flex items-center flex-shrink-0 rounded-full glass-bright">
|
<div class="flex items-center flex-shrink-0 rounded-full pill-glass">
|
||||||
<div class="flex items-center gap-2 px-4 py-2">
|
<div class="flex items-center gap-2 px-4 py-2">
|
||||||
<NuxtLink :to="localePath('/')" class="flex items-center gap-2">
|
<NuxtLink :to="localePath('/')" class="flex items-center gap-2">
|
||||||
<span class="font-bold text-xl" :class="useWhiteText ? 'text-white' : 'text-base-content'">Optovia</span>
|
<span class="font-black text-xl tracking-tight" :class="useWhiteText ? 'text-white' : 'text-base-content'">Optovia</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<button
|
<button
|
||||||
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"
|
||||||
@@ -109,7 +107,7 @@
|
|||||||
|
|
||||||
<!-- Client Area tabs -->
|
<!-- Client Area tabs -->
|
||||||
<template v-if="isClientArea">
|
<template v-if="isClientArea">
|
||||||
<div class="flex items-center gap-1 rounded-full glass-bright p-1">
|
<div class="flex items-center gap-1 rounded-full pill-glass p-1">
|
||||||
<!-- BUYER tabs -->
|
<!-- BUYER tabs -->
|
||||||
<template v-if="currentRole !== 'SELLER'">
|
<template v-if="currentRole !== 'SELLER'">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
@@ -143,7 +141,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 glass-bright overflow-hidden">
|
<div class="flex items-center w-full rounded-full pill-glass 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"
|
||||||
@@ -194,7 +192,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 glass-bright 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 pill-glass 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" />
|
||||||
@@ -239,7 +237,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right: Globe + Team + User (top aligned like logo) -->
|
<!-- Right: Globe + Team + User (top aligned like logo) -->
|
||||||
<div class="flex items-center flex-shrink-0 rounded-full glass-bright">
|
<div class="flex items-center flex-shrink-0 rounded-full pill-glass">
|
||||||
<div class="w-px h-6 bg-white/20 self-center" />
|
<div class="w-px h-6 bg-white/20 self-center" />
|
||||||
<div class="flex items-center px-2 py-2">
|
<div class="flex items-center px-2 py-2">
|
||||||
<!-- Globe (language/currency) dropdown -->
|
<!-- Globe (language/currency) dropdown -->
|
||||||
@@ -536,16 +534,8 @@ const centerStyle = computed(() => {
|
|||||||
return { marginTop: `${top}px` }
|
return { marginTop: `${top}px` }
|
||||||
})
|
})
|
||||||
|
|
||||||
// Header background classes
|
// Header background classes — two-layer glass is handled by .header-glass + .header-glass-backdrop
|
||||||
const headerClasses = computed(() => {
|
const headerClasses = computed(() => '')
|
||||||
if (props.isHomePage && !props.isCollapsed) {
|
|
||||||
return 'bg-transparent'
|
|
||||||
}
|
|
||||||
if (props.isCollapsed) {
|
|
||||||
return 'bg-transparent backdrop-blur-xl'
|
|
||||||
}
|
|
||||||
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)
|
||||||
const useWhiteText = computed(() => props.isCollapsed || props.isHomePage)
|
const useWhiteText = computed(() => props.isCollapsed || props.isHomePage)
|
||||||
|
|||||||
@@ -26,16 +26,16 @@
|
|||||||
<!-- View mode loading indicator -->
|
<!-- View mode loading indicator -->
|
||||||
<div
|
<div
|
||||||
v-if="clusterLoading || loading"
|
v-if="clusterLoading || loading"
|
||||||
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"
|
class="absolute top-[116px] left-1/2 -translate-x-1/2 z-30 flex items-center gap-2 pill-glass rounded-full px-4 py-2"
|
||||||
>
|
>
|
||||||
<span class="loading loading-spinner loading-sm text-white" />
|
<span class="loading loading-spinner loading-sm text-base-content" />
|
||||||
<span class="text-white text-sm">{{ $t('common.loading') }}</span>
|
<span class="text-base-content text-sm font-medium">{{ $t('common.loading') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 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 glass-soft rounded-full px-3 py-1.5 text-white text-sm hover:bg-white/15 transition-colors"
|
class="absolute top-[116px] left-4 z-20 hidden lg:flex items-center gap-2 pill-glass rounded-full px-3 py-1.5 text-base-content text-sm hover:bg-white/20 transition-colors"
|
||||||
@click="openPanel"
|
@click="openPanel"
|
||||||
>
|
>
|
||||||
<Icon name="lucide:menu" size="16" />
|
<Icon name="lucide:menu" size="16" />
|
||||||
@@ -45,7 +45,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-[calc(1rem+32rem+1rem)] 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"
|
class="absolute top-[116px] left-[calc(1rem+32rem+1rem)] z-20 hidden lg:flex items-center gap-2 pill-glass rounded-full px-3 py-1.5 cursor-pointer text-base-content text-sm hover:bg-white/20 transition-colors"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -60,11 +60,11 @@
|
|||||||
<!-- 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 glass-bright rounded-full p-1">
|
<div class="flex gap-1 pill-glass rounded-full p-1">
|
||||||
<button
|
<button
|
||||||
v-if="showOffersToggle"
|
v-if="showOffersToggle"
|
||||||
class="flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-medium transition-colors"
|
class="flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-bold 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-base-content' : 'text-base-content/70 hover:text-base-content hover:bg-white/10'"
|
||||||
@click="setMapViewMode('offers')"
|
@click="setMapViewMode('offers')"
|
||||||
>
|
>
|
||||||
<span class="w-5 h-5 rounded-full flex items-center justify-center" style="background-color: #f97316">
|
<span class="w-5 h-5 rounded-full flex items-center justify-center" style="background-color: #f97316">
|
||||||
@@ -74,8 +74,8 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="showHubsToggle"
|
v-if="showHubsToggle"
|
||||||
class="flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-medium transition-colors"
|
class="flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-bold transition-colors"
|
||||||
:class="mapViewMode === 'hubs' ? 'bg-white/20 text-white' : 'text-white/70 hover:text-white hover:bg-white/10'"
|
:class="mapViewMode === 'hubs' ? 'bg-white/20 text-base-content' : 'text-base-content/70 hover:text-base-content hover:bg-white/10'"
|
||||||
@click="setMapViewMode('hubs')"
|
@click="setMapViewMode('hubs')"
|
||||||
>
|
>
|
||||||
<span class="w-5 h-5 rounded-full flex items-center justify-center" style="background-color: #22c55e">
|
<span class="w-5 h-5 rounded-full flex items-center justify-center" style="background-color: #22c55e">
|
||||||
@@ -85,8 +85,8 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="showSuppliersToggle"
|
v-if="showSuppliersToggle"
|
||||||
class="flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-medium transition-colors"
|
class="flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-bold transition-colors"
|
||||||
:class="mapViewMode === 'suppliers' ? 'bg-white/20 text-white' : 'text-white/70 hover:text-white hover:bg-white/10'"
|
:class="mapViewMode === 'suppliers' ? 'bg-white/20 text-base-content' : 'text-base-content/70 hover:text-base-content hover:bg-white/10'"
|
||||||
@click="setMapViewMode('suppliers')"
|
@click="setMapViewMode('suppliers')"
|
||||||
>
|
>
|
||||||
<span class="w-5 h-5 rounded-full flex items-center justify-center" style="background-color: #3b82f6">
|
<span class="w-5 h-5 rounded-full flex items-center justify-center" style="background-color: #3b82f6">
|
||||||
@@ -104,7 +104,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="glass-soft rounded-2xl shadow-lg h-full flex flex-col text-white">
|
<div class="bg-white/90 backdrop-blur-[14px] border border-white/50 rounded-[1.1rem] shadow-2xl h-full flex flex-col text-base-content">
|
||||||
<slot name="panel" />
|
<slot name="panel" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,7 +116,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 glass-soft rounded-full px-3 py-2 text-white text-sm"
|
class="flex items-center gap-2 pill-glass rounded-full px-3 py-2 text-base-content text-sm font-medium"
|
||||||
@click="openPanel"
|
@click="openPanel"
|
||||||
>
|
>
|
||||||
<Icon name="lucide:menu" size="16" />
|
<Icon name="lucide:menu" size="16" />
|
||||||
@@ -124,7 +124,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 glass-bright rounded-full p-1">
|
<div v-if="!isInfoMode && !hideViewToggle" class="flex gap-1 pill-glass rounded-full p-1">
|
||||||
<button
|
<button
|
||||||
v-if="showOffersToggle"
|
v-if="showOffersToggle"
|
||||||
class="flex items-center justify-center w-8 h-8 rounded-full transition-colors"
|
class="flex items-center justify-center w-8 h-8 rounded-full transition-colors"
|
||||||
@@ -162,14 +162,14 @@
|
|||||||
<Transition name="slide-up">
|
<Transition name="slide-up">
|
||||||
<div
|
<div
|
||||||
v-if="isPanelOpen"
|
v-if="isPanelOpen"
|
||||||
class="glass-soft rounded-t-2xl shadow-lg transition-all duration-300 text-white h-[60vh]"
|
class="bg-white rounded-t-3xl shadow-[0_-8px_40px_rgba(0,0,0,0.12)] transition-all duration-300 text-base-content h-[60vh]"
|
||||||
>
|
>
|
||||||
<!-- Drag handle / close -->
|
<!-- Drag handle / close -->
|
||||||
<div
|
<div
|
||||||
class="flex justify-center py-2 cursor-pointer"
|
class="flex justify-center py-2 cursor-pointer"
|
||||||
@click="closePanel"
|
@click="closePanel"
|
||||||
>
|
>
|
||||||
<div class="w-10 h-1 bg-white/30 rounded-full" />
|
<div class="w-10 h-1 bg-base-300 rounded-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-4 pb-4 overflow-y-auto h-[calc(60vh-2rem)]">
|
<div class="px-4 pb-4 overflow-y-auto h-[calc(60vh-2rem)]">
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
<!-- Hero content for home page -->
|
<!-- Hero content for home page -->
|
||||||
<template v-if="isHomePage && collapseProgress < 1" #hero>
|
<template v-if="isHomePage && collapseProgress < 1" #hero>
|
||||||
<h1
|
<h1
|
||||||
class="text-3xl lg:text-4xl font-bold text-white mb-4"
|
class="text-3xl lg:text-5xl font-black tracking-tight text-white mb-4"
|
||||||
:style="{ opacity: 1 - collapseProgress }"
|
:style="{ opacity: 1 - collapseProgress }"
|
||||||
>
|
>
|
||||||
{{ $t('hero.tagline', 'Make trade easy') }}
|
{{ $t('hero.tagline', 'Make trade easy') }}
|
||||||
|
|||||||
Reference in New Issue
Block a user