diff --git a/package-lock.json b/package-lock.json
index 8ca7a43..0ee1cb1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"@wordpress/data": "^10.10.0",
"@wordpress/dom-ready": "^4.37.0",
"@wordpress/element": "6.38.0",
+ "@wordpress/hooks": "4.41.0",
"@wordpress/i18n": "^6.10.0",
"@wordpress/icons": "11.5.0",
"@wordpress/interactivity": "6.37.0",
@@ -9825,9 +9826,9 @@
}
},
"node_modules/@wordpress/hooks": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-4.38.0.tgz",
- "integrity": "sha512-nrLo2semyTID4yIlu9/DSKVM9v61Mgrkyr+MNj7LgzlD3PuGjYNzXVh5+ngfgPoKVdhV3kzFhda+1PZ8SK8cYg==",
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-4.41.0.tgz",
+ "integrity": "sha512-WDbLcLA3DOcjDGNLcxHZTPyhltWd/75G2hxFphe/hzcJUNmgysDTSSXO/bBrIWf6rwWD6TS3ejCaGC9J6DwYiw==",
"license": "GPL-2.0-or-later",
"engines": {
"node": ">=18.12.0",
diff --git a/package.json b/package.json
index d397d14..02d2253 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
"@wordpress/data": "^10.10.0",
"@wordpress/dom-ready": "^4.37.0",
"@wordpress/element": "6.38.0",
+ "@wordpress/hooks": "4.41.0",
"@wordpress/i18n": "^6.10.0",
"@wordpress/icons": "11.5.0",
"@wordpress/interactivity": "6.37.0",
diff --git a/src/blocks/carousel/__tests__/templates.test.ts b/src/blocks/carousel/__tests__/templates.test.ts
new file mode 100644
index 0000000..91a2716
--- /dev/null
+++ b/src/blocks/carousel/__tests__/templates.test.ts
@@ -0,0 +1,180 @@
+/**
+ * Unit tests for slide template definitions and the template registry.
+ *
+ * Verifies:
+ * - All default templates have the required shape
+ * - Template inner blocks produce valid BlockInstance arrays
+ * - Query Loop template is flagged correctly
+ * - The `rtcamp.carouselKit.slideTemplates` filter hook is applied
+ *
+ * @package
+ */
+
+///
+
+import { applyFilters } from '@wordpress/hooks';
+import { getSlideTemplates, type SlideTemplate } from '../templates';
+
+/* ── Mocks ────────────────────────────────────────────────────────────────── */
+
+// Provide a minimal createBlock mock that returns a plain object.
+jest.mock( '@wordpress/blocks', () => ( {
+ createBlock: jest.fn( ( name: string, attrs = {}, inner = [] ) => ( {
+ name,
+ attributes: attrs,
+ innerBlocks: inner,
+ clientId: `mock-${ name }-${ Math.random().toString( 36 ).slice( 2, 8 ) }`,
+ } ) ),
+} ) );
+
+jest.mock( '@wordpress/hooks', () => ( {
+ applyFilters: jest.fn( ( _hookName: string, value: unknown ) => value ),
+} ) );
+
+jest.mock( '@wordpress/i18n', () => ( {
+ __: jest.fn( ( str: string ) => str ),
+} ) );
+
+const mockedApplyFilters = jest.mocked( applyFilters );
+let consoleWarnSpy: jest.SpiedFunction< typeof console.warn >;
+
+/* ── Tests ────────────────────────────────────────────────────────────────── */
+
+describe( 'Slide Templates', () => {
+ beforeEach( () => {
+ consoleWarnSpy = jest.spyOn( console, 'warn' ).mockImplementation( () => undefined );
+ mockedApplyFilters.mockClear();
+ mockedApplyFilters.mockImplementation( ( _hookName: string, value: unknown ) => value );
+ } );
+
+ afterEach( () => {
+ consoleWarnSpy.mockRestore();
+ } );
+
+ describe( 'getSlideTemplates()', () => {
+ it( 'returns an array of templates', () => {
+ const templates = getSlideTemplates();
+ expect( Array.isArray( templates ) ).toBe( true );
+ expect( templates.length ).toBeGreaterThanOrEqual( 5 );
+ } );
+
+ it( 'applies the rtcamp.carouselKit.slideTemplates filter', () => {
+ getSlideTemplates();
+ expect( mockedApplyFilters ).toHaveBeenCalledWith(
+ 'rtcamp.carouselKit.slideTemplates',
+ expect.any( Array ),
+ );
+ } );
+
+ it( 'passes a fresh copy of the default templates to filters', () => {
+ mockedApplyFilters.mockImplementationOnce( ( _hookName: string, value: unknown ) => {
+ ( value as SlideTemplate[] ).push( {
+ name: 'testimonial',
+ label: 'Testimonial',
+ description: 'Quote with author name.',
+ icon: 'format-quote',
+ innerBlocks: () => [],
+ } );
+ return value;
+ } );
+
+ const mutatedTemplates = getSlideTemplates();
+ const freshTemplates = getSlideTemplates();
+
+ expect( mutatedTemplates.map( ( template ) => template.name ) ).toContain( 'testimonial' );
+ expect( freshTemplates.map( ( template ) => template.name ) ).not.toContain( 'testimonial' );
+ } );
+
+ it( 'falls back to defaults when a filter returns a non-array value', () => {
+ mockedApplyFilters.mockImplementationOnce( () => 'invalid' as never );
+
+ const templates = getSlideTemplates();
+
+ expect( Array.isArray( templates ) ).toBe( true );
+ expect( templates.length ).toBeGreaterThanOrEqual( 5 );
+ expect( templates.map( ( template ) => template.name ) ).toContain( 'text' );
+ expect( consoleWarnSpy ).toHaveBeenCalledWith(
+ 'rtcamp.carouselKit.slideTemplates filter returned a non-array value. Falling back to default slide templates.',
+ 'invalid',
+ );
+ } );
+ } );
+
+ describe( 'Template Shape', () => {
+ const templates = getSlideTemplates();
+ const templateCases: Array<[ string, SlideTemplate ]> = templates.map( ( template ) => [
+ template.name,
+ template,
+ ] );
+
+ it.each<[ string, SlideTemplate ]>( templateCases )(
+ 'template "%s" has required properties',
+ ( _name, template ) => {
+ expect( typeof template.name ).toBe( 'string' );
+ expect( template.name.length ).toBeGreaterThan( 0 );
+ expect( typeof template.label ).toBe( 'string' );
+ expect( typeof template.description ).toBe( 'string' );
+ expect( template.icon ).toBeDefined();
+ expect( template.icon ).not.toBeNull();
+ expect( [ 'string', 'function', 'object' ] ).toContain(
+ typeof template.icon,
+ );
+ expect( typeof template.innerBlocks ).toBe( 'function' );
+ },
+ );
+
+ it( 'each template has a unique name', () => {
+ const names = templates.map( ( t ) => t.name );
+ expect( new Set( names ).size ).toBe( names.length );
+ } );
+ } );
+
+ describe( 'Default Templates', () => {
+ const templates = getSlideTemplates();
+ const byName = ( name: string ) =>
+ templates.find( ( t ) => t.name === name )!;
+
+ it( 'text template produces a paragraph block', () => {
+ const blocks = byName( 'text' ).innerBlocks();
+ expect( blocks ).toHaveLength( 1 );
+ expect( blocks[ 0 ]!.name ).toBe( 'core/paragraph' );
+ } );
+
+ it( 'image template produces an image block', () => {
+ const blocks = byName( 'image' ).innerBlocks();
+ expect( blocks ).toHaveLength( 1 );
+ expect( blocks[ 0 ]!.name ).toBe( 'core/image' );
+ } );
+
+ it( 'hero template produces a cover with heading, paragraph, and button', () => {
+ const blocks = byName( 'hero' ).innerBlocks();
+ expect( blocks ).toHaveLength( 1 );
+ expect( blocks[ 0 ]!.name ).toBe( 'core/cover' );
+ const inner = blocks[ 0 ]!.innerBlocks;
+ expect( inner ).toHaveLength( 3 );
+ expect( inner[ 0 ]!.name ).toBe( 'core/heading' );
+ expect( inner[ 1 ]!.name ).toBe( 'core/paragraph' );
+ expect( inner[ 2 ]!.name ).toBe( 'core/buttons' );
+ } );
+
+ it( 'image-caption template produces an image and a paragraph', () => {
+ const blocks = byName( 'image-caption' ).innerBlocks();
+ expect( blocks ).toHaveLength( 2 );
+ expect( blocks[ 0 ]!.name ).toBe( 'core/image' );
+ expect( blocks[ 1 ]!.name ).toBe( 'core/paragraph' );
+ } );
+
+ it( 'query-loop template is flagged as isQueryLoop', () => {
+ const ql = byName( 'query-loop' );
+ expect( ql.isQueryLoop ).toBe( true );
+ } );
+
+ it( 'non-query-loop templates are not flagged as isQueryLoop', () => {
+ templates
+ .filter( ( t ) => t.name !== 'query-loop' )
+ .forEach( ( t ) => {
+ expect( t.isQueryLoop ).toBeFalsy();
+ } );
+ } );
+ } );
+} );
diff --git a/src/blocks/carousel/components/TemplatePicker.tsx b/src/blocks/carousel/components/TemplatePicker.tsx
new file mode 100644
index 0000000..117380d
--- /dev/null
+++ b/src/blocks/carousel/components/TemplatePicker.tsx
@@ -0,0 +1,61 @@
+/**
+ * TemplatePicker — grid of slide template options shown during block setup.
+ *
+ * @package
+ */
+
+import { __ } from '@wordpress/i18n';
+import { Button, Icon } from '@wordpress/components';
+import { useRef, useEffect } from '@wordpress/element';
+import type { SlideTemplate } from '../templates';
+
+interface TemplatePickerProps {
+ templates: SlideTemplate[];
+ onSelect: ( template: SlideTemplate ) => void;
+ onBack: () => void;
+}
+
+export default function TemplatePicker( {
+ templates,
+ onSelect,
+ onBack,
+}: TemplatePickerProps ) {
+ const gridRef = useRef< HTMLDivElement >( null );
+
+ useEffect( () => {
+ const firstButton = gridRef.current?.querySelector< HTMLButtonElement >( 'button' );
+ firstButton?.focus();
+ }, [] );
+
+ return (
+
+
+ { templates.map( ( template ) => (
+
+ ) ) }
+
+
+
+ );
+}
diff --git a/src/blocks/carousel/edit.tsx b/src/blocks/carousel/edit.tsx
index 9edf641..89d2635 100644
--- a/src/blocks/carousel/edit.tsx
+++ b/src/blocks/carousel/edit.tsx
@@ -20,11 +20,15 @@ import {
} from '@wordpress/components';
import { plus } from '@wordpress/icons';
import { useSelect, useDispatch } from '@wordpress/data';
-import { useState, useMemo, useCallback, useEffect } from '@wordpress/element';
+import { useState, useMemo, useCallback, useEffect, useRef } from '@wordpress/element';
import { createBlock, type BlockConfiguration } from '@wordpress/blocks';
import type { CarouselAttributes } from './types';
import { EditorCarouselContext } from './editor-context';
import type { EmblaCarouselType } from 'embla-carousel';
+import { getSlideTemplates, type SlideTemplate } from './templates';
+import TemplatePicker from './components/TemplatePicker';
+
+type SetupStep = 'slide-count' | 'template';
export default function Edit( {
attributes,
@@ -55,10 +59,14 @@ export default function Edit( {
const [ emblaApi, setEmblaApi ] = useState();
const [ canScrollPrev, setCanScrollPrev ] = useState( false );
const [ canScrollNext, setCanScrollNext ] = useState( false );
+ const [ setupStep, setSetupStep ] = useState( 'slide-count' );
+ const [ pendingSlideCount, setPendingSlideCount ] = useState( 0 );
const [ scrollProgress, setScrollProgress ] = useState( 0 );
const [ selectedIndex, setSelectedIndex ] = useState( 0 );
const [ slideCount, setSlideCount ] = useState( 0 );
+ const slideTemplates = useMemo( getSlideTemplates, [ getSlideTemplates ] );
+
const { replaceInnerBlocks, insertBlock } = useDispatch( 'core/block-editor' );
const hasInnerBlocks = useSelect(
@@ -85,6 +93,26 @@ export default function Edit( {
}, [ insertBlock, viewportClientId ] );
const showSetup = ! hasInnerBlocks;
+ const prevShowSetup = useRef( showSetup );
+
+ // Reset the setup flow when the placeholder reopens after all inner blocks are removed.
+ // When setup completes, focus the carousel block so focus stays in the canvas.
+ // Supports both iframed and non-iframed editors.
+ useEffect( () => {
+ if ( ! prevShowSetup.current && showSetup ) {
+ setSetupStep( 'slide-count' );
+ setPendingSlideCount( 0 );
+ }
+
+ if ( prevShowSetup.current && ! showSetup ) {
+ const iframe = document.querySelector< HTMLIFrameElement >( 'iframe[name="editor-canvas"]' );
+ const blockNode =
+ iframe?.contentDocument?.getElementById( `block-${ clientId }` ) ??
+ document.getElementById( `block-${ clientId }` );
+ blockNode?.focus();
+ }
+ prevShowSetup.current = showSetup;
+ }, [ showSetup, clientId ] );
// Fetch registered block types for the allowed-blocks token field
const blockTypes = useSelect( ( select ) => {
@@ -197,16 +225,35 @@ export default function Edit( {
],
);
- const handleSetup = ( slideCount: number ) => {
- const slides = Array.from( { length: slideCount }, () =>
- createBlock( 'rt-carousel/carousel-slide', {}, [
- createBlock( 'core/paragraph', {} ),
- ] ),
- );
+ /**
+ * Handle the initial setup of the carousel block
+ *
+ * @param {number} count - The number of slides selected by the user.
+ */
+ const handleSlideCountPicked = ( count: number ) => {
+ setPendingSlideCount( count );
+ setSetupStep( 'template' );
+ };
+
+ /**
+ * Handle the selection of a slide template during setup.
+ *
+ * @param {SlideTemplate} template - The slide template selected by the user.
+ */
+ const handleTemplateSelected = ( template: SlideTemplate ) => {
+ // Query Loop goes directly inside the viewport; regular templates get slide wrappers.
+ const viewportChildren = template.isQueryLoop
+ ? [ createBlock( 'core/query', {}, [] ) ]
+ : Array.from( { length: Math.max( pendingSlideCount, 1 ) }, () =>
+ createBlock( 'rt-carousel/carousel-slide', {}, template.innerBlocks() ),
+ );
replaceInnerBlocks(
clientId,
- [ createBlock( 'rt-carousel/carousel-viewport', {}, slides ), createNavGroup() ],
+ [
+ createBlock( 'rt-carousel/carousel-viewport', {}, viewportChildren ),
+ createNavGroup(),
+ ],
false,
);
};
@@ -435,30 +482,45 @@ export default function Edit( {
-
- { [ 1, 2, 3, 4 ].map( ( count ) => (
+ { setupStep === 'slide-count' && (
+ <>
+
+ { [ 1, 2, 3, 4 ].map( ( count ) => (
+
+ ) ) }
+
- ) ) }
-
-
+ >
+ ) }
+ { setupStep === 'template' && (
+ setSetupStep( 'slide-count' ) }
+ />
+ ) }
diff --git a/src/blocks/carousel/editor.scss b/src/blocks/carousel/editor.scss
index 17dbac6..247d468 100644
--- a/src/blocks/carousel/editor.scss
+++ b/src/blocks/carousel/editor.scss
@@ -2,22 +2,102 @@
* Editor-only styles for Carousel
*/
+:root {
+ // Border colors
+ --carousel-kit-border-default: #ddd;
+ --carousel-kit-border-dashed: #ccc;
+
+ // Background colors
+ --carousel-kit-bg-white: #fff;
+ --carousel-kit-bg-icon: #f0f0f0;
+
+ // Text colors
+ --carousel-kit-text-primary: #1e1e1e;
+ --carousel-kit-text-muted: #757575;
+
+ // Accent / interactive (falls back to WP admin theme color)
+ --carousel-kit-accent: var(--wp-admin-theme-color, #3858e9);
+}
+
// ── Setup chooser ────────────────────────────────────────────────────────────
.rt-carousel-setup {
- .rt-carousel-setup__options {
+ &__options {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 12px;
}
- .rt-carousel-setup__option {
+ &__option {
min-width: 80px;
justify-content: center;
}
- .rt-carousel-setup__skip {
+ &__skip {
+ display: block;
+ margin-top: 4px;
+ }
+}
+
+// ── Template picker ──────────────────────────────────────────────────────────
+.rt-carousel-template-picker {
+ width: 100%;
+
+ &__grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+ gap: 12px;
+ margin-bottom: 12px;
+ }
+
+ &__item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 6px;
+ padding: 16px 12px;
+ border: 1px solid var(--carousel-kit-border-default);
+ border-radius: 4px;
+ background: var(--carousel-kit-bg-white);
+ cursor: pointer;
+ text-align: center;
+ transition:
+ border-color 0.15s,
+ box-shadow 0.15s;
+
+ &:hover,
+ &:focus-visible {
+ border-color: var(--carousel-kit-accent);
+ box-shadow: 0 0 0 1px var(--carousel-kit-accent);
+ outline: none;
+ }
+ }
+
+ &__icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ background: var(--carousel-kit-bg-icon);
+ color: var(--carousel-kit-text-primary);
+ }
+
+ &__label {
+ font-weight: 600;
+ font-size: 13px;
+ line-height: 1.3;
+ }
+
+ &__description {
+ font-size: 12px;
+ color: var(--carousel-kit-text-muted);
+ line-height: 1.4;
+ }
+
+ &__back {
display: block;
margin-top: 4px;
}
@@ -32,7 +112,7 @@
justify-content: center;
align-items: center;
padding: 2rem;
- border: 1px dashed #ccc;
+ border: 1px dashed var(--carousel-kit-border-dashed);
border-radius: 2px;
box-sizing: border-box;
}
@@ -47,16 +127,16 @@
/* Ensure selectable area */
padding: 0.625rem;
- border: 1px dashed #ccc;
+ border: 1px dashed var(--carousel-kit-border-dashed);
box-sizing: border-box;
&.is-selected {
- border-color: var(--wp-admin-theme-color);
+ border-color: var(--carousel-kit-accent);
}
/* Add dashed border in editor to make it visible if empty */
&.is-empty {
- border: 1px dashed #ccc;
+ border: 1px dashed var(--carousel-kit-border-dashed);
min-height: 50px;
}
diff --git a/src/blocks/carousel/templates.ts b/src/blocks/carousel/templates.ts
new file mode 100644
index 0000000..0a86822
--- /dev/null
+++ b/src/blocks/carousel/templates.ts
@@ -0,0 +1,179 @@
+/**
+ * Slide template definitions for the Carousel block.
+ *
+ * Developers can register additional templates via the
+ * `rtcamp.carouselKit.slideTemplates` WordPress filter (applied with `applyFilters`).
+ *
+ * @package
+ */
+
+import { createBlock, type BlockInstance } from '@wordpress/blocks';
+import { type IconType } from '@wordpress/components';
+import { applyFilters } from '@wordpress/hooks';
+import { __ } from '@wordpress/i18n';
+import { columns, image, layout, gallery, post } from '@wordpress/icons';
+
+export interface SlideTemplate {
+ /** Unique machine-readable name. */
+ name: string;
+ /** Human-readable title shown in the picker. */
+ label: string;
+ /** Short description shown below the label. */
+ description: string;
+ /** WordPress icon component used in the picker. Accepts any value supported by `` from `@wordpress/components`. */
+ icon: IconType;
+ /**
+ * Whether this template uses a Query Loop instead of individual slides.
+ * When true, `slideCount` is ignored and a `core/query` block is placed
+ * directly inside the carousel viewport.
+ */
+ isQueryLoop?: boolean;
+ /**
+ * Build the inner blocks for a single slide.
+ * Called once per slide (or not at all for Query Loop templates).
+ */
+ innerBlocks: () => BlockInstance[];
+}
+
+// ── Default templates ────────────────────────────────────────────────────────
+
+const textSlide: SlideTemplate = {
+ name: 'text',
+ label: __( 'Text Slides', 'rt-carousel' ),
+ description: __( 'Slides starting with a paragraph you can replace or extend.', 'rt-carousel' ),
+ icon: columns,
+ innerBlocks: () => [ createBlock( 'core/paragraph', {} ) ],
+};
+
+const imageSlide: SlideTemplate = {
+ name: 'image',
+ label: __( 'Image Slides', 'rt-carousel' ),
+ description: __( 'Slides prefilled with an image block.', 'rt-carousel' ),
+ icon: image,
+ innerBlocks: () => [ createBlock( 'core/image', {} ) ],
+};
+
+const heroSlide: SlideTemplate = {
+ name: 'hero',
+ label: __( 'Image + Heading + Text + CTA', 'rt-carousel' ),
+ description: __( 'Marketing slider with heading, paragraph, and button.', 'rt-carousel' ),
+ icon: layout,
+ innerBlocks: () => [
+ createBlock( 'core/cover', {}, [
+ createBlock( 'core/heading', {
+ level: 2,
+ placeholder: __( 'Slide Heading', 'rt-carousel' ),
+ } ),
+ createBlock( 'core/paragraph', {
+ placeholder: __( 'Slide description text…', 'rt-carousel' ),
+ } ),
+ createBlock( 'core/buttons', {}, [
+ createBlock( 'core/button', {} ),
+ ] ),
+ ] ),
+ ],
+};
+
+const imageCaptionSlide: SlideTemplate = {
+ name: 'image-caption',
+ label: __( 'Image + Caption', 'rt-carousel' ),
+ description: __( 'Image with supporting text below.', 'rt-carousel' ),
+ icon: gallery,
+ innerBlocks: () => [
+ createBlock( 'core/image', {} ),
+ createBlock( 'core/paragraph', {
+ placeholder: __( 'Caption text…', 'rt-carousel' ),
+ } ),
+ ],
+};
+
+const queryLoopSlide: SlideTemplate = {
+ name: 'query-loop',
+ label: __( 'Query Loop Slides', 'rt-carousel' ),
+ description: __( 'Dynamically generate slides from posts.', 'rt-carousel' ),
+ icon: post,
+ isQueryLoop: true,
+ innerBlocks: () => [], // Not used — Query Loop is handled specially.
+};
+
+const DEFAULT_TEMPLATES: SlideTemplate[] = [
+ textSlide,
+ imageSlide,
+ heroSlide,
+ imageCaptionSlide,
+ queryLoopSlide,
+];
+
+function getDefaultTemplates(): SlideTemplate[] {
+ return DEFAULT_TEMPLATES.map( ( template ) => ( {
+ ...template,
+ } ) );
+}
+
+/**
+ * Retrieve all available slide templates.
+ *
+ * External code can add templates via:
+ *
+ * ```js
+ * import { addFilter } from '@wordpress/hooks';
+ *
+ * addFilter(
+ * 'rtcamp.carouselKit.slideTemplates',
+ * 'my-plugin/custom-templates',
+ * ( templates ) => [
+ * ...templates,
+ * {
+ * name: 'testimonial',
+ * label: 'Testimonial',
+ * description: 'Quote with author name.',
+ * icon: 'format-quote',
+ * innerBlocks: () => [
+ * createBlock( 'core/quote', {} ),
+ * createBlock( 'core/paragraph', { placeholder: '— Author' } ),
+ * ],
+ * },
+ * ],
+ * );
+ * ```
+ */
+export function getSlideTemplates(): SlideTemplate[] {
+ const defaultTemplates = getDefaultTemplates();
+ const templates = applyFilters(
+ 'rtcamp.carouselKit.slideTemplates',
+ defaultTemplates,
+ );
+
+ if ( Array.isArray( templates ) ) {
+ const valid = ( templates as unknown[] ).filter( ( t ): t is SlideTemplate => {
+ if (
+ t !== null &&
+ t !== undefined &&
+ typeof t === 'object' &&
+ typeof ( t as SlideTemplate ).name === 'string' &&
+ typeof ( t as SlideTemplate ).label === 'string' &&
+ typeof ( t as SlideTemplate ).description === 'string' &&
+ ( t as SlideTemplate ).icon !== undefined &&
+ ( t as SlideTemplate ).icon !== null &&
+ typeof ( t as SlideTemplate ).innerBlocks === 'function'
+ ) {
+ return true;
+ }
+ // eslint-disable-next-line no-console
+ console.warn(
+ 'rtcamp.carouselKit.slideTemplates: dropping invalid template entry.',
+ t,
+ );
+ return false;
+ } );
+ return valid;
+ }
+
+ // eslint-disable-next-line no-console
+ console.warn(
+ 'rtcamp.carouselKit.slideTemplates filter returned a non-array value. Falling back to default slide templates.',
+ templates,
+ );
+
+ return defaultTemplates;
+}