diff --git a/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Carousel/Carousel.Card.fusion b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Carousel/Carousel.Card.fusion
new file mode 100644
index 000000000..d69ba20ff
--- /dev/null
+++ b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Carousel/Carousel.Card.fusion
@@ -0,0 +1,14 @@
+prototype(Neos.Presentation:Molecule.Carousel.Card) < prototype(Neos.Fusion:Component) {
+
+ headline = ''
+ text = ''
+ link = ''
+
+ renderer = afx`
+
+
+
+
+
+ `
+}
diff --git a/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Carousel/Carousel.fusion b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Carousel/Carousel.fusion
new file mode 100644
index 000000000..4faef6111
--- /dev/null
+++ b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Carousel/Carousel.fusion
@@ -0,0 +1,65 @@
+prototype(Neos.Presentation:Carousel) < prototype(Neos.Fusion:Component) {
+
+ @styleguide {
+ title = 'Carousel'
+ props {
+ headline = 'A nice looking carousel!'
+ cards = Neos.Fusion:DataStructure {
+ 0 {
+ headline = "Card One Headline"
+ text = "Card One Text"
+ link = "Card One Link"
+ }
+ 1 {
+ headline = "Card Two Headline"
+ text = "Card Two Text"
+ link = "Card Two Link"
+ }
+ 2 {
+ headline = "Card Three Headline"
+ text = "Card Three Text"
+ link = "Card Three Link"
+ }
+ 3 {
+ headline = "Card Four Headline"
+ text = "Card Four Text"
+ link = "Card Four Link"
+ }
+ }
+ }
+ }
+
+ @propTypes {
+ headline = PropTypes:String
+ cards = PropTypes:Array {
+ type = PropTypes:DataStructure {
+ headline = PropTypes:String
+ text = PropTypes:String
+ link = PropTypes:String
+ }
+ }
+ }
+
+ @private {
+ cards = Neos.Fusion:Map {
+ items = ${props.cards}
+ itemRenderer = afx`
+
+ `
+ }
+ }
+
+ options = Neos.Fusion:DataStructure {
+ perPage = 3
+ perMove = 1
+ }
+
+ renderer = afx`
+
+ `
+}
diff --git a/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Fragment/Item.fusion b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Fragment/Item.fusion
new file mode 100644
index 000000000..e4cab7aae
--- /dev/null
+++ b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Fragment/Item.fusion
@@ -0,0 +1,17 @@
+prototype(Neos.Presentation:Slider.Fragment.Item) < prototype(Neos.Fusion:Component) {
+ videoUri = null
+ youtubeId = null
+ vimdeoId = null
+ content = null
+ class = 'flex flex-col items-center justify-center'
+ renderer = afx`
+
+ {props.content}
+
+ `
+}
diff --git a/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Slider.fusion b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Slider.fusion
new file mode 100644
index 000000000..914c8ac50
--- /dev/null
+++ b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Slider.fusion
@@ -0,0 +1,54 @@
+prototype(Neos.Presentation:Slider) < prototype(Neos.Fusion:Component) {
+ // This is used for the living styleguide (Monocle)
+ // Read more about this in the README.md
+ @styleguide.props.items = Neos.Fusion:Map {
+ items = ${Array.range(1, 10)}
+ itemRenderer = afx`
+
+ `
+ }
+
+ tagName = 'section'
+ sliderIsDecoration = false
+ class = null
+ slideItemClass = 'flex flex-col items-center justify-center'
+
+ options = Neos.Fusion:DataStructure {
+ # The gap between slides. The CSS format is acceptable, such as 1em.
+ gap = 12
+ }
+
+ attributes = Neos.Fusion:DataStructure
+
+ i18n = Neos.Fusion:Map {
+ items = ${['prev', 'next', 'first', 'last', 'slideX', 'pageX', 'play', 'pause', 'carousel', 'select', 'slide', 'slideLabel', 'playVideo']}
+ keyRenderer = ${item}
+ itemRenderer = ${I18n.translate('Neos.Presentation:Main:splide.' + item)}
+ }
+
+ _hasItems = ${Type.isArray(this.items) && Array.length(this.items)}
+ @if.hasItemsOrContent = ${this._hasItems || this.content}
+
+ renderer = Neos.Fusion:Tag {
+ tagName = ${props.tagName}
+ attributes {
+ x-data = 'slider'
+ data-splide = ${Json.stringify(Array.concat({i18n:props.i18n}, props.options))}
+ aria-label = ${props.label}
+ role = ${props.sliderIsDecoration ? 'group' : null}
+ class = ${Array.push('splide', props.class)}
+ @apply.attributes = ${props.attributes}
+ }
+ content = afx`
+
+ `
+ }
+}
diff --git a/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Slider.js b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Slider.js
new file mode 100644
index 000000000..b2d0350c7
--- /dev/null
+++ b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Slider.js
@@ -0,0 +1,62 @@
+import Alpine from 'alpinejs';
+import Splide from '@splidejs/splide';
+import { Video } from '@splidejs/splide-extension-video';
+
+function getFirstNode(nodeList) {
+ return [...nodeList].filter((node) => node.tagName === 'LI')[0];
+}
+
+function getIndexOfElement(element) {
+ return Array.from(element.parentElement.children).indexOf(element);
+}
+
+Alpine.data('slider', () => ({
+ init() {
+ const rootElement = this.$root;
+ const splide = new Splide(rootElement);
+ const inNeosBackend = window.name === 'neos-content-main';
+
+ // We are in the backend, so we need to refresh the instance on change
+ if (inNeosBackend) {
+ splide.on('mounted', function () {
+ // Update if a slide is added or removed
+ const observeTarget = rootElement.querySelector('.splide__list');
+ const observer = new MutationObserver((mutations) => {
+ mutations.forEach((mutation) => {
+ const addedNode = getFirstNode(mutation.addedNodes);
+ const removedNode = getFirstNode(mutation.removedNodes);
+ if (addedNode || removedNode) {
+ console.log('Refreshing instance');
+ splide.refresh();
+ }
+ if (addedNode) {
+ // Scroll to the new slide
+ splide.go(getIndexOfElement(addedNode));
+ }
+ });
+ });
+ observer.observe(observeTarget, { childList: true });
+
+ // Go to the slide if it gets selceted in the node tree
+ document.addEventListener('Neos.NodeSelected', (event) => {
+ const element = event.detail.element;
+ if (!element.classList.contains('splide__slide')) {
+ return;
+ }
+ splide.go(getIndexOfElement(element));
+ });
+ });
+ }
+
+ splide.mount({ Video });
+ // Disable the play button in the backend
+ splide.Components.Video.disable(inNeosBackend);
+ const maxIndex = splide.length - 1;
+ splide.on('autoplay:playing', (rate) => {
+ // Go to the first slide after the last slide
+ if (rate === 1 && maxIndex === splide.index) {
+ splide.go(0);
+ }
+ });
+ },
+}));
diff --git a/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Slider.pcss b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Slider.pcss
new file mode 100644
index 000000000..59f4f2971
--- /dev/null
+++ b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Slider.pcss
@@ -0,0 +1,19 @@
+@import "@splidejs/splide/dist/css/splide-core.min.css";
+@import "@splidejs/splide/dist/css/themes/splide-default.min.css";
+@import "@splidejs/splide-extension-video/dist/css/splide-extension-video.min.css";
+
+.splide__pagination__page.is-active {
+ background: rgb(50, 50, 50);
+}
+
+.splide__slide {
+ & > figure {
+ width: 100%;
+ margin: 0;
+
+ & > img {
+ width: 100%;
+ height: auto;
+ }
+ }
+}
diff --git a/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Scripts/index.ts b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Scripts/index.ts
index b77cf77ce..d61fc28b8 100644
--- a/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Scripts/index.ts
+++ b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Scripts/index.ts
@@ -8,6 +8,7 @@ import collapse from '@alpinejs/collapse';
import clipboard from '@ryangjchandler/alpine-clipboard';
import typewriter from '@marcreichel/alpine-typewriter';
import '../Molecule/LogoBar/LogoBar';
+import '../Molecule/Slider/Slider';
// @ts-ignore
Alpine.plugin([anchor, clipboard, collapse, focus, intersect, typewriter]);
diff --git a/package.json b/package.json
index edd045995..083bd11e4 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,8 @@
"@floating-ui/dom": "^1.6.3",
"@marcreichel/alpine-typewriter": "^1.2.0",
"@ryangjchandler/alpine-clipboard": "^2.3.0",
+ "@splidejs/splide": "^4.1.4",
+ "@splidejs/splide-extension-video": "^0.8.0",
"alpinejs": "^3.13.5"
}
}