CluPilotCloud/resources/views/components/shell/nav.blade.php

152 lines
8.0 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,
])
@php
// Die Zahlen fuer den Block „Braucht dich". Einmal je Seitenaufruf geholt,
// nicht je Eintrag — und dahinter ein Zwischenspeicher, weil diese Leiste
// auf JEDER Konsolenseite rendert. Das Portal hat keine, dort bleibt es
// ein leeres Feld ohne Abfrage.
$counts = $console ? \App\Support\Navigation::attentionCounts() : [];
@endphp
{{--
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.brand :size="$console ? 'sm' : 'md'" />
@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)
@php
// Sichtbarkeit EINMAL je Gruppe entscheiden, nicht erst je
// Eintrag. Vorher wurde die Überschrift bedingungslos gedruckt
// und stand deshalb auch über einer Gruppe, von der dieser
// Betreiber keinen einzigen Eintrag sehen darf — eine
// Überschrift mit nichts darunter liest sich wie ein
// Ladefehler, nicht wie eine Berechtigung.
//
// Bis zur Neuordnung der Konsolen-Navigation fiel das nicht
// auf, weil jede Gruppe mindestens einen ungeschützten Eintrag
// trug. „Post" tut das nicht.
//
// $capability ist ein einzelner Fähigkeitsname, ein Array von
// Namen im Sinne von „irgendeine davon" (App\Support\Navigation
// hält fest, welche Einträge das benutzen), oder null für
// „alle". Laravels eigenes can() liest ein Array als „alle
// davon" — die falsche Logik für einen Eintrag, den man mit
// der einen ODER der anderen Fähigkeit erreicht. Daher die
// ausgeschriebene Verzweigung statt eines bloßen ->can().
$visible = collect($group['items'])->filter(function (array $item) {
$capability = $item[3];
if ($capability === null) {
return true;
}
return is_array($capability)
? collect($capability)->contains(fn ($one) => auth()->user()?->can($one))
: (bool) auth()->user()?->can($capability);
});
@endphp
@continue($visible->isEmpty())
@if (! empty($group['label']))
<p class="px-3 pb-2 pt-5 lbl">{{ $group['label'] }}</p>
@endif
<nav class="space-y-0.5">
@foreach ($visible as $item)
@php
[$route, $icon, $key] = $item;
// Fuenftes Feld, optional: der Schluessel, unter dem
// Navigation::attentionCounts() die Zahl fuehrt. Als
// Index gelesen statt destrukturiert — das Portal hat
// vier Felder je Eintrag, und eine Destrukturierung auf
// fuenf warnte dort bei jedem einzelnen.
$count = isset($item[4]) ? ($counts[$item[4]] ?? 0) : 0;
@endphp
<x-ui.nav-item
:href="route($route)"
:active="\App\Support\Navigation::isCurrent($route, $console)"
@click="nav = false"
>
<x-slot:icon><x-ui.icon :name="$icon" class="size-[1.15rem]" /></x-slot:icon>
{{ __($prefix.'.'.$key) }}
{{-- Bei null KEINE Plakette, der Eintrag bleibt aber
stehen: eine Seite, die verschwindet, sobald nichts
offen ist, ist genau dann nicht erreichbar, wenn man
nachsehen will, ob wirklich nichts offen ist. --}}
@if ($count > 0)
<x-slot:badge>
<span class="rounded-pill bg-accent px-2 py-0.5 text-xs font-semibold text-on-accent">{{ $count }}</span>
</x-slot:badge>
@endif
</x-ui.nav-item>
@endforeach
</nav>
@endforeach
</div>
{{-- Die laufende Version und, wenn es eine neuere gibt, dass es sie gibt.
Sie stand hier schon, aber als toter Text: die Seite, auf der man
etwas damit tun kann, liegt seit dem Umbau der Leiste im
Einrichtungsbereich. Der Betreiber hat genau das gemeldet „man sieht
es nicht, ohne genau hinzuklicken".
Also bleibt die Zeile, wo sie war, und wird zum Weg dorthin. Ist etwas
verfuegbar, sagt sie es in der Akzentfarbe statt in Grau; ist nichts
verfuegbar, bleibt sie so leise wie vorher. Eine Zeile, die immer
ruft, ruft nie. --}}
@if ($footer)
@php $updateWaiting = ($counts['update'] ?? 0) > 0; @endphp
@if ($console)
<a href="{{ route('admin.settings') }}?tab=installation"
@class([
'shrink-0 rounded px-3 py-2 mt-6 block font-mono text-[11px] leading-relaxed transition-colors',
'bg-accent-subtle text-accent-text hover:bg-accent-subtle/70' => $updateWaiting,
'text-muted hover:text-ink hover:bg-surface-hover' => ! $updateWaiting,
])>
{{ $footer }}
@if ($updateWaiting)
<span class="mt-0.5 block font-sans font-semibold">{{ __('admin.nav_update_waiting') }}</span>
@endif
</a>
@else
<p class="shrink-0 px-3 pt-6 font-mono text-[11px] leading-relaxed text-muted">{{ $footer }}</p>
@endif
@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>