<?xml version="1.0"?><artefact xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="artefact.xsd" name="DisplayKit" slug="telex-post-listing-filter" type="code-package" schemaVersion="2">
  <file path="readme.txt">
    <description>This file contains the readme information for the block.</description>
    <content><![CDATA[=== DisplayKit - Page, Post & Product Display Block ===

Contributors:      upluggit
Donate link:       https://profiles.wordpress.org/upluggit/
Tags:              post grid, product display, gutenberg block, woocommerce, post list
Tested up to:      6.9
Requires at least: 6.2
Requires PHP:      7.4
Stable tag:        1.0.0
License:           GPLv2 or later
License URI:       https://www.gnu.org/licenses/gpl-2.0.html

A flexible block for displaying pages, posts, and WooCommerce products with grid and list layouts, rich customization, and dynamic querying.

== Description ==

DisplayKit allows WordPress users to display content from any post type — including pages, posts, and WooCommerce products — with powerful layout and styling options, all without writing a single line of code.

= Key Features =

* **Any Content Type** — Display Posts, Pages, WooCommerce Products, or any registered Custom Post Type.
* **Specific Item Selection** — Hand-pick individual posts or products to showcase by searching and selecting them in the editor.
* **Multiple Layouts** — Grid (1–6 columns) and List layouts with customizable card dimensions, alignment, and responsive breakpoints.
* **WooCommerce Integration** — Product price, sale badges, star ratings, stock status, and Add to Cart buttons are displayed automatically when showcasing products.
* **Post Card Customization** — Toggle featured images, titles, excerpts, author/date meta, and read-more buttons. Control image aspect ratios, typography, colors, spacing, and border radius.
* **Open in New Tab** — Optionally open all card links in a new browser tab.
* **Performance Optimized** — Skeleton loading states, lazy-loaded images, and smooth CSS transitions ensure great Core Web Vitals scores.
* **Mobile-First Design** — Layouts adapt beautifully to any screen size with responsive breakpoints.
* **Security** — All inputs are sanitized and outputs are properly escaped.

= Editor Experience =

Settings are organized into intuitive panels in the block sidebar:

* **Query Panel** — Post type, taxonomy, terms, author, order, count, specific posts/products selection
* **Layout Panel** — Grid/List, columns, gaps, card dimensions, alignment
* **Display Panel** — Toggle post card elements, open in new tab, image aspect ratio
* **WooCommerce Panel** — Price, rating, sale badge, add to cart controls (appears when product post type is selected)
* **Style Panel** — Colors, typography, spacing, border radius

= Third-Party Services =

This plugin does not connect to any third-party or external services. All data is queried and rendered locally from your WordPress installation using the WordPress REST API.

== Installation ==

1. Upload the plugin files to the `/wp-content/plugins/displaykit` directory, or install the plugin through the WordPress plugins screen directly.
2. Activate the plugin through the 'Plugins' screen in WordPress.
3. In the block editor, search for "DisplayKit" and add it to your page.

== Frequently Asked Questions ==

= Does it work with Custom Post Types? =

Yes! The block dynamically fetches all public post types registered on your site.

= Can I filter by custom taxonomies? =

The block supports filtering by any public taxonomy associated with the selected post type.

= Does it work with WooCommerce? =

Yes! Select the "Products" post type and DisplayKit will automatically show product-specific information like price, ratings, sale badges, and an Add to Cart button. WooCommerce must be installed and activated separately.

= Can I display specific posts or products? =

Yes! Use the "Specific Posts/Products" control in the Query panel to search and hand-pick individual items to display.

= What are the minimum requirements? =

WordPress 6.2 or higher and PHP 7.4 or higher. For WooCommerce product features, WooCommerce must be installed and active.

== Screenshots ==

1. Block in the editor with Grid layout and customization controls.
2. Frontend view displaying WooCommerce products with prices and ratings.
3. List layout with customizable card dimensions and alignment.

== Changelog ==

= 1.0.0 =
* Initial release with page/post/product display, multiple layouts, WooCommerce integration, and rich customization.

== Upgrade Notice ==

= 1.0.0 =
First release.
]]></content>
  </file>
  <file path="src/block.json">
    <description>Block metadata with attributes for all customization options.</description>
    <content><![CDATA[{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "displaykit/post-display",
	"version": "1.0.0",
	"title": "DisplayKit - Page, Post & Product Display Block",
	"category": "widgets",
	"icon": "screenoptions",
	"description": "A flexible block for displaying pages, posts, and WooCommerce products with multiple layouts, rich customization, and dynamic querying.",
	"example": {},
	"attributes": {
		"postType": {
			"type": "string",
			"default": "post"
		},
		"postsPerPage": {
			"type": "number",
			"default": 6
		},
		"orderby": {
			"type": "string",
			"default": "date"
		},
		"order": {
			"type": "string",
			"default": "DESC"
		},
		"taxonomy": {
			"type": "string",
			"default": ""
		},
		"terms": {
			"type": "array",
			"default": []
		},
		"author": {
			"type": "string",
			"default": ""
		},
		"layout": {
			"type": "string",
			"default": "grid"
		},
		"columns": {
			"type": "number",
			"default": 3
		},
		"gap": {
			"type": "number",
			"default": 24
		},
		"showImage": {
			"type": "boolean",
			"default": true
		},
		"showTitle": {
			"type": "boolean",
			"default": true
		},
		"showExcerpt": {
			"type": "boolean",
			"default": true
		},
		"showMeta": {
			"type": "boolean",
			"default": true
		},
		"showReadMore": {
			"type": "boolean",
			"default": true
		},
		"readMoreText": {
			"type": "string",
			"default": "Read More"
		},
		"enableFilters": {
			"type": "boolean",
			"default": true
		},
		"enableSearch": {
			"type": "boolean",
			"default": true
		},
		"filterTaxonomy": {
			"type": "string",
			"default": "category"
		},
		"paginationType": {
			"type": "string",
			"default": "load-more"
		},
		"imageRatio": {
			"type": "string",
			"default": "16-9"
		},
		"cardBgColor": {
			"type": "string",
			"default": "#ffffff"
		},
		"cardTextColor": {
			"type": "string",
			"default": "#1e1e1e"
		},
		"cardBorderRadius": {
			"type": "number",
			"default": 8
		},
		"titleFontSize": {
			"type": "number",
			"default": 18
		},
		"excerptFontSize": {
			"type": "number",
			"default": 14
		},
		"accentColor": {
			"type": "string",
			"default": "#0073aa"
		},
		"cardPadding": {
			"type": "number",
			"default": 16
		},
		"cardShadow": {
			"type": "boolean",
			"default": true
		},
		"showPrice": {
			"type": "boolean",
			"default": true
		},
		"showRating": {
			"type": "boolean",
			"default": true
		},
		"showSaleBadge": {
			"type": "boolean",
			"default": true
		},
		"showAddToCart": {
			"type": "boolean",
			"default": true
		},
		"addToCartText": {
			"type": "string",
			"default": "Add to Cart"
		},
		"specificPosts": {
			"type": "array",
			"default": []
		},
		"listCardWidth": {
			"type": "number",
			"default": 0
		},
		"listCardHeight": {
			"type": "number",
			"default": 0
		},
		"listAlignment": {
			"type": "string",
			"default": "left"
		},
		"listPostsPerPage": {
			"type": "number",
			"default": 0
		},
		"openInNewTab": {
			"type": "boolean",
			"default": false
		}
	},
	"supports": {
		"html": false,
		"align": [ "wide", "full" ],
		"spacing": {
			"margin": true,
			"padding": true
		}
	},
	"textdomain": "displaykit",
	"editorScript": "file:./index.js",
	"editorStyle": "file:./index.css",
	"style": "file:./style-index.css",
	"viewScript": "file:./view.js",
	"render": "file:./render.php"
}
]]></content>
  </file>
  <file path="src/index.js">
    <description>Block registration entry point.</description>
    <content><![CDATA[import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
import metadata from './block.json';

registerBlockType( metadata.name, {
	edit: Edit,
} );
]]></content>
  </file>
  <file path="src/edit.js">
    <description>Editor component with full sidebar controls and live preview.</description>
    <content><![CDATA[import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import {
	PanelBody,
	SelectControl,
	RangeControl,
	ToggleControl,
	TextControl,
	ColorPicker,
	Spinner,
	Notice,
	Button,
	BaseControl,
} from '@wordpress/components';
import { useState, useEffect, useCallback } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import './editor.scss';

const EXCLUDED_POST_TYPES = [
	'attachment',
	'wp_navigation',
	'wp_block',
	'wp_template',
	'wp_template_part',
	'wp_global_styles',
	'wp_font_face',
	'wp_font_family',
	'nav_menu_item',
	'oembed_cache',
	'user_request',
	'wp_changeset',
	'custom_css',
	'customize_changeset',
];

function isExcludedPostType( slug ) {
	if ( EXCLUDED_POST_TYPES.includes( slug ) ) {
		return true;
	}
	if ( slug.startsWith( 'popup' ) || slug.indexOf( 'popup' ) !== -1 ) {
		return true;
	}
	return false;
}

export default function Edit( { attributes, setAttributes } ) {
	const {
		postType,
		postsPerPage,
		orderby,
		order,
		taxonomy,
		terms,
		author,
		layout,
		columns,
		gap,
		showImage,
		showTitle,
		showExcerpt,
		showMeta,
		showReadMore,
		readMoreText,
		imageRatio,
		cardBgColor,
		cardTextColor,
		cardBorderRadius,
		titleFontSize,
		excerptFontSize,
		accentColor,
		cardPadding,
		cardShadow,
		showPrice,
		showRating,
		showSaleBadge,
		showAddToCart,
		addToCartText,
		specificPosts,
		listCardWidth,
		listCardHeight,
		listAlignment,
		openInNewTab,
	} = attributes;

	const [ postTypes, setPostTypes ] = useState( [] );
	const [ taxonomies, setTaxonomies ] = useState( [] );
	const [ termsList, setTermsList ] = useState( [] );

	const [ posts, setPosts ] = useState( [] );
	const [ loading, setLoading ] = useState( false );
	const [ authors, setAuthors ] = useState( [] );
	const [ wooActive, setWooActive ] = useState( false );

	const [ specificSearch, setSpecificSearch ] = useState( '' );
	const [ specificResults, setSpecificResults ] = useState( [] );
	const [ specificSearching, setSpecificSearching ] = useState( false );
	const [ selectedPostDetails, setSelectedPostDetails ] = useState( [] );

	const isProduct = postType === 'product';
	const hasSpecificPosts = specificPosts && specificPosts.length > 0;

	useEffect( () => {
		apiFetch( { path: '/wp/v2/types?context=edit' } )
			.then( ( types ) => {
				const items = Object.values( types )
					.filter( ( t ) => {
						if ( ! t.slug || isExcludedPostType( t.slug ) ) {
							return false;
						}
						if ( t.visibility ) {
							return t.visibility.show_ui !== false;
						}
						return t.viewable !== false;
					} )
					.map( ( t ) => ( { label: t.name, value: t.slug } ) );
				setPostTypes( items );
			} )
			.catch( () => {
				apiFetch( { path: '/wp/v2/types' } )
					.then( ( types ) => {
						const items = Object.values( types )
							.filter( ( t ) => t.slug && ! isExcludedPostType( t.slug ) )
							.map( ( t ) => ( { label: t.name, value: t.slug } ) );
						setPostTypes( items );
					} )
					.catch( () => {
						setPostTypes( [
							{ label: 'Posts', value: 'post' },
							{ label: 'Pages', value: 'page' },
						] );
					} );
			} );
	}, [] );

	useEffect( () => {
		apiFetch( { path: '/displaykit/v1/woo-status' } )
			.then( ( data ) => {
				setWooActive( data.active || false );
			} )
			.catch( () => setWooActive( false ) );
	}, [] );

	useEffect( () => {
		apiFetch( { path: '/wp/v2/users?per_page=100&context=view' } )
			.then( ( users ) => {
				setAuthors(
					users.map( ( u ) => ( { label: u.name, value: String( u.id ) } ) )
				);
			} )
			.catch( () => setAuthors( [] ) );
	}, [] );

	useEffect( () => {
		if ( postType ) {
			apiFetch( {
				path: `/displaykit/v1/taxonomies?post_type=${ postType }`,
			} ).then( ( taxs ) => {
				setTaxonomies( taxs.map( ( t ) => ( { label: t.label, value: t.name } ) ) );
			} );
		}
	}, [ postType ] );

	useEffect( () => {
		if ( taxonomy ) {
			apiFetch( {
				path: `/displaykit/v1/terms?taxonomy=${ taxonomy }`,
			} ).then( ( t ) => setTermsList( t ) );
		} else {
			setTermsList( [] );
		}
	}, [ taxonomy ] );

	useEffect( () => {
		if ( ! specificPosts || specificPosts.length === 0 ) {
			setSelectedPostDetails( [] );
			return;
		}
		const idsParam = specificPosts.join( ',' );
		apiFetch( {
			path: `/displaykit/v1/posts?post_type=${ encodeURIComponent( postType ) }&include=${ idsParam }&posts_per_page=${ specificPosts.length }`,
		} )
			.then( ( data ) => {
				const items = data.posts || [];
				const details = specificPosts.map( ( id ) => {
					const item = items.find( ( p ) => p.id === id );
					if ( item ) {
						return { id: item.id, title: item.title || `#${ item.id }` };
					}
					return { id, title: `#${ id }` };
				} );
				setSelectedPostDetails( details );
			} )
			.catch( () => {
				setSelectedPostDetails(
					specificPosts.map( ( id ) => ( { id, title: `#${ id }` } ) )
				);
			} );
	}, [ specificPosts, postType ] );

	useEffect( () => {
		setLoading( true );
		const params = new URLSearchParams( {
			post_type: postType,
			posts_per_page: String( postsPerPage ),
			orderby,
			order,
		} );
		if ( hasSpecificPosts ) {
			params.set( 'include', specificPosts.join( ',' ) );
		}
		if ( taxonomy && terms.length > 0 && ! hasSpecificPosts ) {
			params.set( 'taxonomy', taxonomy );
			params.set( 'terms', terms.join( ',' ) );
		}
		if ( author && ! hasSpecificPosts ) {
			params.set( 'author', author );
		}
		apiFetch( {
			path: `/displaykit/v1/posts?${ params.toString() }`,
		} )
			.then( ( data ) => {
				setPosts( data.posts || [] );
				setLoading( false );
			} )
			.catch( () => {
				setPosts( [] );
				setLoading( false );
			} );
	}, [ postType, postsPerPage, orderby, order, taxonomy, terms, author, specificPosts ] );

	useEffect( () => {
		if ( specificSearch.length < 2 ) {
			setSpecificResults( [] );
			return;
		}
		const timer = setTimeout( () => {
			setSpecificSearching( true );
			apiFetch( {
				path: `/displaykit/v1/posts?post_type=${ encodeURIComponent( postType ) }&search=${ encodeURIComponent( specificSearch ) }&posts_per_page=10`,
			} )
				.then( ( data ) => {
					const items = data.posts || [];
					setSpecificResults(
						items
							.filter( ( item ) => ! specificPosts.includes( item.id ) )
							.map( ( item ) => ( {
								id: item.id,
								title: item.title || `#${ item.id }`,
							} ) )
					);
					setSpecificSearching( false );
				} )
				.catch( () => {
					setSpecificResults( [] );
					setSpecificSearching( false );
				} );
		}, 350 );
		return () => clearTimeout( timer );
	}, [ specificSearch, postType, specificPosts ] );

	const addSpecificPost = useCallback(
		( postId ) => {
			if ( ! specificPosts.includes( postId ) ) {
				setAttributes( { specificPosts: [ ...specificPosts, postId ] } );
			}
			setSpecificSearch( '' );
			setSpecificResults( [] );
		},
		[ specificPosts, setAttributes ]
	);

	const removeSpecificPost = useCallback(
		( postId ) => {
			setAttributes( {
				specificPosts: specificPosts.filter( ( id ) => id !== postId ),
			} );
		},
		[ specificPosts, setAttributes ]
	);

	const blockProps = useBlockProps( {
		className: `tplf-block tplf-layout-${ layout }${ isProduct ? ' tplf-is-product' : '' }`,
	} );

	const imageRatioMap = {
		'16-9': '56.25%',
		'4-3': '75%',
		'1-1': '100%',
		'3-2': '66.67%',
		'auto': '0',
	};

	const listAlignMap = {
		left: 'flex-start',
		center: 'center',
		right: 'flex-end',
	};

	const gridStyle = layout === 'grid' ? {
		display: 'grid',
		gridTemplateColumns: `repeat(${ columns }, 1fr)`,
		gap: `${ gap }px`,
	} : {
		display: 'flex',
		flexDirection: 'column',
		gap: `${ gap }px`,
		alignItems: listAlignMap[ listAlignment ] || 'flex-start',
	};

	const cardStyle = {
		backgroundColor: cardBgColor,
		color: cardTextColor,
		borderRadius: `${ cardBorderRadius }px`,
		padding: `${ cardPadding }px`,
		boxShadow: cardShadow ? '0 2px 8px rgba(0,0,0,0.1)' : 'none',
		overflow: 'hidden',
	};

	const handleTermToggle = ( termId ) => {
		const newTerms = terms.includes( termId )
			? terms.filter( ( t ) => t !== termId )
			: [ ...terms, termId ];
		setAttributes( { terms: newTerms } );
	};

	const renderStars = ( rating ) => {
		const stars = [];
		const fullStars = Math.floor( rating );
		const hasHalf = rating - fullStars >= 0.5;
		for ( let i = 0; i < 5; i++ ) {
			if ( i < fullStars ) {
				stars.push( <span key={ i } className="tplf-star tplf-star-full">&#9733;</span> );
			} else if ( i === fullStars && hasHalf ) {
				stars.push( <span key={ i } className="tplf-star tplf-star-half">&#9733;</span> );
			} else {
				stars.push( <span key={ i } className="tplf-star tplf-star-empty">&#9734;</span> );
			}
		}
		return stars;
	};

	const orderByOptions = [
		{ label: __( 'Date', 'displaykit' ), value: 'date' },
		{ label: __( 'Title', 'displaykit' ), value: 'title' },
		{ label: __( 'Random', 'displaykit' ), value: 'rand' },
		{ label: __( 'Modified', 'displaykit' ), value: 'modified' },
		{ label: __( 'Comment Count', 'displaykit' ), value: 'comment_count' },
	];

	if ( isProduct && wooActive ) {
		orderByOptions.push(
			{ label: __( 'Price', 'displaykit' ), value: 'price' },
			{ label: __( 'Popularity', 'displaykit' ), value: 'popularity' },
			{ label: __( 'Rating', 'displaykit' ), value: 'rating' }
		);
	}

	return (
		<div { ...blockProps }>
			<InspectorControls>
				<PanelBody title={ __( 'Query', 'displaykit' ) } initialOpen={ true }>
					{ postTypes.length > 0 && (
						<SelectControl
							label={ __( 'Post Type', 'displaykit' ) }
							value={ postType }
							options={ postTypes }
							onChange={ ( val ) => {
								setAttributes( { postType: val, taxonomy: '', terms: [], specificPosts: [] } );
							} }
						/>
					) }
					{ isProduct && ! wooActive && (
						<Notice status="warning" isDismissible={ false }>
							{ __( 'WooCommerce is not active. Product features will not be available on the frontend.', 'displaykit' ) }
						</Notice>
					) }

					<BaseControl
						label={ __( 'Specific Posts/Products', 'displaykit' ) }
						help={ hasSpecificPosts
							? __( 'Only the selected items will be displayed. Other query filters are ignored.', 'displaykit' )
							: __( 'Search and select specific items to display, or leave empty to use query filters below.', 'displaykit' )
						}
					>
						<div className="tplf-specific-posts-control">
							<div className="tplf-specific-search-wrap">
								<TextControl
									placeholder={ isProduct
										? __( 'Search for a product…', 'displaykit' )
										: __( 'Search for a post…', 'displaykit' )
									}
									value={ specificSearch }
									onChange={ setSpecificSearch }
									__nextHasNoMarginBottom
								/>
								{ specificSearching && (
									<div className="tplf-specific-searching">
										<Spinner />
									</div>
								) }
								{ specificResults.length > 0 && (
									<ul className="tplf-specific-results">
										{ specificResults.map( ( item ) => (
											<li key={ item.id }>
												<Button
													variant="secondary"
													className="tplf-specific-result-btn"
													onClick={ () => addSpecificPost( item.id ) }
												>
													<span dangerouslySetInnerHTML={ { __html: item.title } } />
													<span className="tplf-specific-add">+</span>
												</Button>
											</li>
										) ) }
									</ul>
								) }
							</div>
							{ selectedPostDetails.length > 0 && (
								<ul className="tplf-specific-selected">
									{ selectedPostDetails.map( ( item ) => (
										<li key={ item.id } className="tplf-specific-selected-item">
											<span
												className="tplf-specific-selected-title"
												dangerouslySetInnerHTML={ { __html: item.title } }
											/>
											<Button
												isDestructive
												isSmall
												icon="no-alt"
												label={ __( 'Remove', 'displaykit' ) }
												onClick={ () => removeSpecificPost( item.id ) }
											/>
										</li>
									) ) }
								</ul>
							) }
							{ hasSpecificPosts && (
								<Button
									variant="link"
									isDestructive
									onClick={ () => setAttributes( { specificPosts: [] } ) }
									style={ { marginTop: '8px' } }
								>
									{ __( 'Clear all selections', 'displaykit' ) }
								</Button>
							) }
						</div>
					</BaseControl>

					{ ! hasSpecificPosts && (
						<>
							<RangeControl
								label={ __( 'Posts Per Page', 'displaykit' ) }
								value={ postsPerPage }
								onChange={ ( val ) => setAttributes( { postsPerPage: val } ) }
								min={ 1 }
								max={ 50 }
							/>
							<SelectControl
								label={ __( 'Order By', 'displaykit' ) }
								value={ orderby }
								options={ orderByOptions }
								onChange={ ( val ) => setAttributes( { orderby: val } ) }
							/>
							<SelectControl
								label={ __( 'Order', 'displaykit' ) }
								value={ order }
								options={ [
									{ label: __( 'Descending', 'displaykit' ), value: 'DESC' },
									{ label: __( 'Ascending', 'displaykit' ), value: 'ASC' },
								] }
								onChange={ ( val ) => setAttributes( { order: val } ) }
							/>
							{ taxonomies.length > 0 && (
								<SelectControl
									label={ __( 'Taxonomy Filter', 'displaykit' ) }
									value={ taxonomy }
									options={ [
										{ label: __( '— None —', 'displaykit' ), value: '' },
										...taxonomies,
									] }
									onChange={ ( val ) => setAttributes( { taxonomy: val, terms: [] } ) }
								/>
							) }
							{ taxonomy && termsList.length > 0 && (
								<div className="tplf-terms-checklist">
									<p className="components-base-control__label">
										{ __( 'Filter by Terms', 'displaykit' ) }
									</p>
									{ termsList.map( ( term ) => (
										<ToggleControl
											key={ term.id }
											label={ `${ term.name } (${ term.count })` }
											checked={ terms.includes( term.id ) }
											onChange={ () => handleTermToggle( term.id ) }
										/>
									) ) }
								</div>
							) }
							{ authors.length > 0 && (
								<SelectControl
									label={ __( 'Author', 'displaykit' ) }
									value={ author }
									options={ [
										{ label: __( '— All Authors —', 'displaykit' ), value: '' },
										...authors,
									] }
									onChange={ ( val ) => setAttributes( { author: val } ) }
								/>
							) }
						</>
					) }
				</PanelBody>

				<PanelBody title={ __( 'Layout', 'displaykit' ) } initialOpen={ false }>
					<SelectControl
						label={ __( 'Layout', 'displaykit' ) }
						value={ layout }
						options={ [
							{ label: __( 'Grid', 'displaykit' ), value: 'grid' },
							{ label: __( 'List', 'displaykit' ), value: 'list' },
						] }
						onChange={ ( val ) => setAttributes( { layout: val } ) }
					/>
					{ layout === 'grid' && (
						<RangeControl
							label={ __( 'Columns', 'displaykit' ) }
							value={ columns }
							onChange={ ( val ) => setAttributes( { columns: val } ) }
							min={ 1 }
							max={ 6 }
						/>
					) }
					{ layout === 'list' && (
						<>
							<RangeControl
								label={ __( 'List Card Width (px)', 'displaykit' ) }
								value={ listCardWidth }
								onChange={ ( val ) => setAttributes( { listCardWidth: val } ) }
								min={ 0 }
								max={ 1200 }
								help={ listCardWidth === 0 ? __( 'Full width (default)', 'displaykit' ) : '' }
							/>
							<RangeControl
								label={ __( 'List Card Height (px)', 'displaykit' ) }
								value={ listCardHeight }
								onChange={ ( val ) => setAttributes( { listCardHeight: val } ) }
								min={ 0 }
								max={ 500 }
								help={ listCardHeight === 0 ? __( 'Auto height', 'displaykit' ) : '' }
							/>
							<SelectControl
								label={ __( 'List Alignment', 'displaykit' ) }
								value={ listAlignment }
								options={ [
									{ label: __( 'Left', 'displaykit' ), value: 'left' },
									{ label: __( 'Center', 'displaykit' ), value: 'center' },
									{ label: __( 'Right', 'displaykit' ), value: 'right' },
								] }
								onChange={ ( val ) => setAttributes( { listAlignment: val } ) }
							/>
						</>
					) }
					<RangeControl
						label={ __( 'Gap (px)', 'displaykit' ) }
						value={ gap }
						onChange={ ( val ) => setAttributes( { gap: val } ) }
						min={ 0 }
						max={ 60 }
					/>
				</PanelBody>

				<PanelBody title={ __( 'Display', 'displaykit' ) } initialOpen={ false }>
					<ToggleControl
						label={ __( 'Show Featured Image', 'displaykit' ) }
						checked={ showImage }
						onChange={ ( val ) => setAttributes( { showImage: val } ) }
					/>
					<ToggleControl
						label={ __( 'Show Title', 'displaykit' ) }
						checked={ showTitle }
						onChange={ ( val ) => setAttributes( { showTitle: val } ) }
					/>
					<ToggleControl
						label={ __( 'Show Excerpt', 'displaykit' ) }
						checked={ showExcerpt }
						onChange={ ( val ) => setAttributes( { showExcerpt: val } ) }
					/>
					<ToggleControl
						label={ __( 'Show Meta (Author, Date)', 'displaykit' ) }
						checked={ showMeta }
						onChange={ ( val ) => setAttributes( { showMeta: val } ) }
					/>
					<ToggleControl
						label={ __( 'Show Read More', 'displaykit' ) }
						checked={ showReadMore }
						onChange={ ( val ) => setAttributes( { showReadMore: val } ) }
					/>
					{ showReadMore && (
						<TextControl
							label={ __( 'Read More Text', 'displaykit' ) }
							value={ readMoreText }
							onChange={ ( val ) => setAttributes( { readMoreText: val } ) }
						/>
					) }
					<ToggleControl
						label={ __( 'Open Links in New Tab', 'displaykit' ) }
						checked={ openInNewTab }
						onChange={ ( val ) => setAttributes( { openInNewTab: val } ) }
					/>
					<SelectControl
						label={ __( 'Image Aspect Ratio', 'displaykit' ) }
						value={ imageRatio }
						options={ [
							{ label: '16:9', value: '16-9' },
							{ label: '4:3', value: '4-3' },
							{ label: '1:1', value: '1-1' },
							{ label: '3:2', value: '3-2' },
							{ label: __( 'Auto', 'displaykit' ), value: 'auto' },
						] }
						onChange={ ( val ) => setAttributes( { imageRatio: val } ) }
					/>
				</PanelBody>

				{ isProduct && wooActive && (
					<PanelBody title={ __( 'WooCommerce', 'displaykit' ) } initialOpen={ true }>
						<ToggleControl
							label={ __( 'Show Price', 'displaykit' ) }
							checked={ showPrice }
							onChange={ ( val ) => setAttributes( { showPrice: val } ) }
						/>
						<ToggleControl
							label={ __( 'Show Rating', 'displaykit' ) }
							checked={ showRating }
							onChange={ ( val ) => setAttributes( { showRating: val } ) }
						/>
						<ToggleControl
							label={ __( 'Show Sale Badge', 'displaykit' ) }
							checked={ showSaleBadge }
							onChange={ ( val ) => setAttributes( { showSaleBadge: val } ) }
						/>
						<ToggleControl
							label={ __( 'Show Add to Cart', 'displaykit' ) }
							checked={ showAddToCart }
							onChange={ ( val ) => setAttributes( { showAddToCart: val } ) }
						/>
						{ showAddToCart && (
							<TextControl
								label={ __( 'Add to Cart Text', 'displaykit' ) }
								value={ addToCartText }
								onChange={ ( val ) => setAttributes( { addToCartText: val } ) }
							/>
						) }
					</PanelBody>
				) }

				<PanelBody title={ __( 'Style', 'displaykit' ) } initialOpen={ false }>
					<RangeControl
						label={ __( 'Title Font Size (px)', 'displaykit' ) }
						value={ titleFontSize }
						onChange={ ( val ) => setAttributes( { titleFontSize: val } ) }
						min={ 12 }
						max={ 40 }
					/>
					<RangeControl
						label={ __( 'Excerpt Font Size (px)', 'displaykit' ) }
						value={ excerptFontSize }
						onChange={ ( val ) => setAttributes( { excerptFontSize: val } ) }
						min={ 10 }
						max={ 24 }
					/>
					<RangeControl
						label={ __( 'Card Border Radius (px)', 'displaykit' ) }
						value={ cardBorderRadius }
						onChange={ ( val ) => setAttributes( { cardBorderRadius: val } ) }
						min={ 0 }
						max={ 30 }
					/>
					<RangeControl
						label={ __( 'Card Padding (px)', 'displaykit' ) }
						value={ cardPadding }
						onChange={ ( val ) => setAttributes( { cardPadding: val } ) }
						min={ 0 }
						max={ 40 }
					/>
					<ToggleControl
						label={ __( 'Card Shadow', 'displaykit' ) }
						checked={ cardShadow }
						onChange={ ( val ) => setAttributes( { cardShadow: val } ) }
					/>
					<div className="tplf-color-control">
						<p className="components-base-control__label">
							{ __( 'Card Background', 'displaykit' ) }
						</p>
						<ColorPicker
							color={ cardBgColor }
							onChange={ ( val ) => setAttributes( { cardBgColor: val } ) }
							enableAlpha
						/>
					</div>
					<div className="tplf-color-control">
						<p className="components-base-control__label">
							{ __( 'Card Text Color', 'displaykit' ) }
						</p>
						<ColorPicker
							color={ cardTextColor }
							onChange={ ( val ) => setAttributes( { cardTextColor: val } ) }
							enableAlpha
						/>
					</div>
					<div className="tplf-color-control">
						<p className="components-base-control__label">
							{ __( 'Accent Color', 'displaykit' ) }
						</p>
						<ColorPicker
							color={ accentColor }
							onChange={ ( val ) => setAttributes( { accentColor: val } ) }
							enableAlpha
						/>
					</div>
				</PanelBody>
			</InspectorControls>

			<div className="tplf-editor-wrapper">
				{ loading && (
					<div className="tplf-loading">
						<Spinner />
						<span>{ isProduct ? __( 'Loading products…', 'displaykit' ) : __( 'Loading posts…', 'displaykit' ) }</span>
					</div>
				) }

				{ ! loading && posts.length === 0 && (
					<div className="tplf-empty">
						<p>{ isProduct ? __( 'No products found. Adjust your query settings.', 'displaykit' ) : __( 'No posts found. Adjust your query settings.', 'displaykit' ) }</p>
					</div>
				) }

				{ ! loading && posts.length > 0 && (
					<div className="tplf-posts-grid" style={ gridStyle }>
						{ posts.map( ( post ) => (
							<div
								key={ post.id }
								className={ `tplf-card${ layout === 'list' ? ' tplf-card-list' : '' }${ post.product ? ' tplf-card-product' : '' }` }
								style={ layout === 'list'
									? {
										...cardStyle,
										padding: 0,
										display: 'flex',
										flexDirection: 'row',
										minHeight: listCardHeight > 0 ? `${ listCardHeight }px` : 'auto',
										maxWidth: listCardWidth > 0 ? `${ listCardWidth }px` : '100%',
										width: '100%',
									}
									: cardStyle
								}
							>
								{ showImage && post.thumbnail && (
									<div
										className="tplf-card-image"
										style={ layout === 'list' ? {
											width: '40%',
											minHeight: '140px',
											flexShrink: 0,
											backgroundImage: `url(${ post.thumbnail })`,
											backgroundSize: 'cover',
											backgroundPosition: 'center',
											position: 'relative',
										} : imageRatio !== 'auto' ? {
											paddingTop: imageRatioMap[ imageRatio ],
											backgroundImage: `url(${ post.thumbnail })`,
											backgroundSize: 'cover',
											backgroundPosition: 'center',
											borderRadius: `${ cardBorderRadius }px ${ cardBorderRadius }px 0 0`,
											margin: `-${ cardPadding }px -${ cardPadding }px 0 -${ cardPadding }px`,
											width: `calc(100% + ${ cardPadding * 2 }px)`,
										} : {
											margin: `-${ cardPadding }px -${ cardPadding }px 0 -${ cardPadding }px`,
											width: `calc(100% + ${ cardPadding * 2 }px)`,
											borderRadius: `${ cardBorderRadius }px ${ cardBorderRadius }px 0 0`,
											overflow: 'hidden',
										} }
									>
										{ post.product && post.product.on_sale && showSaleBadge && (
											<span className="tplf-sale-badge">{ __( 'Sale!', 'displaykit' ) }</span>
										) }
										{ imageRatio === 'auto' && layout !== 'list' && (
											<img
												src={ post.thumbnail }
												alt={ post.title }
												style={ {
													width: '100%',
													display: 'block',
												} }
											/>
										) }
									</div>
								) }
								{ showImage && ! post.thumbnail && (
									<div
										className="tplf-card-image tplf-card-no-image"
										style={ layout === 'list' ? {
											width: '40%',
											minHeight: '140px',
											flexShrink: 0,
											backgroundColor: '#f0f0f0',
											display: 'flex',
											alignItems: 'center',
											justifyContent: 'center',
											position: 'relative',
										} : {
											paddingTop: imageRatioMap[ imageRatio ] || '56.25%',
											backgroundColor: '#f0f0f0',
											margin: `-${ cardPadding }px -${ cardPadding }px 0 -${ cardPadding }px`,
											width: `calc(100% + ${ cardPadding * 2 }px)`,
											borderRadius: `${ cardBorderRadius }px ${ cardBorderRadius }px 0 0`,
											display: 'flex',
											alignItems: 'center',
											justifyContent: 'center',
											position: 'relative',
										} }
									>
										{ post.product && post.product.on_sale && showSaleBadge && (
											<span className="tplf-sale-badge">{ __( 'Sale!', 'displaykit' ) }</span>
										) }
										<span
											style={ {
												position: 'absolute',
												top: '50%',
												left: '50%',
												transform: 'translate(-50%, -50%)',
												color: '#999',
												fontSize: '12px',
											} }
										>
											{ __( 'No Image', 'displaykit' ) }
										</span>
									</div>
								) }
								<div className="tplf-card-content" style={ layout === 'list'
									? { flex: 1, padding: `${ cardPadding }px`, marginTop: '0' }
									: { marginTop: showImage ? `${ cardPadding }px` : '0' }
								}>
									{ showTitle && (
										<h3
											className="tplf-card-title"
											style={ {
												fontSize: `${ titleFontSize }px`,
												color: cardTextColor,
											} }
										>
											{ post.title }
										</h3>
									) }
									{ post.product && showPrice && post.product.price_html && (
										<div
											className="tplf-product-price"
											dangerouslySetInnerHTML={ { __html: post.product.price_html } }
										/>
									) }
									{ post.product && showRating && post.product.average_rating > 0 && (
										<div className="tplf-product-rating">
											<span className="tplf-stars" style={ { color: accentColor } }>
												{ renderStars( post.product.average_rating ) }
											</span>
											<span className="tplf-rating-count">({ post.product.rating_count })</span>
										</div>
									) }
									{ showMeta && ! post.product && (
										<div className="tplf-card-meta" style={ { color: accentColor } }>
											<span>{ post.author }</span>
											<span className="tplf-meta-sep">&middot;</span>
											<span>{ post.date }</span>
										</div>
									) }
									{ showExcerpt && (
										<p
											className="tplf-card-excerpt"
											style={ { fontSize: `${ excerptFontSize }px` } }
											dangerouslySetInnerHTML={ { __html: post.excerpt } }
										/>
									) }
									{ post.product && showAddToCart && (
										<span
											className="tplf-add-to-cart"
											style={ { backgroundColor: accentColor, color: '#fff' } }
										>
											{ addToCartText } &#128722;
										</span>
									) }
									{ ! post.product && showReadMore && (
										<span
											className="tplf-read-more"
											style={ { color: accentColor } }
										>
											{ readMoreText } &rarr;
										</span>
									) }
									{ post.product && ! post.product.in_stock && (
										<span className="tplf-out-of-stock">{ __( 'Out of Stock', 'displaykit' ) }</span>
									) }
								</div>
							</div>
						) ) }
					</div>
				) }
			</div>
		</div>
	);
}]]></content>
  </file>
  <file path="src/save.js">
    <description>Dynamic block — save returns null.</description>
    <content><![CDATA[
]]></content>
  </file>
  <file path="src/style.scss">
    <description>Shared styles for both editor and frontend.</description>
    <content><![CDATA[
.wp-block-displaykit-post-display {
	font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;

	*,
	*::before,
	*::after {
		box-sizing: border-box;
	}

	.tplf-filters {
		display: flex;
		flex-wrap: wrap;
		gap: 12px;
		margin-bottom: 24px;
		align-items: center;

		&-toggle {
			display: none;
			width: 100%;
			padding: 10px 16px;
			border: 1px solid #ddd;
			border-radius: 6px;
			background: #f9f9f9;
			cursor: pointer;
			font-size: 14px;
			font-weight: 500;
			text-align: center;

			&::after {
				content: " ▾";
			}

			&.is-open::after {
				content: " ▴";
			}
		}

		&-inner {
			display: flex;
			flex-wrap: wrap;
			gap: 12px;
			align-items: center;
			width: 100%;
			transition: max-height 0.3s ease, opacity 0.3s ease;
		}
	}

	.tplf-filter-search {
		flex: 1;
		min-width: 200px;

		input {
			width: 100%;
			padding: 10px 14px;
			border: 1px solid #ddd;
			border-radius: 6px;
			font-size: 14px;
			transition: border-color 0.2s ease;

			&:focus {
				outline: none;
				border-color: var(--tplf-accent, #0073aa);
				box-shadow: 0 0 0 1px var(--tplf-accent, #0073aa);
			}
		}
	}

	.tplf-filter-select {
		select {
			padding: 10px 32px 10px 14px;
			border: 1px solid #ddd;
			border-radius: 6px;
			font-size: 14px;
			background: #fff;
			cursor: pointer;
			min-width: 160px;
			appearance: none;
			background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
			background-repeat: no-repeat;
			background-position: right 12px center;

			&:focus {
				outline: none;
				border-color: var(--tplf-accent, #0073aa);
			}
		}
	}

	.tplf-filter-reset {
		padding: 10px 18px;
		border: 1px solid #ddd;
		border-radius: 6px;
		background: #f5f5f5;
		cursor: pointer;
		font-size: 14px;
		transition: background-color 0.2s ease, border-color 0.2s ease;

		&:hover {
			background: #e5e5e5;
			border-color: #bbb;
		}
	}

	.tplf-posts-grid {
		transition: opacity 0.3s ease;

		&.is-loading {
			opacity: 0.5;
			pointer-events: none;
		}
	}

	.tplf-card {
		display: flex;
		flex-direction: column;
		transition: transform 0.2s ease, box-shadow 0.2s ease;
		animation: tplfFadeIn 0.3s ease;

		&:hover {
			transform: translateY(-2px);
			box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12) !important;
		}

		&-list {
			flex-direction: row;
			padding: 0 !important;

			.tplf-card-image {
				width: 240px;
				min-height: 180px;
				flex-shrink: 0;
				border-radius: 0 !important;
				margin: 0 !important;

				img {
					width: 100%;
					height: 100%;
					object-fit: cover;
				}
			}

			.tplf-card-content {
				flex: 1;
				margin-top: 0 !important;
			}
		}
	}

	.tplf-card-image {
		position: relative;
		overflow: hidden;
		background-color: #f0f0f0;

		img {
			display: block;
			width: 100%;
			height: auto;
		}
	}

	.tplf-card-content {
		display: flex;
		flex-direction: column;
		flex: 1;
	}

	.tplf-card-title {
		margin: 0 0 8px;
		line-height: 1.3;
		font-weight: 600;

		a {
			color: inherit;
			text-decoration: none;

			&:hover {
				text-decoration: underline;
			}
		}
	}

	.tplf-card-meta {
		font-size: 12px;
		margin-bottom: 8px;
		opacity: 0.8;

		.tplf-meta-sep {
			margin: 0 6px;
		}
	}

	.tplf-card-excerpt {
		margin: 0 0 12px;
		line-height: 1.6;
		opacity: 0.85;
	}

	.tplf-read-more {
		font-size: 14px;
		font-weight: 500;
		text-decoration: none;
		margin-top: auto;
		display: inline-block;

		&:hover {
			text-decoration: underline;
		}
	}

	// WooCommerce Product Styles.
	.tplf-sale-badge {
		position: absolute;
		top: 10px;
		left: 10px;
		background-color: #e74c3c;
		color: #fff;
		padding: 4px 10px;
		border-radius: 4px;
		font-size: 12px;
		font-weight: 700;
		text-transform: uppercase;
		letter-spacing: 0.5px;
		z-index: 2;
		line-height: 1.4;
	}

	.tplf-product-price {
		font-size: 16px;
		font-weight: 700;
		margin-bottom: 8px;
		line-height: 1.4;

		del {
			opacity: 0.5;
			font-weight: 400;
			font-size: 14px;
			margin-right: 6px;
		}

		ins {
			text-decoration: none;
			color: #e74c3c;
		}

		.woocommerce-Price-amount {
			font-weight: 700;
		}
	}

	.tplf-product-rating {
		display: flex;
		align-items: center;
		gap: 6px;
		margin-bottom: 8px;
		font-size: 14px;
	}

	.tplf-stars {
		display: inline-flex;
		gap: 1px;
		line-height: 1;
	}

	.tplf-star {
		font-size: 16px;
		line-height: 1;

		&-full {
			opacity: 1;
		}

		&-half {
			opacity: 0.6;
		}

		&-empty {
			opacity: 0.3;
		}
	}

	.tplf-rating-count {
		font-size: 12px;
		color: #888;
	}

	.tplf-add-to-cart {
		display: inline-block;
		padding: 10px 20px;
		border: none;
		border-radius: 6px;
		font-size: 14px;
		font-weight: 600;
		cursor: pointer;
		text-decoration: none;
		text-align: center;
		transition: opacity 0.2s ease, transform 0.2s ease;
		margin-top: auto;
		line-height: 1.4;

		&:hover {
			opacity: 0.9;
			transform: translateY(-1px);
		}
	}

	.tplf-out-of-stock {
		display: inline-block;
		font-size: 12px;
		font-weight: 600;
		color: #e74c3c;
		background: rgba(231, 76, 60, 0.1);
		padding: 4px 10px;
		border-radius: 4px;
		margin-top: 8px;
	}

	.tplf-card-product {
		.tplf-card-content {
			gap: 2px;
		}
	}

	.tplf-skeleton {
		display: grid;
		gap: 24px;

		&-card {
			border-radius: 8px;
			overflow: hidden;
			background: #fff;
			box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
		}

		&-image {
			background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
			background-size: 200% 100%;
			animation: tplfShimmer 1.5s infinite;
		}

		&-content {
			padding: 16px;
		}

		&-line {
			height: 12px;
			background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
			background-size: 200% 100%;
			animation: tplfShimmer 1.5s infinite;
			border-radius: 4px;
			margin-bottom: 8px;

			&.wide {
				width: 80%;
				height: 16px;
			}

			&.medium {
				width: 60%;
			}

			&.short {
				width: 40%;
			}
		}
	}

	.tplf-empty {
		text-align: center;
		padding: 48px 24px;
		color: #666;

		&-icon {
			font-size: 48px;
			margin-bottom: 12px;
			opacity: 0.4;
		}

		p {
			font-size: 16px;
			margin: 0;
		}
	}

	.tplf-pagination {
		display: flex;
		justify-content: center;
		align-items: center;
		margin-top: 32px;
		gap: 8px;
	}

	.tplf-load-more {
		padding: 12px 32px;
		border: none;
		border-radius: 6px;
		font-size: 14px;
		font-weight: 500;
		cursor: pointer;
		transition: opacity 0.2s ease, transform 0.2s ease;
		color: #fff;

		&:hover {
			opacity: 0.9;
			transform: translateY(-1px);
		}

		&:disabled {
			opacity: 0.6;
			cursor: not-allowed;
			transform: none;
		}
	}

	.tplf-page-numbers {
		display: flex;
		gap: 4px;

		button {
			width: 36px;
			height: 36px;
			border: 1px solid #ddd;
			border-radius: 6px;
			background: #fff;
			cursor: pointer;
			font-size: 14px;
			transition: all 0.2s ease;

			&.active {
				color: #fff;
				border-color: transparent;
			}

			&:hover:not(.active) {
				background: #f5f5f5;
			}
		}
	}

	.tplf-infinite-trigger {
		height: 1px;
		width: 100%;
	}

	.tplf-loading-more {
		text-align: center;
		padding: 16px;
		color: #666;
		font-size: 14px;
	}

	@keyframes tplfFadeIn {
		from {
			opacity: 0;
			transform: translateY(8px);
		}
		to {
			opacity: 1;
			transform: translateY(0);
		}
	}

	@keyframes tplfShimmer {
		0% {
			background-position: -200% 0;
		}
		100% {
			background-position: 200% 0;
		}
	}

	@media (max-width: 782px) {
		.tplf-filters-toggle {
			display: block;
		}

		.tplf-filters-inner {
			&.is-collapsed {
				max-height: 0;
				opacity: 0;
				overflow: hidden;
			}

			&.is-expanded {
				max-height: 500px;
				opacity: 1;
			}
		}

		.tplf-posts-grid {
			grid-template-columns: 1fr !important;
		}

		.tplf-card-list {
			flex-direction: column !important;

			.tplf-card-image {
				width: 100% !important;
			}
		}
	}

	@media (max-width: 600px) {
		.tplf-filter-search {
			min-width: 100%;
		}

		.tplf-filter-select select {
			min-width: 100%;
		}
	}
}
]]></content>
  </file>
  <file path="src/editor.scss">
    <description>Editor-only styles.</description>
    <content><![CDATA[
.wp-block-displaykit-post-display {
	.tplf-editor-wrapper {
		width: 100%;
	}

	.tplf-filters-preview {
		display: flex;
		flex-wrap: wrap;
		gap: 12px;
		margin-bottom: 24px;
		align-items: center;
		padding: 16px;
		background: #f9f9f9;
		border-radius: 8px;
		border: 1px dashed #ccc;

		input {
			flex: 1;
			min-width: 200px;
			padding: 8px 12px;
			border: 1px solid #ddd;
			border-radius: 4px;
			background: #fff;
		}

		select {
			padding: 8px 12px;
			border: 1px solid #ddd;
			border-radius: 4px;
			background: #fff;
			min-width: 150px;
		}

		.tplf-reset-preview {
			padding: 8px 16px;
			border: 1px solid #ddd;
			border-radius: 4px;
			background: #fff;
			cursor: default;
		}
	}

	.tplf-loading {
		display: flex;
		align-items: center;
		justify-content: center;
		gap: 8px;
		padding: 48px;
		color: #666;
	}

	.tplf-empty {
		text-align: center;
		padding: 48px 24px;
		color: #999;
		background: #f9f9f9;
		border-radius: 8px;

		p {
			font-size: 14px;
		}
	}

	.tplf-card {
		display: flex;
		flex-direction: column;
		overflow: hidden;
		transition: transform 0.2s ease;

		&-list {
			flex-direction: row;
			padding: 0 !important;

			.tplf-card-image {
				width: 200px;
				min-height: 140px;
				flex-shrink: 0;
				border-radius: 0 !important;
				margin: 0 !important;

				img {
					width: 100%;
					height: 100%;
					object-fit: cover;
				}
			}

			.tplf-card-content {
				flex: 1;
				padding: 12px 16px;
				margin-top: 0 !important;
			}
		}

		&-image {
			position: relative;
			overflow: hidden;
			background-color: #f0f0f0;
		}

		&-title {
			margin: 0 0 6px;
			font-weight: 600;
			line-height: 1.3;
		}

		&-meta {
			font-size: 12px;
			margin-bottom: 6px;
			opacity: 0.7;

			.tplf-meta-sep {
				margin: 0 4px;
			}
		}

		&-excerpt {
			margin: 0 0 10px;
			line-height: 1.5;
			opacity: 0.8;
		}

		&-product {
			.tplf-card-content {
				gap: 2px;
			}
		}
	}

	.tplf-read-more {
		font-weight: 500;
		font-size: 13px;
	}

	// WooCommerce editor styles.
	.tplf-sale-badge {
		position: absolute;
		top: 10px;
		left: 10px;
		background-color: #e74c3c;
		color: #fff;
		padding: 4px 10px;
		border-radius: 4px;
		font-size: 11px;
		font-weight: 700;
		text-transform: uppercase;
		letter-spacing: 0.5px;
		z-index: 2;
		line-height: 1.4;
	}

	.tplf-product-price {
		font-size: 15px;
		font-weight: 700;
		margin-bottom: 4px;
		line-height: 1.4;

		del {
			opacity: 0.5;
			font-weight: 400;
			font-size: 13px;
			margin-right: 4px;
		}

		ins {
			text-decoration: none;
			color: #e74c3c;
		}
	}

	.tplf-product-rating {
		display: flex;
		align-items: center;
		gap: 4px;
		margin-bottom: 4px;
		font-size: 13px;
	}

	.tplf-stars {
		display: inline-flex;
		gap: 1px;
		line-height: 1;
	}

	.tplf-star {
		font-size: 14px;
		line-height: 1;

		&-full {
			opacity: 1;
		}

		&-half {
			opacity: 0.6;
		}

		&-empty {
			opacity: 0.3;
		}
	}

	.tplf-rating-count {
		font-size: 11px;
		color: #888;
	}

	.tplf-add-to-cart {
		display: inline-block;
		padding: 8px 16px;
		border: none;
		border-radius: 5px;
		font-size: 13px;
		font-weight: 600;
		cursor: default;
		text-decoration: none;
		text-align: center;
		margin-top: auto;
		line-height: 1.4;
	}

	.tplf-out-of-stock {
		display: inline-block;
		font-size: 11px;
		font-weight: 600;
		color: #e74c3c;
		background: rgba(231, 76, 60, 0.1);
		padding: 3px 8px;
		border-radius: 3px;
		margin-top: 6px;
	}

	.tplf-pagination-preview {
		display: flex;
		justify-content: center;
		margin-top: 24px;

		.tplf-load-more-preview {
			padding: 10px 28px;
			border: none;
			border-radius: 6px;
			font-size: 14px;
			cursor: default;
		}

		.tplf-numbered-preview {
			display: flex;
			gap: 6px;

			.tplf-page-num {
				display: flex;
				align-items: center;
				justify-content: center;
				width: 32px;
				height: 32px;
				border: 1px solid #ddd;
				border-radius: 4px;
				font-size: 13px;

				&.active {
					border-color: transparent;
				}
			}
		}

		.tplf-infinite-label {
			color: #999;
			font-size: 13px;
			text-align: center;
		}
	}

	.tplf-terms-checklist {
		margin-top: 8px;

		.components-base-control__label {
			font-weight: 500;
			margin-bottom: 4px;
		}
	}

	.tplf-color-control {
		margin-bottom: 16px;

		.components-base-control__label {
			font-weight: 500;
			margin-bottom: 8px;
		}
	}

	.tplf-specific-posts-control {
		margin-top: 8px;
	}

	.tplf-specific-search-wrap {
		position: relative;

		.tplf-specific-searching {
			position: absolute;
			right: 8px;
			top: 8px;
		}
	}

	.tplf-specific-results {
		margin: 0;
		padding: 0;
		list-style: none;
		border: 1px solid #ddd;
		border-radius: 4px;
		max-height: 200px;
		overflow-y: auto;
		background: #fff;

		li {
			margin: 0;
			border-bottom: 1px solid #f0f0f0;

			&:last-child {
				border-bottom: none;
			}
		}

		.tplf-specific-result-btn {
			width: 100%;
			justify-content: space-between;
			padding: 8px 12px;
			border: none;
			border-radius: 0;
			text-align: left;
			font-size: 13px;

			&:hover {
				background: #f0f6fc;
			}

			.tplf-specific-add {
				color: #0073aa;
				font-weight: 700;
				font-size: 16px;
			}
		}
	}

	.tplf-specific-selected {
		margin: 8px 0 0;
		padding: 0;
		list-style: none;
	}

	.tplf-specific-selected-item {
		display: flex;
		align-items: center;
		justify-content: space-between;
		padding: 6px 10px;
		margin-bottom: 4px;
		background: #f0f6fc;
		border-radius: 4px;
		font-size: 13px;

		.tplf-specific-selected-title {
			flex: 1;
			margin-right: 8px;
			overflow: hidden;
			text-overflow: ellipsis;
			white-space: nowrap;
		}
	}
}
]]></content>
  </file>
  <file path="src/view.js">
    <description>Frontend JavaScript for AJAX filtering, pagination, and interactive behaviors.</description>
    <content><![CDATA[( function () {
	'use strict';

	document.addEventListener( 'DOMContentLoaded', function () {
		var blocks = document.querySelectorAll( '.wp-block-displaykit-post-display' );

		blocks.forEach( function ( block ) {
			var configRaw = block.getAttribute( 'data-config' );
			var config = {};
			if ( configRaw ) {
				try {
					config = JSON.parse( configRaw );
				} catch ( e ) {
					return;
				}
			}

			var restUrl = block.getAttribute( 'data-rest-url' ) || '';
			var nonce = block.getAttribute( 'data-nonce' ) || '';

			if ( ! restUrl ) {
				return;
			}

			var state = {
				posts: [],
				loading: false,
				initialRender: true,
			};

			var postsContainer = block.querySelector( '.tplf-posts-grid' );
			var skeletonContainer = block.querySelector( '.tplf-skeleton' );
			var emptyContainer = block.querySelector( '.tplf-empty' );
			var isProduct = config.isProduct || false;
			var serverCardsExist = postsContainer && postsContainer.children.length > 0;

			function applyGridStyles() {
				if ( ! postsContainer ) {
					return;
				}
				var currentLayout = config.layout || 'grid';
				var cols = config.columns || 3;
				var gapVal = ( config.gap !== undefined ) ? config.gap : 24;
				var listAlignMap = { left: 'flex-start', center: 'center', right: 'flex-end' };

				if ( currentLayout === 'grid' ) {
					postsContainer.style.display = 'grid';
					postsContainer.style.gridTemplateColumns = 'repeat(' + cols + ', 1fr)';
					postsContainer.style.gap = gapVal + 'px';
					postsContainer.style.flexDirection = '';
					postsContainer.style.alignItems = '';
				} else {
					postsContainer.style.display = 'flex';
					postsContainer.style.flexDirection = 'column';
					postsContainer.style.gap = gapVal + 'px';
					postsContainer.style.gridTemplateColumns = '';
					postsContainer.style.alignItems = listAlignMap[ config.listAlignment || 'left' ] || 'flex-start';
				}
			}

			function buildQueryString() {
				var params = [];
				params.push( 'post_type=' + encodeURIComponent( config.postType || 'post' ) );
				params.push( 'posts_per_page=' + encodeURIComponent( String( config.postsPerPage || 6 ) ) );
				params.push( 'orderby=' + encodeURIComponent( config.orderby || 'date' ) );
				params.push( 'order=' + encodeURIComponent( config.order || 'DESC' ) );

				if ( config.specificPosts && config.specificPosts.length > 0 ) {
					params.push( 'include=' + encodeURIComponent( config.specificPosts.join( ',' ) ) );
				}

				if ( config.taxonomy && config.terms && config.terms.length > 0 ) {
					params.push( 'taxonomy=' + encodeURIComponent( config.taxonomy ) );
					params.push( 'terms=' + encodeURIComponent( config.terms.join( ',' ) ) );
				}

				if ( config.author ) {
					params.push( 'author=' + encodeURIComponent( config.author ) );
				}

				return params.join( '&' );
			}

			function renderStarsHTML( rating, accentColor ) {
				var fullStars = Math.floor( rating );
				var hasHalf = ( rating - fullStars ) >= 0.5;
				var html = '<span class="tplf-stars" style="color:' + escapeAttr( accentColor || '#0073aa' ) + ';">';
				for ( var i = 0; i < 5; i++ ) {
					if ( i < fullStars ) {
						html += '<span class="tplf-star tplf-star-full">\u2605</span>';
					} else if ( i === fullStars && hasHalf ) {
						html += '<span class="tplf-star tplf-star-half">\u2605</span>';
					} else {
						html += '<span class="tplf-star tplf-star-empty">\u2606</span>';
					}
				}
				html += '</span>';
				return html;
			}

			function escapeHTML( str ) {
				var div = document.createElement( 'div' );
				div.appendChild( document.createTextNode( str || '' ) );
				return div.innerHTML;
			}

			function escapeAttr( str ) {
				return ( str || '' )
					.replace( /&/g, '&amp;' )
					.replace( /"/g, '&quot;' )
					.replace( /'/g, '&#39;' )
					.replace( /</g, '&lt;' )
					.replace( />/g, '&gt;' );
			}

			function createCardHTML( post ) {
				var openInNewTab = config.openInNewTab || false;
				var linkTarget = openInNewTab ? ' target="_blank" rel="noopener noreferrer"' : '';
				var imageRatioMap = {
					'16-9': '56.25%',
					'4-3': '75%',
					'1-1': '100%',
					'3-2': '66.67%',
					'auto': '0',
				};
				var ratio = config.imageRatio || '16-9';
				var isAuto = ratio === 'auto';
				var paddingTop = imageRatioMap[ ratio ] || '56.25%';
				var isListLayout = config.layout === 'list';
				var hasProduct = post.product && isProduct;
				var onSale = hasProduct && post.product.on_sale;
				var showSaleBadge = config.showSaleBadge !== false;
				var listCardW = config.listCardWidth || 0;
				var listCardH = config.listCardHeight || 0;
				var cPadding = config.cardPadding || 16;
				var cRadius = config.cardBorderRadius || 8;
				var cardBg = config.cardBgColor || '#ffffff';
				var cardText = config.cardTextColor || '#1e1e1e';

				var cardPaddingStyle = isListLayout ? '0' : cPadding + 'px';
				var cardMinHeight = ( isListLayout && listCardH > 0 ) ? 'min-height:' + listCardH + 'px;' : '';
				var cardMaxWidth = ( isListLayout && listCardW > 0 ) ? 'max-width:' + listCardW + 'px;' : '';

				var html = '<div class="tplf-card' + ( isListLayout ? ' tplf-card-list' : '' ) + ( hasProduct ? ' tplf-card-product' : '' ) + '" style="' +
					'background-color:' + escapeAttr( cardBg ) + ';' +
					'color:' + escapeAttr( cardText ) + ';' +
					'border-radius:' + cRadius + 'px;' +
					'padding:' + cardPaddingStyle + ';' +
					( config.cardShadow !== false ? 'box-shadow:0 2px 8px rgba(0,0,0,0.1);' : '' ) +
					cardMinHeight +
					cardMaxWidth +
					( isListLayout ? 'width:100%;' : '' ) +
					'overflow:hidden;">';

				var saleBadgeHTML = ( onSale && showSaleBadge ) ? '<span class="tplf-sale-badge">Sale!</span>' : '';

				if ( config.showImage !== false ) {
					if ( post.thumbnail ) {
						if ( isListLayout ) {
							html += '<div class="tplf-card-image" style="' +
								'width:40%;min-height:160px;flex-shrink:0;position:relative;' +
								'background-image:url(' + escapeAttr( post.thumbnail ) + ');' +
								'background-size:cover;background-position:center;">' + saleBadgeHTML + '</div>';
						} else if ( isAuto ) {
							html += '<div class="tplf-card-image" style="' +
								'margin:-' + cPadding + 'px -' + cPadding + 'px 0 -' + cPadding + 'px;' +
								'width:calc(100% + ' + ( cPadding * 2 ) + 'px);' +
								'border-radius:' + cRadius + 'px ' + cRadius + 'px 0 0;' +
								'overflow:hidden;position:relative;">' +
								saleBadgeHTML +
								'<img src="' + escapeAttr( post.thumbnail ) + '" alt="' + escapeAttr( post.title ) + '" style="width:100%;display:block;" loading="lazy" />' +
								'</div>';
						} else {
							html += '<div class="tplf-card-image" style="' +
								'padding-top:' + paddingTop + ';' +
								'background-image:url(' + escapeAttr( post.thumbnail ) + ');' +
								'background-size:cover;background-position:center;' +
								'margin:-' + cPadding + 'px -' + cPadding + 'px 0 -' + cPadding + 'px;' +
								'width:calc(100% + ' + ( cPadding * 2 ) + 'px);' +
								'border-radius:' + cRadius + 'px ' + cRadius + 'px 0 0;' +
								'position:relative;">' + saleBadgeHTML + '</div>';
						}
					} else {
						if ( isListLayout ) {
							html += '<div class="tplf-card-image tplf-card-no-image" style="' +
								'width:40%;min-height:160px;flex-shrink:0;' +
								'background-color:#f0f0f0;position:relative;display:flex;align-items:center;justify-content:center;">' +
								saleBadgeHTML +
								'<span style="color:#999;font-size:12px;">No Image</span>' +
								'</div>';
						} else {
							html += '<div class="tplf-card-image tplf-card-no-image" style="' +
								'padding-top:' + ( paddingTop || '56.25%' ) + ';' +
								'background-color:#f0f0f0;' +
								'margin:-' + cPadding + 'px -' + cPadding + 'px 0 -' + cPadding + 'px;' +
								'width:calc(100% + ' + ( cPadding * 2 ) + 'px);' +
								'border-radius:' + cRadius + 'px ' + cRadius + 'px 0 0;' +
								'position:relative;">' +
								saleBadgeHTML +
								'<span style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#999;font-size:12px;">No Image</span>' +
								'</div>';
						}
					}
				}

				var contentStyle = '';
				if ( isListLayout ) {
					contentStyle = 'flex:1;padding:' + cPadding + 'px;display:flex;flex-direction:column;margin-top:0;';
				} else {
					var contentMargin = ( config.showImage !== false ) ? cPadding + 'px' : '0';
					contentStyle = 'margin-top:' + contentMargin + ';display:flex;flex-direction:column;flex:1;';
				}
				html += '<div class="tplf-card-content" style="' + contentStyle + '">';

				if ( config.showTitle !== false ) {
					html += '<h3 class="tplf-card-title" style="font-size:' + ( config.titleFontSize || 18 ) + 'px;color:' + escapeAttr( cardText ) + ';">' +
						'<a href="' + escapeAttr( post.permalink ) + '"' + linkTarget + ' style="color:inherit;text-decoration:none;">' + escapeHTML( post.title ) + '</a></h3>';
				}

				if ( hasProduct && config.showPrice !== false && post.product.price_html ) {
					html += '<div class="tplf-product-price">' + post.product.price_html + '</div>';
				}

				if ( hasProduct && config.showRating !== false && post.product.average_rating > 0 ) {
					html += '<div class="tplf-product-rating">' +
						renderStarsHTML( post.product.average_rating, config.accentColor ) +
						'<span class="tplf-rating-count">(' + parseInt( post.product.rating_count, 10 ) + ')</span>' +
						'</div>';
				}

				if ( config.showMeta !== false && ! hasProduct ) {
					html += '<div class="tplf-card-meta" style="color:' + escapeAttr( config.accentColor || '#0073aa' ) + ';">' +
						'<span>' + escapeHTML( post.author ) + '</span>' +
						'<span class="tplf-meta-sep">\u00b7</span>' +
						'<span>' + escapeHTML( post.date ) + '</span>' +
						'</div>';
				}

				if ( config.showExcerpt !== false ) {
					html += '<p class="tplf-card-excerpt" style="font-size:' + ( config.excerptFontSize || 14 ) + 'px;">' + escapeHTML( post.excerpt ) + '</p>';
				}

				if ( hasProduct && config.showAddToCart !== false ) {
					html += '<a href="' + escapeAttr( post.product.add_to_cart_url ) + '" class="tplf-add-to-cart"' + linkTarget + ' style="background-color:' + escapeAttr( config.accentColor || '#0073aa' ) + ';color:#fff;" data-product-id="' + parseInt( post.id, 10 ) + '">' +
						escapeHTML( config.addToCartText || 'Add to Cart' ) + ' \uD83D\uDED2</a>';
				}

				if ( ! hasProduct && config.showReadMore !== false ) {
					html += '<a href="' + escapeAttr( post.permalink ) + '" class="tplf-read-more"' + linkTarget + ' style="color:' + escapeAttr( config.accentColor || '#0073aa' ) + ';">' +
						escapeHTML( config.readMoreText || 'Read More' ) + ' \u2192</a>';
				}

				if ( hasProduct && ! post.product.in_stock ) {
					html += '<span class="tplf-out-of-stock">Out of Stock</span>';
				}

				html += '</div></div>';
				return html;
			}

			function renderPosts() {
				if ( ! postsContainer ) {
					return;
				}

				postsContainer.innerHTML = '';

				if ( state.posts.length === 0 ) {
					postsContainer.style.display = 'none';
					if ( emptyContainer ) {
						emptyContainer.style.display = 'block';
					}
					return;
				}

				applyGridStyles();
				if ( emptyContainer ) {
					emptyContainer.style.display = 'none';
				}

				state.posts.forEach( function ( post ) {
					var temp = document.createElement( 'div' );
					temp.innerHTML = createCardHTML( post );
					if ( temp.firstElementChild ) {
						postsContainer.appendChild( temp.firstElementChild );
					}
				} );
			}

			function showSkeleton() {
				if ( skeletonContainer ) {
					var currentLayout = config.layout || 'grid';
					var cols = config.columns || 3;
					var gapVal = ( config.gap !== undefined ) ? config.gap : 24;
					if ( currentLayout === 'grid' ) {
						skeletonContainer.style.display = 'grid';
						skeletonContainer.style.gridTemplateColumns = 'repeat(' + cols + ', 1fr)';
						skeletonContainer.style.gap = gapVal + 'px';
					} else {
						skeletonContainer.style.display = 'flex';
						skeletonContainer.style.flexDirection = 'column';
						skeletonContainer.style.gap = gapVal + 'px';
					}
				}
				if ( postsContainer ) {
					postsContainer.style.display = 'none';
				}
				if ( emptyContainer ) {
					emptyContainer.style.display = 'none';
				}
			}

			function hideSkeleton() {
				if ( skeletonContainer ) {
					skeletonContainer.style.display = 'none';
				}
			}

			function fetchPosts() {
				if ( state.loading ) {
					return;
				}

				state.loading = true;

				if ( postsContainer ) {
					postsContainer.classList.add( 'is-loading' );
				}

				var url = restUrl + 'posts?' + buildQueryString();
				var headers = {};
				if ( nonce ) {
					headers[ 'X-WP-Nonce' ] = nonce;
				}

				fetch( url, {
					credentials: 'same-origin',
					headers: headers,
				} )
					.then( function ( response ) {
						if ( ! response.ok ) {
							throw new Error( 'HTTP ' + response.status );
						}
						return response.json();
					} )
					.then( function ( data ) {
						state.posts = data.posts || [];
						state.loading = false;

						hideSkeleton();
						if ( postsContainer ) {
							postsContainer.classList.remove( 'is-loading' );
						}

						if ( state.initialRender && serverCardsExist && state.posts.length > 0 ) {
							state.initialRender = false;
							applyGridStyles();
							postsContainer.style.display !== 'none' && applyGridStyles();
						} else {
							state.initialRender = false;
							renderPosts();
						}
					} )
					.catch( function () {
						state.loading = false;
						state.initialRender = false;
						hideSkeleton();
						if ( postsContainer ) {
							postsContainer.classList.remove( 'is-loading' );
						}
						if ( serverCardsExist ) {
							applyGridStyles();
						} else if ( emptyContainer ) {
							emptyContainer.style.display = 'block';
						}
					} );
			}

			applyGridStyles();
			fetchPosts();
		} );
	} );
}() );]]></content>
  </file>
  <file path="src/save.js">
    <description>Dynamic block — save returns null.</description>
    <content><![CDATA[
]]></content>
  </file>
  <file path="src/render.php">
    <description>Server-side render for the dynamic block.</description>
    <content><![CDATA[<?php
/**
 * Server-side rendering for the DisplayKit block.
 *
 * @var array    $attributes Block attributes.
 * @var string   $content    Block default content.
 * @var WP_Block $block      Block instance.
 *
 * @package DisplayKit
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! function_exists( 'displaykit_sanitize_color_render' ) ) {
	/**
	 * Sanitize a CSS color value (hex, rgb, rgba, hsl, hsla).
	 *
	 * @param string $color The color value to sanitize.
	 * @return string Sanitized color string or empty string if invalid.
	 */
	function displaykit_sanitize_color_render( $color ) {
		$color = trim( $color );

		if ( preg_match( '/^#([0-9a-fA-F]{3,8})$/', $color ) ) {
			return $color;
		}

		if ( preg_match( '/^rgba?\(\s*[\d\s,.\/%]+\)$/i', $color ) ) {
			return $color;
		}

		if ( preg_match( '/^hsla?\(\s*[\d\s,.\/%deg]+\)$/i', $color ) ) {
			return $color;
		}

		$hex = sanitize_hex_color( $color );
		return $hex ? $hex : '';
	}
}

$displaykit_post_type       = ! empty( $attributes['postType'] ) ? sanitize_text_field( $attributes['postType'] ) : 'post';
$displaykit_posts_per_page  = ! empty( $attributes['postsPerPage'] ) ? absint( $attributes['postsPerPage'] ) : 6;
$displaykit_orderby         = ! empty( $attributes['orderby'] ) ? sanitize_text_field( $attributes['orderby'] ) : 'date';
$displaykit_order           = ! empty( $attributes['order'] ) ? sanitize_text_field( $attributes['order'] ) : 'DESC';
$displaykit_taxonomy        = ! empty( $attributes['taxonomy'] ) ? sanitize_text_field( $attributes['taxonomy'] ) : '';
$displaykit_terms           = ! empty( $attributes['terms'] ) ? array_map( 'absint', $attributes['terms'] ) : array();
$displaykit_author          = ! empty( $attributes['author'] ) ? sanitize_text_field( $attributes['author'] ) : '';
$displaykit_layout          = ! empty( $attributes['layout'] ) ? sanitize_text_field( $attributes['layout'] ) : 'grid';
$displaykit_columns         = ! empty( $attributes['columns'] ) ? absint( $attributes['columns'] ) : 3;
$displaykit_gap             = isset( $attributes['gap'] ) ? absint( $attributes['gap'] ) : 24;
$displaykit_show_image      = isset( $attributes['showImage'] ) ? (bool) $attributes['showImage'] : true;
$displaykit_show_title      = isset( $attributes['showTitle'] ) ? (bool) $attributes['showTitle'] : true;
$displaykit_show_excerpt    = isset( $attributes['showExcerpt'] ) ? (bool) $attributes['showExcerpt'] : true;
$displaykit_show_meta       = isset( $attributes['showMeta'] ) ? (bool) $attributes['showMeta'] : true;
$displaykit_show_read_more  = isset( $attributes['showReadMore'] ) ? (bool) $attributes['showReadMore'] : true;
$displaykit_read_more_text  = ! empty( $attributes['readMoreText'] ) ? sanitize_text_field( $attributes['readMoreText'] ) : __( 'Read More', 'displaykit' );
$displaykit_image_ratio     = ! empty( $attributes['imageRatio'] ) ? sanitize_text_field( $attributes['imageRatio'] ) : '16-9';

$displaykit_card_bg         = ! empty( $attributes['cardBgColor'] ) ? displaykit_sanitize_color_render( $attributes['cardBgColor'] ) : '#ffffff';
$displaykit_card_text       = ! empty( $attributes['cardTextColor'] ) ? displaykit_sanitize_color_render( $attributes['cardTextColor'] ) : '#1e1e1e';

$displaykit_card_radius     = isset( $attributes['cardBorderRadius'] ) ? absint( $attributes['cardBorderRadius'] ) : 8;
$displaykit_title_font_size = isset( $attributes['titleFontSize'] ) ? absint( $attributes['titleFontSize'] ) : 18;
$displaykit_excerpt_font_size = isset( $attributes['excerptFontSize'] ) ? absint( $attributes['excerptFontSize'] ) : 14;

$displaykit_accent_color    = ! empty( $attributes['accentColor'] ) ? displaykit_sanitize_color_render( $attributes['accentColor'] ) : '#0073aa';

$displaykit_card_padding    = isset( $attributes['cardPadding'] ) ? absint( $attributes['cardPadding'] ) : 16;
$displaykit_card_shadow     = isset( $attributes['cardShadow'] ) ? (bool) $attributes['cardShadow'] : true;

$displaykit_show_price       = isset( $attributes['showPrice'] ) ? (bool) $attributes['showPrice'] : true;
$displaykit_show_rating      = isset( $attributes['showRating'] ) ? (bool) $attributes['showRating'] : true;
$displaykit_show_sale_badge  = isset( $attributes['showSaleBadge'] ) ? (bool) $attributes['showSaleBadge'] : true;
$displaykit_show_add_to_cart = isset( $attributes['showAddToCart'] ) ? (bool) $attributes['showAddToCart'] : true;
$displaykit_add_to_cart_text = ! empty( $attributes['addToCartText'] ) ? sanitize_text_field( $attributes['addToCartText'] ) : __( 'Add to Cart', 'displaykit' );
$displaykit_specific_posts   = ! empty( $attributes['specificPosts'] ) ? array_map( 'absint', $attributes['specificPosts'] ) : array();
$displaykit_list_card_width  = isset( $attributes['listCardWidth'] ) ? absint( $attributes['listCardWidth'] ) : 0;
$displaykit_list_card_height = isset( $attributes['listCardHeight'] ) ? absint( $attributes['listCardHeight'] ) : 0;
$displaykit_list_alignment   = ! empty( $attributes['listAlignment'] ) ? sanitize_text_field( $attributes['listAlignment'] ) : 'left';
$displaykit_open_in_new_tab  = isset( $attributes['openInNewTab'] ) ? (bool) $attributes['openInNewTab'] : false;

$displaykit_is_product  = ( 'product' === $displaykit_post_type );
$displaykit_woo_active  = class_exists( 'WooCommerce' );
$displaykit_block_id    = 'displaykit-' . wp_unique_id();

$displaykit_config = wp_json_encode( array(
	'postType'         => $displaykit_post_type,
	'postsPerPage'     => $displaykit_posts_per_page,
	'orderby'          => $displaykit_orderby,
	'order'            => $displaykit_order,
	'taxonomy'         => $displaykit_taxonomy,
	'terms'            => $displaykit_terms,
	'author'           => $displaykit_author,
	'layout'           => $displaykit_layout,
	'columns'          => $displaykit_columns,
	'gap'              => $displaykit_gap,
	'showImage'        => $displaykit_show_image,
	'showTitle'        => $displaykit_show_title,
	'showExcerpt'      => $displaykit_show_excerpt,
	'showMeta'         => $displaykit_show_meta,
	'showReadMore'     => $displaykit_show_read_more,
	'readMoreText'     => $displaykit_read_more_text,
	'imageRatio'       => $displaykit_image_ratio,
	'cardBgColor'      => $displaykit_card_bg,
	'cardTextColor'    => $displaykit_card_text,
	'cardBorderRadius' => $displaykit_card_radius,
	'titleFontSize'    => $displaykit_title_font_size,
	'excerptFontSize'  => $displaykit_excerpt_font_size,
	'accentColor'      => $displaykit_accent_color,
	'cardPadding'      => $displaykit_card_padding,
	'cardShadow'       => $displaykit_card_shadow,
	'showPrice'        => $displaykit_show_price,
	'showRating'       => $displaykit_show_rating,
	'showSaleBadge'    => $displaykit_show_sale_badge,
	'showAddToCart'    => $displaykit_show_add_to_cart,
	'addToCartText'    => $displaykit_add_to_cart_text,
	'isProduct'        => $displaykit_is_product && $displaykit_woo_active,
	'specificPosts'    => $displaykit_specific_posts,
	'listCardWidth'    => $displaykit_list_card_width,
	'listCardHeight'   => $displaykit_list_card_height,
	'listAlignment'    => $displaykit_list_alignment,
	'openInNewTab'     => $displaykit_open_in_new_tab,
) );

$displaykit_query_args = array(
	'post_type'      => $displaykit_post_type,
	'posts_per_page' => $displaykit_posts_per_page,
	'orderby'        => $displaykit_orderby,
	'order'          => $displaykit_order,
	'post_status'    => 'publish',
);

if ( ! empty( $displaykit_specific_posts ) ) {
	$displaykit_query_args['post__in']       = $displaykit_specific_posts;
	$displaykit_query_args['orderby']        = 'post__in';
	$displaykit_query_args['posts_per_page'] = count( $displaykit_specific_posts );
}

if ( $displaykit_is_product && $displaykit_woo_active && empty( $displaykit_specific_posts ) ) {
	if ( 'price' === $displaykit_orderby ) {
		$displaykit_query_args['orderby']  = 'meta_value_num';
		$displaykit_query_args['meta_key'] = '_price'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
	} elseif ( 'popularity' === $displaykit_orderby ) {
		$displaykit_query_args['orderby']  = 'meta_value_num';
		$displaykit_query_args['meta_key'] = 'total_sales'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
	} elseif ( 'rating' === $displaykit_orderby ) {
		$displaykit_query_args['orderby']  = 'meta_value_num';
		$displaykit_query_args['meta_key'] = '_wc_average_rating'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
	}
}

$displaykit_tax_query_parts = array();

if ( ! empty( $displaykit_taxonomy ) && ! empty( $displaykit_terms ) ) {
	$displaykit_tax_query_parts[] = array(
		'taxonomy' => $displaykit_taxonomy,
		'field'    => 'term_id',
		'terms'    => $displaykit_terms,
	);
}

if ( ! empty( $displaykit_author ) ) {
	$displaykit_query_args['author'] = absint( $displaykit_author );
}

if ( $displaykit_is_product && $displaykit_woo_active ) {
	$displaykit_tax_query_parts[] = array(
		'taxonomy' => 'product_visibility',
		'field'    => 'name',
		'terms'    => array( 'exclude-from-catalog' ),
		'operator' => 'NOT IN',
	);
}

if ( ! empty( $displaykit_tax_query_parts ) ) {
	if ( count( $displaykit_tax_query_parts ) > 1 ) {
		$displaykit_tax_query_parts['relation'] = 'AND';
	}
	$displaykit_query_args['tax_query'] = $displaykit_tax_query_parts; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
}

$displaykit_query = new WP_Query( $displaykit_query_args );

$displaykit_image_ratio_map = array(
	'16-9' => '56.25%',
	'4-3'  => '75%',
	'1-1'  => '100%',
	'3-2'  => '66.67%',
	'auto' => '0',
);

$displaykit_ratio_padding = isset( $displaykit_image_ratio_map[ $displaykit_image_ratio ] ) ? $displaykit_image_ratio_map[ $displaykit_image_ratio ] : '56.25%';
$displaykit_is_auto_ratio = ( 'auto' === $displaykit_image_ratio );

if ( 'grid' === $displaykit_layout ) {
	$displaykit_grid_style = 'display:grid;grid-template-columns:repeat(' . esc_attr( intval( $displaykit_columns ) ) . ',1fr);gap:' . esc_attr( intval( $displaykit_gap ) ) . 'px;';
} else {
	$displaykit_list_align_map = array(
		'left'   => 'flex-start',
		'center' => 'center',
		'right'  => 'flex-end',
	);
	$displaykit_align_value = isset( $displaykit_list_align_map[ $displaykit_list_alignment ] ) ? $displaykit_list_align_map[ $displaykit_list_alignment ] : 'flex-start';
	$displaykit_grid_style  = 'display:flex;flex-direction:column;gap:' . esc_attr( intval( $displaykit_gap ) ) . 'px;align-items:' . esc_attr( $displaykit_align_value ) . ';';
}

$displaykit_is_list = ( 'list' === $displaykit_layout );

$displaykit_card_style_parts = array(
	'background-color:' . esc_attr( $displaykit_card_bg ),
	'color:' . esc_attr( $displaykit_card_text ),
	'border-radius:' . esc_attr( intval( $displaykit_card_radius ) ) . 'px',
	'overflow:hidden',
);

if ( $displaykit_card_shadow ) {
	$displaykit_card_style_parts[] = 'box-shadow:0 2px 8px rgba(0,0,0,0.1)';
}

if ( $displaykit_is_list ) {
	$displaykit_card_style_parts[] = 'display:flex';
	$displaykit_card_style_parts[] = 'flex-direction:row';
	$displaykit_card_style_parts[] = 'width:100%';
	if ( $displaykit_list_card_width > 0 ) {
		$displaykit_card_style_parts[] = 'max-width:' . esc_attr( intval( $displaykit_list_card_width ) ) . 'px';
	}
	if ( $displaykit_list_card_height > 0 ) {
		$displaykit_card_style_parts[] = 'min-height:' . esc_attr( intval( $displaykit_list_card_height ) ) . 'px';
	}
} else {
	$displaykit_card_style_parts[] = 'padding:' . esc_attr( intval( $displaykit_card_padding ) ) . 'px';
}

$displaykit_card_style = implode( ';', $displaykit_card_style_parts ) . ';';

$displaykit_wrapper_classes = 'tplf-block tplf-layout-' . esc_attr( $displaykit_layout );
if ( $displaykit_is_product && $displaykit_woo_active ) {
	$displaykit_wrapper_classes .= ' tplf-is-product';
}

$displaykit_wrapper_attributes = get_block_wrapper_attributes( array(
	'class'         => $displaykit_wrapper_classes,
	'data-config'   => $displaykit_config,
	'data-block-id' => esc_attr( $displaykit_block_id ),
	'data-rest-url' => esc_url( rest_url( 'displaykit/v1/' ) ),
	'data-nonce'    => wp_create_nonce( 'displaykit_nonce' ),
	'style'         => '--tplf-accent:' . esc_attr( $displaykit_accent_color ) . ';',
) );

if ( ! function_exists( 'displaykit_render_star_rating' ) ) {
	/**
	 * Render star rating HTML.
	 *
	 * @param float  $rating    The average rating.
	 * @param string $accent_clr The accent color hex value.
	 * @return string HTML for star rating.
	 */
	function displaykit_render_star_rating( $rating, $accent_clr ) {
		$full_stars = (int) floor( $rating );
		$has_half   = ( $rating - $full_stars ) >= 0.5;
		$html       = '<span class="tplf-stars" style="color:' . esc_attr( $accent_clr ) . ';">';
		for ( $i = 0; $i < 5; $i++ ) {
			if ( $i < $full_stars ) {
				$html .= '<span class="tplf-star tplf-star-full">&#9733;</span>';
			} elseif ( $i === $full_stars && $has_half ) {
				$html .= '<span class="tplf-star tplf-star-half">&#9733;</span>';
			} else {
				$html .= '<span class="tplf-star tplf-star-empty">&#9734;</span>';
			}
		}
		$html .= '</span>';
		return $html;
	}
}

if ( ! function_exists( 'displaykit_build_image_style' ) ) {
	/**
	 * Build inline style for card image div.
	 *
	 * @param array $args Style arguments.
	 * @return string Escaped inline style string.
	 */
	function displaykit_build_image_style( $args ) {
		$parts = array();
		foreach ( $args as $prop => $val ) {
			$parts[] = esc_attr( $prop ) . ':' . esc_attr( $val );
		}
		return implode( ';', $parts ) . ';';
	}
}

// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_block_wrapper_attributes returns escaped output.
echo '<div ' . $displaykit_wrapper_attributes . '>';

echo '<div class="tplf-skeleton" style="' . esc_attr( $displaykit_grid_style ) . 'display:none;">';
$displaykit_skeleton_count = min( $displaykit_posts_per_page, 6 );
for ( $displaykit_i = 0; $displaykit_i < $displaykit_skeleton_count; $displaykit_i++ ) {
	echo '<div class="tplf-skeleton-card">';
	if ( $displaykit_show_image ) {
		$displaykit_skel_padding = $displaykit_is_auto_ratio ? '56.25%' : $displaykit_ratio_padding;
		echo '<div class="tplf-skeleton-image" style="padding-top:' . esc_attr( $displaykit_skel_padding ) . ';"></div>';
	}
	echo '<div class="tplf-skeleton-content">';
	echo '<div class="tplf-skeleton-line wide"></div>';
	echo '<div class="tplf-skeleton-line medium"></div>';
	echo '<div class="tplf-skeleton-line short"></div>';
	echo '</div>';
	echo '</div>';
}
echo '</div>';

echo '<div class="tplf-posts-grid" style="' . esc_attr( $displaykit_grid_style ) . '">';

if ( $displaykit_query->have_posts() ) {
	while ( $displaykit_query->have_posts() ) {
		$displaykit_query->the_post();
		$displaykit_current_id  = get_the_ID();
		$displaykit_product_obj = null;
		if ( $displaykit_is_product && $displaykit_woo_active && function_exists( 'wc_get_product' ) ) {
			$displaykit_product_obj = wc_get_product( $displaykit_current_id );
		}
		$displaykit_card_classes = 'tplf-card';
		if ( $displaykit_is_list ) {
			$displaykit_card_classes .= ' tplf-card-list';
		}
		if ( $displaykit_product_obj ) {
			$displaykit_card_classes .= ' tplf-card-product';
		}

		echo '<div class="' . esc_attr( $displaykit_card_classes ) . '" style="' . esc_attr( $displaykit_card_style ) . '">';

		if ( $displaykit_show_image ) {
			$displaykit_has_thumb = has_post_thumbnail( $displaykit_current_id );
			$displaykit_thumb_url = $displaykit_has_thumb ? get_the_post_thumbnail_url( $displaykit_current_id, 'medium_large' ) : '';
			$displaykit_on_sale   = $displaykit_product_obj && $displaykit_product_obj->is_on_sale() && $displaykit_show_sale_badge;
			$displaykit_sale_html = '';
			if ( $displaykit_on_sale ) {
				$displaykit_sale_html = '<span class="tplf-sale-badge">' . esc_html__( 'Sale!', 'displaykit' ) . '</span>';
			}

			$displaykit_int_padding = intval( $displaykit_card_padding );
			$displaykit_int_radius  = intval( $displaykit_card_radius );
			$displaykit_double_pad  = $displaykit_int_padding * 2;

			if ( $displaykit_has_thumb ) {
				if ( $displaykit_is_list ) {
					$displaykit_img_style = displaykit_build_image_style( array(
						'width'               => '40%',
						'min-height'          => '180px',
						'flex-shrink'         => '0',
						'background-image'    => 'url(' . esc_url( $displaykit_thumb_url ) . ')',
						'background-size'     => 'cover',
						'background-position' => 'center',
						'position'            => 'relative',
					) );
					echo '<div class="tplf-card-image" style="' . esc_attr( $displaykit_img_style ) . '">';
					echo wp_kses_post( $displaykit_sale_html );
					echo '</div>';
				} elseif ( $displaykit_is_auto_ratio ) {
					$displaykit_img_style = displaykit_build_image_style( array(
						'margin'        => '-' . $displaykit_int_padding . 'px -' . $displaykit_int_padding . 'px 0 -' . $displaykit_int_padding . 'px',
						'width'         => 'calc(100% + ' . $displaykit_double_pad . 'px)',
						'border-radius' => $displaykit_int_radius . 'px ' . $displaykit_int_radius . 'px 0 0',
						'overflow'      => 'hidden',
						'position'      => 'relative',
					) );
					echo '<div class="tplf-card-image" style="' . esc_attr( $displaykit_img_style ) . '">';
					echo wp_kses_post( $displaykit_sale_html );
					echo '<img src="' . esc_url( $displaykit_thumb_url ) . '" alt="' . esc_attr( get_the_title() ) . '" style="width:100%;display:block;" loading="lazy" />';
					echo '</div>';
				} else {
					$displaykit_img_style = displaykit_build_image_style( array(
						'padding-top'         => $displaykit_ratio_padding,
						'background-image'    => 'url(' . esc_url( $displaykit_thumb_url ) . ')',
						'background-size'     => 'cover',
						'background-position' => 'center',
						'margin'              => '-' . $displaykit_int_padding . 'px -' . $displaykit_int_padding . 'px 0 -' . $displaykit_int_padding . 'px',
						'width'               => 'calc(100% + ' . $displaykit_double_pad . 'px)',
						'border-radius'       => $displaykit_int_radius . 'px ' . $displaykit_int_radius . 'px 0 0',
						'position'            => 'relative',
					) );
					echo '<div class="tplf-card-image" style="' . esc_attr( $displaykit_img_style ) . '">';
					echo wp_kses_post( $displaykit_sale_html );
					echo '</div>';
				}
			} else {
				if ( $displaykit_is_list ) {
					$displaykit_img_style = displaykit_build_image_style( array(
						'width'            => '40%',
						'min-height'       => '180px',
						'flex-shrink'      => '0',
						'background-color' => '#f0f0f0',
						'position'         => 'relative',
						'display'          => 'flex',
						'align-items'      => 'center',
						'justify-content'  => 'center',
					) );
					echo '<div class="tplf-card-image tplf-card-no-image" style="' . esc_attr( $displaykit_img_style ) . '">';
					echo wp_kses_post( $displaykit_sale_html );
					echo '<span style="color:#999;font-size:12px;">' . esc_html__( 'No Image', 'displaykit' ) . '</span>';
					echo '</div>';
				} else {
					$displaykit_no_img_padding = $displaykit_is_auto_ratio ? '56.25%' : $displaykit_ratio_padding;
					$displaykit_img_style = displaykit_build_image_style( array(
						'padding-top'      => $displaykit_no_img_padding,
						'background-color' => '#f0f0f0',
						'margin'           => '-' . $displaykit_int_padding . 'px -' . $displaykit_int_padding . 'px 0 -' . $displaykit_int_padding . 'px',
						'width'            => 'calc(100% + ' . $displaykit_double_pad . 'px)',
						'border-radius'    => $displaykit_int_radius . 'px ' . $displaykit_int_radius . 'px 0 0',
						'position'         => 'relative',
					) );
					echo '<div class="tplf-card-image tplf-card-no-image" style="' . esc_attr( $displaykit_img_style ) . '">';
					echo wp_kses_post( $displaykit_sale_html );
					echo '<span style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#999;font-size:12px;">' . esc_html__( 'No Image', 'displaykit' ) . '</span>';
					echo '</div>';
				}
			}
		}

		if ( $displaykit_is_list ) {
			$displaykit_content_style = 'flex:1;padding:' . esc_attr( intval( $displaykit_card_padding ) ) . 'px;display:flex;flex-direction:column;';
		} else {
			$displaykit_content_margin = $displaykit_show_image ? esc_attr( intval( $displaykit_card_padding ) ) . 'px' : '0';
			$displaykit_content_style  = 'margin-top:' . $displaykit_content_margin . ';display:flex;flex-direction:column;flex:1;';
		}
		echo '<div class="tplf-card-content" style="' . esc_attr( $displaykit_content_style ) . '">';

		if ( $displaykit_show_title ) {
			echo '<h3 class="tplf-card-title" style="font-size:' . esc_attr( intval( $displaykit_title_font_size ) ) . 'px;color:' . esc_attr( $displaykit_card_text ) . ';">';
			echo '<a href="' . esc_url( get_the_permalink() ) . '"';
			if ( $displaykit_open_in_new_tab ) {
				echo ' target="_blank" rel="noopener noreferrer"';
			}
			echo ' style="color:inherit;text-decoration:none;">';
			echo esc_html( get_the_title() );
			echo '</a></h3>';
		}

		if ( $displaykit_product_obj && $displaykit_show_price ) {
			echo '<div class="tplf-product-price">';
			echo wp_kses_post( $displaykit_product_obj->get_price_html() );
			echo '</div>';
		}

		if ( $displaykit_product_obj && $displaykit_show_rating && $displaykit_product_obj->get_average_rating() > 0 ) {
			echo '<div class="tplf-product-rating">';
			echo wp_kses_post( displaykit_render_star_rating( $displaykit_product_obj->get_average_rating(), $displaykit_accent_color ) );
			echo '<span class="tplf-rating-count">(' . esc_html( $displaykit_product_obj->get_rating_count() ) . ')</span>';
			echo '</div>';
		}

		if ( $displaykit_show_meta && ! $displaykit_product_obj ) {
			echo '<div class="tplf-card-meta" style="color:' . esc_attr( $displaykit_accent_color ) . ';">';
			echo '<span>' . esc_html( get_the_author() ) . '</span>';
			echo '<span class="tplf-meta-sep">&middot;</span>';
			echo '<span>' . esc_html( get_the_date() ) . '</span>';
			echo '</div>';
		}

		if ( $displaykit_show_excerpt ) {
			echo '<p class="tplf-card-excerpt" style="font-size:' . esc_attr( intval( $displaykit_excerpt_font_size ) ) . 'px;">';
			echo esc_html( wp_trim_words( get_the_excerpt(), 20, '...' ) );
			echo '</p>';
		}

		if ( $displaykit_product_obj && $displaykit_show_add_to_cart ) {
			echo '<a href="' . esc_url( $displaykit_product_obj->add_to_cart_url() ) . '" class="tplf-add-to-cart"';
			if ( $displaykit_open_in_new_tab ) {
				echo ' target="_blank" rel="noopener noreferrer"';
			}
			echo ' style="background-color:' . esc_attr( $displaykit_accent_color ) . ';color:#fff;"';
			echo ' data-product-id="' . esc_attr( intval( $displaykit_current_id ) ) . '">';
			echo esc_html( $displaykit_add_to_cart_text ) . ' &#128722;</a>';
		}

		if ( ! $displaykit_product_obj && $displaykit_show_read_more ) {
			echo '<a href="' . esc_url( get_the_permalink() ) . '" class="tplf-read-more"';
			if ( $displaykit_open_in_new_tab ) {
				echo ' target="_blank" rel="noopener noreferrer"';
			}
			echo ' style="color:' . esc_attr( $displaykit_accent_color ) . ';">';
			echo esc_html( $displaykit_read_more_text ) . ' &rarr;</a>';
		}

		if ( $displaykit_product_obj && ! $displaykit_product_obj->is_in_stock() ) {
			echo '<span class="tplf-out-of-stock">' . esc_html__( 'Out of Stock', 'displaykit' ) . '</span>';
		}

		echo '</div>';
		echo '</div>';
	}
	wp_reset_postdata();
}

echo '</div>';

$displaykit_empty_display = $displaykit_query->have_posts() ? 'none' : 'block';
echo '<div class="tplf-empty" style="display:' . esc_attr( $displaykit_empty_display ) . ';">';
echo '<div class="tplf-empty-icon">&#128196;</div>';
if ( $displaykit_is_product ) {
	echo '<p>' . esc_html__( 'No products found.', 'displaykit' ) . '</p>';
} else {
	echo '<p>' . esc_html__( 'No posts found.', 'displaykit' ) . '</p>';
}
echo '</div>';

echo '</div>';]]></content>
  </file>
  <file path="package.json">
    <description>Package configuration.</description>
    <content><![CDATA[{
	"name": "displaykit",
	"version": "1.0.0",
	"description": "A flexible Gutenberg block for displaying pages, posts, and WooCommerce products with multiple layouts, rich customization, and dynamic querying.",
	"author": "Upluggit",
	"license": "GPL-2.0-or-later",
	"main": "build/index.js",
	"scripts": {
		"build": "wp-scripts build",
		"format": "wp-scripts format",
		"lint:css": "wp-scripts lint-style",
		"lint:js": "wp-scripts lint-js",
		"packages-update": "wp-scripts packages-update",
		"plugin-zip": "wp-scripts plugin-zip",
		"start": "wp-scripts start"
	},
	"devDependencies": {
		"@wordpress/scripts": "^30.15.0"
	}
}
]]></content>
  </file>
  <file path="displaykit.php">
    <content><![CDATA[<?php
/**
 * Plugin Name:       DisplayKit - Page, Post & Product Display Block
 * Plugin URI:        https://wordpress.org/plugins/displaykit/
 * Description:       A flexible block for displaying pages, posts, and WooCommerce products with multiple layouts, rich customization, and dynamic querying.
 * Version:           1.0.0
 * Requires at least: 6.2
 * Requires PHP:      7.4
 * Author:            Upluggit
 * Author URI:        https://profiles.wordpress.org/upluggit/
 * License:           GPLv2 or later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       displaykit
 *
 * @package DisplayKit
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! function_exists( 'displaykit_sanitize_color' ) ) {
	/**
	 * Sanitize a CSS color value (hex, rgb, rgba, hsl, hsla).
	 *
	 * @param string $color The color value to sanitize.
	 * @return string Sanitized color string or empty string if invalid.
	 */
	function displaykit_sanitize_color( $color ) {
		$color = trim( $color );

		// Standard hex: #fff, #ffffff, #ffffff80.
		if ( preg_match( '/^#([0-9a-fA-F]{3,8})$/', $color ) ) {
			return $color;
		}

		// rgb() or rgba().
		if ( preg_match( '/^rgba?\(\s*[\d\s,.\/%]+\)$/i', $color ) ) {
			return $color;
		}

		// hsl() or hsla().
		if ( preg_match( '/^hsla?\(\s*[\d\s,.\/%deg]+\)$/i', $color ) ) {
			return $color;
		}

		// Fallback: try sanitize_hex_color.
		$hex = sanitize_hex_color( $color );
		return $hex ? $hex : '';
	}
}

if ( ! function_exists( 'displaykit_block_init' ) ) {
	/**
	 * Registers the block using the metadata loaded from the `block.json` file.
	 *
	 * @return void
	 */
	function displaykit_block_init() {
		register_block_type( __DIR__ . '/build/' );
	}
	add_action( 'init', 'displaykit_block_init' );
}

if ( ! function_exists( 'displaykit_is_woocommerce_active' ) ) {
	/**
	 * Check if WooCommerce is active.
	 *
	 * @return bool
	 */
	function displaykit_is_woocommerce_active() {
		return class_exists( 'WooCommerce' );
	}
}

if ( ! function_exists( 'displaykit_register_rest_routes' ) ) {
	/**
	 * Registers REST API routes for AJAX filtering.
	 *
	 * @return void
	 */
	function displaykit_register_rest_routes() {
		register_rest_route(
			'displaykit/v1',
			'/posts',
			array(
				'methods'             => 'GET',
				'callback'            => 'displaykit_get_posts',
				'permission_callback' => '__return_true',
				'args'                => array(
					'post_type'      => array(
						'type'              => 'string',
						'default'           => 'post',
						'sanitize_callback' => 'sanitize_text_field',
					),
					'posts_per_page' => array(
						'type'              => 'integer',
						'default'           => 6,
						'sanitize_callback' => 'absint',
					),
					'orderby'        => array(
						'type'              => 'string',
						'default'           => 'date',
						'sanitize_callback' => 'sanitize_text_field',
					),
					'order'          => array(
						'type'              => 'string',
						'default'           => 'DESC',
						'sanitize_callback' => 'sanitize_text_field',
					),
					'taxonomy'       => array(
						'type'              => 'string',
						'default'           => '',
						'sanitize_callback' => 'sanitize_text_field',
					),
					'terms'          => array(
						'type'              => 'string',
						'default'           => '',
						'sanitize_callback' => 'sanitize_text_field',
					),
					'search'         => array(
						'type'              => 'string',
						'default'           => '',
						'sanitize_callback' => 'sanitize_text_field',
					),
					'page'           => array(
						'type'              => 'integer',
						'default'           => 1,
						'sanitize_callback' => 'absint',
					),
					'filter_taxonomy' => array(
						'type'              => 'string',
						'default'           => '',
						'sanitize_callback' => 'sanitize_text_field',
					),
					'filter_terms'   => array(
						'type'              => 'string',
						'default'           => '',
						'sanitize_callback' => 'sanitize_text_field',
					),
					'author'         => array(
						'type'              => 'string',
						'default'           => '',
						'sanitize_callback' => 'sanitize_text_field',
					),
					'include'        => array(
						'type'              => 'string',
						'default'           => '',
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
			)
		);

		register_rest_route(
			'displaykit/v1',
			'/taxonomies',
			array(
				'methods'             => 'GET',
				'callback'            => 'displaykit_get_taxonomies',
				'permission_callback' => '__return_true',
				'args'                => array(
					'post_type' => array(
						'type'              => 'string',
						'default'           => 'post',
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
			)
		);

		register_rest_route(
			'displaykit/v1',
			'/terms',
			array(
				'methods'             => 'GET',
				'callback'            => 'displaykit_get_terms',
				'permission_callback' => '__return_true',
				'args'                => array(
					'taxonomy' => array(
						'type'              => 'string',
						'default'           => 'category',
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
			)
		);

		register_rest_route(
			'displaykit/v1',
			'/woo-status',
			array(
				'methods'             => 'GET',
				'callback'            => 'displaykit_get_woo_status',
				'permission_callback' => '__return_true',
			)
		);
	}
	add_action( 'rest_api_init', 'displaykit_register_rest_routes' );
}

if ( ! function_exists( 'displaykit_get_woo_status' ) ) {
	/**
	 * REST callback to check WooCommerce status.
	 *
	 * @return WP_REST_Response
	 */
	function displaykit_get_woo_status() {
		return new WP_REST_Response(
			array(
				'active' => displaykit_is_woocommerce_active(),
			),
			200
		);
	}
}

if ( ! function_exists( 'displaykit_get_product_data' ) ) {
	/**
	 * Get WooCommerce product data for a post.
	 *
	 * @param int $post_id The post ID.
	 * @return array|null Product data or null if not a product.
	 */
	function displaykit_get_product_data( $post_id ) {
		if ( ! displaykit_is_woocommerce_active() || ! function_exists( 'wc_get_product' ) ) {
			return null;
		}

		if ( 'product' !== get_post_type( $post_id ) ) {
			return null;
		}

		$product = wc_get_product( $post_id );
		if ( ! $product ) {
			return null;
		}

		$rating_count   = $product->get_rating_count();
		$average_rating = $product->get_average_rating();

		return array(
			'price_html'      => $product->get_price_html(),
			'regular_price'   => $product->get_regular_price(),
			'sale_price'      => $product->get_sale_price(),
			'on_sale'         => $product->is_on_sale(),
			'in_stock'        => $product->is_in_stock(),
			'stock_status'    => $product->get_stock_status(),
			'average_rating'  => (float) $average_rating,
			'rating_count'    => (int) $rating_count,
			'add_to_cart_url' => $product->add_to_cart_url(),
			'product_type'    => $product->get_type(),
			'currency_symbol' => function_exists( 'get_woocommerce_currency_symbol' ) ? get_woocommerce_currency_symbol() : '$',
		);
	}
}

if ( ! function_exists( 'displaykit_get_posts' ) ) {
	/**
	 * REST callback to fetch filtered posts.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	function displaykit_get_posts( $request ) {

		$post_type      = $request->get_param( 'post_type' );
		$posts_per_page = $request->get_param( 'posts_per_page' );
		$orderby        = $request->get_param( 'orderby' );
		$order          = $request->get_param( 'order' );
		$taxonomy       = $request->get_param( 'taxonomy' );
		$terms          = $request->get_param( 'terms' );
		$search         = $request->get_param( 'search' );
		$paged          = $request->get_param( 'page' );
		$filter_tax     = $request->get_param( 'filter_taxonomy' );
		$filter_terms   = $request->get_param( 'filter_terms' );
		$author         = $request->get_param( 'author' );
		$include        = $request->get_param( 'include' );

		$allowed_post_types = get_post_types( array( 'public' => true ), 'names' );
		if ( ! in_array( $post_type, $allowed_post_types, true ) ) {
			$post_type = 'post';
		}

		$allowed_orderby = array( 'date', 'title', 'rand', 'modified', 'comment_count', 'menu_order', 'price', 'popularity', 'rating' );
		if ( ! in_array( $orderby, $allowed_orderby, true ) ) {
			$orderby = 'date';
		}

		$order = strtoupper( $order );
		if ( ! in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
			$order = 'DESC';
		}

		$query_args = array(
			'post_type'      => $post_type,
			'posts_per_page' => min( $posts_per_page, 50 ),
			'orderby'        => $orderby,
			'order'          => $order,
			'paged'          => max( 1, $paged ),
			'post_status'    => 'publish',
		);

		if ( ! empty( $include ) ) {
			$include_ids = array_map( 'absint', array_filter( explode( ',', $include ) ) );
			if ( ! empty( $include_ids ) ) {
				$query_args['post__in']       = $include_ids;
				$query_args['orderby']        = 'post__in';
				$query_args['posts_per_page'] = count( $include_ids );
			}
		}

		if ( 'product' === $post_type && displaykit_is_woocommerce_active() ) {
			if ( 'price' === $orderby ) {
				$query_args['orderby']  = 'meta_value_num';
				$query_args['meta_key'] = '_price'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
			} elseif ( 'popularity' === $orderby ) {
				$query_args['orderby']  = 'meta_value_num';
				$query_args['meta_key'] = 'total_sales'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
			} elseif ( 'rating' === $orderby ) {
				$query_args['orderby']  = 'meta_value_num';
				$query_args['meta_key'] = '_wc_average_rating'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
			}
		}

		if ( ! empty( $search ) ) {
			$query_args['s'] = $search;
		}

		if ( ! empty( $author ) ) {
			$query_args['author'] = absint( $author );
		}

		$tax_query = array();

		if ( ! empty( $taxonomy ) && ! empty( $terms ) ) {
			$term_ids = array_map( 'absint', array_filter( explode( ',', $terms ) ) );
			if ( ! empty( $term_ids ) ) {
				$tax_query[] = array(
					'taxonomy' => $taxonomy,
					'field'    => 'term_id',
					'terms'    => $term_ids,
				);
			}
		}

		if ( ! empty( $filter_tax ) && ! empty( $filter_terms ) ) {
			$filter_term_ids = array_map( 'absint', array_filter( explode( ',', $filter_terms ) ) );
			if ( ! empty( $filter_term_ids ) ) {
				$tax_query[] = array(
					'taxonomy' => $filter_tax,
					'field'    => 'term_id',
					'terms'    => $filter_term_ids,
				);
			}
		}

		if ( 'product' === $post_type && displaykit_is_woocommerce_active() ) {
			$tax_query[] = array(
				'taxonomy' => 'product_visibility',
				'field'    => 'name',
				'terms'    => array( 'exclude-from-catalog' ),
				'operator' => 'NOT IN',
			);
		}

		if ( ! empty( $tax_query ) ) {
			if ( count( $tax_query ) > 1 ) {
				$tax_query['relation'] = 'AND';
			}
			$query_args['tax_query'] = $tax_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
		}

		$query      = new WP_Query( $query_args );
		$posts_data = array();

		if ( $query->have_posts() ) {
			while ( $query->have_posts() ) {
				$query->the_post();
				$post_id = get_the_ID();

				$thumbnail = '';
				if ( has_post_thumbnail( $post_id ) ) {
					$thumbnail = get_the_post_thumbnail_url( $post_id, 'medium_large' );
				}

				$post_item = array(
					'id'        => $post_id,
					'title'     => get_the_title(),
					'excerpt'   => wp_trim_words( get_the_excerpt(), 20, '&hellip;' ),
					'permalink' => get_the_permalink(),
					'thumbnail' => $thumbnail ? $thumbnail : '',
					'date'      => get_the_date(),
					'author'    => get_the_author(),
					'comments'  => get_comments_number(),
				);

				$product_data = displaykit_get_product_data( $post_id );
				if ( $product_data ) {
					$post_item['product'] = $product_data;
				}

				$posts_data[] = $post_item;
			}
			wp_reset_postdata();
		}

		return new WP_REST_Response(
			array(
				'posts'        => $posts_data,
				'total'        => (int) $query->found_posts,
				'total_pages'  => (int) $query->max_num_pages,
				'current_page' => (int) $paged,
			),
			200
		);
	}
}

if ( ! function_exists( 'displaykit_get_taxonomies' ) ) {
	/**
	 * REST callback to fetch taxonomies for a post type.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	function displaykit_get_taxonomies( $request ) {
		$post_type  = $request->get_param( 'post_type' );
		$taxonomies = get_object_taxonomies( $post_type, 'objects' );
		$result     = array();

		foreach ( $taxonomies as $tax ) {
			if ( $tax->public ) {
				$result[] = array(
					'name'  => $tax->name,
					'label' => $tax->label,
				);
			}
		}

		return new WP_REST_Response( $result, 200 );
	}
}

if ( ! function_exists( 'displaykit_get_terms' ) ) {
	/**
	 * REST callback to fetch terms for a taxonomy.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	function displaykit_get_terms( $request ) {
		$taxonomy = $request->get_param( 'taxonomy' );

		if ( ! taxonomy_exists( $taxonomy ) ) {
			return new WP_REST_Response( array(), 200 );
		}

		$terms  = get_terms(
			array(
				'taxonomy'   => $taxonomy,
				'hide_empty' => true,
			)
		);
		$result = array();

		if ( ! is_wp_error( $terms ) ) {
			foreach ( $terms as $term ) {
				$result[] = array(
					'id'    => $term->term_id,
					'name'  => $term->name,
					'slug'  => $term->slug,
					'count' => $term->count,
				);
			}
		}

		return new WP_REST_Response( $result, 200 );
	}
}]]></content>
  </file>
</artefact>