84 lines
4.5 KiB
PHP
84 lines
4.5 KiB
PHP
@props([
|
|
// [[route, icon, translation key], …] or [['label' => …, 'items' => [...]], …]
|
|
'groups',
|
|
// dot-prefix for the labels, e.g. 'dashboard.nav' or 'admin.nav'
|
|
'prefix',
|
|
// console routes are matched through AdminArea so recovery hostnames still
|
|
// highlight; the portal uses the router directly
|
|
'console' => false,
|
|
'footer' => null,
|
|
])
|
|
{{--
|
|
The sidebar, shared by both shells.
|
|
|
|
Light, like everything else. The console used to be dark, which made it a
|
|
second product: two ideas of what a card is, two oranges, two status
|
|
palettes. What actually differs between an operator and a customer is
|
|
density, and that lives in the tokens — not here.
|
|
--}}
|
|
<aside
|
|
class="fixed inset-y-0 left-0 z-50 flex w-[15.5rem] flex-col border-r border-line bg-surface px-3 py-4 shadow-md transition-transform lg:static lg:h-screen lg:translate-x-0 lg:shadow-none"
|
|
:class="nav ? 'translate-x-0' : '-translate-x-full'"
|
|
>
|
|
<div class="flex shrink-0 items-center justify-between gap-2 px-2.5 pb-5">
|
|
<a href="{{ $console ? \App\Support\AdminArea::home() : route('dashboard') }}" class="flex items-center gap-2.5">
|
|
<x-ui.logo class="size-7" />
|
|
{{-- One element: as separate flex children the gap lands inside the
|
|
word and reads as "Clu Pilot". --}}
|
|
<span class="whitespace-nowrap text-lg font-bold tracking-tight text-ink">Clu<span class="text-accent-text">Pilot</span></span>
|
|
@if ($console)
|
|
<span class="rounded-sm bg-accent-subtle px-1.5 py-0.5 font-mono text-[9.5px] font-semibold uppercase tracking-[0.1em] text-accent-text">{{ __('admin.badge') }}</span>
|
|
@endif
|
|
</a>
|
|
<button type="button" class="lg:hidden -mr-1 inline-flex min-h-11 min-w-11 items-center justify-center rounded text-muted"
|
|
@click="nav = false" aria-label="{{ __('dashboard.close_nav') }}">
|
|
<x-ui.icon name="x" />
|
|
</button>
|
|
</div>
|
|
|
|
{{-- The only scroll region in the sidebar: header above and footer below
|
|
stay put (operator requirement), only the nav list itself scrolls
|
|
when it is taller than the viewport. overflow-y-auto (not -hidden)
|
|
on a flex-1 child resolves its automatic min-height to 0, which is
|
|
what lets it shrink to the available space instead of stretching the
|
|
aside past the viewport — same mechanism <main> already relies on. --}}
|
|
<div class="min-h-0 flex-1 overflow-y-auto">
|
|
@foreach ($groups as $group)
|
|
@if (! empty($group['label']))
|
|
<p class="px-3 pb-2 pt-5 lbl">{{ $group['label'] }}</p>
|
|
@endif
|
|
<nav class="space-y-0.5">
|
|
@foreach ($group['items'] as [$route, $icon, $key, $capability])
|
|
{{-- $capability is a single ability name, an array of
|
|
ability names meaning "any of these" (App\Support\
|
|
Navigation documents which entries use that), or
|
|
null for "everyone". Laravel's own can() treats an
|
|
array as "all of these", which is the wrong logic
|
|
for an entry reachable with either of two
|
|
capabilities — hence the explicit branch instead of
|
|
a bare ->can($capability) for both shapes. --}}
|
|
@continue($capability !== null && ! (is_array($capability)
|
|
? collect($capability)->contains(fn ($one) => auth()->user()?->can($one))
|
|
: auth()->user()?->can($capability)))
|
|
<x-ui.nav-item
|
|
:href="route($route)"
|
|
:active="$console ? \App\Support\AdminArea::routeIs($route) : request()->routeIs($route)"
|
|
@click="nav = false"
|
|
>
|
|
<x-slot:icon><x-ui.icon :name="$icon" class="size-[1.15rem]" /></x-slot:icon>
|
|
{{ __($prefix.'.'.$key) }}
|
|
</x-ui.nav-item>
|
|
@endforeach
|
|
</nav>
|
|
@endforeach
|
|
</div>
|
|
|
|
@if ($footer)
|
|
<p class="shrink-0 px-3 pt-6 font-mono text-[11px] leading-relaxed text-muted">{{ $footer }}</p>
|
|
@endif
|
|
</aside>
|
|
|
|
{{-- Backdrop. Tapping it closes the drawer, which is the behaviour every
|
|
phone user tries first. --}}
|
|
<div x-show="nav" x-cloak @click="nav = false" class="fixed inset-0 z-40 bg-[color-mix(in_srgb,var(--plate)_42%,transparent)] lg:hidden"></div>
|