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', { Alpine.store('sidebar', {
mobileOpen: false, mobileOpen: false,
collapsed: localStorage.getItem('sidebarCollapsed') === 'true',
isMobile: window.innerWidth < 768,
isCollapsed: false,
init() { init() {
this.isCollapsed = this.collapsed && ! this.isMobile;
this._syncDataset();
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
const wasMobile = this.isMobile; if (window.innerWidth >= 768) { this.mobileOpen = false; }
this.isMobile = window.innerWidth < 768;
this.isCollapsed = this.collapsed && ! this.isMobile;
this._syncDataset();
if (this.isMobile && ! wasMobile) { 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; }, toggleMobile() { this.mobileOpen = ! this.mobileOpen; },
closeMobile() { this.mobileOpen = false; }, closeMobile() { this.mobileOpen = false; },
}); });

View File

@ -38,33 +38,36 @@
{{-- ═══ Header ═══ --}} {{-- ═══ Header ═══ --}}
<header class="flex-shrink-0 px-3 py-3 border-b border-white/[.06]" @click.outside="wsOpen = false"> <header class="flex-shrink-0 px-3 py-3 border-b border-white/[.06]" @click.outside="wsOpen = false">
<button <div class="flex items-center gap-1">
@click="$store.sidebar?.isCollapsed && !$store.sidebar?.isMobile ? $store.sidebar.toggle() : (wsOpen = !wsOpen)" <button
:aria-expanded="wsOpen.toString()" @click="wsOpen = !wsOpen"
class="ws-trigger w-full flex items-center gap-3 px-2.5 py-2 rounded-xl hover:bg-s2 transition-colors duration-150" :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"> <img src="/brand/logo-mark.svg" alt="Nimuli" class="ws-logo w-8 h-8 flex-shrink-0 rounded-lg">
<span class="ws-brand text-accent-gradient font-bold text-sm tracking-tight">nimuli</span> <span class="ws-text flex-1 min-w-0 text-left grid leading-snug">
<span class="ws-name text-xs text-t2 truncate">{{ $workspace?->name ?? 'Select workspace' }}</span> <span class="ws-brand text-accent-gradient font-bold text-sm tracking-tight">nimuli</span>
</span> <span class="ws-name text-xs text-t2 truncate">{{ $workspace?->name ?? 'Select workspace' }}</span>
<svg class="ws-chevron w-4 h-4 text-t3 flex-shrink-0 transition-transform duration-200" </span>
:class="wsOpen && 'rotate-180'" <svg class="ws-chevron w-4 h-4 text-t3 flex-shrink-0 transition-transform duration-200"
fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" :class="wsOpen && 'rotate-180'"
stroke-linecap="round" stroke-linejoin="round"> fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"
<polyline points="6 9 12 15 18 9"/> stroke-linecap="round" stroke-linejoin="round">
</svg> <polyline points="6 9 12 15 18 9"/>
</button> </svg>
</button>
{{-- Mobile close --}} {{-- Mobile close --}}
<button <button
@click="$store.sidebar.closeMobile()" @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" 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 class="w-5 h-5" fill="none" viewBox="0 0 20 20" stroke="currentColor" stroke-width="1.5">
</svg> <path stroke-linecap="round" stroke-linejoin="round" d="M6 6l8 8M6 14L14 6"/>
</button> </svg>
</button>
</div>
{{-- Workspace dropdown --}} {{-- Workspace dropdown --}}
<div <div
@ -83,7 +86,7 @@
href="{{ route('w.links.index', $ws->ulid) }}" href="{{ route('w.links.index', $ws->ulid) }}"
wire:navigate wire:navigate
@click="wsOpen = false" @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' }}" {{ $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"> <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 @endforelse
<button <button
@click="$dispatch('openModal', {component: 'modals.create-workspace'}); wsOpen = false" @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" <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"> stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@ -132,7 +135,6 @@
wire:navigate wire:navigate
data-nav-link="{{ $item['route'] }}" data-nav-link="{{ $item['route'] }}"
@isset($item['exact']) data-nav-exact="1" @endisset @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' : '' }}" 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"> <span class="w-[18px] h-[18px] flex-shrink-0">
@ -157,7 +159,6 @@
href="{{ $url }}" href="{{ $url }}"
wire:navigate wire:navigate
data-nav-link="{{ $item['route'] }}" 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' : '' }}" 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"> <span class="w-[18px] h-[18px] flex-shrink-0">
@ -197,19 +198,6 @@
{{-- ═══ Footer ═══ --}} {{-- ═══ Footer ═══ --}}
<footer class="flex-shrink-0 border-t border-white/[.06]"> <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 --}} {{-- User row --}}
@auth @auth
<div class="user-row flex items-center gap-3 px-3.5 py-3 min-w-0"> <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 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"> <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 --> <!-- Theme-init: runs synchronously before any paint eliminates FOUC -->
<script> <script>
(function () { (function () {
@ -57,7 +46,7 @@
--nim-accent-bg: rgba(139,92,246,.14); --nim-accent-bg-strong: rgba(139,92,246,.22); --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-accent-glow: rgba(139,92,246,.6);
--nim-brand-grad: linear-gradient(135deg,#a855f7 0%,#ec4899 100%); --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"] { [data-theme="light"] {
--c-bg: #ffffff; --c-s1: #ffffff; --c-s2: #f9fafb; --c-bg: #ffffff; --c-s1: #ffffff; --c-s2: #f9fafb;
@ -92,8 +81,6 @@
border-top-color: var(--nim-accent); border-radius: 50%; border-top-color: var(--nim-accent); border-radius: 50%;
animation: nim-spin .55s linear infinite; z-index: 10; 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; } 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); } } @keyframes nim-spin { to { transform: translate(-50%,-50%) rotate(360deg); } }
@ -104,14 +91,12 @@
border-right: 1px solid var(--color-b1); border-right: 1px solid var(--color-b1);
overflow-x: hidden; overflow-x: hidden;
font-family: 'Geist', 'Inter', ui-sans-serif, system-ui, sans-serif; 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) { @media (min-width: 768px) {
.nim-content { margin-left: var(--nim-sb-w); } .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 */ /* Nav items */
@ -131,44 +116,21 @@
box-shadow: 0 0 12px rgba(124,58,237,.5); 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 */ /* Nav scrollbar */
.nim-sb-nav { scrollbar-width: thin; scrollbar-color: var(--nim-border-strong) transparent; } .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 { width: 6px; }
.nim-sb-nav::-webkit-scrollbar-thumb { background: var(--nim-border-strong); border-radius: 3px; } .nim-sb-nav::-webkit-scrollbar-thumb { background: var(--nim-border-strong); border-radius: 3px; }
.nim-sb-nav::-webkit-scrollbar-track { background: transparent; } .nim-sb-nav::-webkit-scrollbar-track { background: transparent; }
/* Logout hover (Tailwind handles this via hover:text-red-400 hover:bg-red-400/10) */ /* Sidebar element padding — shorthand overrides Tailwind v4 logical-property variants */
.ws-item { padding: 12px 14px; }
/* Collapse state — desktop only */ .user-row { padding: 14px; }
@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;
}
}
</style> </style>
@vite(['resources/js/app.js']) @vite(['resources/js/app.js'])
@ -180,10 +142,10 @@
<div class="skel-topbar" aria-hidden="true"></div> <div class="skel-topbar" aria-hidden="true"></div>
<div class="app-spinner" 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 --> <!-- Mobile Sidebar Overlay -->
<div <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-show="$store.sidebar?.mobileOpen"
x-transition:enter="transition ease-out duration-200" x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0" x-transition:enter-start="opacity-0"