Akkordeons weicher aufklappen, starken Hover entfernen

Die Klappelemente sprangen auf und zu: x-show gibt dem Element sofort die
volle Hoehe und blendet nur den Inhalt ein. Ersetzt durch eine Animation von
grid-template-rows 0fr → 1fr ueber 500 ms mit cubic-bezier(0.33, 1, 0.68, 1),
die die tatsaechliche Hoehe bewegt – ohne das Alpine-Collapse-Plugin, das in
diesem Projekt nicht registriert ist.

- Der zugeklappte Zustand steht auch statisch im class-Attribut, damit vor dem
  Start von Alpine nichts aufgeklappt aufblitzt.
- aria-hidden haengt am Zustand, weil der Inhalt jetzt dauerhaft im DOM steht.
- Hover: die Flaechenaufhellung (hover:bg-muted/30) ist raus, geblieben ist ein
  feiner Farbwechsel der Ueberschrift. Im FAQ entfaellt zusaetzlich der
  Hintergrundwechsel am Icon-Kreis.
- Der Pfeil dreht in derselben Dauer und Kurve wie das Aufklappen.

Betrifft beide Akkordeons – die Vertiefungen der Markenseiten und das FAQ.

Tests: 861 gruen (3189 Assertions), Pint sauber, npm run build:b2in gebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-03 10:00:39 +00:00
parent a52347845e
commit f971f416db
5 changed files with 115 additions and 54 deletions

View file

@ -66,6 +66,7 @@ resources/views/livewire/web/components/sections/brand-detail.blade.php
resources/views/livewire/web/components/sections/brand-overview.blade.php
resources/views/livewire/web/components/sections/core-brands.blade.php
resources/views/livewire/web/components/sections/display-feed.blade.php
resources/views/livewire/web/components/sections/f-a-q.blade.php
resources/views/livewire/web/components/sections/immobilien-teaser.blade.php
resources/views/livewire/web/components/sections/magazin-list.blade.php
resources/views/livewire/web/components/sections/marken-cta.blade.php

View file

@ -19,7 +19,7 @@ eingearbeitet. Offen: Bildmaterial, Rechtstexte und die Herstellerfreigaben.
**Stand 03.09.2026:** T1 und B1–B13 abgearbeitet, **alle Arbeitspakete AP0–AP6 umgesetzt**, die
Bestandsinhalte auf den Interior-Auftritt umgestellt, die Seite abgestimmt und alle vier Punkte
der Abstimmungsrunde vom 03.09. umgesetzt (Nachtrag 5). Alle drei Markenseiten haben jetzt einen
ausführlichen Text. `php artisan test --compact`: **859 grün** (3180 Assertions), Pint sauber,
ausführlichen Text. `php artisan test --compact`: **861 grün** (3189 Assertions), Pint sauber,
`npm run build:b2in` gebaut.
**Die Seite ist testbereit** – die ausstehenden Zulieferungen trägt Marcel selbst im CMS nach.
@ -986,6 +986,28 @@ Textlink. Neu:
Die Textinhalte kommen unverändert aus dem CMS (`cta_title` / `cta_text`) – Punkt 4 der Liste war
inhaltlich bereits durch die neuen Texte erledigt („Ihre Küche. Persönlich geplant.").
#### 5 · Akkordeons weicher (Nachreichung aus derselben Runde)
Rückmeldung: Die Klappelemente springen zu schnell auf und zu, der Hover-Effekt ist zu stark.
Ursache war `x-show` mit einer Opacity-Bewegung: Das Element nimmt sofort seine volle Höhe ein
und blendet nur den Inhalt ein – es sieht aus wie ein Sprung. Ersetzt durch eine Animation von
`grid-template-rows: 0fr → 1fr` über 500 ms mit `cubic-bezier(0.33, 1, 0.68, 1)`, die die
tatsächliche Höhe bewegt. Das Alpine-Collapse-Plugin ist in diesem Projekt nicht registriert;
diese Lösung kommt ohne neue Abhängigkeit aus.
- Der zugeklappte Zustand steht **auch statisch im `class`-Attribut**, damit vor dem Start von
Alpine nichts aufgeklappt aufblitzt. Die Objekt-Schreibweise von `x-bind:class` entfernt ihn
beim Öffnen wieder.
- `aria-hidden` hängt am Zustand, weil der Inhalt jetzt dauerhaft im DOM steht.
- **Hover:** Die Flächenaufhellung (`hover:bg-muted/30`) ist raus. Geblieben ist ein feiner
Farbwechsel der Überschrift auf die Sekundärfarbe. Im FAQ entfällt zusätzlich der
Hintergrundwechsel am Icon-Kreis.
- Der Pfeil dreht in derselben Dauer und Kurve wie das Aufklappen, damit beides zusammenläuft.
Betrifft **beide** Akkordeons – die Vertiefungen auf den Markenseiten und das FAQ; sie hatten
dasselbe Verhalten.
#### Tests
`BrandPageSectionsTest` und `BrandPageTest` erweitert:
@ -1000,8 +1022,11 @@ inhaltlich bereits durch die neuen Texte erledigt („Ihre Küche. Persönlich g
erscheint auf der deutschen Seite.
- Demogalerien tragen keinen „BEISPIEL"-Vermerk mehr, dafür durchgehend eine Kategorie.
- Der Normalizer entfernt den Vermerk, ohne Bild, Kategorie oder Reihenfolge zu verändern.
- Jede Vertiefung rendert zugeklappt und mit der Höhen-Animation; die alte Sprung-Bewegung ist
weg.
- Weder Markenseite noch FAQ verwenden noch den Flächen-Hover.
`php artisan test --compact`: **859 grün** (3180 Assertions), Pint sauber, `npm run build:b2in`
`php artisan test --compact`: **861 grün** (3189 Assertions), Pint sauber, `npm run build:b2in`
gebaut.
---

View file

@ -88,27 +88,32 @@
<button type="button" x-on:click="open = !open"
x-bind:aria-expanded="open.toString()"
aria-controls="detail-{{ $index }}"
class="flex w-full items-center justify-between gap-4 p-5 text-left cursor-pointer hover:bg-muted/30 transition-colors">
<span class="font-medium">{{ $section['detail']['title'] }}</span>
<svg class="h-5 w-5 shrink-0 text-secondary transition-transform duration-200"
x-bind:class="open ? 'rotate-180' : ''" fill="none" stroke="currentColor"
class="group flex w-full items-center justify-between gap-4 p-5 text-left cursor-pointer">
<span class="font-medium transition-colors duration-300 group-hover:text-secondary">{{ $section['detail']['title'] }}</span>
<svg class="h-5 w-5 shrink-0 text-secondary transition-transform duration-500 ease-[cubic-bezier(0.33,1,0.68,1)]"
x-bind:class="{ 'rotate-180': open }" fill="none" stroke="currentColor"
stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</button>
{{-- x-transition statt x-collapse: das Collapse-Plugin ist in diesem
Projekt nicht registriert, dieselbe Bewegung wie im FAQ-Akkordeon. --}}
<div id="detail-{{ $index }}" x-show="open" x-cloak
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 -translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-end="opacity-0 -translate-y-2">
<div class="px-5 pb-5 border-t border-border/60 pt-4">
@foreach ($section['detail']['paragraphs'] as $paragraph)
<p class="text-muted-foreground leading-relaxed mt-3 first:mt-0">{{ $paragraph }}</p>
@endforeach
{{-- Weiches Auf- und Zuklappen ueber grid-template-rows (0fr → 1fr):
das animiert die tatsaechliche Hoehe, waehrend x-show den Inhalt
schlagartig einblendet. Das Alpine-Collapse-Plugin ist in diesem
Projekt nicht registriert, dies kommt ohne Abhaengigkeit aus. --}}
{{-- Der zugeklappte Zustand steht auch im statischen class-Attribut,
damit vor dem Start von Alpine nichts aufgeklappt aufblitzt. Die
Objekt-Schreibweise von x-bind:class entfernt ihn beim Oeffnen. --}}
<div id="detail-{{ $index }}"
class="grid grid-rows-[0fr] opacity-0 transition-all duration-500 ease-[cubic-bezier(0.33,1,0.68,1)]"
x-bind:class="{ 'grid-rows-[1fr] opacity-100': open, 'grid-rows-[0fr] opacity-0': !open }"
x-bind:aria-hidden="(!open).toString()">
<div class="overflow-hidden">
<div class="px-5 pb-5 border-t border-border/60 pt-4">
@foreach ($section['detail']['paragraphs'] as $paragraph)
<p class="text-muted-foreground leading-relaxed mt-3 first:mt-0">{{ $paragraph }}</p>
@endforeach
</div>
</div>
</div>
</div>

View file

@ -25,34 +25,39 @@
<dt>
<button
@click="openIndex = openIndex === {{ $index }} ? null : {{ $index }}"
class="flex w-full items-center justify-between p-6 text-left hover:bg-muted/30 transition-colors duration-200"
class="group flex w-full items-center justify-between p-6 text-left cursor-pointer"
:aria-expanded="openIndex === {{ $index }}"
>
<span class="text-lg font-medium text-foreground pr-6">
<span class="text-lg font-medium text-foreground pr-6 transition-colors duration-300 group-hover:text-secondary">
{{ $faq['question'] }}
</span>
<span class="flex-shrink-0 ml-6 flex h-8 w-8 items-center justify-center rounded-full bg-secondary/10 transition-all duration-200"
:class="openIndex === {{ $index }} ? 'bg-secondary text-white' : 'text-secondary hover:bg-secondary/20'">
<svg class="w-5 h-5 transition-transform duration-200"
:class="openIndex === {{ $index }} ? 'rotate-180' : ''"
<span class="flex-shrink-0 ml-6 flex h-8 w-8 items-center justify-center rounded-full bg-secondary/10 transition-colors duration-300"
:class="openIndex === {{ $index }} ? 'bg-secondary text-white' : 'text-secondary'">
<svg class="w-5 h-5 transition-transform duration-500 ease-[cubic-bezier(0.33,1,0.68,1)]"
:class="{ 'rotate-180': openIndex === {{ $index }} }"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</span>
</button>
</dt>
<dd x-show="openIndex === {{ $index }}"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 transform -translate-y-2"
x-transition:enter-end="opacity-100 transform translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 transform translate-y-0"
x-transition:leave-end="opacity-0 transform -translate-y-2"
class="px-6 pb-6">
<div class="border-t border-border pt-4">
<p class="text-muted-foreground leading-relaxed">
{{ $faq['answer'] }}
</p>
{{-- grid-template-rows 0fr → 1fr animiert die tatsaechliche Hoehe. Der
zugeklappte Zustand steht auch statisch im class-Attribut, damit vor
dem Start von Alpine nichts aufgeklappt aufblitzt. --}}
<dd class="grid grid-rows-[0fr] opacity-0 transition-all duration-500 ease-[cubic-bezier(0.33,1,0.68,1)]"
:class="{
'grid-rows-[1fr] opacity-100': openIndex === {{ $index }},
'grid-rows-[0fr] opacity-0': openIndex !== {{ $index }},
}"
:aria-hidden="(openIndex !== {{ $index }}).toString()">
<div class="overflow-hidden">
<div class="px-6 pb-6">
<div class="border-t border-border pt-4">
<p class="text-muted-foreground leading-relaxed">
{{ $faq['answer'] }}
</p>
</div>
</div>
</div>
</dd>
</div>
@ -80,34 +85,39 @@
<dt>
<button
@click="openIndex = openIndex === {{ $index }} ? null : {{ $index }}"
class="flex w-full items-center justify-between p-6 text-left hover:bg-muted/30 transition-colors duration-200"
class="group flex w-full items-center justify-between p-6 text-left cursor-pointer"
:aria-expanded="openIndex === {{ $index }}"
>
<span class="text-lg font-medium text-foreground pr-6">
<span class="text-lg font-medium text-foreground pr-6 transition-colors duration-300 group-hover:text-secondary">
{{ $faq['question'] }}
</span>
<span class="flex-shrink-0 ml-6 flex h-8 w-8 items-center justify-center rounded-full bg-secondary/10 transition-all duration-200"
:class="openIndex === {{ $index }} ? 'bg-secondary text-white' : 'text-secondary hover:bg-secondary/20'">
<svg class="w-5 h-5 transition-transform duration-200"
:class="openIndex === {{ $index }} ? 'rotate-180' : ''"
<span class="flex-shrink-0 ml-6 flex h-8 w-8 items-center justify-center rounded-full bg-secondary/10 transition-colors duration-300"
:class="openIndex === {{ $index }} ? 'bg-secondary text-white' : 'text-secondary'">
<svg class="w-5 h-5 transition-transform duration-500 ease-[cubic-bezier(0.33,1,0.68,1)]"
:class="{ 'rotate-180': openIndex === {{ $index }} }"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</span>
</button>
</dt>
<dd x-show="openIndex === {{ $index }}"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 transform -translate-y-2"
x-transition:enter-end="opacity-100 transform translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 transform translate-y-0"
x-transition:leave-end="opacity-0 transform -translate-y-2"
class="px-6 pb-6">
<div class="border-t border-border pt-4">
<p class="text-muted-foreground leading-relaxed">
{{ $faq['answer'] }}
</p>
{{-- grid-template-rows 0fr → 1fr animiert die tatsaechliche Hoehe. Der
zugeklappte Zustand steht auch statisch im class-Attribut, damit vor
dem Start von Alpine nichts aufgeklappt aufblitzt. --}}
<dd class="grid grid-rows-[0fr] opacity-0 transition-all duration-500 ease-[cubic-bezier(0.33,1,0.68,1)]"
:class="{
'grid-rows-[1fr] opacity-100': openIndex === {{ $index }},
'grid-rows-[0fr] opacity-0': openIndex !== {{ $index }},
}"
:aria-hidden="(openIndex !== {{ $index }}).toString()">
<div class="overflow-hidden">
<div class="px-6 pb-6">
<div class="border-t border-border pt-4">
<p class="text-muted-foreground leading-relaxed">
{{ $faq['answer'] }}
</p>
</div>
</div>
</div>
</dd>
</div>

View file

@ -53,6 +53,26 @@ it('turns every marked deepening into a collapsible area', function () {
->and($html)->toContain('NEW DIMENSIONS');
});
it('renders every deepening collapsed and animates its height', function () {
$html = $this->get('/marken/rotpunkt')->assertSuccessful()->getContent();
// Der zugeklappte Zustand muss im statischen class-Attribut stehen, sonst blitzt der Inhalt
// vor dem Start von Alpine aufgeklappt auf.
expect(substr_count($html, 'grid grid-rows-[0fr] opacity-0 transition-all'))->toBe(4)
// grid-template-rows animiert die tatsaechliche Hoehe – die alte Bewegung blendete den
// Inhalt nur ein und sprang dabei auf die volle Hoehe.
->and($html)->not->toContain('x-transition:enter-start="opacity-0 -translate-y-2"')
->and($html)->toContain("'grid-rows-[1fr] opacity-100': open");
});
it('uses no full-surface hover on the accordion buttons', function () {
foreach (['/marken/rotpunkt', '/faq'] as $path) {
$html = $this->get($path)->assertSuccessful()->getContent();
expect($html)->not->toContain('hover:bg-muted/30');
}
});
it('merges the two sustainability sections as the corrected version requires', function () {
$html = $this->get('/marken/rotpunkt')->assertSuccessful()->getContent();