Commit 07d2add6 authored by qingyuzhang's avatar qingyuzhang
Browse files

fix(sidebar): smooth collapse transitions

parent 00c08c57
...@@ -7,20 +7,18 @@ ...@@ -7,20 +7,18 @@
]" ]"
> >
<!-- Logo/Brand --> <!-- Logo/Brand -->
<div class="sidebar-header"> <div class="sidebar-header" :class="{ 'sidebar-header-collapsed': sidebarCollapsed }">
<!-- Custom Logo or Default Logo --> <!-- Custom Logo or Default Logo -->
<div class="flex h-9 w-9 items-center justify-center overflow-hidden rounded-xl shadow-glow"> <div class="sidebar-logo flex h-9 w-9 items-center justify-center overflow-hidden rounded-xl shadow-glow">
<img v-if="settingsLoaded" :src="siteLogo || '/logo.png'" alt="Logo" class="h-full w-full object-contain" /> <img v-if="settingsLoaded" :src="siteLogo || '/logo.png'" alt="Logo" class="h-full w-full object-contain" />
</div> </div>
<transition name="fade"> <div class="sidebar-brand" :class="{ 'sidebar-brand-collapsed': sidebarCollapsed }" :aria-hidden="sidebarCollapsed ? 'true' : 'false'">
<div v-if="!sidebarCollapsed" class="flex flex-col"> <span class="sidebar-brand-title text-lg font-bold text-gray-900 dark:text-white">
<span class="text-lg font-bold text-gray-900 dark:text-white"> {{ siteName }}
{{ siteName }} </span>
</span> <!-- Version Badge -->
<!-- Version Badge --> <VersionBadge :version="siteVersion" />
<VersionBadge :version="siteVersion" /> </div>
</div>
</transition>
</div> </div>
<!-- Navigation --> <!-- Navigation -->
...@@ -34,7 +32,7 @@ ...@@ -34,7 +32,7 @@
:key="item.path" :key="item.path"
:to="item.path" :to="item.path"
class="sidebar-link mb-1" class="sidebar-link mb-1"
:class="{ 'sidebar-link-active': isActive(item.path) }" :class="{ 'sidebar-link-active': isActive(item.path), 'sidebar-link-collapsed': sidebarCollapsed }"
:title="sidebarCollapsed ? item.label : undefined" :title="sidebarCollapsed ? item.label : undefined"
:id=" :id="
item.path === '/admin/accounts' item.path === '/admin/accounts'
...@@ -49,34 +47,31 @@ ...@@ -49,34 +47,31 @@
> >
<span v-if="item.iconSvg" class="h-5 w-5 flex-shrink-0 sidebar-svg-icon" v-html="sanitizeSvg(item.iconSvg)"></span> <span v-if="item.iconSvg" class="h-5 w-5 flex-shrink-0 sidebar-svg-icon" v-html="sanitizeSvg(item.iconSvg)"></span>
<component v-else :is="item.icon" class="h-5 w-5 flex-shrink-0" /> <component v-else :is="item.icon" class="h-5 w-5 flex-shrink-0" />
<transition name="fade"> <span class="sidebar-label" :class="{ 'sidebar-label-collapsed': sidebarCollapsed }" :aria-hidden="sidebarCollapsed ? 'true' : 'false'">{{ item.label }}</span>
<span v-if="!sidebarCollapsed">{{ item.label }}</span>
</transition>
</router-link> </router-link>
</div> </div>
<!-- Personal Section for Admin (hidden in simple mode) --> <!-- Personal Section for Admin (hidden in simple mode) -->
<div v-if="!authStore.isSimpleMode" class="sidebar-section"> <div v-if="!authStore.isSimpleMode" class="sidebar-section">
<div v-if="!sidebarCollapsed" class="sidebar-section-title"> <div class="sidebar-section-title" :class="{ 'sidebar-section-title-collapsed': sidebarCollapsed }" :aria-hidden="sidebarCollapsed ? 'true' : 'false'">
{{ t('nav.myAccount') }} <span class="sidebar-section-title-text" :class="{ 'sidebar-section-title-text-collapsed': sidebarCollapsed }">
{{ t('nav.myAccount') }}
</span>
</div> </div>
<div v-else class="mx-3 my-3 h-px bg-gray-200 dark:bg-dark-700"></div>
<router-link <router-link
v-for="item in personalNavItems" v-for="item in personalNavItems"
:key="item.path" :key="item.path"
:to="item.path" :to="item.path"
class="sidebar-link mb-1" class="sidebar-link mb-1"
:class="{ 'sidebar-link-active': isActive(item.path) }" :class="{ 'sidebar-link-active': isActive(item.path), 'sidebar-link-collapsed': sidebarCollapsed }"
:title="sidebarCollapsed ? item.label : undefined" :title="sidebarCollapsed ? item.label : undefined"
:data-tour="item.path === '/keys' ? 'sidebar-my-keys' : undefined" :data-tour="item.path === '/keys' ? 'sidebar-my-keys' : undefined"
@click="handleMenuItemClick(item.path)" @click="handleMenuItemClick(item.path)"
> >
<span v-if="item.iconSvg" class="h-5 w-5 flex-shrink-0 sidebar-svg-icon" v-html="sanitizeSvg(item.iconSvg)"></span> <span v-if="item.iconSvg" class="h-5 w-5 flex-shrink-0 sidebar-svg-icon" v-html="sanitizeSvg(item.iconSvg)"></span>
<component v-else :is="item.icon" class="h-5 w-5 flex-shrink-0" /> <component v-else :is="item.icon" class="h-5 w-5 flex-shrink-0" />
<transition name="fade"> <span class="sidebar-label" :class="{ 'sidebar-label-collapsed': sidebarCollapsed }" :aria-hidden="sidebarCollapsed ? 'true' : 'false'">{{ item.label }}</span>
<span v-if="!sidebarCollapsed">{{ item.label }}</span>
</transition>
</router-link> </router-link>
</div> </div>
</template> </template>
...@@ -89,16 +84,14 @@ ...@@ -89,16 +84,14 @@
:key="item.path" :key="item.path"
:to="item.path" :to="item.path"
class="sidebar-link mb-1" class="sidebar-link mb-1"
:class="{ 'sidebar-link-active': isActive(item.path) }" :class="{ 'sidebar-link-active': isActive(item.path), 'sidebar-link-collapsed': sidebarCollapsed }"
:title="sidebarCollapsed ? item.label : undefined" :title="sidebarCollapsed ? item.label : undefined"
:data-tour="item.path === '/keys' ? 'sidebar-my-keys' : undefined" :data-tour="item.path === '/keys' ? 'sidebar-my-keys' : undefined"
@click="handleMenuItemClick(item.path)" @click="handleMenuItemClick(item.path)"
> >
<span v-if="item.iconSvg" class="h-5 w-5 flex-shrink-0 sidebar-svg-icon" v-html="sanitizeSvg(item.iconSvg)"></span> <span v-if="item.iconSvg" class="h-5 w-5 flex-shrink-0 sidebar-svg-icon" v-html="sanitizeSvg(item.iconSvg)"></span>
<component v-else :is="item.icon" class="h-5 w-5 flex-shrink-0" /> <component v-else :is="item.icon" class="h-5 w-5 flex-shrink-0" />
<transition name="fade"> <span class="sidebar-label" :class="{ 'sidebar-label-collapsed': sidebarCollapsed }" :aria-hidden="sidebarCollapsed ? 'true' : 'false'">{{ item.label }}</span>
<span v-if="!sidebarCollapsed">{{ item.label }}</span>
</transition>
</router-link> </router-link>
</div> </div>
</template> </template>
...@@ -110,28 +103,26 @@ ...@@ -110,28 +103,26 @@
<button <button
@click="toggleTheme" @click="toggleTheme"
class="sidebar-link mb-2 w-full" class="sidebar-link mb-2 w-full"
:class="{ 'sidebar-link-collapsed': sidebarCollapsed }"
:title="sidebarCollapsed ? (isDark ? t('nav.lightMode') : t('nav.darkMode')) : undefined" :title="sidebarCollapsed ? (isDark ? t('nav.lightMode') : t('nav.darkMode')) : undefined"
> >
<SunIcon v-if="isDark" class="h-5 w-5 flex-shrink-0 text-amber-500" /> <SunIcon v-if="isDark" class="h-5 w-5 flex-shrink-0 text-amber-500" />
<MoonIcon v-else class="h-5 w-5 flex-shrink-0" /> <MoonIcon v-else class="h-5 w-5 flex-shrink-0" />
<transition name="fade"> <span class="sidebar-label" :class="{ 'sidebar-label-collapsed': sidebarCollapsed }" :aria-hidden="sidebarCollapsed ? 'true' : 'false'">{{
<span v-if="!sidebarCollapsed">{{ isDark ? t('nav.lightMode') : t('nav.darkMode')
isDark ? t('nav.lightMode') : t('nav.darkMode') }}</span>
}}</span>
</transition>
</button> </button>
<!-- Collapse Button --> <!-- Collapse Button -->
<button <button
@click="toggleSidebar" @click="toggleSidebar"
class="sidebar-link w-full" class="sidebar-link w-full"
:class="{ 'sidebar-link-collapsed': sidebarCollapsed }"
:title="sidebarCollapsed ? t('nav.expand') : t('nav.collapse')" :title="sidebarCollapsed ? t('nav.expand') : t('nav.collapse')"
> >
<ChevronDoubleLeftIcon v-if="!sidebarCollapsed" class="h-5 w-5 flex-shrink-0" /> <ChevronDoubleLeftIcon v-if="!sidebarCollapsed" class="h-5 w-5 flex-shrink-0" />
<ChevronDoubleRightIcon v-else class="h-5 w-5 flex-shrink-0" /> <ChevronDoubleRightIcon v-else class="h-5 w-5 flex-shrink-0" />
<transition name="fade"> <span class="sidebar-label" :class="{ 'sidebar-label-collapsed': sidebarCollapsed }" :aria-hidden="sidebarCollapsed ? 'true' : 'false'">{{ t('nav.collapse') }}</span>
<span v-if="!sidebarCollapsed">{{ t('nav.collapse') }}</span>
</transition>
</button> </button>
</div> </div>
</aside> </aside>
...@@ -659,14 +650,113 @@ onMounted(() => { ...@@ -659,14 +650,113 @@ onMounted(() => {
</script> </script>
<style scoped> <style scoped>
.fade-enter-active, .sidebar-logo {
.fade-leave-active { flex: 0 0 2.25rem;
transition: opacity 0.2s ease; min-width: 2.25rem;
}
.sidebar-header-collapsed {
gap: 0;
padding-left: 1.125rem;
padding-right: 1.125rem;
}
.sidebar-brand {
min-width: 0;
flex: 1 1 auto;
overflow: hidden;
white-space: nowrap;
transition:
max-width 0.22s ease,
opacity 0.14s ease,
transform 0.14s ease;
max-width: 12rem;
}
.sidebar-brand-collapsed {
max-width: 0;
opacity: 0;
transform: translateX(-4px);
pointer-events: none;
}
.sidebar-brand-title {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sidebar-link-collapsed {
gap: 0;
padding-left: 0.875rem;
padding-right: 0.875rem;
}
.sidebar-section-title {
position: relative;
display: flex;
align-items: center;
min-height: 1.25rem;
overflow: hidden;
white-space: nowrap;
}
.sidebar-section-title-text {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition:
opacity 0.16s ease,
transform 0.16s ease;
}
.sidebar-section-title::after {
content: '';
position: absolute;
left: 0.75rem;
right: 0.75rem;
top: 50%;
height: 1px;
background: rgb(229 231 235);
opacity: 0;
transform: translateY(-50%);
transition: opacity 0.18s ease;
}
.dark .sidebar-section-title::after {
background: rgb(55 65 81);
}
.sidebar-section-title-text-collapsed {
opacity: 0;
transform: translateX(-4px);
}
.sidebar-section-title-collapsed::after {
opacity: 1;
transition-delay: 0.08s;
}
.sidebar-label {
display: block;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition:
max-width 0.2s ease,
opacity 0.12s ease,
transform 0.12s ease;
max-width: 12rem;
} }
.fade-enter-from, .sidebar-label-collapsed {
.fade-leave-to { max-width: 0;
opacity: 0; opacity: 0;
transform: translateX(-4px);
pointer-events: none;
} }
/* Custom SVG icon in sidebar: inherit color, constrain size */ /* Custom SVG icon in sidebar: inherit color, constrain size */
......
...@@ -529,12 +529,17 @@ ...@@ -529,12 +529,17 @@
@apply border-r border-gray-200 dark:border-dark-800; @apply border-r border-gray-200 dark:border-dark-800;
@apply flex flex-col; @apply flex flex-col;
@apply transition-transform duration-300; @apply transition-transform duration-300;
transition-property: width, transform;
} }
.sidebar-header { .sidebar-header {
@apply h-16 px-6; @apply h-16 px-6;
@apply flex items-center gap-3; @apply flex items-center gap-3;
@apply overflow-hidden;
@apply border-b border-gray-100 dark:border-dark-800; @apply border-b border-gray-100 dark:border-dark-800;
transition:
padding 0.2s ease,
gap 0.2s ease;
} }
.sidebar-nav { .sidebar-nav {
...@@ -542,12 +547,15 @@ ...@@ -542,12 +547,15 @@
} }
.sidebar-link { .sidebar-link {
@apply flex items-center gap-3 rounded-xl px-3 py-2.5; @apply flex items-center gap-3 rounded-xl py-2.5;
@apply overflow-hidden;
@apply text-sm font-medium; @apply text-sm font-medium;
@apply text-gray-600 dark:text-dark-300; @apply text-gray-600 dark:text-dark-300;
@apply transition-all duration-200; @apply transition-all duration-200;
@apply hover:bg-gray-100 dark:hover:bg-dark-800; @apply hover:bg-gray-100 dark:hover:bg-dark-800;
@apply hover:text-gray-900 dark:hover:text-white; @apply hover:text-gray-900 dark:hover:text-white;
padding-left: 1.0625rem;
padding-right: 0.875rem;
} }
.sidebar-link-active { .sidebar-link-active {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment