Move the navigation into a floating toolbar at the bottom

The top app bar repeated the site's name, which already heads the upload
and download pages, and spread its actions to the window's far edge. A
floating toolbar centred above the bottom edge, as wide as its buttons,
now holds them: Upload and the admin pages with the current one filled,
and the account menu; guests get Upload, the theme toggle and Log in,
without tooltips. On a phone the sign-in card no longer stretches to the
full height, and the admin dashboard's empty state sits outside the
scrolling table.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
This commit is contained in:
Andreas Reinhold / reini
2026-09-13 11:12:12 +02:00
co-authored by Claude Opus 5
parent 2a363defd1
commit accbc17c1c
8 changed files with 161 additions and 98 deletions
+4 -4
View File
@@ -3,13 +3,13 @@
<head>
@include('partials.head')
</head>
<body class="min-h-dvh bg-surface font-sans text-on-surface antialiased">
@include('partials.app-bar')
<main class="mx-auto w-full max-w-5xl px-4 pt-4 pb-16 sm:px-6">
<body class="min-h-dvh bg-surface font-sans text-on-surface antialiased [--material-bottom-bar:calc(5rem+env(safe-area-inset-bottom))]">
<main class="mx-auto w-full max-w-5xl px-4 pt-8 pb-32 sm:px-6 sm:pt-12">
{{ $slot }}
</main>
@include('partials.toolbar')
<x-toast />
</body>
</html>
+4 -4
View File
@@ -3,10 +3,8 @@
<head>
@include('partials.head')
</head>
<body class="flex min-h-dvh flex-col bg-surface font-sans text-on-surface antialiased">
@include('partials.app-bar')
<main class="flex flex-1 justify-center px-4 pt-6 pb-16 sm:items-center sm:pt-0">
<body class="flex min-h-dvh flex-col bg-surface font-sans text-on-surface antialiased [--material-bottom-bar:calc(5rem+env(safe-area-inset-bottom))]">
<main class="flex flex-1 items-start justify-center px-4 pt-8 pb-32 sm:items-center">
<div class="w-full max-w-md rounded-corner-xl bg-surface-container-low p-6 sm:p-8">
<div class="flex flex-col gap-6">
{{ $slot }}
@@ -14,6 +12,8 @@
</div>
</main>
@include('partials.toolbar')
<x-toast />
</body>
</html>
@@ -11,51 +11,50 @@
</div>
<x-card :title="__('All Shares')" variant="outlined">
<div class="-mx-4 overflow-x-auto">
<x-table>
<thead>
<tr>
<x-sort-header column="token" :sort-by="$sortBy">{{ __('Token') }}</x-sort-header>
<x-sort-header column="files_count" :sort-by="$sortBy" class="text-end">{{ __('Files') }}</x-sort-header>
<x-sort-header column="total_size" :sort-by="$sortBy" class="text-end">{{ __('Size') }}</x-sort-header>
<x-sort-header column="download_count" :sort-by="$sortBy" class="text-end">{{ __('Downloads') }}</x-sort-header>
<x-sort-header column="expires_at" :sort-by="$sortBy">{{ __('Expires') }}</x-sort-header>
<x-sort-header column="created_at" :sort-by="$sortBy">{{ __('Created') }}</x-sort-header>
<th><span class="sr-only">{{ __('Actions') }}</span></th>
</tr>
</thead>
<tbody>
@forelse ($shares as $share)
<tr wire:key="share-{{ $share->id }}">
<td class="font-mono">{{ $share->token }}</td>
<td class="text-end tabular-nums">{{ $share->files_count }}</td>
<td class="text-end tabular-nums whitespace-nowrap">{{ Number::fileSize($share->total_size) }}</td>
<td class="text-end tabular-nums">{{ $share->download_count }}</td>
<td class="whitespace-nowrap">
@if ($share->expires_at)
<span @class(['text-error' => $share->isExpired()])>{{ $share->expires_at->diffForHumans() }}</span>
@else
<span class="text-on-surface-variant">{{ __('Never') }}</span>
@endif
</td>
<td class="whitespace-nowrap">{{ $share->created_at->diffForHumans() }}</td>
<td class="text-end whitespace-nowrap">
<x-button icon="open_in_new" :tooltip="__('Open')" :link="route('share.download', $share)" external />
<x-button icon="delete" :tooltip="__('Delete')" color="error" wire:click="$set('deletingShareId', {{ $share->id }})" data-test="delete-share-{{ $share->id }}" />
</td>
</tr>
@empty
{{-- Outside the table, so it stays centred on a phone instead of scrolling with the columns. --}}
@if ($shares->total() === 0)
<x-empty-state icon="link_off" :title="__('No shares yet')" :description="__('Shares appear here once someone uploads files.')" />
@else
<div class="-mx-4 overflow-x-auto">
<x-table>
<thead>
<tr>
<td colspan="7">
<x-empty-state icon="link_off" :title="__('No shares yet')" :description="__('Shares appear here once someone uploads files.')" />
</td>
<x-sort-header column="token" :sort-by="$sortBy">{{ __('Token') }}</x-sort-header>
<x-sort-header column="files_count" :sort-by="$sortBy" class="text-end">{{ __('Files') }}</x-sort-header>
<x-sort-header column="total_size" :sort-by="$sortBy" class="text-end">{{ __('Size') }}</x-sort-header>
<x-sort-header column="download_count" :sort-by="$sortBy" class="text-end">{{ __('Downloads') }}</x-sort-header>
<x-sort-header column="expires_at" :sort-by="$sortBy">{{ __('Expires') }}</x-sort-header>
<x-sort-header column="created_at" :sort-by="$sortBy">{{ __('Created') }}</x-sort-header>
<th><span class="sr-only">{{ __('Actions') }}</span></th>
</tr>
@endforelse
</tbody>
</x-table>
</div>
</thead>
<tbody>
@foreach ($shares as $share)
<tr wire:key="share-{{ $share->id }}">
<td class="font-mono">{{ $share->token }}</td>
<td class="text-end tabular-nums">{{ $share->files_count }}</td>
<td class="text-end tabular-nums whitespace-nowrap">{{ Number::fileSize($share->total_size) }}</td>
<td class="text-end tabular-nums">{{ $share->download_count }}</td>
<td class="whitespace-nowrap">
@if ($share->expires_at)
<span @class(['text-error' => $share->isExpired()])>{{ $share->expires_at->diffForHumans() }}</span>
@else
<span class="text-on-surface-variant">{{ __('Never') }}</span>
@endif
</td>
<td class="whitespace-nowrap">{{ $share->created_at->diffForHumans() }}</td>
<td class="text-end whitespace-nowrap">
<x-button icon="open_in_new" :tooltip="__('Open')" :link="route('share.download', $share)" external />
<x-button icon="delete" :tooltip="__('Delete')" color="error" wire:click="$set('deletingShareId', {{ $share->id }})" data-test="delete-share-{{ $share->id }}" />
</td>
</tr>
@endforeach
</tbody>
</x-table>
</div>
<div class="mt-4">{{ $shares->links() }}</div>
<div class="mt-4">{{ $shares->links() }}</div>
@endif
</x-card>
<x-modal wire:model="deletingShareId" :title="__('Delete this share?')" icon="delete">
@@ -1,47 +0,0 @@
{{-- The one top app bar on every page: the site's logo and title, then the account menu for a
signed-in user, or the theme toggle and a way to sign in for everyone else. --}}
@php
$siteTitle = \App\Models\Setting::get('site_title') ?: config('app.name', 'SealShare');
$siteLogo = \App\Models\Setting::get('site_logo');
@endphp
<x-app-bar>
<x-slot:navigation>
<a href="{{ route('home') }}" class="focus-ring flex min-w-0 items-center gap-3 rounded-corner-full py-2 ps-3 pe-4" data-test="app-bar-home">
@if ($siteLogo)
<img src="{{ Storage::disk('public')->url($siteLogo) }}" alt="" class="h-8 w-auto" />
@else
<x-app-logo-icon class="size-8 shrink-0 text-primary" />
@endif
<span class="truncate type-title-lg text-on-surface">{{ $siteTitle }}</span>
</a>
</x-slot:navigation>
<x-slot:actions>
@auth
<span class="me-2 inline-flex">
<x-account-menu :name="auth()->user()->name" :email="auth()->user()->email">
<x-menu-item :label="__('Upload')" icon="upload" :link="route('upload')" />
@if (auth()->user()->is_admin)
<x-menu-item :label="__('Admin dashboard')" icon="dashboard" :link="route('admin.dashboard')" />
<x-menu-item :label="__('Admin settings')" icon="admin_panel_settings" :link="route('admin.settings')" />
@endif
<x-menu-item :label="__('Settings')" icon="settings" :link="route('profile.edit')" />
<x-slot:footer>
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-menu-item :label="__('Log out')" icon="logout" type="submit" data-test="logout-button" />
</form>
</x-slot:footer>
</x-account-menu>
</span>
@else
<x-theme-toggle />
@if (Route::has('login') && ! request()->routeIs('login'))
<span class="me-1 inline-flex"><x-button :label="__('Log in')" :link="route('login')" /></span>
@endif
@endauth
</x-slot:actions>
</x-app-bar>
@@ -0,0 +1,44 @@
{{-- The one navigation on every page: a floating toolbar centred above the bottom edge, as wide as
its buttons. The site's name and logo head the upload and download pages instead of a bar.
A signed-in user gets the pages they can reach and the account menu; everyone else gets the
upload page, the theme toggle and a way to sign in without tooltips, so the download page
stays free of anchored components. The current page's button is filled. --}}
@php
$onUpload = request()->routeIs('upload', 'share.created');
$onDashboard = request()->routeIs('admin.dashboard');
$onAdminSettings = request()->routeIs('admin.settings');
@endphp
<nav aria-label="{{ __('Main') }}">
<x-toolbar :label="__('Main')" place="bottom" data-test="app-toolbar">
@auth
<x-button icon="upload" :tooltip="__('Upload')" :link="route('upload')" :variant="$onUpload ? 'filled' : 'text'" :aria-current="$onUpload ? 'page' : null" />
@if (auth()->user()->is_admin)
<x-button icon="dashboard" :tooltip="__('Admin dashboard')" :link="route('admin.dashboard')" :variant="$onDashboard ? 'filled' : 'text'" :aria-current="$onDashboard ? 'page' : null" />
<x-button icon="admin_panel_settings" :tooltip="__('Admin settings')" :link="route('admin.settings')" :variant="$onAdminSettings ? 'filled' : 'text'" :aria-current="$onAdminSettings ? 'page' : null" />
@endif
<span class="ms-1 inline-flex">
<x-account-menu :name="auth()->user()->name" :email="auth()->user()->email" position="top-end">
<x-menu-item :label="__('Settings')" icon="settings" :link="route('profile.edit')" />
<x-slot:footer>
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-menu-item :label="__('Log out')" icon="logout" type="submit" data-test="logout-button" />
</form>
</x-slot:footer>
</x-account-menu>
</span>
@else
<x-button icon="upload" :aria-label="__('Upload')" :link="route('upload')" :variant="$onUpload ? 'filled' : 'text'" :aria-current="$onUpload ? 'page' : null" />
<x-theme-toggle />
@if (Route::has('login') && ! request()->routeIs('login'))
<x-button :label="__('Log in')" :link="route('login')" />
@endif
@endauth
</x-toolbar>
</nav>