fix(portal): move notices into the header, so the page stops scrolling twice
The banners sat outside the h-screen shell, so the page scrolled and the content scrolled — two scrollbars whenever a maintenance window was active. Maintenance notices are now a bell with a count in the header, next to the user menu that was otherwise the only thing up there. Impersonation stays visible as a header chip rather than moving into the dropdown: it is a mode, not a notice, and acting as someone else without noticing is how mistakes happen. Also: the VPN status pill wrapped onto two lines in a narrow column. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/portal-design
parent
3fad319053
commit
062d337dd0
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
'notices' => 'Hinweise',
|
||||||
'title' => 'Wartungen',
|
'title' => 'Wartungen',
|
||||||
'subtitle' => 'Wartungsfenster anlegen, mehreren Hosts zuweisen und betroffene Kunden benachrichtigen.',
|
'subtitle' => 'Wartungsfenster anlegen, mehreren Hosts zuweisen und betroffene Kunden benachrichtigen.',
|
||||||
'empty' => 'Noch keine Wartungsfenster.',
|
'empty' => 'Noch keine Wartungsfenster.',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
'notices' => 'Notices',
|
||||||
'title' => 'Maintenance',
|
'title' => 'Maintenance',
|
||||||
'subtitle' => 'Create maintenance windows, assign them to multiple hosts and notify affected customers.',
|
'subtitle' => 'Create maintenance windows, assign them to multiple hosts and notify affected customers.',
|
||||||
'empty' => 'No maintenance windows yet.',
|
'empty' => 'No maintenance windows yet.',
|
||||||
|
|
|
||||||
|
|
@ -34,31 +34,6 @@
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@foreach ($maintenanceWindows as $mw)
|
|
||||||
@php $isActive = now()->greaterThanOrEqualTo($mw->starts_at) && now()->lessThanOrEqualTo($mw->ends_at); @endphp
|
|
||||||
<div class="flex flex-wrap items-center justify-center gap-x-3 gap-y-1 border-b {{ $isActive ? 'border-warning-border bg-warning-bg' : 'border-info-border bg-info-bg' }} px-4 py-2 text-center text-sm text-ink">
|
|
||||||
<x-ui.icon name="alert-triangle" class="size-4 shrink-0 {{ $isActive ? 'text-warning' : 'text-info' }}" />
|
|
||||||
<span class="font-medium">{{ $mw->title }}</span>
|
|
||||||
<span class="text-muted">
|
|
||||||
{{ $isActive
|
|
||||||
? __('maintenance.banner_active', ['end' => $mw->ends_at->isoFormat('LT')])
|
|
||||||
: __('maintenance.banner_upcoming', ['start' => $mw->starts_at->isoFormat('LLL'), 'end' => $mw->ends_at->isoFormat('LT')]) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
@if (session('impersonator_id'))
|
|
||||||
{{-- Admin impersonation banner — visible on every portal page until the admin returns. --}}
|
|
||||||
<div class="flex flex-wrap items-center justify-center gap-x-3 gap-y-1 border-b border-warning-border bg-warning-bg px-4 py-2 text-center text-sm font-medium text-ink">
|
|
||||||
<span>{{ __('impersonate.banner', ['name' => auth()->user()->name]) }}</span>
|
|
||||||
<form method="POST" action="{{ route('impersonate.leave') }}" class="inline">
|
|
||||||
@csrf
|
|
||||||
<button type="submit" class="inline-flex items-center gap-1 rounded-pill border border-warning-border px-3 py-0.5 font-semibold text-warning underline-offset-2 hover:underline">
|
|
||||||
<x-ui.icon name="log-out" class="size-4" />
|
|
||||||
{{ __('impersonate.leave') }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
<div class="flex min-h-screen lg:h-screen lg:overflow-hidden">
|
<div class="flex min-h-screen lg:h-screen lg:overflow-hidden">
|
||||||
{{-- Sidebar: fixed drawer on small screens, fixed full-height column on large (R7). --}}
|
{{-- Sidebar: fixed drawer on small screens, fixed full-height column on large (R7). --}}
|
||||||
<aside
|
<aside
|
||||||
|
|
@ -101,8 +76,58 @@
|
||||||
<button type="button" class="lg:hidden inline-flex min-h-11 min-w-11 items-center justify-center rounded text-muted hover:bg-surface-hover" @click="nav = true" aria-label="{{ __('dashboard.open_nav') }}">
|
<button type="button" class="lg:hidden inline-flex min-h-11 min-w-11 items-center justify-center rounded text-muted hover:bg-surface-hover" @click="nav = true" aria-label="{{ __('dashboard.open_nav') }}">
|
||||||
<x-ui.icon name="menu" />
|
<x-ui.icon name="menu" />
|
||||||
</button>
|
</button>
|
||||||
|
{{-- Impersonation is a MODE, not a notice: it stays visible rather
|
||||||
|
than hiding in a dropdown, because acting as someone else
|
||||||
|
without noticing is how mistakes happen. --}}
|
||||||
|
@if (session('impersonator_id'))
|
||||||
|
<div class="ml-2 flex min-w-0 items-center gap-2 rounded-pill border border-warning-border bg-warning-bg px-3 py-1">
|
||||||
|
<x-ui.icon name="users" class="size-4 shrink-0 text-warning" />
|
||||||
|
<span class="truncate text-xs font-medium text-ink">{{ __('impersonate.banner', ['name' => auth()->user()->name]) }}</span>
|
||||||
|
<form method="POST" action="{{ route('impersonate.leave') }}" class="inline shrink-0">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="inline-flex items-center gap-1 rounded-pill px-2 py-0.5 text-xs font-semibold text-warning hover:underline">
|
||||||
|
<x-ui.icon name="log-out" class="size-3.5" />
|
||||||
|
<span class="hidden sm:inline">{{ __('impersonate.leave') }}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="flex-1"></div>
|
<div class="flex-1"></div>
|
||||||
|
|
||||||
|
{{-- Notices --}}
|
||||||
|
@if ($maintenanceWindows->isNotEmpty())
|
||||||
|
<div class="relative mr-1" x-data="{ open: false }" @keydown.escape="open = false">
|
||||||
|
<button type="button" @click="open = !open" :aria-expanded="open" aria-haspopup="menu"
|
||||||
|
class="relative grid size-9 place-items-center rounded-md text-muted transition hover:bg-surface-hover hover:text-ink"
|
||||||
|
aria-label="{{ __('maintenance.notices') }}">
|
||||||
|
<x-ui.icon name="bell" class="size-5" />
|
||||||
|
<span class="absolute right-1 top-1 grid size-4 place-items-center rounded-pill bg-warning text-[10px] font-bold text-white">{{ $maintenanceWindows->count() }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div x-show="open" x-cloak @click.outside="open = false"
|
||||||
|
class="absolute right-0 z-50 mt-1 w-80 overflow-hidden rounded-lg border border-line bg-surface shadow-md">
|
||||||
|
<p class="border-b border-line px-4 py-2.5 text-xs font-semibold uppercase tracking-wide text-faint">{{ __('maintenance.notices') }}</p>
|
||||||
|
<div class="max-h-80 divide-y divide-line overflow-y-auto">
|
||||||
|
@foreach ($maintenanceWindows as $mw)
|
||||||
|
@php $isActive = now()->betweenIncluded($mw->starts_at, $mw->ends_at); @endphp
|
||||||
|
<div class="flex gap-2.5 px-4 py-3">
|
||||||
|
<x-ui.icon name="alert-triangle" class="mt-0.5 size-4 shrink-0 {{ $isActive ? 'text-warning' : 'text-info' }}" />
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="text-sm font-medium text-ink">{{ $mw->title }}</p>
|
||||||
|
<p class="mt-0.5 text-xs leading-relaxed text-muted">
|
||||||
|
{{ $isActive
|
||||||
|
? __('maintenance.banner_active', ['end' => $mw->ends_at->isoFormat('LT')])
|
||||||
|
: __('maintenance.banner_upcoming', ['start' => $mw->starts_at->isoFormat('LLL'), 'end' => $mw->ends_at->isoFormat('LT')]) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
@auth
|
@auth
|
||||||
<div class="relative" x-data="{ open: false }" @keydown.escape="open = false">
|
<div class="relative" x-data="{ open: false }" @keydown.escape="open = false">
|
||||||
<button type="button" class="flex items-center gap-2 rounded px-2 py-1.5 hover:bg-surface-hover" @click="open = !open" :aria-expanded="open" aria-haspopup="menu">
|
<button type="button" class="flex items-center gap-2 rounded px-2 py-1.5 hover:bg-surface-hover" @click="open = !open" :aria-expanded="open" aria-haspopup="menu">
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@
|
||||||
{{ $peer->last_handshake_at?->diffForHumans() ?? __('vpn.never') }}
|
{{ $peer->last_handshake_at?->diffForHumans() ?? __('vpn.never') }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<span class="inline-flex items-center gap-1.5 rounded-pill border px-2.5 py-0.5 text-xs font-medium {{ $tone }}">
|
<span class="inline-flex items-center gap-1.5 whitespace-nowrap rounded-pill border px-2.5 py-0.5 text-xs font-medium {{ $tone }}">
|
||||||
<span class="size-1.5 rounded-pill bg-current" aria-hidden="true"></span>
|
<span class="size-1.5 rounded-pill bg-current" aria-hidden="true"></span>
|
||||||
{{ __('vpn.status_'.$status) }}
|
{{ __('vpn.status_'.$status) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue