Skip to content

Commit 0ef2d84

Browse files
feat(ui): enhance UI components and layout rendering (#1899)
- Add manual resize capability to Connections table columns - Dynamically match theme names and check icons with their respective colors in ThemeSelector - Remove blur effect on Modals to eliminate rendering delay and increase max width for settings - Refine collapsed Sidebar traffic indicator layout for compact vertical display - Improve input field accessibility and appearance in Config page (URL, Search, Select)
1 parent 6550ec2 commit 0ef2d84

File tree

3 files changed

+220
-33
lines changed

3 files changed

+220
-33
lines changed

components/MobileBottomNav.vue

Lines changed: 219 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,135 @@
11
<script setup lang="ts">
2-
import type { Component } from 'vue'
2+
import {
3+
IconChartAreaLine,
4+
IconFileStack,
5+
IconGlobe,
6+
IconHome,
7+
IconNetwork,
8+
IconPlus,
9+
IconRuler,
10+
IconSettings,
11+
IconX,
12+
} from '@tabler/icons-vue'
313
4-
interface NavItem {
5-
href: string
6-
name: string
7-
icon: Component
8-
}
14+
const { t } = useI18n()
15+
const route = useRoute()
916
10-
defineProps<{
11-
navItems: NavItem[]
12-
}>()
17+
// Primary 4 nav items (around the FAB)
18+
const primaryItems = computed(() => [
19+
{ href: '/overview', name: t('overview'), icon: IconHome },
20+
{ href: '/proxies', name: t('proxies'), icon: IconGlobe },
21+
{ href: '/rules', name: t('rules'), icon: IconRuler },
22+
{ href: '/connections', name: t('connections'), icon: IconNetwork },
23+
])
1324
14-
const route = useRoute()
25+
// Secondary items in the FAB popup
26+
const secondaryItems = computed(() => [
27+
{ href: '/traffic', name: t('dataUsage'), icon: IconChartAreaLine },
28+
{ href: '/logs', name: t('logs'), icon: IconFileStack },
29+
{ href: '/config', name: t('config'), icon: IconSettings },
30+
])
1531
1632
const isActive = (href: string) => route.path === href
1733
18-
// Entrance animation state
19-
const isVisible = ref(false)
34+
// FAB popup state
35+
const popupOpen = ref(false)
36+
const togglePopup = () => {
37+
popupOpen.value = !popupOpen.value
38+
}
39+
const closePopup = () => {
40+
popupOpen.value = false
41+
}
42+
43+
// Close popup when navigating
44+
watch(() => route.path, closePopup)
45+
46+
// Whether the active route is a secondary item
47+
const isSecondaryActive = computed(() =>
48+
secondaryItems.value.some((item) => item.href === route.path),
49+
)
2050
51+
// Entrance animation
52+
const isVisible = ref(false)
2153
onMounted(() => {
22-
// Trigger entrance animation
2354
requestAnimationFrame(() => {
2455
isVisible.value = true
2556
})
2657
})
2758
</script>
2859

2960
<template>
61+
<!-- Backdrop to close popup on outside click -->
62+
<Transition name="fade">
63+
<div
64+
v-if="popupOpen"
65+
class="fixed inset-0 z-40 lg:hidden"
66+
aria-hidden="true"
67+
@click="closePopup"
68+
/>
69+
</Transition>
70+
71+
<!-- Secondary popup panel -->
72+
<Transition name="slide-up">
73+
<div
74+
v-if="popupOpen"
75+
class="fixed inset-x-0 bottom-[4.5rem] z-50 mx-auto w-max lg:hidden"
76+
>
77+
<div
78+
class="mx-auto flex w-max flex-col overflow-hidden rounded-2xl shadow-xl backdrop-blur-[16px]"
79+
:style="{
80+
border:
81+
'1px solid color-mix(in oklch, var(--color-base-content) 12%, transparent)',
82+
background:
83+
'color-mix(in oklch, var(--color-base-300) 95%, transparent)',
84+
}"
85+
>
86+
<NuxtLink
87+
v-for="item in secondaryItems"
88+
:key="item.href"
89+
:to="item.href"
90+
class="flex items-center gap-3 px-5 py-3 text-sm font-medium no-underline transition-colors duration-200"
91+
:class="
92+
isActive(item.href)
93+
? 'text-primary bg-primary/10'
94+
: 'text-base-content/70 hover:text-base-content hover:bg-base-content/5'
95+
"
96+
>
97+
<component :is="item.icon" class="h-5 w-5 shrink-0" />
98+
<span>{{ item.name }}</span>
99+
<!-- Active indicator dot -->
100+
<span
101+
v-if="isActive(item.href)"
102+
class="ml-auto h-1.5 w-1.5 rounded-full bg-primary"
103+
/>
104+
</NuxtLink>
105+
</div>
106+
</div>
107+
</Transition>
108+
109+
<!-- Bottom nav bar -->
30110
<nav
31111
aria-label="Mobile bottom navigation"
32112
class="fixed inset-x-0 bottom-0 z-50 transition-all duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] lg:hidden"
33113
:class="
34114
isVisible ? 'translate-y-0 opacity-100' : 'translate-y-full opacity-0'
35115
"
36116
>
37-
<!-- Backdrop blur container -->
38117
<div
39-
class="mx-1 mb-2 overflow-hidden rounded-2xl shadow-lg backdrop-blur-[12px] sm:mx-2"
118+
class="mx-1 mb-2 overflow-visible rounded-2xl shadow-lg backdrop-blur-[12px] sm:mx-2"
40119
:style="{
41120
border:
42121
'1px solid color-mix(in oklch, var(--color-base-content) 10%, transparent)',
43122
background:
44123
'color-mix(in oklch, var(--color-base-300) 90%, transparent)',
45124
}"
46125
>
47-
<div class="grid h-16 w-full grid-cols-7">
126+
<div class="grid h-16 w-full grid-cols-5">
127+
<!-- Left 2 items: Overview, Proxies -->
48128
<NuxtLink
49-
v-for="nav in navItems"
129+
v-for="nav in primaryItems.slice(0, 2)"
50130
:key="nav.href"
51131
:to="nav.href"
52-
class="group relative flex flex-col items-center justify-center gap-0.5 no-underline transition-all duration-300 ease-in-out active:scale-90"
132+
class="group relative flex flex-col items-center justify-center gap-0.5 no-underline transition-all duration-200 ease-in-out active:scale-90"
53133
:class="
54134
isActive(nav.href)
55135
? 'text-primary'
@@ -65,32 +145,103 @@ onMounted(() => {
65145
: 'bg-transparent group-hover:bg-base-content/5'
66146
"
67147
/>
68-
69148
<!-- Active indicator pill -->
70149
<div
71150
class="absolute top-1 h-1 rounded-full bg-primary transition-all duration-300 ease-in-out"
72151
:class="isActive(nav.href) ? 'w-8 opacity-100' : 'w-0 opacity-0'"
73152
/>
74-
75-
<!-- Icon with scale animation -->
76153
<div
77-
class="relative z-10 transition-all duration-300 ease-in-out group-hover:scale-105"
78-
:class="
79-
isActive(nav.href) ? 'scale-110 text-xl' : 'scale-100 text-lg'
80-
"
154+
class="relative z-10 transition-transform duration-300 ease-in-out group-hover:scale-105"
155+
:class="isActive(nav.href) ? 'scale-110' : 'scale-100'"
81156
>
82-
<component :is="nav.icon" />
157+
<component :is="nav.icon" class="h-5 w-5" />
83158
</div>
159+
<span class="sr-only">{{ `Navigate to ${nav.name}` }}</span>
160+
<span
161+
aria-hidden="true"
162+
class="relative z-10 truncate px-0.5 text-[9px] font-medium transition-all duration-300 sm:text-[10px]"
163+
:class="isActive(nav.href) ? 'opacity-100' : 'opacity-80'"
164+
>
165+
{{ nav.name }}
166+
</span>
167+
</NuxtLink>
84168

85-
<!-- Screen reader label -->
86-
<span class="sr-only">
87-
{{ `Navigate to ${nav.name}` }}
169+
<!-- Center FAB button -->
170+
<div class="relative flex items-center justify-center">
171+
<button
172+
class="group relative -top-3 flex h-14 w-14 flex-col items-center justify-center rounded-2xl shadow-lg transition-all duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)] active:scale-90"
173+
:class="
174+
popupOpen || isSecondaryActive
175+
? 'bg-primary text-primary-content shadow-primary/40'
176+
: 'bg-base-content/10 text-base-content hover:bg-base-content/15'
177+
"
178+
:style="
179+
popupOpen || isSecondaryActive
180+
? 'box-shadow: 0 4px 20px color-mix(in oklch, var(--color-primary) 40%, transparent)'
181+
: ''
182+
"
183+
aria-label="More menu"
184+
@click="togglePopup"
185+
>
186+
<Transition name="icon-spin" mode="out-in">
187+
<IconX v-if="popupOpen" class="h-6 w-6" />
188+
<IconPlus v-else class="h-6 w-6" />
189+
</Transition>
190+
<!-- Indicator dot when a secondary page is active -->
191+
<span
192+
v-if="isSecondaryActive && !popupOpen"
193+
class="absolute -top-0.5 -right-0.5 h-2.5 w-2.5 rounded-full border-2 border-base-300 bg-primary"
194+
/>
195+
</button>
196+
<span
197+
aria-hidden="true"
198+
class="absolute bottom-1 text-[9px] font-medium transition-all duration-300 sm:text-[10px]"
199+
:class="
200+
popupOpen || isSecondaryActive
201+
? 'text-primary opacity-100'
202+
: 'text-base-content/60 opacity-80'
203+
"
204+
>
205+
{{ t('more') }}
88206
</span>
207+
</div>
89208

90-
<!-- Visual label with fade animation -->
209+
<!-- Right 2 items: Rules, Connections -->
210+
<NuxtLink
211+
v-for="nav in primaryItems.slice(2, 4)"
212+
:key="nav.href"
213+
:to="nav.href"
214+
class="group relative flex flex-col items-center justify-center gap-0.5 no-underline transition-all duration-200 ease-in-out active:scale-90"
215+
:class="
216+
isActive(nav.href)
217+
? 'text-primary'
218+
: 'text-base-content/60 hover:text-base-content'
219+
"
220+
>
221+
<!-- Active background glow -->
222+
<div
223+
class="absolute inset-1 rounded-xl transition-all duration-300 ease-in-out"
224+
:class="
225+
isActive(nav.href)
226+
? 'bg-primary/10'
227+
: 'bg-transparent group-hover:bg-base-content/5'
228+
"
229+
/>
230+
<!-- Active indicator pill -->
231+
<div
232+
class="absolute top-1 h-1 rounded-full bg-primary transition-all duration-300 ease-in-out"
233+
:class="isActive(nav.href) ? 'w-8 opacity-100' : 'w-0 opacity-0'"
234+
/>
235+
<div
236+
class="relative z-10 transition-transform duration-300 ease-in-out group-hover:scale-105"
237+
:class="isActive(nav.href) ? 'scale-110' : 'scale-100'"
238+
>
239+
<component :is="nav.icon" class="h-5 w-5" />
240+
</div>
241+
<span class="sr-only">{{ `Navigate to ${nav.name}` }}</span>
91242
<span
92243
aria-hidden="true"
93-
class="relative z-10 truncate px-0.5 text-[9px] font-medium transition-all duration-300 ease-in-out group-hover:opacity-100 sm:text-[10px]"
244+
class="relative z-10 truncate px-0.5 text-[9px] font-medium transition-all duration-300 sm:text-[10px]"
94245
:class="isActive(nav.href) ? 'opacity-100' : 'opacity-80'"
95246
>
96247
{{ nav.name }}
@@ -100,3 +251,40 @@ onMounted(() => {
100251
</div>
101252
</nav>
102253
</template>
254+
255+
<style scoped>
256+
/* Popup slide-up transition */
257+
.slide-up-enter-active,
258+
.slide-up-leave-active {
259+
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
260+
}
261+
.slide-up-enter-from,
262+
.slide-up-leave-to {
263+
opacity: 0;
264+
transform: translateY(12px) scale(0.95);
265+
}
266+
267+
/* Backdrop fade */
268+
.fade-enter-active,
269+
.fade-leave-active {
270+
transition: opacity 0.2s ease;
271+
}
272+
.fade-enter-from,
273+
.fade-leave-to {
274+
opacity: 0;
275+
}
276+
277+
/* Icon spin for + <-> X */
278+
.icon-spin-enter-active,
279+
.icon-spin-leave-active {
280+
transition: all 0.2s ease;
281+
}
282+
.icon-spin-enter-from {
283+
opacity: 0;
284+
transform: rotate(-90deg) scale(0.5);
285+
}
286+
.icon-spin-leave-to {
287+
opacity: 0;
288+
transform: rotate(90deg) scale(0.5);
289+
}
290+
</style>

components/Sidebar.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,6 @@ const toggleSidebar = () => {
224224
<!-- Mobile Bottom Navigation (when enabled) -->
225225
<MobileBottomNav
226226
v-if="configStore.useMobileBottomNav && route.path !== '/setup'"
227-
:nav-items="navItems"
228227
/>
229228
</div>
230229
</template>

i18n/locales/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@
208208
"destinations": "Destinations",
209209
"waitingForConnections": "Waiting for connections...",
210210
"conn": "conn",
211-
"more": "more",
211+
"more": "More",
212212
"connectedTo": "Connected to",
213213
"clients": "Clients",
214214
"groups": "Groups",

0 commit comments

Comments
 (0)