feat(ui): align auth pages to shared shell and truncate team names

Move email verification and team invitation into x-auth.shell with
shared guidance/alert patterns. Truncate long team names in the mobile
header switcher so actions stay visible. Cover both with markup tests.
This commit is contained in:
Andras Bacsai
2026-08-05 21:35:13 +02:00
parent 4d35c63d89
commit 8e57a9ab6b
8 changed files with 94 additions and 57 deletions
+16 -9
View File
@@ -1,12 +1,19 @@
<x-layout>
<section class="flex flex-col h-full lg:items-center lg:justify-center">
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto max-w-7xl lg:py-0">
<h1> Verification Email Sent </h1>
<div class="flex justify-center gap-2 text-center">
<br>To activate your account, please open the email and follow the
instructions.
<x-layout-simple>
<x-auth.shell title="Coolify" description="Verify your email address to activate your account.">
<div class="flex flex-col gap-4">
<div class="auth-guidance">
<x-reicon name="mail" class="mt-0.5 size-4 shrink-0" />
<p>We sent a verification link to your email address. Open it to continue to Coolify.</p>
</div>
<livewire:verify-email />
</div>
</section>
</x-layout>
<x-slot:footer>
<span class="text-center">
<span class="block sm:inline">Didnt receive the email?</span>
<span class="block sm:ml-1 sm:inline">Check your spam folder or resend it.</span>
</span>
</x-slot:footer>
</x-auth.shell>
</x-layout-simple>
+28 -39
View File
@@ -1,43 +1,32 @@
<x-layout-simple>
<section class="bg-gray-50 dark:bg-base">
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<div class="w-full max-w-md space-y-8">
<div class="text-center space-y-2">
<h1 class="!text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white">
Coolify
</h1>
</div>
<div class="space-y-6">
<div class="p-6 rounded-lg border border-neutral-500/20 bg-white/5">
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Team Invitation</h2>
<p class="text-sm text-gray-600 dark:text-neutral-400 mb-2">
You have been invited to join:
</p>
<p class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{{ $team->name }}
</p>
<p class="text-sm text-gray-600 dark:text-neutral-400 mb-1">
Role: <span class="font-medium text-gray-900 dark:text-white">{{ ucfirst($invitation->role) }}</span>
</p>
@if ($alreadyMember)
<div class="mt-4 p-3 bg-warning/10 border border-warning rounded-lg">
<p class="text-sm text-warning">You are already a member of this team.</p>
</div>
@endif
<form method="POST" action="{{ route('team.invitation.accept', $invitation->uuid) }}" class="mt-6">
@csrf
<x-forms.button class="w-full justify-center py-3 box-boarding" type="submit" isHighlighted>
{{ $alreadyMember ? 'Dismiss Invitation' : 'Accept Invitation' }}
</x-forms.button>
</form>
</div>
</div>
<x-auth.shell title="Coolify" description="Review your invitation to join a team.">
<div class="flex flex-col gap-4">
<div class="auth-guidance">
<x-reicon name="teams" class="mt-0.5 size-4 shrink-0" />
<p>You have been invited to collaborate on Coolify.</p>
</div>
<dl class="divide-y divide-neutral-200 rounded-lg border border-neutral-200 text-sm dark:divide-white/10 dark:border-white/10">
<div class="flex items-center justify-between gap-4 px-3 py-2.5">
<dt class="text-neutral-500 dark:text-fg-dim">Team</dt>
<dd class="min-w-0 truncate font-medium text-neutral-900 dark:text-white">{{ $team->name }}</dd>
</div>
<div class="flex items-center justify-between gap-4 px-3 py-2.5">
<dt class="text-neutral-500 dark:text-fg-dim">Role</dt>
<dd class="font-medium text-neutral-900 dark:text-white">{{ ucfirst($invitation->role) }}</dd>
</div>
</dl>
@if ($alreadyMember)
<x-auth.alert type="warning">You are already a member of this team. Dismiss the invitation to continue.</x-auth.alert>
@endif
<form method="POST" action="{{ route('team.invitation.accept', $invitation->uuid) }}">
@csrf
<x-forms.button class="w-full justify-center" type="submit" isHighlighted>
{{ $alreadyMember ? 'Dismiss invitation' : 'Accept invitation' }}
</x-forms.button>
</form>
</div>
</section>
</x-auth.shell>
</x-layout-simple>
+4 -3
View File
@@ -94,15 +94,16 @@
{{-- ============ MOBILE TOP BAR ============ --}}
<div
class="sticky top-0 z-40 flex items-center justify-between px-4 py-3 gap-x-4 sm:px-6 lg:hidden bg-white/95 dark:bg-panel/95 backdrop-blur-sm border-b border-neutral-200/60 dark:border-white/[0.06]">
<div class="flex items-center gap-2.5 flex-shrink-0 min-w-0">
<a href="/" class="flex items-center justify-center size-8 rounded-lg bg-neutral-100 dark:bg-white/[0.06] hover:opacity-80 transition-opacity">
<div class="flex min-w-0 flex-1 items-center gap-2.5">
<a href="/"
class="flex size-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 transition-opacity hover:opacity-80 dark:bg-white/[0.06]">
<img src="/coolify-logo.svg" alt="Coolify" class="w-[18px] h-[18px]" />
</a>
<div class="min-w-0" x-data="{ collapsed: false }">
<livewire:switch-team />
</div>
</div>
<div class="flex min-w-0 items-center gap-1">
<div class="flex shrink-0 items-center gap-1">
{{-- Dev Server-Timing HUD docks here on <lg (desktop uses #server-timing-hud-slot) --}}
<div id="server-timing-hud-slot-mobile" data-server-timing-hud-slot
class="hidden shrink-0 items-center"></div>
@@ -4,12 +4,12 @@
@endphp
<div class="min-w-0">
{{-- Expanded: inline switcher (team name + up/down chevron) --}}
<div class="relative" :class="collapsed && 'lg:hidden'"
<div class="relative min-w-0" :class="collapsed && 'lg:hidden'"
x-data="{ open: false }" @keydown.escape.window="open = false">
<button type="button" @click="open = !open" @click.outside="open = false"
title="Switch team"
class="group/team flex items-center gap-1.5 h-8 px-2 -ml-1 rounded-lg text-left opacity-70 transition-[background-color,opacity] hover:opacity-100 hover:bg-neutral-100 dark:hover:bg-white/[0.05]">
<span class="whitespace-nowrap text-[13px] font-semibold text-black dark:text-fg">{{ $currentTeam->name }}</span>
class="group/team flex h-8 min-w-0 max-w-full items-center gap-1.5 rounded-lg px-2 -ml-1 text-left opacity-70 transition-[background-color,opacity] hover:bg-neutral-100 hover:opacity-100 dark:hover:bg-white/[0.05]">
<span class="min-w-0 truncate text-[13px] font-semibold text-black dark:text-fg">{{ $currentTeam->name }}</span>
<svg class="size-4 shrink-0 text-neutral-400 dark:text-fg-faint" viewBox="0 0 24 24" fill="none">
<path d="M8 9l4-4 4 4M8 15l4 4 4-4" stroke="currentColor" stroke-width="1.6"
stroke-linecap="round" stroke-linejoin="round" />
@@ -1,3 +1,5 @@
<div class="pt-4">
<x-forms.button wire:click="again">Send Verification Email Again</x-forms.button>
<div>
<x-forms.button class="w-full justify-center" wire:click="again" isHighlighted>
Resend verification email
</x-forms.button>
</div>
+25
View File
@@ -45,3 +45,28 @@ test('two factor challenge page uses the shared auth shell', function () {
->not->toContain('bg-gray-50 dark:bg-base')
->not->toContain('!text-5xl font-extrabold');
});
test('email verification page uses the shared auth shell', function () {
$verification = file_get_contents(resource_path('views/auth/verify-email.blade.php'));
expect($verification)
->toContain('x-layout-simple')
->toContain('x-auth.shell')
->toContain('title="Coolify"')
->toContain('auth-guidance')
->toContain('block sm:inline')
->not->toContain('<x-layout>')
->not->toContain('md:h-screen');
});
test('team invitation page uses the shared auth shell', function () {
$invitation = file_get_contents(resource_path('views/invitation/accept.blade.php'));
expect($invitation)
->toContain('x-auth.shell')
->toContain('title="Coolify"')
->toContain('x-auth.alert')
->toContain('Accept invitation')
->not->toContain('bg-gray-50 dark:bg-base')
->not->toContain('!text-5xl font-extrabold');
});
@@ -40,6 +40,19 @@ it('separates the mobile sidebar from the page with a visible border', function
expect($layout)->toContain('max-w-56 min-w-0 flex-col border-l border-neutral-200 bg-white shadow-xl dark:border-white/[0.12] dark:bg-panel');
});
it('truncates long team names within the mobile header', function () {
$layout = file_get_contents(resource_path('views/layouts/app.blade.php'));
$switcher = file_get_contents(resource_path('views/livewire/switch-team.blade.php'));
expect($layout)
->toContain('flex min-w-0 flex-1 items-center gap-2.5')
->toContain('size-8 shrink-0 items-center justify-center rounded-lg')
->toContain('flex shrink-0 items-center gap-1')
->and($switcher)
->toContain('group/team flex h-8 min-w-0 max-w-full items-center')
->toContain('min-w-0 truncate text-[13px]');
});
it('shows section titles and descriptions above settings navigation on smaller screens', function (string $view, string $title, string $description) {
$layout = file_get_contents(resource_path("views/components/{$view}"));
$css = file_get_contents(resource_path('css/app.css'));
@@ -30,7 +30,7 @@ test('GET invitation shows landing page without accepting', function () {
$response->assertStatus(200);
$response->assertViewIs('invitation.accept');
$response->assertSee($this->team->name);
$response->assertSee('Accept Invitation');
$response->assertSee('Accept invitation');
// Invitation should NOT be deleted (not accepted yet)
$this->assertDatabaseHas('team_invitations', [