coolify/resources/views/components/dashboard/navbar.blade.php
Andras Bacsai 2a5197fb30 feat(ui): add shared table controls and multi-select filters
Introduce x-table toolbar/search/filter/sort/loading components and
multi-select deployment history filters with OR within groups and AND
across groups. Add live database/service status for breadcrumbs, shared
variables layout controls, logs viewer toolbar polish, and infrastructure
list filter consistency. Document patterns in UI_REDESIGN.md and cover
with feature tests.
2026-08-06 15:37:30 +02:00

153 lines
6.7 KiB
PHP

@props([
'section',
'parameters' => [],
'title' => null,
'subtitle' => null,
// When false (default), hide the in-flow H1 once the desktop shell is active
// (lg+: main sidebar + fixed layer-2 tabs). Below lg the mobile topbar is used
// and the page title stays visible. Pass true for resource-detail names.
'titleOnDesktop' => false,
'mobileTitleOnly' => false,
])
@php
$items = match ($section) {
'team' => [
['label' => 'General', 'route' => 'team.index', 'active' => request()->routeIs('team.index', 'team.member.index', 'team.admin-view', 'team.danger-zone')],
],
'profile' => [
['label' => 'General', 'route' => 'profile', 'active' => request()->routeIs('profile')],
['label' => 'Appearance', 'route' => 'profile.appearance', 'active' => request()->routeIs('profile.appearance')],
],
'notifications' => [
['label' => 'Email', 'route' => 'notifications.email', 'active' => request()->routeIs('notifications.*')],
],
'security' => [
['label' => 'Private Keys', 'route' => 'security.private-key.index', 'active' => request()->routeIs('security.*')],
],
'settings' => [
['label' => 'General', 'route' => 'settings.index', 'active' => request()->routeIs('settings.*')],
],
'source' => [
[
'label' => 'General',
'route' => 'source.github.show',
'active' => request()->routeIs('source.github.show', 'source.github.permissions', 'source.github.resources', 'source.github.danger'),
],
],
'destination' => [
['label' => 'General', 'route' => 'destination.show', 'active' => request()->routeIs('destination.show', 'destination.resources', 'destination.danger')],
],
'storage' => [
[
'label' => 'General',
'route' => 'storage.show',
'active' => request()->routeIs('storage.show', 'storage.danger', 'storage.resources'),
],
],
'subscription' => [
[
'label' => 'Plan',
'route' => 'subscription.show',
'active' => request()->routeIs('subscription.show'),
'icon' => 'subscription',
// Unsubscribed accounts cannot open /subscription (middleware forces /subscription/new).
'visible' => isSubscriptionActive() || isSubscriptionOnGracePeriod(),
],
[
'label' => 'Pricing',
'route' => 'subscription.index',
'active' => request()->routeIs('subscription.index'),
'icon' => 'dashboard',
// Active subscriptions are redirected away from pricing by middleware.
'visible' => ! isSubscriptionActive(),
],
],
default => [],
};
$items = array_values(array_filter($items, fn (array $item): bool => $item['visible'] ?? true));
// A single self-link tab (e.g. only "Pricing" on /subscription/new) is not navigation.
$showTabs = count($items) >= 2;
$hasTitle = filled($title);
$hasActions = isset($actions);
$showNav = $showTabs || $hasActions;
@endphp
@if ($hasTitle)
<div class="flex flex-col">
<header @class([
'order-1 mb-4 flex min-w-0 items-start justify-between gap-4 lg:order-2',
'lg:mb-8' => $titleOnDesktop || ! $showNav,
// Hide with the desktop chrome (sidebar + fixed tabs), not xl.
// Keep title on desktop when there is no fixed layer-2 tab strip.
'lg:hidden' => $mobileTitleOnly || (! $titleOnDesktop && $showNav),
])>
<div class="min-w-0 flex-1">
<div class="flex min-w-0 flex-wrap items-center gap-2">
<h1 class="min-w-0 truncate text-[24px]! leading-7! font-semibold! tracking-tight!">{{ $title }}</h1>
@isset($titleMeta)
<div class="flex shrink-0 flex-wrap items-center gap-2">
{{ $titleMeta }}
</div>
@endisset
</div>
@if (filled($subtitle))
<p class="mt-1 text-[13px] text-neutral-500 dark:text-fg-dim">{{ $subtitle }}</p>
@endif
</div>
@isset($titleActions)
<div class="flex shrink-0 flex-wrap items-center justify-end gap-2">
{{ $titleActions }}
</div>
@endisset
</header>
@if ($showNav)
<div class="order-2 lg:order-1">
@endif
@endif
@if ($showNav)
<nav class="mb-6 w-full lg:mb-0">
<div class="w-full lg:fixed lg:top-12 lg:right-0 lg:z-30 lg:h-12 lg:w-auto lg:border-b lg:border-neutral-200 lg:bg-white/95 lg:pr-4 lg:pl-2 lg:backdrop-blur lg:transition-[left] lg:duration-200 lg:dark:border-white/[0.06] lg:dark:bg-panel/95"
:class="[typeof collapsed !== 'undefined' && collapsed ? 'lg:left-16' : 'lg:left-56']">
{{-- Mobile: stack tabs then actions so long buttons never crush the tab strip.
Desktop: single fixed row with tabs left and actions right. --}}
<div
class="flex w-full flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3 lg:h-full lg:gap-4">
@if ($showTabs)
<div class="flex min-w-0 w-full items-center gap-0.5 overflow-x-auto rounded-[10px] border border-neutral-200 bg-neutral-100 p-1 sm:flex-1 dark:border-white/[0.07] dark:bg-white/[0.035]">
@foreach ($items as $item)
<a @class([
'app-tab shrink-0',
'bg-coollabs/10 text-coollabs shadow-sm ring-1 ring-coollabs/25 hover:bg-coollabs/15 dark:bg-warning/15 dark:text-warning dark:ring-warning/25 dark:hover:bg-warning/20' => $item['active'],
])
{{ wireNavigate() }} href="{{ route($item['route'], $parameters) }}">
@if ($item['icon'] ?? null)
<x-reicon :name="$item['icon']" class="size-3.5" />
@endif
{{ $item['label'] }}
</a>
@endforeach
</div>
@endif
@isset($actions)
<div
class="flex w-full shrink-0 flex-wrap items-center justify-end gap-2 sm:w-auto [&_.button]:whitespace-nowrap">
{{ $actions }}
</div>
@endisset
</div>
</div>
<div class="hidden lg:block lg:h-12" aria-hidden="true"></div>
</nav>
@endif
@if ($hasTitle)
@if ($showNav)
</div>
@endif
</div>
@endif