fix(BUG-2): drive sidebar collapse/expand via CSS attribute, not Alpine reactivity

x-show and :class bindings inside @persist blocks do not re-evaluate when
Alpine store changes. Replace all collapse-dependent bindings with CSS utility
classes (nim-sb-expanded, nim-sb-collapsed, sidebar-nav-link) driven by
html[data-sidebar-collapsed] in the critical inline style block.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
boban 2026-05-17 13:05:33 +02:00
parent 3897d54ba3
commit e71bd33e63
2 changed files with 18 additions and 13 deletions

View File

@ -104,14 +104,14 @@
<!-- Header: Logo + Workspace Switcher -->
<div class="flex-shrink-0 relative" @click.outside="wsOpen = false">
<!-- Collapsed state: centered logo acts as expand toggle -->
<div x-show="$store.sidebar.isCollapsed" class="flex items-center justify-center py-4 border-b border-white/[.06]">
<div class="nim-sb-collapsed flex items-center justify-center py-4 border-b border-white/[.06]">
<button @click="$store.sidebar.toggle()" title="Expand sidebar" class="flex items-center justify-center w-7 h-7 hover:opacity-75 transition-opacity">
<img src="/brand/logo-mark.svg" alt="Nimuli" class="w-7 h-7">
</button>
</div>
<!-- Expanded state: full header with workspace switcher -->
<div x-show="!$store.sidebar.isCollapsed">
<div class="nim-sb-expanded">
<div class="flex items-stretch border-b border-white/[.06]">
<button
@click="wsOpen = !wsOpen"
@ -192,8 +192,7 @@
data-nav-link="{{ $item['route'] }}"
@if(in_array($item['route'], ['w.dashboard', 'dashboard'])) data-nav-exact="1" @endif
:title="$store.sidebar.isCollapsed ? '{{ $item['label'] }}' : ''"
:class="$store.sidebar.isCollapsed ? 'justify-center px-2 gap-0' : 'gap-3 px-3'"
class="flex items-center py-2 rounded-lg text-sm transition-colors group
class="sidebar-nav-link flex items-center py-2 rounded-lg text-sm transition-colors group
{{ $isActive
? 'bg-blue/10 text-blue'
: 'text-t2 hover:text-t1 hover:bg-s2' }}"
@ -241,7 +240,7 @@
</svg>
@endif
</span>
<span x-show="!$store.sidebar.isCollapsed" class="truncate">{{ $item['label'] }}</span>
<span class="nim-sb-expanded truncate">{{ $item['label'] }}</span>
</a>
@endforeach
</nav>
@ -251,23 +250,23 @@
<button
@click="$store.sidebar.toggle()"
:title="$store.sidebar.isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'"
class="flex items-center gap-3 w-full px-3 py-2 rounded-lg text-t3 hover:text-t1 hover:bg-s2 transition-colors"
class="sidebar-nav-link flex items-center w-full py-2 rounded-lg text-t3 hover:text-t1 hover:bg-s2 transition-colors"
>
<span class="w-4 h-4 flex-shrink-0">
<svg x-show="!$store.sidebar.isCollapsed" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
<svg class="nim-sb-expanded" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 3L5 8l5 5"/>
</svg>
<svg x-show="$store.sidebar.isCollapsed" x-cloak fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
<svg class="nim-sb-collapsed" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 3l5 5-5 5"/>
</svg>
</span>
<span x-show="!$store.sidebar.isCollapsed" class="text-xs">Collapse</span>
<span class="nim-sb-expanded text-xs">Collapse</span>
</button>
</div>
<!-- Plan Usage Box -->
@if($workspace && $plan)
<div x-show="!$store.sidebar.isCollapsed" class="mx-3 mb-3 p-3 bg-s2 border border-white/[.06] rounded-lg">
<div class="nim-sb-expanded mx-3 mb-3 p-3 bg-s2 border border-white/[.06] rounded-lg">
<div class="flex items-center justify-between mb-1.5">
<span class="text-xs font-medium text-t2">{{ ucfirst($plan->name) }} Plan</span>
@if(str_contains(strtolower($plan->name), 'free'))
@ -290,11 +289,11 @@
<div class="w-8 h-8 rounded-full bg-blue flex items-center justify-center text-white text-xs font-semibold flex-shrink-0">
{{ $initials ?: strtoupper(substr(auth()->user()->email, 0, 2)) }}
</div>
<div x-show="!$store.sidebar.isCollapsed" class="flex-1 min-w-0">
<div class="nim-sb-expanded flex-1 min-w-0">
<div class="text-xs font-medium text-t1 truncate">{{ auth()->user()->name }}</div>
<div class="text-xs text-t3 truncate">{{ auth()->user()->email }}</div>
</div>
<form x-show="!$store.sidebar.isCollapsed" method="POST" action="{{ route('logout') }}">
<form class="nim-sb-expanded" method="POST" action="{{ route('logout') }}">
@csrf
<button
type="submit"

View File

@ -89,12 +89,18 @@
}
html[data-sidebar-collapsed] .skel-sidebar { width: 64px; }
html[data-sidebar-collapsed] .skel-topbar { left: 64px; }
.sidebar-aside { width: 15rem; }
.sidebar-aside { width: 15rem; overflow-x: hidden; }
@media (min-width: 768px) {
.nim-content { margin-left: 210px; }
html[data-sidebar-collapsed] .nim-content { margin-left: 55px; }
html[data-sidebar-collapsed] .sidebar-aside { width: 4rem; }
}
/* Sidebar collapsed/expanded visibility — CSS-driven, bypasses @persist Alpine issue */
html[data-sidebar-collapsed] .nim-sb-expanded { display: none !important; }
.nim-sb-collapsed { display: none !important; }
html[data-sidebar-collapsed] .nim-sb-collapsed { display: flex !important; }
.sidebar-nav-link { gap: 0.75rem; padding-left: 0.75rem; padding-right: 0.75rem; }
html[data-sidebar-collapsed] .sidebar-nav-link { justify-content: center; padding-left: 0.5rem; padding-right: 0.5rem; gap: 0; }
body.app-ready .app-spinner,
body.app-ready .skel-sidebar,
body.app-ready .skel-topbar { display: none; }