diff --git a/CLAUDE.md b/CLAUDE.md index 9be1550..6b7bda2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -267,3 +267,48 @@ Identitäten nach R21), ein Benutzerzugang entzogen. `tests/Feature/ConfirmInModalTest.php` — kein Blade-File im Repo darf `wire:confirm` enthalten, und kein JavaScript darf `confirm(` aufrufen. + +--- + +## R24 — Ein Modal wird nie höher als der Bildschirm + +**Kopf und Fuß stehen fest, gescrollt wird nur die Mitte.** + +Verboten: + +1. **Ein Modal, das mit seinem Inhalt wächst.** Es läuft unten aus dem Fenster + heraus und nimmt seinen eigenen Speichern-Knopf mit — erreichbar nur, indem + man die Seite *hinter* dem Overlay scrollt, auf dem Telefon also gar nicht. +2. **Titel oder Knopfzeile im Scrollbereich.** Was man lesen muss, um zu wissen, + wo man ist, und was man erreichen muss, um fertig zu werden, gehören beide + außerhalb. +3. **Eine Blade-Direktive in der Attributliste eines Komponenten-Tags** + (``). Sie landet im Attribut-Beutel und + zerlegt die View — dieselbe Falle wie `@disabled` an einer Komponente. Solche + Attribute gehören auf ein eigenes Element im Inhalt. + +### Wie es gebaut ist + +- `resources/views/vendor/wire-elements-modal/modal.blade.php` begrenzt das + Panel **einmal für alle Modals**: `max-h-[calc(100dvh-3rem)]`, Spalte, + `overflow-hidden`. `dvh` statt `vh`, weil am Telefon die Browserleiste zu `vh` + zählt — genau der Fall, in dem der letzte Zentimeter zählt. +- `resources/views/components/ui/modal.blade.php` ist die andere Hälfte: + `header`-Slot (oder `title`/`subtitle`), scrollender Rumpf, `footer`-Slot. + `min-h-0` am Rumpf ist die Zeile, die es wirken lässt — ein Flex-Kind wird + ohne sie nicht kleiner als sein Inhalt, und `overflow` greift nie. +- Ein Knopf im Fuß, der ein Formular im Rumpf abschickt, benutzt + `type="submit" form="…"`. Das ist der Preis für einen Fuß, der nicht + wegscrollt — und ein HTML-Attribut, kein Trick. + +### Gilt für + +Jedes Modal, in dem ein **Eingabefeld** steckt (``, und keine Blade-Direktive steht in der +Attributliste eines Komponenten-Tags. diff --git a/VERSION b/VERSION index 6972043..3eba9bd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.41 +1.3.42 diff --git a/resources/views/components/ui/modal.blade.php b/resources/views/components/ui/modal.blade.php new file mode 100644 index 0000000..ab383e3 --- /dev/null +++ b/resources/views/components/ui/modal.blade.php @@ -0,0 +1,45 @@ +@props([ + // The heading, and the sentence under it. Both optional: a modal that is one + // question does not need a subtitle, and a couple have their own header + // markup (a state badge beside the title, an icon). + 'title' => null, + 'subtitle' => null, +]) + +{{-- R24: header and footer stay put, the middle scrolls. + + A modal grew with its content, so a form ran off the bottom of the window + and took its own save button with it — reachable only by scrolling the page + behind the backdrop, which on a phone means not reachable. The panel is + capped in resources/views/vendor/wire-elements-modal/modal.blade.php; this + is the other half: the part somebody has to read (what am I looking at) and + the part they have to reach (the button) are outside the scroll region. + + min-h-0 on the body is the line that makes it work: a flex child refuses to + shrink below its content without it, and the overflow never engages. --}} +
class(['flex min-h-0 flex-1 flex-col']) }}> + @if ($title !== null || isset($header)) +
+ @isset($header) + {{ $header }} + @else +

{{ $title }}

+ @if ($subtitle !== null) +

{{ $subtitle }}

+ @endif + @endisset +
+ @endif + + {{-- The only part that scrolls. overscroll-contain so reaching the end of it + does not start scrolling the page behind the backdrop. --}} +
+ {{ $slot }} +
+ + @isset($footer) +
+ {{ $footer }} +
+ @endisset +
diff --git a/resources/views/livewire/admin/edit-customer.blade.php b/resources/views/livewire/admin/edit-customer.blade.php index c72f86b..433b065 100644 --- a/resources/views/livewire/admin/edit-customer.blade.php +++ b/resources/views/livewire/admin/edit-customer.blade.php @@ -1,8 +1,8 @@ -
-

{{ __('edit_customer.title') }}

-

{{ __('edit_customer.subtitle') }}

- -
+{{-- R24: the form scrolls, the title and the buttons do not. This modal is the + reason the rule exists — with eight fields it was taller than the window and + its own save button was off the bottom of it. --}} + +
@@ -60,9 +60,15 @@ {{-- What it actually decides: the language of the mail they get. --}}

{{ __('edit_customer.locale_hint') }}

+ -
- + +
+ {{-- form=, because the button now lives outside the
it submits: + that is the price of a footer that does not scroll away, and it + is an HTML attribute rather than a workaround. --}} + {{ __('edit_customer.save') }} {{-- Livewire.dispatch, not Alpine's: the modal listens for a Livewire @@ -71,5 +77,5 @@ {{ __('edit_customer.cancel') }}
- -
+ +
diff --git a/resources/views/livewire/admin/edit-datacenter.blade.php b/resources/views/livewire/admin/edit-datacenter.blade.php index 78261ec..a785083 100644 --- a/resources/views/livewire/admin/edit-datacenter.blade.php +++ b/resources/views/livewire/admin/edit-datacenter.blade.php @@ -1,4 +1,4 @@ -
+

{{ __('datacenters.edit_title') }}

{{ $code }} @@ -63,8 +63,10 @@

{{ __('datacenters.active_hint') }}

-
- {{ __('datacenters.cancel') }} - {{ __('datacenters.save') }} -
-
+ +
+ {{ __('datacenters.cancel') }} + {{ __('datacenters.save') }} +
+
+ diff --git a/resources/views/livewire/admin/edit-export-target.blade.php b/resources/views/livewire/admin/edit-export-target.blade.php index f0c0809..ce55631 100644 --- a/resources/views/livewire/admin/edit-export-target.blade.php +++ b/resources/views/livewire/admin/edit-export-target.blade.php @@ -1,7 +1,5 @@ -
-

{{ $uuid === '' ? __('finance.targets_add') : __('finance.targets_edit') }}

- -
+ +
@@ -49,8 +47,10 @@
-
- {{ __('finance.cancel') }} - {{ __('finance.save') }} -
-
+ +
+ {{ __('finance.cancel') }} + {{ __('finance.save') }} +
+
+ diff --git a/resources/views/livewire/admin/edit-invoice-series.blade.php b/resources/views/livewire/admin/edit-invoice-series.blade.php index 02ba45c..6195295 100644 --- a/resources/views/livewire/admin/edit-invoice-series.blade.php +++ b/resources/views/livewire/admin/edit-invoice-series.blade.php @@ -1,4 +1,4 @@ -
+

{{ __('finance.series_edit_title') }}

{{ $prefix }} @@ -47,8 +47,10 @@
-
- {{ __('finance.cancel') }} - {{ __('finance.save') }} -
-
+ +
+ {{ __('finance.cancel') }} + {{ __('finance.save') }} +
+
+ diff --git a/resources/views/livewire/admin/edit-mail-template.blade.php b/resources/views/livewire/admin/edit-mail-template.blade.php index 2300876..b9f3470 100644 --- a/resources/views/livewire/admin/edit-mail-template.blade.php +++ b/resources/views/livewire/admin/edit-mail-template.blade.php @@ -1,8 +1,6 @@ -
-

{{ __('templates.edit_title') }}

-

{{ __('templates.edit_body') }}

- -
+{{-- R24: the ten-row textarea scrolls, the title and the buttons stay. --}} + + @@ -33,15 +31,21 @@

-
- + + + +
+ {{-- form=, because the button lives outside the
it submits — + the price of a footer that does not scroll away, and an HTML + attribute rather than a workaround. --}} + {{ __('templates.save') }} {{-- Livewire.dispatch, not Alpine's $dispatch. Alpine's fires a browser DOM event, and the modal listens for a LIVEWIRE event of - that name — so the button did nothing at all. Every other modal - in this repo does it this way; this one was the odd one out. --}} + that name — so the button did nothing at all. --}} {{ __('templates.cancel') }}
- -
+ + diff --git a/resources/views/livewire/admin/edit-plan-family.blade.php b/resources/views/livewire/admin/edit-plan-family.blade.php index 5aacb83..7284e2e 100644 --- a/resources/views/livewire/admin/edit-plan-family.blade.php +++ b/resources/views/livewire/admin/edit-plan-family.blade.php @@ -1,4 +1,4 @@ -
+

{{ __('plans.marketing_title') }}

{{ $family->name }} @@ -26,8 +26,10 @@

{{ __('plans.recommended_hint') }}

-
- {{ __('plans.cancel') }} - {{ __('plans.save') }} -
-
+ +
+ {{ __('plans.cancel') }} + {{ __('plans.save') }} +
+
+ diff --git a/resources/views/livewire/admin/grant-plan.blade.php b/resources/views/livewire/admin/grant-plan.blade.php index 2ac0bd0..1baf21c 100644 --- a/resources/views/livewire/admin/grant-plan.blade.php +++ b/resources/views/livewire/admin/grant-plan.blade.php @@ -1,9 +1,16 @@ -
-
-

{{ __('admin.grant.title') }}

- {{ $customerName }} -
+{{-- R24: the longest modal in the console — every plan, every add-on, a note + field. It was well past the bottom of the window and took its own grant + button with it. --}} + + + {{-- Its own header: whose account this is belongs beside the title, and + the plain `title` prop cannot say two things. --}} +
+

{{ __('admin.grant.title') }}

+ {{ $customerName }} +
+
@if (count($grants) > 0)
@@ -173,10 +180,12 @@
-
- {{ __('admin.grant.cancel') }} - - {{ __('admin.grant.submit') }} - -
-
+ +
+ {{ __('admin.grant.cancel') }} + + {{ __('admin.grant.submit') }} + +
+
+
diff --git a/resources/views/livewire/admin/instance-admin-access.blade.php b/resources/views/livewire/admin/instance-admin-access.blade.php index 51ffbb2..a569376 100644 --- a/resources/views/livewire/admin/instance-admin-access.blade.php +++ b/resources/views/livewire/admin/instance-admin-access.blade.php @@ -1,8 +1,20 @@ -
-

{{ __('instances.admin_title', ['name' => $subdomain]) }}

+{{-- R24. --}} + + {{-- The poll on an element of its own, not on the component tag: a Blade + directive inside a component's attribute list compiles into the + attribute bag and breaks the view outright — the same trap as @disabled + on a component. Livewire polls the whole component whichever element + carries it, so this does the same job from inside the body. + + Only while there is something to wait for: a poll that keeps running + after the credentials are on screen is a request every two seconds for + nothing. --}} + @if ($waiting && ! $credentials && ! $failed) + + @endif @if ($credentials) -

{{ __('instances.admin_ready') }}

+

{{ __('instances.admin_ready') }}

@@ -21,34 +33,35 @@

{{ __('instances.admin_once') }}

- -
- {{ __('common.close') }} -
@elseif ($failed) -

+

{{ __('instances.admin_failed') }}

-
- {{ __('common.close') }} -
@elseif ($waiting) -
+
{{ __('instances.admin_working') }}
@else -

{{ __('instances.admin_body') }}

+

{{ __('instances.admin_body') }}

-
+ -
- {{ __('instances.cancel') }} - - {{ __('instances.admin_confirm') }} - -
@endif -
+ + +
+ + {{ $credentials || $failed ? __('common.close') : __('instances.cancel') }} + + @if (! $credentials && ! $failed && ! $waiting) + {{-- form=, because the footer sits outside the
it submits. --}} + + {{ __('instances.admin_confirm') }} + + @endif +
+
+
diff --git a/resources/views/livewire/admin/vpn-config-access.blade.php b/resources/views/livewire/admin/vpn-config-access.blade.php index 87ca32c..9fff13c 100644 --- a/resources/views/livewire/admin/vpn-config-access.blade.php +++ b/resources/views/livewire/admin/vpn-config-access.blade.php @@ -1,23 +1,17 @@ -
-

{{ __('vpn.get_config_title', ['name' => $name]) }}

- +{{-- R24: the configuration itself is long, so it is the part that scrolls — + the title and the close button stay where they were. --}} + @if ($config === null) -

{{ __('vpn.get_config_body') }}

+

{{ __('vpn.get_config_body') }}

- + {{-- autocomplete hint keeps password managers from offering to save this as a new login for the site. --}} -
- {{ __('vpn.cancel') }} - - {{ __('vpn.reveal') }} - -
@else -
+
{{ $config }}
@@ -40,9 +34,20 @@
@endif -
- {{ __('common.close') }} -
@endif -
+ + +
+ + {{ $config === null ? __('vpn.cancel') : __('common.close') }} + + @if ($config === null) + {{-- form=, because the footer sits outside the
. --}} + + {{ __('vpn.reveal') }} + + @endif +
+
+
diff --git a/resources/views/livewire/confirm-cancel-package.blade.php b/resources/views/livewire/confirm-cancel-package.blade.php index 841ff0f..4aac027 100644 --- a/resources/views/livewire/confirm-cancel-package.blade.php +++ b/resources/views/livewire/confirm-cancel-package.blade.php @@ -1,4 +1,4 @@ -
+
@@ -21,10 +21,12 @@ @error('confirmName')

{{ $message }}

@enderror
-
- {{ __('settings.keep') }} - + +
+ {{ __('settings.keep') }} + {{ __('settings.cancel_confirm') }} -
-
+
+ + diff --git a/resources/views/livewire/confirm-close-account.blade.php b/resources/views/livewire/confirm-close-account.blade.php index dec22d8..9c045c1 100644 --- a/resources/views/livewire/confirm-close-account.blade.php +++ b/resources/views/livewire/confirm-close-account.blade.php @@ -1,37 +1,43 @@ -
- @if ($blocked) +{{-- R24: the header and the footer are outside the scroll region, and both + branches of this modal use the same two slots — the alternative is two + modals in one file, which is how the two drift apart. --}} + +
- + $blocked, + 'bg-danger-bg text-danger' => ! $blocked, + ])>
-

{{ __('settings.close_blocked_title') }}

-

{{ __('settings.close_blocked') }}

+

+ {{ $blocked ? __('settings.close_blocked_title') : __('settings.close_title') }} +

+

+ {{ $blocked ? __('settings.close_blocked') : __('settings.close_body') }} +

-
- {{ __('settings.keep') }} -
- @else -
- - - -
-

{{ __('settings.close_title') }}

-

{{ __('settings.close_body') }}

-
-
-
+ + + @if (! $blocked) +
@error('confirmWord')

{{ $message }}

@enderror
-
- {{ __('settings.keep') }} - - {{ __('settings.close_confirm') }} - -
@endif -
+ + +
+ {{ __('settings.keep') }} + @if (! $blocked) + + {{ __('settings.close_confirm') }} + + @endif +
+
+
diff --git a/resources/views/livewire/edit-mailbox.blade.php b/resources/views/livewire/edit-mailbox.blade.php index 9ce4bb0..39ec75e 100644 --- a/resources/views/livewire/edit-mailbox.blade.php +++ b/resources/views/livewire/edit-mailbox.blade.php @@ -1,7 +1,5 @@ -
-

{{ __('mail_settings.edit') }}

- -
+ +
@@ -50,8 +48,10 @@
-
- {{ __('common.cancel') }} - {{ __('mail_settings.save') }} -
-
+ +
+ {{ __('common.cancel') }} + {{ __('mail_settings.save') }} +
+
+ diff --git a/resources/views/livewire/edit-seat.blade.php b/resources/views/livewire/edit-seat.blade.php index 19c4cac..c18b8f5 100644 --- a/resources/views/livewire/edit-seat.blade.php +++ b/resources/views/livewire/edit-seat.blade.php @@ -1,12 +1,16 @@ -
-
-

{{ __('users.edit_title') }}

- @if ($isOwner) - {{ __('users.role_owner') }} - @endif -
+ + {{-- Its own header: the badge belongs beside the title, which the plain + `title` prop cannot express. --}} + +
+

{{ __('users.edit_title') }}

+ @if ($isOwner) + {{ __('users.role_owner') }} + @endif +
+
-
+
-
- {{ __('common.cancel') }} - {{ __('users.save') }} -
-
+ +
+ {{ __('common.cancel') }} + {{ __('users.save') }} +
+
+ diff --git a/resources/views/livewire/new-support-request.blade.php b/resources/views/livewire/new-support-request.blade.php index 22fdd0d..3c3b213 100644 --- a/resources/views/livewire/new-support-request.blade.php +++ b/resources/views/livewire/new-support-request.blade.php @@ -1,8 +1,5 @@ -
-

{{ __('support.new') }}

-

{{ __('support.new_hint') }}

- -
+ +