feat(ui): auto-collapse sidebar on settings pages; fixed full-height rail

Auto-collapse the main sidebar on pages that have a settings rail (follows
the page when enabled; expanded elsewhere), with an animated slide and a
profile-menu toggle (localStorage) to disable it.

Rework the settings rail to position:fixed like the main sidebar: full
height (3rem -> viewport bottom), left tracks the sidebar's collapsing
width via --sidebar-w, content pinned to grid column 2. Fixes the sticky
scroll crop and top/bottom gaps.
This commit is contained in:
Aditya Tripathi 2026-09-08 00:47:58 +00:00 committed by Andras Bacsai
parent fc93001cb8
commit 9fbeb352a4
3 changed files with 62 additions and 13 deletions

View file

@ -1765,15 +1765,13 @@ .server-settings-workspace > :not(.application-settings-navigation) {
}
@media (min-width: 1280px) {
/* Flat rail welded to the main sidebar: full height (3rem bottom), surface
bled left across <main>'s gutter. Negative margins cancel main's pt breathing
and the 2.5rem gutter (lg:px-10). */
/* Fixed like the main sidebar (3rem bottom); left tracks its width (--sidebar-w).
Fixed leaves the grid, so the content sibling is pinned to column 2 below. */
.application-settings-navigation {
position: sticky;
position: fixed;
top: 3rem;
margin-top: -1.75rem;
margin-left: -2.5rem;
align-self: start;
left: var(--sidebar-w, 14rem);
width: calc(210px + 2.5rem);
height: calc(100dvh - 3rem);
overflow-x: hidden;
overflow-y: auto;
@ -1785,6 +1783,11 @@ @media (min-width: 1280px) {
padding: 0.5rem;
background: var(--rail-fill);
border-right: 1px solid var(--coollabs-hairline);
transition: left 200ms ease;
}
.application-settings-navigation ~ * {
grid-column: 2;
}
html:not(.dark) .application-settings-navigation {
@ -1808,13 +1811,14 @@ @media (min-width: 1280px) {
background: var(--coollabs-line);
}
/* Embeds: keep the surface, drop the weld/full-height. */
/* Embeds: keep the surface, drop the fixed positioning. */
.application-settings-navigation.is-flush {
position: static;
inset: auto;
width: auto;
height: auto;
overflow: visible;
margin-top: 0;
margin-left: 0;
transition: none;
}
}

View file

@ -84,6 +84,22 @@ class="flex size-5 shrink-0 items-center justify-center rounded-full bg-neutral-
<x-theme-controls variant="menu" />
</div>
<button type="button" class="listbox-option w-full" @click="toggleAutoCollapse()"
:aria-pressed="autoCollapse" title="Collapse the sidebar on pages that have a settings menu">
<span class="flex items-center gap-2">
<svg class="size-4 opacity-80" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<rect x="3" y="4" width="18" height="16" rx="2" stroke="currentColor" stroke-width="1.6" />
<path d="M9 4v16" stroke="currentColor" stroke-width="1.6" />
</svg>
Auto-collapse sidebar
</span>
<svg x-show="autoCollapse" x-cloak class="size-4 shrink-0 text-black dark:text-fg" viewBox="0 0 24 24"
fill="none" aria-hidden="true">
<path d="M5 12l5 5L20 7" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</button>
<div class="my-1 h-px bg-neutral-200 dark:bg-white/[0.07]"></div>
<livewire:settings-dropdown trigger="account-menu" />

View file

@ -9,21 +9,50 @@
@auth
<div x-data="{
open: false,
collapsed: localStorage.getItem('sidebarCollapsed') === 'true',
userCollapsed: localStorage.getItem('sidebarCollapsed') === 'true',
autoCollapse: localStorage.getItem('sidebarAutoCollapse') !== 'false',
hasSecondBar: false,
collapsed: false,
pageWidth: localStorage.getItem('pageWidth') || 'full',
sidebarReady: false,
init() {
this.applyCollapsed(false);
this.$nextTick(() => {
requestAnimationFrame(() => {
this.sidebarReady = true;
});
});
},
targetCollapsed() {
this.hasSecondBar = !!document.querySelector('.application-settings-navigation');
return this.autoCollapse ? this.hasSecondBar : this.userCollapsed;
},
applyCollapsed(animate) {
const target = this.targetCollapsed();
if (target === this.collapsed) return;
if (animate) {
// Let the new page paint at the current width, then animate the
// slide a frame later so the auto-collapse reads as a motion.
this.sidebarReady = true;
requestAnimationFrame(() => requestAnimationFrame(() => { this.collapsed = target; }));
} else {
this.collapsed = target;
}
},
toggleSidebar() {
this.collapsed = !this.collapsed;
localStorage.setItem('sidebarCollapsed', this.collapsed);
if (!this.autoCollapse) {
this.userCollapsed = this.collapsed;
localStorage.setItem('sidebarCollapsed', this.collapsed);
}
},
toggleAutoCollapse() {
this.autoCollapse = !this.autoCollapse;
localStorage.setItem('sidebarAutoCollapse', this.autoCollapse);
this.applyCollapsed(true);
}
}" @open-global-search.window="open = false" @page-width-changed.window="pageWidth = $event.detail" x-cloak
}" @open-global-search.window="open = false" @page-width-changed.window="pageWidth = $event.detail" x-on:livewire:navigated.window="applyCollapsed(true)"
:style="{ '--sidebar-w': collapsed ? '4rem' : '14rem' }" x-cloak
class="dark:text-inherit text-black">
<livewire:deployments-indicator />