PRD · P2503-164

Slider-Container & Slider-Item SDC — Erweiterungen

done

TL;DR. Die bestehenden SDCs byte_theme:slider-container und byte_theme:slider-item werden um drei Funktionen erweitert: (1) Infinite Carousel mit automatischem Mobile-Disable unter 768 px, (2) Grab-Cursor beim Hovern und Dragging (inkl. Kind-Elementen), (3) Bleed-Prop für einen seitlichen Kartenüberlauf-Effekt. Alle Änderungen sind rückwärtskompatibel — bestehende Canvas-Seiten, die den Slider verwenden, ändern ihr Verhalten nicht.

Problem Statement

Redakteure und Site-Builder haben drei Anforderungen, die der Slider-Container vor dieser Story nicht erfüllen konnte:

  1. Infinite Carousel. Bestimmte Sektionen (z. B. Experten-Karussell) sollen ein endlos-loopierendes Carousel zeigen, bei dem das aktive Slide zentriert und Nachbar-Slides beidseitig sichtbar sind. Der bisherige Slider-Container konnte nur seitenbasiert navigieren.
  2. Fehlende Drag-Geste. Obwohl der Slider per Maus-Drag bedienbar war, zeigte der Cursor kein visuelles Signal. Nutzer erkannten die Drag-Funktion nicht. Besonders auf Child-Elementen (Links, Bilder) wurde der Cursor durch deren eigenen Default-Cursor überschrieben.
  3. Bleed-Effekt. Eine Figma-Spec zeigt 2 Cards im Slider, deren äußere Kanten links und rechts leicht abgeschnitten sind — ein subtiler „es gibt mehr"-Hinweis ohne sichtbare Nachbar-Slides. Weder peek noch ein Custom-CSS-Override konnten diesen Effekt zuverlässig erzeugen.

Solution

Drei additive Erweiterungen am slider-container-SDC (Twig, JS, YAML); slider-item bleibt unverändert. Alle neuen Props haben sichere Defaults (infinite: false, bleed: none), sodass keine bestehende Canvas-Konfiguration bricht.

Das Mobile-Disable-Pattern für Infinite stellt sicher, dass das Carousel auf schmalen Viewports (unter 768 px) immer als Paged-Slider läuft — Infinite-Geometrie ist auf kleinen Screens visuell nicht sinnvoll und erfordert eine andere Slide-Breite.

User Stories

  1. Als Site-Builder möchte ich einen Slider mit Prop infinite: true konfigurieren können, der ein endloses Karussell zeigt, damit ich Experten- oder Partner-Sektionen nach Figma-Spec umsetzen kann.
  2. Als Besucher der Website möchte ich beim Hovern über einen Slider eine Hand-Geste sehen, damit ich erkenne, dass der Slider per Maus bedienbar ist.
  3. Als Site-Builder möchte ich mit dem Prop bleed: sm einen subtilen seitlichen Überlauf-Effekt aktivieren können, damit ich das Figma-Design mit 2 Cards und abgeschnittenen Außenkanten umsetzen kann.
  4. Als Entwickler möchte ich, dass Infinite auf Mobile automatisch deaktiviert wird, damit keine Layout-Probleme auf schmalen Viewports entstehen, ohne dass der Site-Builder das manuell konfigurieren muss.

Features im Detail

1 — Infinite Carousel

Neuer Bool-Prop infinite (Default: false). Im Infinite-Modus:

  • Das aktive Slide ist zentriert; linke und rechte Nachbar-Slides sind je zur Hälfte sichtbar.
  • Navigation wraps endlos — kein Disable der Pfeil-Buttons am ersten/letzten Slide.
  • Die Props loop, peek und slides_per_view werden ignoriert, wenn Infinite aktiv ist.
  • Auf Viewports < 768 px ist Infinite immer deaktiviert, unabhängig vom Prop-Wert (data-infinite="false" im DOM).

Canvas-Registrierung: infinite ist im component.yml als boolean-Prop mit meta:enum-Labels deklariert und erscheint im Canvas-Editor als Toggle.

2 — Cursor-Grab beim Hovern & Dragging

Der Slides-Wrapper erhält die Tailwind-Klassen cursor-grab [&_*]:cursor-grab. Der Descendant-Selektor ist notwendig, weil Kind-Elemente (z. B. <a>-Tags in Cards) den vom Parent geerbten Cursor mit ihrem Default überschreiben würden.

Während des Drag-Vorgangs setzt das JS data-grabbing am Container-Element. Über den CSS-Selektor [.slider-container[data-grabbing]_&]:cursor-grabbing und dessen [&_*]-Pendant wird der grabbing-Cursor auf dem gesamten Slider und allen Kind-Elementen erzwungen.

Im Canvas-Editor (data-canvas-editor) sind alle Grab-Klassen deaktiviert (cursor-default / cursor-auto auf Kind-Elementen).

3 — Bleed-Prop

Neuer Enum-Prop bleed mit den Werten none (Default) und sm (15 px pro Seite). Bei aktivem Bleed:

  • Der Viewport-Wrapper erhält negative Seitenmargins (-15px), sodass der Slide-Inhalt 15 px über den Container hinausragt.
  • Das initiale translateX wird um den Bleed-Betrag nach links korrigiert, damit der erste Slide nicht bündig an der Container-Kante sitzt, sondern ebenfalls links abgeschnitten wird.
  • Bleed wird ignoriert, wenn infinite: true oder peek != none aktiv ist.

Props-Schema

PropTypDefaultEnumCanvas-LabelNeu?
infinitebooleanfalseInfinite carouselNeu
bleedstringnonenone, smCard bleedNeu
peekstringnonenone, sm, md, lgSlide peekvorhanden
loopbooleantrueLoopvorhanden
slides_per_viewnumber31, 2, 3, 4Slides per viewvorhanden
autoplaybooleanfalseAutoplayvorhanden
autoplay_intervalnumber40002000–8000Autoplay intervalvorhanden

Technische Entscheidungen

Mobile-Disable via separatem Active-State-Feld

Das JS trennt den Prop-Wert (#infinite) vom effektiv aktiven Wert (#infiniteActive). #infiniteActive wird in init() aus dem Prop-Wert und einem Breakpoint-Check berechnet. Alle Methoden arbeiten ausschließlich mit #infiniteActive.

Ein resize-Listener aktualisiert #infiniteActive bei Viewport-Änderungen (z. B. Rotation von Portrait → Landscape auf Tablets). Alternative verworfen: CSS-Media-Query allein würde JS-Logik (Clone-Handling, Slide-Breite) nicht deaktivieren — JS muss den Zustand kennen.

data-grabbing statt inline-style

Das JS setzt data-grabbing am Container-Element statt track.style.cursor = "grabbing". Der Inline-Style würde zwar den Track selbst betreffen, aber nicht die Descendant-Selektor-Cascade für Kind-Elemente auslösen. Das data-grabbing-Attribut am äußersten Container erlaubt den vollen CSS-Selektorzugriff auf alle Nachfahren.

Bleed-Korrektur am translateX

Ohne Korrektur liegt der erste Slide bündig am linken Viewport-Rand, da der Track bei translateX(0) startet und die negativen Margins den Viewport nach außen erweitern, nicht nach innen. Die Formel lautet:

translateX = peek_px - bleed_px - (current * slideStep)

Das subtrahierte bleed_px verschiebt den Track beim initialen Render um 15 px nach links, sodass der erste Slide denselben Clip wie alle anderen erhält.

Testing

Kernel-Render-Contract-Tests in tests/src/Kernel/SliderComponentTest.php (#[Group('slider')]). Gepinnt werden strukturelle DOM-Elemente und data-*-Attribute — kein visuelles Styling.

TestPrüft
testSliderContainerRendersStructure.slider-container, .slider__track, .slider__slides, data-loop, data-slides-per-view, data-peek
testSliderContainerAlwaysRendersHeaderRow.slider__header, .slider__prev, .slider__next, .slider__counter
testSliderContainerHeaderShowsHeadlineSlotSlot-Content landet in .slider__headline
testSliderContainerPeekPropSetsDataAttributedata-peek="md", overflow-hidden auf Viewport
testSliderItemRendersAsSlide.slider__slide
testSliderContainerInfiniteDataAttributedata-infinite="true" bei infinite: true
testSliderContainerInfiniteDefaultsFalsedata-infinite="false" ohne Prop

Ausführen: ddev exec ./vendor/bin/phpunit --group slider

Out of Scope

  • Autoplay-Pause bei Hover — wurde nicht angefragt; bestehende Autoplay-Logik unverändert.
  • Swipe-Gesten auf Touch (Mobile) — Touch-Events sind bereits implementiert; kein neues Scope.
  • Infinite + Peek kombiniert — Infinite deaktiviert Peek (dokumentiert in component.yml).
  • Animiertes Infinite-Cloning (GSAP, CSS-Keyframes) — Standard CSS-Transition reicht für den Use-Case.
  • slider-item Änderungen — nicht Teil dieser Story.