Merge pull request #1545 from Zqysl/qingyu/fix-smooth-sidebar-collapse
fix(sidebar): smooth sidebar collapse behavior
This commit is contained in:
@@ -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 -->
|
||||||
@@ -35,17 +33,25 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="sidebar-link mb-1 w-full"
|
class="sidebar-link mb-1 w-full"
|
||||||
:class="{ 'sidebar-link-active': isGroupActive(item) && !isGroupExpanded(item) }"
|
:class="{
|
||||||
|
'sidebar-link-active': isGroupActive(item) && !isGroupExpanded(item),
|
||||||
|
'sidebar-link-collapsed': sidebarCollapsed
|
||||||
|
}"
|
||||||
:title="sidebarCollapsed ? item.label : undefined"
|
:title="sidebarCollapsed ? item.label : undefined"
|
||||||
@click="sidebarCollapsed ? undefined : toggleGroup(item)"
|
@click="sidebarCollapsed ? undefined : toggleGroup(item)"
|
||||||
>
|
>
|
||||||
<component :is="item.icon" class="h-5 w-5 flex-shrink-0" />
|
<component :is="item.icon" class="h-5 w-5 flex-shrink-0" />
|
||||||
<transition name="fade">
|
<span
|
||||||
<span v-if="!sidebarCollapsed" class="flex flex-1 items-center justify-between">
|
class="sidebar-label sidebar-label-flex"
|
||||||
<span>{{ item.label }}</span>
|
:class="{ 'sidebar-label-collapsed': sidebarCollapsed }"
|
||||||
<ChevronDownIcon class="h-4 w-4 flex-shrink-0 transition-transform duration-200" :class="isGroupExpanded(item) ? 'rotate-180' : ''" />
|
:aria-hidden="sidebarCollapsed ? 'true' : 'false'"
|
||||||
</span>
|
>
|
||||||
</transition>
|
<span class="min-w-0 truncate">{{ item.label }}</span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
class="h-4 w-4 flex-shrink-0 transition-transform duration-200"
|
||||||
|
:class="isGroupExpanded(item) ? 'rotate-180' : ''"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<!-- Children -->
|
<!-- Children -->
|
||||||
<div v-if="!sidebarCollapsed && isGroupExpanded(item)" class="mb-1 ml-4 border-l border-gray-200 pl-2 dark:border-dark-600">
|
<div v-if="!sidebarCollapsed && isGroupExpanded(item)" class="mb-1 ml-4 border-l border-gray-200 pl-2 dark:border-dark-600">
|
||||||
@@ -67,7 +73,7 @@
|
|||||||
v-else
|
v-else
|
||||||
: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'
|
||||||
@@ -82,35 +88,32 @@
|
|||||||
>
|
>
|
||||||
<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>
|
||||||
</template>
|
</template>
|
||||||
</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>
|
||||||
@@ -123,16 +126,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>
|
||||||
@@ -144,28 +145,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>
|
||||||
@@ -794,14 +793,120 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-from,
|
.sidebar-header-collapsed {
|
||||||
.fade-leave-to {
|
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;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-label-flex {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-label-collapsed {
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-4px);
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom SVG icon in sidebar: constrain size without overriding uploaded SVG colors */
|
/* Custom SVG icon in sidebar: constrain size without overriding uploaded SVG colors */
|
||||||
|
|||||||
@@ -523,12 +523,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 {
|
||||||
@@ -536,12 +541,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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user