Give every page the share pages' layout, at one width

The pages were built five ways: two layouts, seven widths from 28 to
64rem and four heading styles. Every page now looks like the share
pages: a centred heading over one 40rem column of outlined cards, with
the floating toolbar below.

- <x-page> (components/page.blade.php) is the root of every page. It
  draws the h1 and its line (`brand` takes the site's logo, title and
  description from Admin settings), an optional `mark` and `navigation`
  slot, then the content. It has no width prop: every page is the same
  <x-pane width="narrow">.
- The sign-in, password reset, confirm, verify email, two-factor
  challenge, setup and system password pages move onto layouts/app with
  the brand heading and their form in a card titled with the task.
  layouts/auth, auth-header and the settings heading partial are gone,
  and so is the per-page width CSS.
- Settings put their section nav under the heading; the admin pages get
  a description line each. FileUploader and ShareDownload no longer pass
  the branding to their views.
- The admin dashboard's table needed about 49rem, so its shares are a
  list: created above the token, which opens the share, then files,
  size, downloads and expiry on two lines that wrap instead of clipping,
  and one delete button. A "Sort by" select replaces the column headers
  (newest, oldest, expiring soonest with never-expiring last, largest,
  most downloads, most files) and resets the page. The stats stay two
  by two. table.css and sort-header.css are no longer imported.
- Branding hints in Admin settings name every page the title shows on.
- Tests: PageTemplateTest renders every page once and checks one page
  template, one h1 and the width, and the brand heading with its
  fallbacks. FrameTest measures the page column instead of the auth
  card and the 64rem main; dashboard tests follow the list and the sort
  select, including expiry order. .ai/rules/views.md records <x-page>,
  the CHANGELOG notes the change and the website screenshots are
  regenerated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andreas Reinhold / reini
2026-09-16 21:42:04 +02:00
co-authored by Claude Opus 5
parent 40e35bab0e
commit 5853f1f7d6
77 changed files with 1234 additions and 1243 deletions
+10 -10
View File
@@ -11,17 +11,17 @@
$items[] = ['title' => __('Appearance'), 'icon' => 'contrast', 'url' => route('appearance.edit'), 'active' => request()->routeIs('appearance.edit')];
@endphp
<x-stack gap="space400">
<x-section-nav :items="$items" :label="__('Settings')" />
<x-page :title="__('Settings')" :description="__('Manage your profile and account settings')">
<x-slot:navigation>
<x-section-nav :items="$items" :label="__('Settings')" />
</x-slot:navigation>
{{-- Every settings page is a card headed by its own title, as the admin's settings are. A page
with a section that stands apart from that one subject deleting the account, the recovery
codes puts it in `after`, where it becomes a card of its own beside this one. --}}
<x-stack gap="space300" class="settings-column">
<x-card :title="$heading ?? ''" :subtitle="$subheading ?? ''" heading="h2" variant="outlined">
{{ $slot }}
</x-card>
codes puts it in `after`, where it becomes a card of its own under this one. --}}
<x-card :title="$heading ?? ''" :subtitle="$subheading ?? ''" heading="h2" variant="outlined">
{{ $slot }}
</x-card>
{{ $after ?? '' }}
</x-stack>
</x-stack>
{{ $after ?? '' }}
</x-page>
@@ -6,10 +6,6 @@ new class extends Component {
//
}; ?>
<section>
@include('partials.settings-heading')
<x-pages::settings.layout :heading="__('Appearance')" :subheading="__('Update the appearance settings for your account')">
<x-theme-toggle mode="picker" class="settings-appearance-picker" data-test="appearance-picker" />
</x-pages::settings.layout>
</section>
<x-pages::settings.layout :heading="__('Appearance')" :subheading="__('Update the appearance settings for your account')">
<x-theme-toggle mode="picker" class="settings-appearance-picker" data-test="appearance-picker" />
</x-pages::settings.layout>
@@ -43,18 +43,14 @@ new class extends Component {
}
}; ?>
<section>
@include('partials.settings-heading')
<x-pages::settings.layout :heading="__('Update password')" :subheading="__('Ensure your account is using a long, random password to stay secure')">
<x-form method="POST" wire:submit="updatePassword">
<x-password full wire:model="current_password" :label="__('Current password')" required autocomplete="current-password" />
<x-password full wire:model="password" :label="__('New password')" required autocomplete="new-password" />
<x-password full wire:model="password_confirmation" :label="__('Confirm Password')" required autocomplete="new-password" />
<x-pages::settings.layout :heading="__('Update password')" :subheading="__('Ensure your account is using a long, random password to stay secure')">
<x-form method="POST" wire:submit="updatePassword">
<x-password full wire:model="current_password" :label="__('Current password')" required autocomplete="current-password" />
<x-password full wire:model="password" :label="__('New password')" required autocomplete="new-password" />
<x-password full wire:model="password_confirmation" :label="__('Confirm Password')" required autocomplete="new-password" />
<x-slot:actions>
<x-button type="submit" :label="__('Save')" variant="filled" spinner="updatePassword" data-test="update-password-button" />
</x-slot:actions>
</x-form>
</x-pages::settings.layout>
</section>
<x-slot:actions>
<x-button type="submit" :label="__('Save')" variant="filled" spinner="updatePassword" data-test="update-password-button" />
</x-slot:actions>
</x-form>
</x-pages::settings.layout>
@@ -80,40 +80,36 @@ new class extends Component {
}
}; ?>
<section>
@include('partials.settings-heading')
<x-pages::settings.layout :heading="__('Profile')" :subheading="__('Update your name and email address')">
<x-form wire:submit="updateProfileInformation">
<x-input full wire:model="name" :label="__('Name')" type="text" required autofocus autocomplete="name" icon="person" />
<x-pages::settings.layout :heading="__('Profile')" :subheading="__('Update your name and email address')">
<x-form wire:submit="updateProfileInformation">
<x-input full wire:model="name" :label="__('Name')" type="text" required autofocus autocomplete="name" icon="person" />
<x-stack gap="space100">
<x-input full wire:model="email" :label="__('Email')" type="email" required autocomplete="email" icon="mail" />
<x-stack gap="space100">
<x-input full wire:model="email" :label="__('Email')" type="email" required autocomplete="email" icon="mail" />
@if ($this->hasUnverifiedEmail)
<p class="md-type-body-md md-ink-variant">
{{ __('Your email address is unverified.') }}
@if ($this->hasUnverifiedEmail)
<p class="md-type-body-md md-ink-variant">
{{ __('Your email address is unverified.') }}
<button type="button" class="md-link" wire:click.prevent="resendVerificationNotification">
{{ __('Click here to re-send the verification email.') }}
</button>
</p>
<button type="button" class="md-link" wire:click.prevent="resendVerificationNotification">
{{ __('Click here to re-send the verification email.') }}
</button>
</p>
@if (session('status') === 'verification-link-sent')
<x-alert color="success">{{ __('A new verification link has been sent to your email address.') }}</x-alert>
@endif
@if (session('status') === 'verification-link-sent')
<x-alert color="success">{{ __('A new verification link has been sent to your email address.') }}</x-alert>
@endif
</x-stack>
<x-slot:actions>
<x-button type="submit" :label="__('Save')" variant="filled" spinner="updateProfileInformation" data-test="update-profile-button" />
</x-slot:actions>
</x-form>
<x-slot:after>
@if ($this->showDeleteUser)
<livewire:pages::settings.delete-user-form />
@endif
</x-slot:after>
</x-pages::settings.layout>
</section>
</x-stack>
<x-slot:actions>
<x-button type="submit" :label="__('Save')" variant="filled" spinner="updateProfileInformation" data-test="update-profile-button" />
</x-slot:actions>
</x-form>
<x-slot:after>
@if ($this->showDeleteUser)
<livewire:pages::settings.delete-user-form />
@endif
</x-slot:after>
</x-pages::settings.layout>
@@ -178,94 +178,90 @@ new class extends Component {
}
} ?>
<section>
@include('partials.settings-heading')
<x-pages::settings.layout
:heading="__('Two Factor Authentication')"
:subheading="__('Manage your two-factor authentication settings')"
>
<x-stack gap="space300" wire:cloak>
@if ($twoFactorEnabled)
<x-stack gap="space200" align="start">
<x-badge :value="__('Enabled')" tonal color="success" />
<x-pages::settings.layout
:heading="__('Two Factor Authentication')"
:subheading="__('Manage your two-factor authentication settings')"
>
<x-stack gap="space300" wire:cloak>
@if ($twoFactorEnabled)
<x-stack gap="space200" align="start">
<x-badge :value="__('Enabled')" tonal color="success" />
<p class="md-type-body-md md-ink-variant">
{{ __('With two-factor authentication enabled, you will be prompted for a secure, random pin during login, which you can retrieve from the TOTP-supported application on your phone.') }}
</p>
<p class="md-type-body-md md-ink-variant">
{{ __('With two-factor authentication enabled, you will be prompted for a secure, random pin during login, which you can retrieve from the TOTP-supported application on your phone.') }}
</p>
<x-button :label="__('Disable 2FA')" icon="remove_moderator" danger wire:click="disable" />
</x-stack>
@else
<x-stack gap="space200" align="start">
<x-badge :value="__('Disabled')" tonal color="error" />
<p class="md-type-body-md md-ink-variant">
{{ __('When you enable two-factor authentication, you will be prompted for a secure pin during login. This pin can be retrieved from a TOTP-supported application on your phone.') }}
</p>
<x-button :label="__('Enable 2FA')" icon="shield_lock" variant="filled" wire:click="enable" />
</x-stack>
@endif
</x-stack>
{{-- The recovery codes are their own subject, so they are their own card beside this one. --}}
<x-slot:after>
@if ($twoFactorEnabled)
<livewire:pages::settings.two-factor.recovery-codes :$requiresConfirmation />
@endif
</x-slot:after>
</x-pages::settings.layout>
<x-modal wire:model="showModal" :title="$this->modalConfig['title']" :subtitle="$this->modalConfig['description']" fullscreen>
@if ($showVerificationStep)
<x-input
name="code"
wire:model="code"
:label="__('Code')"
inputmode="numeric"
autocomplete="one-time-code"
maxlength="6"
mono
autofocus
/>
<x-slot:actions>
<x-button :label="__('Back')" wire:click="resetVerification" />
<x-button :label="__('Confirm')" variant="filled" wire:click="confirmTwoFactor" x-bind:disabled="$wire.code.length < 6" />
</x-slot:actions>
@else
<x-stack gap="space300">
@error('setupData')
<x-alert color="error">{{ $message }}</x-alert>
@enderror
{{-- The QR code keeps a white ground in both themes: scanners read dark on light. --}}
<x-row justify="center">
<div class="settings-two-factor-qr">
@empty($qrCodeSvg)
<x-loading :label="__('Loading')" />
@else
{!! $qrCodeSvg !!}
@endempty
</div>
</x-row>
<x-stack gap="space200">
<p class="md-type-label-lg md-ink-variant md-text-center">{{ __('or, enter the code manually') }}</p>
<x-input :label="__('Setup key')" :value="$manualSetupKey" readonly copyable mono />
</x-stack>
<x-button :label="__('Disable 2FA')" icon="remove_moderator" danger wire:click="disable" />
</x-stack>
@else
<x-stack gap="space200" align="start">
<x-badge :value="__('Disabled')" tonal color="error" />
<x-slot:actions>
<x-button
:disabled="$errors->has('setupData')"
:label="$this->modalConfig['buttonText']"
variant="filled"
wire:click="showVerificationIfNecessary"
/>
</x-slot:actions>
<p class="md-type-body-md md-ink-variant">
{{ __('When you enable two-factor authentication, you will be prompted for a secure pin during login. This pin can be retrieved from a TOTP-supported application on your phone.') }}
</p>
<x-button :label="__('Enable 2FA')" icon="shield_lock" variant="filled" wire:click="enable" />
</x-stack>
@endif
</x-modal>
</section>
</x-stack>
{{-- The recovery codes are their own subject, so they are their own card under this one. --}}
<x-slot:after>
@if ($twoFactorEnabled)
<livewire:pages::settings.two-factor.recovery-codes :$requiresConfirmation />
@endif
<x-modal wire:model="showModal" :title="$this->modalConfig['title']" :subtitle="$this->modalConfig['description']" fullscreen>
@if ($showVerificationStep)
<x-input
name="code"
wire:model="code"
:label="__('Code')"
inputmode="numeric"
autocomplete="one-time-code"
maxlength="6"
mono
autofocus
/>
<x-slot:actions>
<x-button :label="__('Back')" wire:click="resetVerification" />
<x-button :label="__('Confirm')" variant="filled" wire:click="confirmTwoFactor" x-bind:disabled="$wire.code.length < 6" />
</x-slot:actions>
@else
<x-stack gap="space300">
@error('setupData')
<x-alert color="error">{{ $message }}</x-alert>
@enderror
{{-- The QR code keeps a white ground in both themes: scanners read dark on light. --}}
<x-row justify="center">
<div class="settings-two-factor-qr">
@empty($qrCodeSvg)
<x-loading :label="__('Loading')" />
@else
{!! $qrCodeSvg !!}
@endempty
</div>
</x-row>
<x-stack gap="space200">
<p class="md-type-label-lg md-ink-variant md-text-center">{{ __('or, enter the code manually') }}</p>
<x-input :label="__('Setup key')" :value="$manualSetupKey" readonly copyable mono />
</x-stack>
</x-stack>
<x-slot:actions>
<x-button
:disabled="$errors->has('setupData')"
:label="$this->modalConfig['buttonText']"
variant="filled"
wire:click="showVerificationIfNecessary"
/>
</x-slot:actions>
@endif
</x-modal>
</x-slot:after>
</x-pages::settings.layout>