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

@ -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>