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
parent
e36ecb1dba
commit
3eada67b9a
|
|
@ -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; },
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue