SOLCOM Bubble Card: SVG-Notch mit JS-getriebener Pille-Width-Anpassung
ResizeObserver-Drupal-Behavior zur Layout-Time auf die tatsächliche Pille-Width abgestimmt; SSR liefert einen plausiblen statischen Fallback.
Kontext
Figma 4.3 (profile-2-2, Node 691:14029 aus File CHHDfbpX8Ys2LBiGs5HVK0) zeigt eine Bubble Card mit einem S-förmigen Notch unten-rechts, in dem eine CTA-Pille sitzt. Die Notch-Geometrie selbst ist Figma-fest: 28-px-Radius S-Curve aus zwei verbundenen Quarter-Circles, 56-px-Step-Down, 36-px-Outer-Corner-Radius. Variabel ist nur die horizontale Position der S-Curve — sie muss bündig mit dem linken Rand der Pille (+4 px Tuck-Under) abschließen.
Constraints aus dem Design
- Card-Width skaliert auf 100 % des Containers (Container-fluid-Layout).
- Card-Height ist content-driven (kein min-height, keine fixe Höhe).
- Pille-Width ist content-driven — der Site-Builder pflegt das Button-Label in einem Drupal-Canvas-Slot; mögliche Texte reichen von „OK" bis „Mehr Informationen anfragen".
Initial-Implementierung war ein einzelner SVG-<path> mit hardcoded viewBox aus den Figma-Maßen (544×581) plus preserveAspectRatio="none". Das streckt den Path proportional zur Card-Width — Notch und S-Curve werden bei breiten Cards horizontal verzerrt, und die S-Curve sitzt bei festem prozentualem Offset, nicht an der Pille.
Entscheidung
Single SVG-Path im SDC, zur Layout-Time refined durch Drupal-Behavior mit ResizeObserver. Konkret:
components/bubble-card-frame/rendert den Figma-Original-Path als SSR-Default. Das Markup ist ohne JavaScript valid und visuell plausibel (mit Aspect-Stretch).js/bubble-card-notch.jssetzt zur Layout-TimeviewBox="0 0 W H"aus den gemessenen Card-Maßen und überschreibt daspath dmit dem Output vonbuildPath(w, h, pw, variant). Der Path-Builder rechnet die Notch-Position aus der Pille-Width zurück.- Pille-Width und Card-Maße werden via zwei
ResizeObserver-Subscriptions beobachtet (Card und Pille separat). Ändert sich eine der beiden, wird der Path nachgezogen. - Pille bleibt CSS-positioniert:
position: absolute; right: 0.75rem; bottom: 0. Der Path richtet sich nach der Pille, nicht umgekehrt. - Geometrie-Konstanten (BR=36 outer-corner-radius, R=28 S-curve-radius, SD=56 step-down-height, K=0.5523 cubic-Bezier-Quarter-Circle-Approximation, PILLE_RIGHT_PAD=12, PILLE_LEFT_OFFSET=4 für den Tuck-Under) sind im JS hardcoded und matchen die Figma-Original-Werte.
- Der Path-Builder clamped die Notch-Position: minimal
BR + 2*R + 4, maximalnotch_x_right - 2. Bei Pille-Width = 0 oder Variant ohne Notch erzeugt derselbe Builder ein plain Rounded Rectangle. - SDC-Library-Attach in
bubble-card-frame.twig:{{ attach_library('byte_theme/bubble-card-notch') }}. Die Library steht inbyte_theme.libraries.ymlmit Dependencies aufcore/drupalundcore/once. - Card-Familie wird auf zwei Komponenten reduziert:
card-bubble-profile(mit CTA-Slot) undcard-bubble-profile-no-button(ohne). Frame-Variantenrow-1,row-2,row-3werden aus dem Schema entfernt.
byte_theme/bubble-card-notch aus byte_theme.libraries.yml entfernen und das JS-File löschen — Cards rendern wieder mit dem statischen Figma-Path im Twig.Rejected alternatives
Twig-Prop button_width (Site-Builder pflegt Notch-Width manuell) rejected
Pure server-side, kein JS, predictable. Funktioniert aber nur bei fester Card-Width — der Path-Generator braucht absolute Maße. Kollidiert frontal mit der 100-%-Width-Anforderung.
3-Layer-DOM mit Flexbox-Sized Notch rejected
Pure CSS, content-driven via flex-shrink: 0, runtime-dynamic ohne JS. Vier blockierende Implementierungs-Issues: (1) Border-Gradient-Continuity über vier Sub-Elemente, (2) border-image macht Corners square, (3) würde den Slot-Wrapper-Selektor für Drupal-Canvas Edit-Mode brechen, (4) Geometrie ist asymmetrisch. Realistisch 2–3 h Aufwand statt der initialen 1-h-Schätzung.
CSS-only mit Pille-Halo-Pseudo-Element (::before) rejected
Pure CSS, kein JS, content-driven. Visuell aber NICHT der Figma-S-Curve — die Halo-Form ist eine Pille-mit-Rim, kein Step-Down mit S-Curve-Übergang. Brand-Drift-Risiko.
CSS Houdini Paint API rejected
Technisch elegant, content-driven via CSS-Variablen. Browser-Baseline disqualifiziert (Chromium-only, kein Safari, kein Firefox).
<path style="d: path('M ...')"> mit CSS-Variablen rejected
Das d-CSS-Property mit dynamischer path()-Funktion ist in Safari < 16 nicht supported, fehlende SVG-2-Adoption macht das fragil. Außerdem können CSS-Variablen nicht direkt in der path()-Funktion interpoliert werden.
Sub-SVG-Composition im Bottom-Frame rejected
Kombiniert die Probleme aus 3-Layer-DOM (Border-Continuity über Segmente) mit der CSS-Variable-Restriction für SVG-Path-d. Kein Vorteil gegenüber der JS-Lösung.
Consequences
- JavaScript-Dependency für visuelle Geometrie ist akzeptiert. Drupal
behaviorsmitResizeObserverist Standard-Pattern in Drupal 11. Die SSR-Default-Path-Variante rendert ohne JS sichtbar — Cards sind funktional, nur der Notch sitzt nicht punktgenau. Bewusster Graceful-Degradation-Trade-off. - FOUC-Risk auf Cold-Render. Beim ersten Page-Load rendert SSR den statischen Path; sobald JavaScript greift, snappt der Notch in Position. In lokalen ddev-Tests war der Sprung nicht sichtbar (< 16 ms). Bei langsamen Connections kann ein User den Snap sehen.
- Initial-Render-Race bei
card.offsetWidth = 0. Wenn das Behavior vor dem ersten Layout-Frame attached, sind die offsetWidth-/-Height-Werte 0; der Path-Builder bricht ab, und derResizeObservergreift erst beim nächsten Layout-Tick. In Tests reproduzierbar, mit funktionalem Outcome. - ResizeObserver-Browser-Baseline ist Safari 13.1+, Chrome 64+, Firefox 69+ — kompatibel mit allen anderen SOLCOM-Browser-Constraints (vgl. ADR-0003, ADR-0004).
- Geometrie-Konstanten leben jetzt JS-seitig, nicht im Twig. Wenn Figma S-Curve-Radien, Step-Down oder Border-Radii ändert, müssen
bubble-card-notch.jsUND die SSR-Default-Pfade inbubble-card-frame.twigparallel geupdatet werden. - Behaviors müssen bei Drupal-AJAX-Reattach idempotent sein.
once('bubble-card-notch', ...)schützt vor doppelt-Attach. DerResizeObserverbleibt bis zum DOM-Detach aktiv — bei Layout-Builder-Edit-Modus ohne explizitesro.disconnect()auf Card-Removal ein potenzieller Memory-Leak. Mitigation bei Bedarf:ro.disconnect()in einemMutationObserverauf Card-Removal nachschärfen. profile-2-2-no-ctarendert keinen Notch und nutzt denselben Path-Builder mithasNotch-Branch. Pille-Width = 0 erzeugt ein plain Rounded Rectangle. Kein Code-Pfad-Duplikat.- Card-Familie ist gemäßigt —
card-bubble-textundcard-bubble-iconsind raus. Style-Helper-CSS-Klassen (.bubble-card__heading,.bubble-card__bullets,.bubble-card__avatar, …) bleiben verfügbar für Site-Builder in Canvas-Inhalten.
FAQ
- Was passiert, wenn JavaScript deaktiviert ist?
- Cards rendern mit dem statischen Figma-Original-Path (SSR-Default) — Notch sitzt nicht pixel-perfekt an der Pille, aber die Card ist voll funktional. Bewusster Graceful-Degradation-Trade-off.
- Wie wird ein Figma-Update der Geometrie eingespielt?
- Geometrie-Konstanten in
bubble-card-notch.jsUND den SSR-Default-Path inbubble-card-frame.twigparallel aktualisieren. Beide Stellen sind im Code kommentiert.