fix(sidebar): remove collapse, fix mobile overlay and blur effect

- Remove desktop collapse feature — sidebar always open at 264px
- Simplify Alpine sidebar store to mobileOpen only
- Fix mobile overlay: add x-data to #nim-app so Alpine processes x-show/x-transition
- Mobile overlay uses direct backdrop-filter: blur(4px) bypassing Tailwind v4 CSS variable chain
- Fix close/workspace-trigger overlap: restructure header as flex row with close as sibling
- Clicking overlay (z-30) closes sidebar; sidebar stays at z-40

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
boban 2026-05-17 14:34:33 +02:00
parent e36ecb1dba
commit 3eada67b9a
3 changed files with 46 additions and 119 deletions

View File

@ -61,34 +61,11 @@ document.addEventListener('alpine:init', () => {
Alpine.store('sidebar', {
mobileOpen: false,
collapsed: localStorage.getItem('sidebarCollapsed') === 'true',
isMobile: window.innerWidth < 768,
isCollapsed: false,
init() {
this.isCollapsed = this.collapsed && ! this.isMobile;
this._syncDataset();
window.addEventListener('resize', () => {
const wasMobile = this.isMobile;
this.isMobile = window.innerWidth < 768;
this.isCollapsed = this.collapsed && ! this.isMobile;
this._syncDataset();
if (this.isMobile && ! wasMobile) { this.mobileOpen = false; }
if (window.innerWidth >= 768) { this.mobileOpen = false; }
});
},
_syncDataset() {
if (this.isCollapsed) {
document.documentElement.dataset.sidebarCollapsed = '1';
} else {
delete document.documentElement.dataset.sidebarCollapsed;
}
},
toggle() {
this.collapsed = ! this.collapsed;
this.isCollapsed = this.collapsed && ! this.isMobile;
localStorage.setItem('sidebarCollapsed', this.collapsed);
this._syncDataset();
window.dispatchEvent(new CustomEvent('sidebar-toggled', { detail: { collapsed: this.collapsed } }));
},
toggleMobile() { this.mobileOpen = ! this.mobileOpen; },
closeMobile() { this.mobileOpen = false; },
});

View File

@ -38,33 +38,36 @@
{{-- ═══ Header ═══ --}}
<header class="flex-shrink-0 px-3 py-3 border-b border-white/[.06]" @click.outside="wsOpen = false">
<button
@click="$store.sidebar?.isCollapsed && !$store.sidebar?.isMobile ? $store.sidebar.toggle() : (wsOpen = !wsOpen)"
:aria-expanded="wsOpen.toString()"
class="ws-trigger w-full flex items-center gap-3 px-2.5 py-2 rounded-xl hover:bg-s2 transition-colors duration-150"
>
<img src="/brand/logo-mark.svg" alt="Nimuli" class="ws-logo w-8 h-8 flex-shrink-0 rounded-lg">
<span class="ws-text flex-1 min-w-0 text-left grid leading-snug">
<span class="ws-brand text-accent-gradient font-bold text-sm tracking-tight">nimuli</span>
<span class="ws-name text-xs text-t2 truncate">{{ $workspace?->name ?? 'Select workspace' }}</span>
</span>
<svg class="ws-chevron w-4 h-4 text-t3 flex-shrink-0 transition-transform duration-200"
:class="wsOpen && 'rotate-180'"
fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
<div class="flex items-center gap-1">
<button
@click="wsOpen = !wsOpen"
:aria-expanded="wsOpen.toString()"
class="ws-trigger flex-1 min-w-0 flex items-center gap-3 px-2.5 py-2 rounded-xl hover:bg-s2 transition-colors duration-150"
>
<img src="/brand/logo-mark.svg" alt="Nimuli" class="ws-logo w-8 h-8 flex-shrink-0 rounded-lg">
<span class="ws-text flex-1 min-w-0 text-left grid leading-snug">
<span class="ws-brand text-accent-gradient font-bold text-sm tracking-tight">nimuli</span>
<span class="ws-name text-xs text-t2 truncate">{{ $workspace?->name ?? 'Select workspace' }}</span>
</span>
<svg class="ws-chevron w-4 h-4 text-t3 flex-shrink-0 transition-transform duration-200"
:class="wsOpen && 'rotate-180'"
fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
{{-- Mobile close --}}
<button
@click="$store.sidebar.closeMobile()"
class="md:hidden absolute right-3 top-3.5 p-2 rounded-lg text-t3 hover:text-t1 hover:bg-s2 transition-colors"
>
<svg class="w-5 h-5" fill="none" viewBox="0 0 20 20" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6l8 8M6 14L14 6"/>
</svg>
</button>
{{-- Mobile close --}}
<button
@click="$store.sidebar.closeMobile()"
class="md:hidden flex-shrink-0 p-2 rounded-lg text-t3 hover:text-t1 hover:bg-s2 transition-colors"
aria-label="Close sidebar"
>
<svg class="w-5 h-5" fill="none" viewBox="0 0 20 20" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6l8 8M6 14L14 6"/>
</svg>
</button>
</div>
{{-- Workspace dropdown --}}
<div
@ -83,7 +86,7 @@
href="{{ route('w.links.index', $ws->ulid) }}"
wire:navigate
@click="wsOpen = false"
class="flex items-center gap-2.5 px-3.5 py-2.5 text-sm border-b border-white/[.06] transition-colors hover:bg-s3
class="ws-item flex items-center gap-2.5 text-sm border-b border-white/[.06] transition-colors hover:bg-s3
{{ $workspace?->id === $ws->id ? 'text-t1 font-medium' : 'text-t2' }}"
>
<span class="w-6 h-6 rounded-md flex items-center justify-center text-white font-bold text-xs flex-shrink-0 bg-accent-gradient">
@ -102,7 +105,7 @@
@endforelse
<button
@click="$dispatch('openModal', {component: 'modals.create-workspace'}); wsOpen = false"
class="w-full flex items-center gap-2.5 px-3.5 py-2.5 text-sm text-t2 hover:text-t1 hover:bg-s3 transition-colors text-left"
class="ws-item w-full flex items-center gap-2.5 text-sm text-t2 hover:text-t1 hover:bg-s3 transition-colors text-left"
>
<svg class="w-4 h-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@ -132,7 +135,6 @@
wire:navigate
data-nav-link="{{ $item['route'] }}"
@isset($item['exact']) data-nav-exact="1" @endisset
data-label="{{ $item['label'] }}"
class="nav-item relative flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium w-full {{ $isActive ? 'nim-nav-active' : '' }}"
>
<span class="w-[18px] h-[18px] flex-shrink-0">
@ -157,7 +159,6 @@
href="{{ $url }}"
wire:navigate
data-nav-link="{{ $item['route'] }}"
data-label="{{ $item['label'] }}"
class="nav-item relative flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium w-full {{ $isActive ? 'nim-nav-active' : '' }}"
>
<span class="w-[18px] h-[18px] flex-shrink-0">
@ -197,19 +198,6 @@
{{-- ═══ Footer ═══ --}}
<footer class="flex-shrink-0 border-t border-white/[.06]">
{{-- Collapse toggle (desktop only) --}}
<button
@click="$store.sidebar.toggle()"
class="collapse-row max-md:hidden w-full flex items-center gap-3 px-[18px] py-2.5 text-sm text-t3 hover:text-t1 border-b border-white/[.06] transition-colors duration-150"
>
<svg class="w-4 h-4 flex-shrink-0 transition-transform duration-200"
fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 18 9 12 15 6"/>
</svg>
<span class="collapse-text">Collapse</span>
</button>
{{-- User row --}}
@auth
<div class="user-row flex items-center gap-3 px-3.5 py-3 min-w-0">

View File

@ -18,17 +18,6 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- Sidebar-init: set collapsed attribute before first paint eliminates width flash -->
<script>
(function () {
try {
if (localStorage.getItem('sidebarCollapsed') === 'true' && window.innerWidth >= 768) {
document.documentElement.dataset.sidebarCollapsed = '1';
}
} catch (e) {}
})();
</script>
<!-- Theme-init: runs synchronously before any paint eliminates FOUC -->
<script>
(function () {
@ -57,7 +46,7 @@
--nim-accent-bg: rgba(139,92,246,.14); --nim-accent-bg-strong: rgba(139,92,246,.22);
--nim-accent-glow: rgba(139,92,246,.6);
--nim-brand-grad: linear-gradient(135deg,#a855f7 0%,#ec4899 100%);
--nim-sb-w: 264px; --nim-sb-c: 72px;
--nim-sb-w: 264px;
}
[data-theme="light"] {
--c-bg: #ffffff; --c-s1: #ffffff; --c-s2: #f9fafb;
@ -92,8 +81,6 @@
border-top-color: var(--nim-accent); border-radius: 50%;
animation: nim-spin .55s linear infinite; z-index: 10;
}
html[data-sidebar-collapsed] .skel-sidebar { width: var(--nim-sb-c); }
html[data-sidebar-collapsed] .skel-topbar { left: var(--nim-sb-c); }
body.app-ready .app-spinner, body.app-ready .skel-sidebar, body.app-ready .skel-topbar { display:none; }
@keyframes nim-spin { to { transform: translate(-50%,-50%) rotate(360deg); } }
@ -104,14 +91,12 @@
border-right: 1px solid var(--color-b1);
overflow-x: hidden;
font-family: 'Geist', 'Inter', ui-sans-serif, system-ui, sans-serif;
transition: width .28s cubic-bezier(.4,0,.2,1), translate .28s cubic-bezier(.4,0,.2,1);
transition: translate .28s cubic-bezier(.4,0,.2,1);
}
/* Content margin */
/* Content margin — desktop: always full sidebar width */
@media (min-width: 768px) {
.nim-content { margin-left: var(--nim-sb-w); }
html[data-sidebar-collapsed] .nim-content { margin-left: var(--nim-sb-c); }
html[data-sidebar-collapsed] .nim-sidebar { width: var(--nim-sb-c); }
}
/* Nav items */
@ -131,44 +116,21 @@
box-shadow: 0 0 12px rgba(124,58,237,.5);
}
/* Mobile overlay blur */
.nim-mobile-overlay {
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
}
/* Nav scrollbar */
.nim-sb-nav { scrollbar-width: thin; scrollbar-color: var(--nim-border-strong) transparent; }
.nim-sb-nav::-webkit-scrollbar { width: 6px; }
.nim-sb-nav::-webkit-scrollbar-thumb { background: var(--nim-border-strong); border-radius: 3px; }
.nim-sb-nav::-webkit-scrollbar-track { background: transparent; }
/* Logout hover (Tailwind handles this via hover:text-red-400 hover:bg-red-400/10) */
/* Collapse state — desktop only */
@media (min-width: 768px) {
html[data-sidebar-collapsed] .ws-text,
html[data-sidebar-collapsed] .ws-chevron,
html[data-sidebar-collapsed] .nav-text,
html[data-sidebar-collapsed] .nav-label,
html[data-sidebar-collapsed] .nav-badge,
html[data-sidebar-collapsed] .nav-plan-usage,
html[data-sidebar-collapsed] .user-info,
html[data-sidebar-collapsed] .user-logout-btn,
html[data-sidebar-collapsed] .collapse-text { display: none !important; }
html[data-sidebar-collapsed] .ws-trigger { justify-content: center !important; padding: 8px !important; }
html[data-sidebar-collapsed] .nav-item { justify-content: center !important; padding: 10px !important; gap: 0 !important; }
html[data-sidebar-collapsed] .nav-item.nim-nav-active::before { display: none; }
html[data-sidebar-collapsed] .collapse-row { justify-content: center !important; padding: 12px !important; }
html[data-sidebar-collapsed] .collapse-row svg { transform: rotate(180deg); }
html[data-sidebar-collapsed] .user-row { justify-content: center !important; padding: 12px !important; gap: 0 !important; }
/* Tooltip on hover when collapsed */
html[data-sidebar-collapsed] .nav-item:hover::after {
content: attr(data-label);
position: absolute; left: calc(100% + 12px); top: 50%; transform: translateY(-50%);
background: var(--color-s3); color: var(--color-t1);
padding: 6px 10px; border-radius: 6px; font-size: 12.5px;
white-space: nowrap; pointer-events: none;
border: 1px solid var(--color-b2);
box-shadow: 0 8px 24px -6px rgba(0,0,0,.5); z-index: 100;
}
}
/* Sidebar element padding — shorthand overrides Tailwind v4 logical-property variants */
.ws-item { padding: 12px 14px; }
.user-row { padding: 14px; }
</style>
@vite(['resources/js/app.js'])
@ -180,10 +142,10 @@
<div class="skel-topbar" aria-hidden="true"></div>
<div class="app-spinner" aria-hidden="true"></div>
<div id="nim-app" class="bg-bg text-t1 font-sans antialiased min-h-screen">
<div id="nim-app" x-data class="bg-bg text-t1 font-sans antialiased min-h-screen">
<!-- Mobile Sidebar Overlay -->
<div
class="fixed inset-0 z-30 bg-black/60 md:hidden"
class="nim-mobile-overlay fixed inset-0 z-30 bg-black/50 md:hidden"
x-show="$store.sidebar?.mobileOpen"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"