Give settings the shape a settings page actually has

Four attempts, and this is what was wrong with the last one: full-width
panels stacked down the page, each carrying its own title INSIDE it. Every
group had two frames — the title bar and the border — so the page read as
a column of long boxes with nothing saying where one topic ended and the
next began, and a phone number got a field a thousand pixels wide.

The shape now:

- **A section list on the left**, sticky, so the sections stay reachable
  however far a form runs. Below `lg` it becomes a scrolling strip of
  chips, because a vertical list on a telephone is four rows of nothing.
  Under it, who is signed in — on a shared office machine that is a real
  question, and a settings page is where it gets asked.
- **A content column of 820px**, not the whole 1240 shell.
- **The heading OUTSIDE the card it belongs to.** A section is a heading, a
  sentence, and then a card of rows. That single move is what stops them
  reading as boxes: the border now frames the fields, not the topic.
- **What cannot be undone is last, in a card whose border says so** — the
  deletion deadlines and the close button together, at the foot of the
  contract section, in danger colours.

The rows are unchanged (label left, control right) and so is every binding;
what changed is the frame around them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feature/host-bootstrap
nexxo 2026-07-30 17:59:16 +02:00
parent 4b0eebeccc
commit 5883989552
5 changed files with 507 additions and 412 deletions

View File

@ -15,4 +15,6 @@ return [
'in_force_note' => 'Dieser Vertrag ist mit Ihrer Bestellung Bestandteil unserer Vereinbarung; eine gesonderte Unterschrift ist nicht nötig. Wenn Sie für Ihre Unterlagen eine ausdrückliche Bestätigung möchten, halten wir Fassung, Zeitpunkt und IP-Adresse fest.', 'in_force_note' => 'Dieser Vertrag ist mit Ihrer Bestellung Bestandteil unserer Vereinbarung; eine gesonderte Unterschrift ist nicht nötig. Wenn Sie für Ihre Unterlagen eine ausdrückliche Bestätigung möchten, halten wir Fassung, Zeitpunkt und IP-Adresse fest.',
'download_agreement' => 'AV-Vertrag laden', 'download_agreement' => 'AV-Vertrag laden',
'download_measures' => 'TOM laden', 'download_measures' => 'TOM laden',
'open' => 'Öffnen',
'state_label' => 'Status',
]; ];

View File

@ -11,6 +11,7 @@ return [
'branding' => 'Erscheinungsbild', 'branding' => 'Erscheinungsbild',
'contract' => 'Vertrag', 'contract' => 'Vertrag',
], ],
'signed_in_as' => 'Angemeldet als',
'title' => 'Einstellungen', 'title' => 'Einstellungen',
'subtitle' => 'Alles, was zu Ihrem Konto gehört — nach Themen getrennt.', 'subtitle' => 'Alles, was zu Ihrem Konto gehört — nach Themen getrennt.',
'save' => 'Speichern', 'save' => 'Speichern',

View File

@ -15,4 +15,6 @@ return [
'in_force_note' => 'This agreement became part of our contract with your order; a separate signature is not required. If you would like an explicit confirmation for your records, we note the version, the moment and the IP address.', 'in_force_note' => 'This agreement became part of our contract with your order; a separate signature is not required. If you would like an explicit confirmation for your records, we note the version, the moment and the IP address.',
'download_agreement' => 'Download agreement', 'download_agreement' => 'Download agreement',
'download_measures' => 'Download measures', 'download_measures' => 'Download measures',
'open' => 'Open',
'state_label' => 'State',
]; ];

View File

@ -11,6 +11,7 @@ return [
'branding' => 'Appearance', 'branding' => 'Appearance',
'contract' => 'Contract', 'contract' => 'Contract',
], ],
'signed_in_as' => 'Signed in as',
'title' => 'Settings', 'title' => 'Settings',
'subtitle' => 'Everything that belongs to your account, grouped by subject.', 'subtitle' => 'Everything that belongs to your account, grouped by subject.',
'save' => 'Save', 'save' => 'Save',

View File

@ -1,27 +1,46 @@
{{-- {{--
The customer's own settings. The customer's own settings.
## What was wrong, three times over ## Four attempts, and what was wrong with each
First it was one column 768px wide in a 1240px shell, holding everything a 1. One column 768px wide in a 1240px shell, everything in it, two thousand
customer might ever change, two thousand pixels deep. Then it became four pixels deep.
tabs better but the cards inside them were still boxes of different 2. Four tabs better but cards of different heights in a grid, which on
heights in a grid, which on the contract tab produced a short one, a wide one the contract tab read as a staircase.
and a short one again: a staircase with no rhythm and half the width empty 3. Panels and rows: right idea, wrong proportions. Full-width slabs stacked
beside it. down the page, each with its title INSIDE it, so the page was a column of
long boxes and nothing said where one topic ended.
## What it is now ## What it is now
Panels and rows (x-ui.panel, x-ui.row). A group is a card with a header that The shape serious settings pages actually use: a section list on the left,
names it; a setting is a ROW inside that card what it is on the left, the a measured content column on the right, and the heading OUTSIDE the card it
control on the right, dividers between. That is the shape every settings page belongs to.
worth copying uses, and it is the shape that uses the width: a label column
of 280px with the control beside it fills the line, where a stack of
full-width inputs leaves two thirds of it empty.
Below `sm` the rows stack, because on a telephone a label belongs above its - The nav is the structure. It is sticky, so the sections stay reachable
field. however far down a form goes, and it says which one you are in without
making you read a row of tabs.
- 820px is the content column. Nobody types a phone number into a field
1100px wide; a settings page that lets them looks like a spreadsheet.
- A section is a heading, a sentence, and then a card of rows. Putting the
heading inside the card gave every group two frames the title bar and
the border and that is what made them read as boxes rather than as
topics.
- What cannot be undone lives at the end, in a card that says so with its
border.
Below `lg` the nav becomes a scrolling strip of chips above the content, and
the rows stack, because on a telephone a label belongs above its field.
--}} --}}
@php
$sections = [
'profile' => 'users',
'security' => 'shield-check',
'branding' => 'pen',
'contract' => 'receipt',
];
@endphp
<div class="space-y-6"> <div class="space-y-6">
<header class="animate-rise"> <header class="animate-rise">
@ -32,425 +51,495 @@
<p class="mt-2 max-w-[76ch] text-sm leading-relaxed text-muted">{{ __('settings.subtitle') }}</p> <p class="mt-2 max-w-[76ch] text-sm leading-relaxed text-muted">{{ __('settings.subtitle') }}</p>
</header> </header>
{{-- The same tab bar the console uses, so one vocabulary holds on both sides <div class="flex flex-col gap-6 lg:flex-row lg:items-start lg:gap-8">
of the login. The choice lives in the query string see the #[Url]
attribute on the component. --}}
<div class="flex flex-wrap items-center gap-x-1 gap-y-2 border-b border-line animate-rise [animation-delay:40ms]" role="tablist">
@foreach ($tabs as $name)
<button type="button" role="tab" wire:click="$set('tab', '{{ $name }}')"
aria-selected="{{ $tab === $name ? 'true' : 'false' }}"
@class([
'flex items-center gap-2 whitespace-nowrap border-b-2 px-4 py-2.5 text-sm font-medium transition-colors -mb-px',
'border-accent-active text-ink' => $tab === $name,
'border-transparent text-muted hover:text-ink' => $tab !== $name,
])>
<x-ui.icon :name="['profile' => 'users', 'security' => 'shield-check', 'branding' => 'pen', 'contract' => 'receipt'][$name] ?? 'settings'"
class="size-4" />{{ __('settings.tab.'.$name) }}
</button>
@endforeach
</div>
{{-- ══ Ihre Daten ═══════════════════════════════════════════════════════ --}} {{-- ── The section list ────────────────────────────────────────────
@if ($tab === 'profile') Sticky on a wide screen: a long form must not scroll its own
<form wire:submit="saveProfile" class="space-y-5 animate-rise [animation-delay:60ms]"> navigation away. A strip of chips below `lg`, because a vertical
<x-ui.panel :title="__('settings.company_title')" :subtitle="__('settings.company_sub')"> list on a telephone is four rows of nothing. --}}
<x-ui.row :label="__('settings.company_name')" for="companyName"> <nav class="-mx-4 shrink-0 overflow-x-auto px-4 lg:mx-0 lg:w-[232px] lg:overflow-visible lg:px-0 lg:sticky lg:top-8 animate-rise [animation-delay:40ms]"
<x-ui.input name="companyName" wire:model="companyName" /> role="tablist" aria-label="{{ __('settings.title') }}">
</x-ui.row> <div class="flex gap-1.5 lg:flex-col">
<x-ui.row :label="__('settings.contact_name')" for="contactName"> @foreach ($sections as $name => $icon)
<x-ui.input name="contactName" wire:model="contactName" /> <button type="button" role="tab" wire:click="$set('tab', '{{ $name }}')"
</x-ui.row> aria-selected="{{ $tab === $name ? 'true' : 'false' }}"
<x-ui.row :label="__('settings.phone')" for="phone"> @class([
<x-ui.input name="phone" wire:model="phone" /> 'flex shrink-0 items-center gap-2.5 whitespace-nowrap rounded px-3 py-2 text-sm font-medium transition-colors lg:w-full',
</x-ui.row> 'bg-surface text-ink shadow-xs ring-1 ring-line' => $tab === $name,
<x-ui.row :label="__('settings.vat_id')" for="vatId"> 'text-muted hover:bg-surface-hover hover:text-ink' => $tab !== $name,
<x-ui.input name="vatId" wire:model="vatId" /> ])>
</x-ui.row> <x-ui.icon :name="$icon" @class(['size-4', 'text-accent-text' => $tab === $name]) />
{{ __('settings.tab.'.$name) }}
</button>
@endforeach
</div>
{{-- Field by field, not a paragraph: what landed in the textarea {{-- Who is signed in, at the foot of the list. A settings page is one
was whatever somebody typed a postcode on the street line, of the few where "whose settings are these" is a real question
a town with no postcode, no country at all and an invoice an office with a shared machine has it every day. --}}
has to name its recipient precisely enough to be a <div class="mt-6 hidden rounded border border-line bg-surface-2 px-3 py-3 lg:block">
document. --}} <p class="lbl">{{ __('settings.signed_in_as') }}</p>
<x-ui.row :label="__('settings.billing_address')" for="billingStreet"> <p class="mt-1.5 truncate text-sm font-medium text-ink">{{ auth()->user()->name }}</p>
<div class="space-y-3"> <p class="truncate font-mono text-xs text-muted">{{ auth()->user()->email }}</p>
<x-ui.input name="billingStreet" wire:model="billingStreet" :placeholder="__('settings.billing_street')" /> </div>
<div class="grid gap-3 sm:grid-cols-[140px_minmax(0,1fr)]"> </nav>
<x-ui.input name="billingPostcode" wire:model="billingPostcode" :placeholder="__('settings.billing_postcode')" />
<x-ui.input name="billingCity" wire:model="billingCity" :placeholder="__('settings.billing_city')" />
</div>
<x-ui.input name="billingCountry" wire:model="billingCountry" :placeholder="__('settings.billing_country')" />
</div>
</x-ui.row>
{{-- A FACT for a record that has one. It decides the fourteen-day {{-- ── The content column ───────────────────────────────────────────
right of withdrawal: a business able to set itself to 820px, not the whole shell. Nobody types a postcode into a field a
"Privatperson" here is a business able to withdraw from a thousand pixels wide. --}}
contract it may not withdraw from. Registration asks; this <div class="min-w-0 flex-1 lg:max-w-[820px]">
page reports. The lock is in Settings::saveProfile, not here
a form that only hides a control has never stopped anybody {{-- ══ Ihre Daten ═══════════════════════════════════════════════ --}}
who can post to /livewire/update. --}} @if ($tab === 'profile')
<x-ui.row :label="__('settings.customer_type')" <form wire:submit="saveProfile" class="space-y-8 animate-rise [animation-delay:60ms]">
:hint="$customer?->customer_type ? __('settings.customer_type_locked') : __('settings.customer_type_once')"> <section>
@if ($customer?->customer_type) <h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('settings.company_title') }}</h2>
<p class="flex items-center gap-2 py-2 text-sm font-semibold text-ink"> <p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('settings.company_sub') }}</p>
<x-ui.icon name="lock" class="size-4 text-muted" />
{{ __('settings.customer_type_'.$customer->customer_type) }} <x-ui.panel class="mt-4">
<x-ui.row :label="__('settings.company_name')" for="companyName">
<x-ui.input name="companyName" wire:model="companyName" />
</x-ui.row>
<x-ui.row :label="__('settings.contact_name')" for="contactName">
<x-ui.input name="contactName" wire:model="contactName" />
</x-ui.row>
<x-ui.row :label="__('settings.phone')" for="phone">
<x-ui.input name="phone" wire:model="phone" />
</x-ui.row>
<x-ui.row :label="__('settings.vat_id')" for="vatId">
<x-ui.input name="vatId" wire:model="vatId" />
</x-ui.row>
{{-- Field by field, not a paragraph: what landed in
the textarea was whatever somebody typed a
postcode on the street line, a town with no
postcode, no country at all and an invoice has
to name its recipient precisely enough to be a
document. --}}
<x-ui.row :label="__('settings.billing_address')" for="billingStreet">
<div class="space-y-3">
<x-ui.input name="billingStreet" wire:model="billingStreet" :placeholder="__('settings.billing_street')" />
<div class="grid gap-3 sm:grid-cols-[130px_minmax(0,1fr)]">
<x-ui.input name="billingPostcode" wire:model="billingPostcode" :placeholder="__('settings.billing_postcode')" />
<x-ui.input name="billingCity" wire:model="billingCity" :placeholder="__('settings.billing_city')" />
</div>
<x-ui.input name="billingCountry" wire:model="billingCountry" :placeholder="__('settings.billing_country')" />
</div>
</x-ui.row>
<x-slot:footer>
<x-ui.button variant="primary" type="submit" wire:loading.attr="disabled" wire:target="saveProfile">
{{ __('settings.save') }}
</x-ui.button>
</x-slot:footer>
</x-ui.panel>
</section>
{{-- A FACT for a record that has one. It decides the
fourteen-day right of withdrawal: a business able to set
itself to "Privatperson" here is a business able to
withdraw from a contract it may not withdraw from.
Registration asks; this page reports. The lock is in
Settings::saveProfile, not here a form that only hides
a control has never stopped anybody who can post to
/livewire/update. --}}
<section>
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('settings.customer_type') }}</h2>
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">
{{ $customer?->customer_type ? __('settings.customer_type_locked') : __('settings.customer_type_once') }}
</p> </p>
@else
<div class="flex flex-wrap gap-2">
@foreach ($customerTypes as $type)
<label class="flex cursor-pointer items-center gap-2 rounded border border-line-strong bg-surface px-3 py-2 text-sm text-ink transition has-[:checked]:border-ink">
<input type="radio" wire:model="customerType" value="{{ $type }}"
class="size-4 shrink-0 border-line text-ink" />
{{ __('settings.customer_type_'.$type) }}
</label>
@endforeach
</div>
@error('customerType')<p class="mt-1.5 text-xs text-danger">{{ $message }}</p>@enderror
@endif
</x-ui.row>
<x-slot:footer> <x-ui.panel class="mt-4">
<x-ui.button variant="primary" type="submit" wire:loading.attr="disabled" wire:target="saveProfile"> <div class="px-6 py-4">
{{ __('settings.save') }} @if ($customer?->customer_type)
</x-ui.button> <p class="flex items-center gap-2 text-sm font-semibold text-ink">
</x-slot:footer> <x-ui.icon name="lock" class="size-4 text-muted" />
</x-ui.panel> {{ __('settings.customer_type_'.$customer->customer_type) }}
</form> </p>
@endif @else
<div class="flex flex-wrap gap-2">
@foreach ($customerTypes as $type)
<label class="flex cursor-pointer items-center gap-2 rounded border border-line-strong bg-surface px-3 py-2 text-sm text-ink transition has-[:checked]:border-ink">
<input type="radio" wire:model="customerType" value="{{ $type }}"
class="size-4 shrink-0 border-line text-ink" />
{{ __('settings.customer_type_'.$type) }}
</label>
@endforeach
</div>
@error('customerType')<p class="mt-2 text-xs text-danger">{{ $message }}</p>@enderror
@endif
</div>
</x-ui.panel>
</section>
</form>
@endif
{{-- ══ Sicherheit ═══════════════════════════════════════════════════════ --}} {{-- ══ Sicherheit ═══════════════════════════════════════════════ --}}
@if ($tab === 'security') @if ($tab === 'security')
<div class="space-y-5 animate-rise [animation-delay:60ms]"> <div class="space-y-8 animate-rise [animation-delay:60ms]">
{{-- Own password. There was no way to change one at all Fortify's {{-- Own password. There was no way to change one at all
updatePasswords feature was switched off. --}} Fortify's updatePasswords feature was switched off. --}}
<form wire:submit="updateOwnPassword"> <section>
<x-ui.panel :title="__('admin_settings.password_title')" :subtitle="__('admin_settings.password_sub')"> <h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('admin_settings.password_title') }}</h2>
<x-ui.row :label="__('admin_settings.password_current')" for="currentPassword"> <p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('admin_settings.password_sub') }}</p>
<x-ui.input name="currentPassword" type="password" autocomplete="current-password" wire:model="currentPassword" />
</x-ui.row>
<x-ui.row :label="__('admin_settings.password_new')" for="newPassword">
<div class="grid gap-3 sm:grid-cols-2">
<x-ui.input name="newPassword" type="password" autocomplete="new-password" wire:model="newPassword" />
<x-ui.input name="newPasswordConfirmation" type="password" autocomplete="new-password"
:placeholder="__('admin_settings.password_repeat')" wire:model="newPasswordConfirmation" />
</div>
</x-ui.row>
<x-slot:footer> <form wire:submit="updateOwnPassword">
<x-ui.button type="submit" variant="primary" wire:loading.attr="disabled" wire:target="updateOwnPassword"> <x-ui.panel class="mt-4">
{{ __('admin_settings.password_save') }} <x-ui.row :label="__('admin_settings.password_current')" for="currentPassword">
</x-ui.button> <x-ui.input name="currentPassword" type="password" autocomplete="current-password" wire:model="currentPassword" />
</x-slot:footer> </x-ui.row>
</x-ui.panel> <x-ui.row :label="__('admin_settings.password_new')" for="newPassword">
</form> <div class="space-y-3">
<x-ui.input name="newPassword" type="password" autocomplete="new-password" wire:model="newPassword" />
<x-ui.input name="newPasswordConfirmation" type="password" autocomplete="new-password"
:placeholder="__('admin_settings.password_repeat')" wire:model="newPasswordConfirmation" />
</div>
</x-ui.row>
{{-- Two-factor. Every action is re-checked server-side against a <x-slot:footer>
recent password confirmation hiding the buttons stops nobody <x-ui.button type="submit" variant="primary" wire:loading.attr="disabled" wire:target="updateOwnPassword">
who can post to /livewire/update. --}} {{ __('admin_settings.password_save') }}
<x-ui.panel> </x-ui.button>
<x-slot:header> </x-slot:footer>
<div class="min-w-0"> </x-ui.panel>
</form>
</section>
{{-- Two-factor. Every action is re-checked server-side
against a recent password confirmation hiding the
buttons stops nobody who can post to /livewire/update. --}}
<section>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<h2 class="text-sm font-bold text-ink">{{ __('settings.twofa_title') }}</h2> <h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('settings.twofa_title') }}</h2>
<span class="inline-flex items-center gap-1.5 rounded-pill border px-2.5 py-0.5 text-xs font-medium <span class="inline-flex items-center gap-1.5 rounded-pill border px-2.5 py-0.5 text-xs font-medium
{{ $twoFactorOn ? 'border-success-border bg-success-bg text-success' : 'border-line-strong bg-surface-2 text-muted' }}"> {{ $twoFactorOn ? 'border-success-border bg-success-bg text-success' : 'border-line-strong bg-surface-2 text-muted' }}">
<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>
{{ $twoFactorOn ? __('settings.twofa_state_on') : __('settings.twofa_state_off') }} {{ $twoFactorOn ? __('settings.twofa_state_on') : __('settings.twofa_state_off') }}
</span> </span>
</div> </div>
<p class="mt-0.5 max-w-[76ch] text-sm leading-relaxed text-muted">{{ __('settings.twofa_sub') }}</p> <p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('settings.twofa_sub') }}</p>
</div>
</x-slot:header>
@if (! $passwordConfirmed) <x-ui.panel class="mt-4">
{{-- The gate. A signed-in session is not enough to change how @if (! $passwordConfirmed)
the account is protected the risk is an unlocked {{-- The gate. A signed-in session is not enough
machine. --}} to change how the account is protected the
<form wire:submit="confirmPassword"> risk is an unlocked machine. --}}
<x-ui.row :label="__('admin_settings.password_current')" :hint="__('settings.twofa_confirm_first')" for="confirmablePassword"> <form wire:submit="confirmPassword">
<div class="flex flex-wrap items-start gap-2"> <x-ui.row :label="__('admin_settings.password_current')" :hint="__('settings.twofa_confirm_first')" for="confirmablePassword">
<div class="min-w-56 flex-1"> <div class="flex flex-wrap items-start gap-2">
<x-ui.input name="confirmablePassword" type="password" autocomplete="current-password" wire:model="confirmablePassword" /> <div class="min-w-48 flex-1">
</div> <x-ui.input name="confirmablePassword" type="password" autocomplete="current-password" wire:model="confirmablePassword" />
<x-ui.button type="submit" variant="secondary" wire:loading.attr="disabled" wire:target="confirmPassword"> </div>
{{ __('settings.twofa_confirm_button') }} <x-ui.button type="submit" variant="secondary" wire:loading.attr="disabled" wire:target="confirmPassword">
</x-ui.button> {{ __('settings.twofa_confirm_button') }}
</div> </x-ui.button>
</x-ui.row> </div>
</form> </x-ui.row>
@elseif ($twoFactorOn) </form>
<x-ui.row :label="__('settings.twofa_state_on')" :hint="__('settings.twofa_codes_hint')"> @elseif ($twoFactorOn)
<div class="flex flex-wrap gap-2"> <x-ui.row :label="__('settings.twofa_new_codes')" :hint="__('settings.twofa_codes_hint')">
<x-ui.button variant="secondary" wire:click="regenerateRecoveryCodes" wire:loading.attr="disabled" wire:target="regenerateRecoveryCodes"> <div class="flex flex-wrap gap-2">
{{ __('settings.twofa_new_codes') }} <x-ui.button variant="secondary" wire:click="regenerateRecoveryCodes" wire:loading.attr="disabled" wire:target="regenerateRecoveryCodes">
</x-ui.button> {{ __('settings.twofa_new_codes') }}
<x-ui.button variant="danger" </x-ui.button>
x-on:click="$dispatch('openModal', { component: 'confirm-disable-two-factor' })"> <x-ui.button variant="danger"
{{ __('settings.twofa_disable') }} x-on:click="$dispatch('openModal', { component: 'confirm-disable-two-factor' })">
</x-ui.button> {{ __('settings.twofa_disable') }}
</div> </x-ui.button>
</x-ui.row> </div>
@elseif ($twoFactorPending) </x-ui.row>
<x-ui.row :label="__('settings.twofa_scan')"> @elseif ($twoFactorPending)
<div class="flex flex-wrap items-start gap-5"> <x-ui.row :label="__('settings.twofa_scan')">
<div class="rounded border border-line bg-white p-3">{!! $twoFactorQr !!}</div> <div class="flex flex-wrap items-start gap-5">
<form wire:submit="confirmTwoFactor" class="space-y-3"> <div class="rounded border border-line bg-white p-3">{!! $twoFactorQr !!}</div>
<div class="max-w-48"> <form wire:submit="confirmTwoFactor" class="space-y-3">
<x-ui.input name="twoFactorCode" inputmode="numeric" autocomplete="one-time-code" <div class="max-w-44">
:label="__('settings.twofa_code')" wire:model="twoFactorCode" /> <x-ui.input name="twoFactorCode" inputmode="numeric" autocomplete="one-time-code"
</div> :label="__('settings.twofa_code')" wire:model="twoFactorCode" />
<x-ui.button type="submit" variant="primary" wire:loading.attr="disabled" wire:target="confirmTwoFactor"> </div>
{{ __('settings.twofa_activate') }} <x-ui.button type="submit" variant="primary" wire:loading.attr="disabled" wire:target="confirmTwoFactor">
</x-ui.button> {{ __('settings.twofa_activate') }}
</form> </x-ui.button>
</div> </form>
</x-ui.row> </div>
@else </x-ui.row>
<x-ui.row :label="__('settings.twofa_state_off')">
<x-ui.button variant="primary" wire:click="enableTwoFactor" wire:loading.attr="disabled" wire:target="enableTwoFactor">
{{ __('settings.twofa_enable') }}
</x-ui.button>
</x-ui.row>
@endif
@if ($recoveryCodes)
{{-- Shown once, on purpose: nobody writes down what they were
never shown, and these are the way back in when the phone
is gone. --}}
<x-ui.row :label="__('settings.twofa_codes_title')" :hint="__('settings.twofa_codes_hint')">
<ul class="grid grid-cols-2 gap-x-6 gap-y-1 rounded border border-warning-border bg-warning-bg p-4 font-mono text-sm text-ink sm:grid-cols-3">
@foreach ($recoveryCodes as $code)<li class="select-all">{{ $code }}</li>@endforeach
</ul>
</x-ui.row>
@endif
</x-ui.panel>
{{-- The devices. It answers the same question two-factor does who
can get into this account and it is a table of its own. --}}
@livewire('sessions')
</div>
@endif
{{-- ══ Erscheinungsbild ═════════════════════════════════════════════════ --}}
@if ($tab === 'branding')
<form wire:submit="saveBranding" class="space-y-5 animate-rise [animation-delay:60ms]">
<x-ui.panel :title="__('settings.branding_title')" :subtitle="__('settings.branding_sub')">
<x-ui.row :label="__('settings.brand_display_name')" for="brandDisplayName">
<x-ui.input name="brandDisplayName" wire:model="brandDisplayName" :placeholder="$branding['display_name'] ?? ''" />
</x-ui.row>
<x-ui.row :label="__('settings.brand_primary')">
<div class="flex items-center gap-2">
<input type="color" wire:model="brandPrimary" value="{{ $brandPrimary ?: ($branding['primary_color'] ?? '#f97316') }}"
class="h-10 w-14 rounded border border-line-strong bg-surface" aria-label="{{ __('settings.brand_primary') }}" />
<input type="text" wire:model="brandPrimary" placeholder="{{ $branding['primary_color'] ?? '#f97316' }}"
class="w-32 rounded border border-line-strong bg-surface px-3 py-2.5 font-mono text-sm text-ink" />
</div>
@error('brandPrimary')<p class="mt-1.5 text-xs text-danger">{{ $message }}</p>@enderror
</x-ui.row>
<x-ui.row :label="__('settings.brand_accent')">
<div class="flex items-center gap-2">
<input type="color" wire:model="brandAccent" value="{{ $brandAccent ?: ($branding['accent_color'] ?? '#c2560a') }}"
class="h-10 w-14 rounded border border-line-strong bg-surface" aria-label="{{ __('settings.brand_accent') }}" />
<input type="text" wire:model="brandAccent" placeholder="{{ $branding['accent_color'] ?? '#c2560a' }}"
class="w-32 rounded border border-line-strong bg-surface px-3 py-2.5 font-mono text-sm text-ink" />
</div>
@error('brandAccent')<p class="mt-1.5 text-xs text-danger">{{ $message }}</p>@enderror
</x-ui.row>
<x-ui.row :label="__('settings.brand_logo')" :hint="__('settings.brand_logo_hint')">
<div class="flex flex-wrap items-center gap-4">
<div class="grid size-20 shrink-0 place-items-center overflow-hidden rounded border border-line bg-surface-2">
@if ($logo)
<img src="{{ $logo->temporaryUrl() }}" alt="" class="max-h-20 max-w-20 object-contain" />
@elseif ($logoUrl)
<img src="{{ $logoUrl }}" alt="" class="max-h-20 max-w-20 object-contain" />
@else @else
<span class="px-2 text-center text-xs text-muted">{{ __('settings.brand_default') }}</span> <x-ui.row :label="__('settings.twofa_enable')" :hint="__('settings.twofa_sub')">
@endif <x-ui.button variant="primary" wire:click="enableTwoFactor" wire:loading.attr="disabled" wire:target="enableTwoFactor">
</div> {{ __('settings.twofa_enable') }}
<div class="min-w-0 flex-1 space-y-2">
<input type="file" wire:model="logo" accept="image/png,image/webp"
class="w-full text-sm text-body file:mr-3 file:rounded file:border-0 file:bg-surface-2 file:px-3 file:py-1.5 file:text-sm file:font-semibold file:text-body" />
@if ($logoUrl)
<button type="button" wire:click="removeLogo" class="text-xs font-semibold text-danger hover:underline">{{ __('settings.brand_logo_remove') }}</button>
@endif
@error('logo')<p class="text-xs text-danger">{{ $message }}</p>@enderror
</div>
</div>
</x-ui.row>
<x-slot:footer>
@if ($branding['is_default'] ?? false)
<p class="mr-auto text-xs text-muted">{{ __('settings.brand_using_default') }}</p>
@endif
<x-ui.button variant="primary" type="submit" wire:loading.attr="disabled" wire:target="saveBranding,logo">
{{ __('settings.save') }}
</x-ui.button>
</x-slot:footer>
</x-ui.panel>
</form>
@endif
{{-- ══ Vertrag ══════════════════════════════════════════════════════════ --}}
@if ($tab === 'contract')
<div class="space-y-5 animate-rise [animation-delay:60ms]">
<x-ui.panel :title="__('settings.contract_title')" :subtitle="__('settings.contract_sub')">
{{-- The package, and the way out of it. --}}
<x-ui.row :label="__('settings.package_title')">
<div class="flex flex-wrap items-center justify-between gap-3">
<p class="min-w-0 text-sm text-body">
@if ($cancellationScheduled)
{{ __('settings.cancel_scheduled_body', ['date' => $instance?->service_ends_at?->local()->isoFormat('LL')]) }}
@elseif ($hasActivePackage)
{{ __('settings.package_active', ['plan' => $instance ? __('billing.plan.'.$instance->plan) : '—']) }}
@else
{{ __('settings.no_package') }}
@endif
</p>
@if ($hasActivePackage && ! $cancellationScheduled)
<x-ui.button variant="secondary" size="sm"
x-on:click="$dispatch('openModal', { component: 'confirm-cancel-package' })">
{{ __('settings.cancel_cta') }}
</x-ui.button>
@elseif (! $hasActivePackage && ! $cancellationScheduled)
<x-ui.button :href="route('order')" variant="secondary" size="sm" wire:navigate>
{{ __('settings.to_packages') }}
</x-ui.button>
@endif
</div>
</x-ui.row>
{{-- ── Das Widerrufsrecht ────────────────────────────────────
Shown to every CONSUMER with a contract, open or not not
only while it is open. It vanished entirely once the
fourteen days ran out, which is indistinguishable from a
feature that was never built: the owner went looking for it
and reported it missing from an account that has no contract
at all, so there was nothing for it to be about.
A business never sees it, and the server refuses them again
in Settings::withdraw() a card that is not rendered has
never stopped anybody who can post to /livewire/update.
Not a variant of the cancellation above: that one ends a
contract validly concluded and keeps the term already paid
for; this one unwinds the contract, ends the service the same
day and sends the whole amount back. --}}
@if ($withdrawal->applies)
<x-ui.row :label="__('withdrawal.card_title')">
<div class="flex flex-wrap items-center justify-between gap-3">
<p class="min-w-0 max-w-[70ch] text-sm text-body">
@if ($withdrawal->open)
{{ __('withdrawal.card_sub', [
'date' => $withdrawal->endsAt->local()->isoFormat('LL'),
'days' => $withdrawal->daysLeft(),
]) }}
@else
{{ $withdrawal->refusal }}
@endif
</p>
@if ($withdrawal->open)
<x-ui.button variant="secondary" size="sm"
x-on:click="$dispatch('openModal', { component: 'confirm-withdraw' })">
{{ __('withdrawal.cta') }}
</x-ui.button>
@endif
</div>
</x-ui.row>
@endif
{{-- ── AV-Vertrag & TOM ──────────────────────────────────────
Art. 28 DSGVO wants a contract wherever personal data is
processed on somebody else's behalf. It is concluded WITH the
terms at checkout (see the AGB), so this is not a second
signing ceremony: the document has to be available, current
and retrievable. The extra confirmation is offered because a
practice or a firm that gets audited often wants one on file.
Nothing renders until an operator has published a version. --}}
@if ($dpa !== null)
<x-ui.row :label="__('dpa.title')" :hint="__('dpa.sub')">
<div class="space-y-3">
<div class="flex flex-wrap items-center gap-2">
<x-ui.button :href="route('dpa.file', 'agreement')" variant="secondary" size="sm" target="_blank">
<x-ui.icon name="file-text" class="size-4" />{{ __('dpa.read_agreement') }}
</x-ui.button>
<x-ui.button :href="route('dpa.file', ['which' => 'agreement', 'download' => 1])" variant="ghost" size="sm" download>
<x-ui.icon name="download" class="size-4" />{{ __('dpa.download') }}
</x-ui.button>
@if ($dpa->measures_path)
<span class="mx-1 h-4 w-px bg-line" aria-hidden="true"></span>
<x-ui.button :href="route('dpa.file', 'measures')" variant="secondary" size="sm" target="_blank">
<x-ui.icon name="shield" class="size-4" />{{ __('dpa.read_measures') }}
</x-ui.button> </x-ui.button>
<x-ui.button :href="route('dpa.file', ['which' => 'measures', 'download' => 1])" variant="ghost" size="sm" download> </x-ui.row>
<x-ui.icon name="download" class="size-4" />{{ __('dpa.download') }}
</x-ui.button>
@endif
<span class="ml-auto font-mono text-xs text-muted">{{ __('dpa.version', ['version' => $dpa->version]) }}</span>
</div>
@if ($dpaAcceptance)
{{-- R19: stored in UTC, read on the wall clock. --}}
<p class="flex items-start gap-2 text-xs leading-relaxed text-muted">
<x-ui.icon name="check" class="mt-0.5 size-3.5 shrink-0 text-success" />
{{ __('dpa.accepted_on', [
'version' => $dpa->version,
'when' => $dpaAcceptance->accepted_at->local()->isoFormat('LL, LT'),
]) }}
</p>
@else
<div class="flex flex-wrap items-center justify-between gap-3 rounded border border-line bg-surface-2 px-4 py-3">
<p class="min-w-0 max-w-[70ch] text-xs leading-relaxed text-muted">{{ __('dpa.in_force_note') }}</p>
<x-ui.button variant="secondary" size="sm"
wire:click="acceptProcessingAgreement" wire:loading.attr="disabled" wire:target="acceptProcessingAgreement">
{{ __('dpa.accept_cta') }}
</x-ui.button>
</div>
@endif @endif
</div>
</x-ui.row>
@endif
{{-- Closing the account. Last row of the panel, where the @if ($recoveryCodes)
irreversible things belong. --}} {{-- Shown once, on purpose: nobody writes down
<x-ui.row :label="__('settings.close_account_title')" :hint="__('settings.close_account_sub')"> what they were never shown, and these are the
<div class="flex justify-start sm:justify-end"> way back in when the phone is gone. --}}
<x-ui.button variant="secondary" size="sm" <x-ui.row :label="__('settings.twofa_codes_title')" :hint="__('settings.twofa_codes_hint')">
x-on:click="$dispatch('openModal', { component: 'confirm-close-account' })"> <ul class="grid grid-cols-2 gap-x-6 gap-y-1 rounded border border-warning-border bg-warning-bg p-4 font-mono text-sm text-ink">
{{ __('settings.close_cta') }} @foreach ($recoveryCodes as $code)<li class="select-all">{{ $code }}</li>@endforeach
</x-ui.button> </ul>
</div> </x-ui.row>
</x-ui.row> @endif
</x-ui.panel> </x-ui.panel>
</section>
{{-- The deadlines, said where somebody asks the question. "Nach fünf {{-- The devices. It answers the same question two-factor does
Tagen gelöscht" on the verification page reads as if it applied who can get into this account. --}}
to every account; it applies to a registration nobody confirmed. <section>
The other rule a year without a package was nowhere at all. @livewire('sessions')
Both come from the commands that enforce them, so the page cannot </section>
drift from what actually happens. --}}
<x-ui.panel :title="__('settings.lifecycle_title')">
<div class="px-6 py-4">
<ul class="space-y-1.5 text-sm leading-relaxed text-muted">
<li>{{ __('settings.lifecycle_unverified', ['days' => App\Console\Commands\PruneUnverifiedAccounts::AFTER_DAYS]) }}</li>
<li>{{ __('settings.lifecycle_dormant', ['warn' => App\Console\Commands\PruneDormantAccounts::WARN_DAYS_BEFORE]) }}</li>
<li>{{ __('settings.lifecycle_kept') }}</li>
</ul>
<a href="{{ route('legal.agb') }}" target="_blank" rel="noopener"
class="mt-3 inline-block text-sm font-medium text-accent-text underline-offset-4 hover:underline">
{{ __('settings.lifecycle_terms') }}
</a>
</div> </div>
</x-ui.panel> @endif
{{-- ══ Erscheinungsbild ═════════════════════════════════════════ --}}
@if ($tab === 'branding')
<form wire:submit="saveBranding" class="animate-rise [animation-delay:60ms]">
<section>
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('settings.branding_title') }}</h2>
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('settings.branding_sub') }}</p>
<x-ui.panel class="mt-4">
<x-ui.row :label="__('settings.brand_display_name')" for="brandDisplayName">
<x-ui.input name="brandDisplayName" wire:model="brandDisplayName" :placeholder="$branding['display_name'] ?? ''" />
</x-ui.row>
<x-ui.row :label="__('settings.brand_primary')">
<div class="flex items-center gap-2">
<input type="color" wire:model="brandPrimary" value="{{ $brandPrimary ?: ($branding['primary_color'] ?? '#f97316') }}"
class="h-10 w-14 rounded border border-line-strong bg-surface" aria-label="{{ __('settings.brand_primary') }}" />
<input type="text" wire:model="brandPrimary" placeholder="{{ $branding['primary_color'] ?? '#f97316' }}"
class="w-32 rounded border border-line-strong bg-surface px-3 py-2.5 font-mono text-sm text-ink" />
</div>
@error('brandPrimary')<p class="mt-1.5 text-xs text-danger">{{ $message }}</p>@enderror
</x-ui.row>
<x-ui.row :label="__('settings.brand_accent')">
<div class="flex items-center gap-2">
<input type="color" wire:model="brandAccent" value="{{ $brandAccent ?: ($branding['accent_color'] ?? '#c2560a') }}"
class="h-10 w-14 rounded border border-line-strong bg-surface" aria-label="{{ __('settings.brand_accent') }}" />
<input type="text" wire:model="brandAccent" placeholder="{{ $branding['accent_color'] ?? '#c2560a' }}"
class="w-32 rounded border border-line-strong bg-surface px-3 py-2.5 font-mono text-sm text-ink" />
</div>
@error('brandAccent')<p class="mt-1.5 text-xs text-danger">{{ $message }}</p>@enderror
</x-ui.row>
<x-ui.row :label="__('settings.brand_logo')" :hint="__('settings.brand_logo_hint')">
<div class="flex flex-wrap items-center gap-4">
<div class="grid size-16 shrink-0 place-items-center overflow-hidden rounded border border-line bg-surface-2">
@if ($logo)
<img src="{{ $logo->temporaryUrl() }}" alt="" class="max-h-16 max-w-16 object-contain" />
@elseif ($logoUrl)
<img src="{{ $logoUrl }}" alt="" class="max-h-16 max-w-16 object-contain" />
@else
<span class="px-2 text-center text-[10px] leading-tight text-muted">{{ __('settings.brand_default') }}</span>
@endif
</div>
<div class="min-w-0 flex-1 space-y-2">
<input type="file" wire:model="logo" accept="image/png,image/webp"
class="w-full text-sm text-body file:mr-3 file:rounded file:border-0 file:bg-surface-2 file:px-3 file:py-1.5 file:text-sm file:font-semibold file:text-body" />
@if ($logoUrl)
<button type="button" wire:click="removeLogo" class="text-xs font-semibold text-danger hover:underline">{{ __('settings.brand_logo_remove') }}</button>
@endif
@error('logo')<p class="text-xs text-danger">{{ $message }}</p>@enderror
</div>
</div>
</x-ui.row>
<x-slot:footer>
@if ($branding['is_default'] ?? false)
<p class="mr-auto text-xs text-muted">{{ __('settings.brand_using_default') }}</p>
@endif
<x-ui.button variant="primary" type="submit" wire:loading.attr="disabled" wire:target="saveBranding,logo">
{{ __('settings.save') }}
</x-ui.button>
</x-slot:footer>
</x-ui.panel>
</section>
</form>
@endif
{{-- ══ Vertrag ══════════════════════════════════════════════════ --}}
@if ($tab === 'contract')
<div class="space-y-8 animate-rise [animation-delay:60ms]">
<section>
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('settings.package_title') }}</h2>
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('settings.contract_sub') }}</p>
<x-ui.panel class="mt-4">
<x-ui.row :label="__('settings.package_title')">
<div class="flex flex-wrap items-center justify-between gap-3">
<p class="min-w-0 text-sm text-body">
@if ($cancellationScheduled)
{{ __('settings.cancel_scheduled_body', ['date' => $instance?->service_ends_at?->local()->isoFormat('LL')]) }}
@elseif ($hasActivePackage)
{{ __('settings.package_active', ['plan' => $instance ? __('billing.plan.'.$instance->plan) : '—']) }}
@else
{{ __('settings.no_package') }}
@endif
</p>
@if ($hasActivePackage && ! $cancellationScheduled)
<x-ui.button variant="secondary" size="sm"
x-on:click="$dispatch('openModal', { component: 'confirm-cancel-package' })">
{{ __('settings.cancel_cta') }}
</x-ui.button>
@elseif (! $hasActivePackage && ! $cancellationScheduled)
<x-ui.button :href="route('order')" variant="secondary" size="sm" wire:navigate>
{{ __('settings.to_packages') }}
</x-ui.button>
@endif
</div>
</x-ui.row>
{{-- ── Das Widerrufsrecht ────────────────────────
Shown to every CONSUMER with a contract, open or
not. It used to render only while the window was
open, so once the fourteen days expired the block
vanished indistinguishable from a feature
nobody built.
A business never sees it, and Settings::withdraw()
refuses them again on the server: a card that is
not rendered has never stopped anybody who can
post to /livewire/update.
Not a variant of the cancellation above it: that
one ends a contract validly concluded and keeps
the term already paid for; this one unwinds the
contract, ends the service the same day and sends
the whole amount back. --}}
@if ($withdrawal->applies)
<x-ui.row :label="__('withdrawal.card_title')">
<div class="flex flex-wrap items-center justify-between gap-3">
<p class="min-w-0 max-w-[60ch] text-sm text-body">
@if ($withdrawal->open)
{{ __('withdrawal.card_sub', [
'date' => $withdrawal->endsAt->local()->isoFormat('LL'),
'days' => $withdrawal->daysLeft(),
]) }}
@else
{{ $withdrawal->refusal }}
@endif
</p>
@if ($withdrawal->open)
<x-ui.button variant="secondary" size="sm"
x-on:click="$dispatch('openModal', { component: 'confirm-withdraw' })">
{{ __('withdrawal.cta') }}
</x-ui.button>
@endif
</div>
</x-ui.row>
@endif
</x-ui.panel>
</section>
{{-- ── AV-Vertrag & TOM ──────────────────────────────────
Art. 28 DSGVO wants a contract wherever personal data is
processed on somebody else's behalf. It is concluded WITH
the terms at checkout (see the AGB), so this is not a
second signing ceremony: the document has to be
available, current and retrievable. The extra
confirmation is offered because a practice or a firm that
gets audited often wants one on file.
Nothing renders until an operator has published a
version. --}}
@if ($dpa !== null)
<section>
<div class="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('dpa.title') }}</h2>
<span class="font-mono text-xs text-muted">{{ __('dpa.version', ['version' => $dpa->version]) }}</span>
</div>
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('dpa.sub') }}</p>
<x-ui.panel class="mt-4">
<x-ui.row :label="__('dpa.read_agreement')">
<div class="flex flex-wrap gap-2">
<x-ui.button :href="route('dpa.file', 'agreement')" variant="secondary" size="sm" target="_blank">
<x-ui.icon name="file-text" class="size-4" />{{ __('dpa.open') }}
</x-ui.button>
<x-ui.button :href="route('dpa.file', ['which' => 'agreement', 'download' => 1])" variant="ghost" size="sm" download>
<x-ui.icon name="download" class="size-4" />{{ __('dpa.download') }}
</x-ui.button>
</div>
</x-ui.row>
@if ($dpa->measures_path)
<x-ui.row :label="__('dpa.read_measures')">
<div class="flex flex-wrap gap-2">
<x-ui.button :href="route('dpa.file', 'measures')" variant="secondary" size="sm" target="_blank">
<x-ui.icon name="shield" class="size-4" />{{ __('dpa.open') }}
</x-ui.button>
<x-ui.button :href="route('dpa.file', ['which' => 'measures', 'download' => 1])" variant="ghost" size="sm" download>
<x-ui.icon name="download" class="size-4" />{{ __('dpa.download') }}
</x-ui.button>
</div>
</x-ui.row>
@endif
<x-ui.row :label="__('dpa.state_label')">
@if ($dpaAcceptance)
{{-- R19: stored in UTC, read on the wall clock. --}}
<p class="flex items-start gap-2 text-sm leading-relaxed text-body">
<x-ui.icon name="check" class="mt-0.5 size-4 shrink-0 text-success" />
{{ __('dpa.accepted_on', [
'version' => $dpa->version,
'when' => $dpaAcceptance->accepted_at->local()->isoFormat('LL, LT'),
]) }}
</p>
@else
<div class="space-y-3">
<p class="max-w-[60ch] text-sm leading-relaxed text-body">{{ __('dpa.in_force_note') }}</p>
<x-ui.button variant="secondary" size="sm"
wire:click="acceptProcessingAgreement" wire:loading.attr="disabled" wire:target="acceptProcessingAgreement">
{{ __('dpa.accept_cta') }}
</x-ui.button>
</div>
@endif
</x-ui.row>
</x-ui.panel>
</section>
@endif
{{-- ── What ends an account ──────────────────────────────
The deadlines and the button that closes it, together and
last, in a card whose border says what kind of thing this
is. Everything irreversible on this page lives here.
The deadlines come from the commands that enforce them,
so the page cannot drift from what actually happens. --}}
<section>
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('settings.close_account_title') }}</h2>
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('settings.close_account_sub') }}</p>
<x-ui.panel class="mt-4 border-danger-border">
<x-ui.row :label="__('settings.lifecycle_title')">
<ul class="space-y-1.5 text-sm leading-relaxed text-muted">
<li>{{ __('settings.lifecycle_unverified', ['days' => App\Console\Commands\PruneUnverifiedAccounts::AFTER_DAYS]) }}</li>
<li>{{ __('settings.lifecycle_dormant', ['warn' => App\Console\Commands\PruneDormantAccounts::WARN_DAYS_BEFORE]) }}</li>
<li>{{ __('settings.lifecycle_kept') }}</li>
</ul>
<a href="{{ route('legal.agb') }}" target="_blank" rel="noopener"
class="mt-3 inline-block text-sm font-medium text-accent-text underline-offset-4 hover:underline">
{{ __('settings.lifecycle_terms') }}
</a>
</x-ui.row>
<x-ui.row :label="__('settings.close_cta')" :hint="__('settings.close_account_sub')">
<div class="flex justify-start sm:justify-end">
<x-ui.button variant="danger" size="sm"
x-on:click="$dispatch('openModal', { component: 'confirm-close-account' })">
{{ __('settings.close_cta') }}
</x-ui.button>
</div>
</x-ui.row>
</x-ui.panel>
</section>
</div>
@endif
</div> </div>
@endif </div>
</div> </div>