diff --git a/assets/before-after/frontblocks-before-after-frontend.js b/assets/before-after/frontblocks-before-after-frontend.js
new file mode 100644
index 0000000..d160e7b
--- /dev/null
+++ b/assets/before-after/frontblocks-before-after-frontend.js
@@ -0,0 +1,85 @@
+( function () {
+ 'use strict';
+
+ function initBeforeAfterBlock( block ) {
+ var initialPosition = parseFloat( block.dataset.initialPosition ) || 50;
+ var beforeEl = block.querySelector( '.frbl-before-after__before' );
+ var handle = block.querySelector( '.frbl-before-after__handle' );
+
+ if ( ! beforeEl || ! handle ) {
+ return;
+ }
+
+ var isDragging = false;
+
+ function clamp( value, min, max ) {
+ return Math.max( min, Math.min( max, value ) );
+ }
+
+ function setPosition( pos ) {
+ pos = clamp( pos, 0, 100 );
+ beforeEl.style.clipPath = 'inset(0 ' + ( 100 - pos ) + '% 0 0)';
+ handle.style.left = pos + '%';
+ handle.setAttribute( 'aria-valuenow', Math.round( pos ) );
+ }
+
+ function getPositionFromEvent( e ) {
+ var rect = block.getBoundingClientRect();
+ var clientX = e.touches && e.touches.length ? e.touches[ 0 ].clientX : e.clientX;
+ return ( ( clientX - rect.left ) / rect.width ) * 100;
+ }
+
+ setPosition( initialPosition );
+
+ handle.addEventListener( 'mousedown', function ( e ) {
+ isDragging = true;
+ e.preventDefault();
+ } );
+
+ handle.addEventListener( 'touchstart', function () {
+ isDragging = true;
+ }, { passive: true } );
+
+ document.addEventListener( 'mousemove', function ( e ) {
+ if ( ! isDragging ) { return; }
+ setPosition( getPositionFromEvent( e ) );
+ } );
+
+ document.addEventListener( 'touchmove', function ( e ) {
+ if ( ! isDragging ) { return; }
+ setPosition( getPositionFromEvent( e ) );
+ }, { passive: true } );
+
+ document.addEventListener( 'mouseup', function () {
+ isDragging = false;
+ } );
+
+ document.addEventListener( 'touchend', function () {
+ isDragging = false;
+ } );
+
+ handle.setAttribute( 'tabindex', '0' );
+
+ handle.addEventListener( 'keydown', function ( e ) {
+ var current = parseFloat( handle.style.left ) || initialPosition;
+ if ( e.key === 'ArrowLeft' ) {
+ setPosition( current - 1 );
+ e.preventDefault();
+ } else if ( e.key === 'ArrowRight' ) {
+ setPosition( current + 1 );
+ e.preventDefault();
+ }
+ } );
+ }
+
+ function init() {
+ var blocks = document.querySelectorAll( '.frbl-before-after:not(.frbl-before-after--editor)' );
+ blocks.forEach( initBeforeAfterBlock );
+ }
+
+ if ( document.readyState === 'loading' ) {
+ document.addEventListener( 'DOMContentLoaded', init );
+ } else {
+ init();
+ }
+} )();
diff --git a/assets/before-after/frontblocks-before-after-option.jsx b/assets/before-after/frontblocks-before-after-option.jsx
new file mode 100644
index 0000000..dc07ef3
--- /dev/null
+++ b/assets/before-after/frontblocks-before-after-option.jsx
@@ -0,0 +1,275 @@
+const { registerBlockType } = wp.blocks;
+const { Fragment } = wp.element;
+const { InspectorControls, MediaUpload, MediaUploadCheck, useBlockProps } = wp.blockEditor;
+const { PanelBody, RangeControl, Button, Placeholder, TextControl, ToggleControl } = wp.components;
+const { __ } = wp.i18n;
+
+/**
+ * Edit component for Before After block.
+ */
+function BeforeAfterEdit( props ) {
+ const { attributes, setAttributes } = props;
+ const {
+ beforeImageId,
+ beforeImageUrl,
+ afterImageId,
+ afterImageUrl,
+ beforeLabel,
+ afterLabel,
+ initialPosition,
+ blockHeight,
+ fixedHeight,
+ } = attributes;
+
+ const blockProps = useBlockProps();
+
+ const hasImages = beforeImageUrl && afterImageUrl;
+
+ return (
+
+
+
+
+
+ setAttributes( {
+ beforeImageId: media.id,
+ beforeImageUrl: media.url,
+ } )
+ }
+ allowedTypes={ [ 'image' ] }
+ value={ beforeImageId }
+ render={ ( { open } ) => (
+
+ { beforeImageUrl && (
+

+ ) }
+
+ { beforeImageUrl && (
+
+ ) }
+
+ ) }
+ />
+
+ setAttributes( { beforeLabel: value } ) }
+ style={ { marginTop: '12px' } }
+ />
+
+
+
+
+
+ setAttributes( {
+ afterImageId: media.id,
+ afterImageUrl: media.url,
+ } )
+ }
+ allowedTypes={ [ 'image' ] }
+ value={ afterImageId }
+ render={ ( { open } ) => (
+
+ { afterImageUrl && (
+

+ ) }
+
+ { afterImageUrl && (
+
+ ) }
+
+ ) }
+ />
+
+ setAttributes( { afterLabel: value } ) }
+ style={ { marginTop: '12px' } }
+ />
+
+
+
+ setAttributes( { initialPosition: value } ) }
+ min={ 0 }
+ max={ 100 }
+ />
+ setAttributes( { fixedHeight: value } ) }
+ />
+ { fixedHeight && (
+ setAttributes( { blockHeight: value } ) }
+ min={ 100 }
+ max={ 1000 }
+ step={ 10 }
+ />
+ ) }
+
+
+
+
+ { ! hasImages ? (
+
+ ) : (
+
+
+

+ { afterLabel && (
+
+ { afterLabel }
+
+ ) }
+
+
+

+ { beforeLabel && (
+
+ { beforeLabel }
+
+ ) }
+
+
+
+ ) }
+
+
+ );
+}
+
+// Register the block.
+registerBlockType( 'frontblocks/before-after', {
+ title: __( 'Before After', 'frontblocks' ),
+ description: __( 'Compare two images with a draggable before/after slider.', 'frontblocks' ),
+ category: 'media',
+ icon: 'image-flip-horizontal',
+ keywords: [
+ __( 'before', 'frontblocks' ),
+ __( 'after', 'frontblocks' ),
+ __( 'comparison', 'frontblocks' ),
+ __( 'slider', 'frontblocks' ),
+ ],
+ attributes: {
+ beforeImageId: {
+ type: 'integer',
+ },
+ beforeImageUrl: {
+ type: 'string',
+ default: '',
+ },
+ afterImageId: {
+ type: 'integer',
+ },
+ afterImageUrl: {
+ type: 'string',
+ default: '',
+ },
+ beforeLabel: {
+ type: 'string',
+ default: 'Before',
+ },
+ afterLabel: {
+ type: 'string',
+ default: 'After',
+ },
+ initialPosition: {
+ type: 'number',
+ default: 50,
+ },
+ fixedHeight: {
+ type: 'boolean',
+ default: false,
+ },
+ blockHeight: {
+ type: 'number',
+ default: 400,
+ },
+ },
+ edit: BeforeAfterEdit,
+ save: function () {
+ return null; // Dynamic block, rendered server-side.
+ },
+} );
diff --git a/assets/before-after/frontblocks-before-after.css b/assets/before-after/frontblocks-before-after.css
new file mode 100644
index 0000000..e4c0e4d
--- /dev/null
+++ b/assets/before-after/frontblocks-before-after.css
@@ -0,0 +1,150 @@
+/* =============================================================================
+ Before After Block — FrontBlocks
+ ============================================================================= */
+
+.frbl-before-after {
+ position: relative;
+ overflow: hidden;
+ cursor: col-resize;
+ -webkit-user-select: none;
+ user-select: none;
+ touch-action: pan-y;
+ display: block;
+ width: 100%;
+}
+
+/* After image — defines the block height */
+.frbl-before-after__after {
+ position: relative;
+ width: 100%;
+ display: block;
+}
+
+.frbl-before-after__after img {
+ display: block;
+ width: 100%;
+ height: auto;
+ pointer-events: none;
+}
+
+/* Fixed height mode: images scale to fit without cropping */
+.frbl-before-after--fixed-height .frbl-before-after__after {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.frbl-before-after--fixed-height .frbl-before-after__after img,
+.frbl-before-after--fixed-height .frbl-before-after__before img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ object-position: center;
+}
+
+/* Before image — absolutely positioned on top, clipped from the right */
+.frbl-before-after__before {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ clip-path: inset( 0 50% 0 0 );
+}
+
+.frbl-before-after__before img {
+ display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ pointer-events: none;
+}
+
+/* Labels */
+.frbl-before-after__label:empty {
+ display: none;
+}
+
+.frbl-before-after__label {
+ position: absolute;
+ top: 16px;
+ padding: 4px 12px;
+ background: rgba( 0, 0, 0, 0.55 );
+ color: #fff;
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 1.4;
+ border-radius: 3px;
+ letter-spacing: 0.02em;
+ z-index: 5;
+ pointer-events: none;
+}
+
+.frbl-before-after__label--before {
+ left: 16px;
+}
+
+.frbl-before-after__label--after {
+ right: 16px;
+}
+
+/* Handle */
+.frbl-before-after__handle {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 50%;
+ transform: translateX( -50% );
+ width: 44px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ z-index: 10;
+ cursor: col-resize;
+}
+
+.frbl-before-after__handle:focus {
+ outline: none;
+}
+
+.frbl-before-after__handle:focus-visible .frbl-before-after__handle-thumb {
+ outline: 2px solid #007cba;
+ outline-offset: 2px;
+}
+
+.frbl-before-after__handle-line {
+ flex: 1;
+ width: 2px;
+ background: #fff;
+ box-shadow: 0 0 4px rgba( 0, 0, 0, 0.4 );
+}
+
+.frbl-before-after__handle-thumb {
+ flex-shrink: 0;
+ width: 44px;
+ height: 44px;
+ border-radius: 50%;
+ background: #fff;
+ box-shadow: 0 2px 10px rgba( 0, 0, 0, 0.35 );
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #555;
+}
+
+.frbl-before-after__handle-thumb svg {
+ display: block;
+}
+
+/* Editor — disable interaction */
+.frbl-before-after--editor {
+ cursor: default;
+}
+
+.frbl-before-after--editor .frbl-before-after__handle {
+ cursor: default;
+ pointer-events: none;
+}
diff --git a/assets/before-after/frontblocks-before-after.js b/assets/before-after/frontblocks-before-after.js
new file mode 100644
index 0000000..e37db4f
--- /dev/null
+++ b/assets/before-after/frontblocks-before-after.js
@@ -0,0 +1,277 @@
+"use strict";
+
+var registerBlockType = wp.blocks.registerBlockType;
+var Fragment = wp.element.Fragment;
+var _wp$blockEditor = wp.blockEditor,
+ InspectorControls = _wp$blockEditor.InspectorControls,
+ MediaUpload = _wp$blockEditor.MediaUpload,
+ MediaUploadCheck = _wp$blockEditor.MediaUploadCheck,
+ useBlockProps = _wp$blockEditor.useBlockProps;
+var _wp$components = wp.components,
+ PanelBody = _wp$components.PanelBody,
+ RangeControl = _wp$components.RangeControl,
+ Button = _wp$components.Button,
+ Placeholder = _wp$components.Placeholder,
+ TextControl = _wp$components.TextControl,
+ ToggleControl = _wp$components.ToggleControl;
+var __ = wp.i18n.__;
+
+/**
+ * Edit component for Before After block.
+ */
+function BeforeAfterEdit(props) {
+ var attributes = props.attributes,
+ setAttributes = props.setAttributes;
+ var beforeImageId = attributes.beforeImageId,
+ beforeImageUrl = attributes.beforeImageUrl,
+ afterImageId = attributes.afterImageId,
+ afterImageUrl = attributes.afterImageUrl,
+ beforeLabel = attributes.beforeLabel,
+ afterLabel = attributes.afterLabel,
+ initialPosition = attributes.initialPosition,
+ blockHeight = attributes.blockHeight,
+ fixedHeight = attributes.fixedHeight;
+ var blockProps = useBlockProps();
+ var hasImages = beforeImageUrl && afterImageUrl;
+ return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(InspectorControls, null, /*#__PURE__*/React.createElement(PanelBody, {
+ title: __('Before Image', 'frontblocks'),
+ initialOpen: true
+ }, /*#__PURE__*/React.createElement(MediaUploadCheck, null, /*#__PURE__*/React.createElement(MediaUpload, {
+ onSelect: function onSelect(media) {
+ return setAttributes({
+ beforeImageId: media.id,
+ beforeImageUrl: media.url
+ });
+ },
+ allowedTypes: ['image'],
+ value: beforeImageId,
+ render: function render(_ref) {
+ var open = _ref.open;
+ return /*#__PURE__*/React.createElement("div", null, beforeImageUrl && /*#__PURE__*/React.createElement("img", {
+ src: beforeImageUrl,
+ alt: "",
+ style: {
+ width: '100%',
+ marginBottom: '8px',
+ borderRadius: '2px'
+ }
+ }), /*#__PURE__*/React.createElement(Button, {
+ onClick: open,
+ variant: beforeImageUrl ? 'secondary' : 'primary',
+ style: {
+ width: '100%'
+ }
+ }, beforeImageUrl ? __('Replace Before Image', 'frontblocks') : __('Select Before Image', 'frontblocks')), beforeImageUrl && /*#__PURE__*/React.createElement(Button, {
+ onClick: function onClick() {
+ return setAttributes({
+ beforeImageId: undefined,
+ beforeImageUrl: ''
+ });
+ },
+ variant: "link",
+ isDestructive: true,
+ style: {
+ marginTop: '4px',
+ display: 'block'
+ }
+ }, __('Remove', 'frontblocks')));
+ }
+ })), /*#__PURE__*/React.createElement(TextControl, {
+ label: __('Before Label', 'frontblocks'),
+ value: beforeLabel,
+ onChange: function onChange(value) {
+ return setAttributes({
+ beforeLabel: value
+ });
+ },
+ style: {
+ marginTop: '12px'
+ }
+ })), /*#__PURE__*/React.createElement(PanelBody, {
+ title: __('After Image', 'frontblocks'),
+ initialOpen: true
+ }, /*#__PURE__*/React.createElement(MediaUploadCheck, null, /*#__PURE__*/React.createElement(MediaUpload, {
+ onSelect: function onSelect(media) {
+ return setAttributes({
+ afterImageId: media.id,
+ afterImageUrl: media.url
+ });
+ },
+ allowedTypes: ['image'],
+ value: afterImageId,
+ render: function render(_ref2) {
+ var open = _ref2.open;
+ return /*#__PURE__*/React.createElement("div", null, afterImageUrl && /*#__PURE__*/React.createElement("img", {
+ src: afterImageUrl,
+ alt: "",
+ style: {
+ width: '100%',
+ marginBottom: '8px',
+ borderRadius: '2px'
+ }
+ }), /*#__PURE__*/React.createElement(Button, {
+ onClick: open,
+ variant: afterImageUrl ? 'secondary' : 'primary',
+ style: {
+ width: '100%'
+ }
+ }, afterImageUrl ? __('Replace After Image', 'frontblocks') : __('Select After Image', 'frontblocks')), afterImageUrl && /*#__PURE__*/React.createElement(Button, {
+ onClick: function onClick() {
+ return setAttributes({
+ afterImageId: undefined,
+ afterImageUrl: ''
+ });
+ },
+ variant: "link",
+ isDestructive: true,
+ style: {
+ marginTop: '4px',
+ display: 'block'
+ }
+ }, __('Remove', 'frontblocks')));
+ }
+ })), /*#__PURE__*/React.createElement(TextControl, {
+ label: __('After Label', 'frontblocks'),
+ value: afterLabel,
+ onChange: function onChange(value) {
+ return setAttributes({
+ afterLabel: value
+ });
+ },
+ style: {
+ marginTop: '12px'
+ }
+ })), /*#__PURE__*/React.createElement(PanelBody, {
+ title: __('Slider Settings', 'frontblocks'),
+ initialOpen: false
+ }, /*#__PURE__*/React.createElement(RangeControl, {
+ label: __('Initial Handle Position (%)', 'frontblocks'),
+ value: initialPosition,
+ onChange: function onChange(value) {
+ return setAttributes({
+ initialPosition: value
+ });
+ },
+ min: 0,
+ max: 100
+ }), /*#__PURE__*/React.createElement(ToggleControl, {
+ label: __('Fixed Height', 'frontblocks'),
+ checked: fixedHeight,
+ onChange: function onChange(value) {
+ return setAttributes({
+ fixedHeight: value
+ });
+ }
+ }), fixedHeight && /*#__PURE__*/React.createElement(RangeControl, {
+ label: __('Height (px)', 'frontblocks'),
+ value: blockHeight,
+ onChange: function onChange(value) {
+ return setAttributes({
+ blockHeight: value
+ });
+ },
+ min: 100,
+ max: 1000,
+ step: 10
+ }))), /*#__PURE__*/React.createElement("div", blockProps, !hasImages ? /*#__PURE__*/React.createElement(Placeholder, {
+ icon: "image-flip-horizontal",
+ label: __('Before / After', 'frontblocks'),
+ instructions: __('Select a "before" image and an "after" image from the sidebar.', 'frontblocks')
+ }) : /*#__PURE__*/React.createElement("div", {
+ className: 'frbl-before-after frbl-before-after--editor' + (fixedHeight ? ' frbl-before-after--fixed-height' : ''),
+ "data-initial-position": initialPosition,
+ style: fixedHeight && blockHeight ? {
+ height: blockHeight + 'px'
+ } : {}
+ }, /*#__PURE__*/React.createElement("div", {
+ className: "frbl-before-after__after"
+ }, /*#__PURE__*/React.createElement("img", {
+ src: afterImageUrl,
+ alt: ""
+ }), afterLabel && /*#__PURE__*/React.createElement("span", {
+ className: "frbl-before-after__label frbl-before-after__label--after"
+ }, afterLabel)), /*#__PURE__*/React.createElement("div", {
+ className: "frbl-before-after__before",
+ style: {
+ clipPath: "inset(0 ".concat(100 - initialPosition, "% 0 0)")
+ }
+ }, /*#__PURE__*/React.createElement("img", {
+ src: beforeImageUrl,
+ alt: ""
+ }), beforeLabel && /*#__PURE__*/React.createElement("span", {
+ className: "frbl-before-after__label frbl-before-after__label--before"
+ }, beforeLabel)), /*#__PURE__*/React.createElement("div", {
+ className: "frbl-before-after__handle",
+ style: {
+ left: "".concat(initialPosition, "%")
+ }
+ }, /*#__PURE__*/React.createElement("span", {
+ className: "frbl-before-after__handle-line"
+ }), /*#__PURE__*/React.createElement("span", {
+ className: "frbl-before-after__handle-thumb"
+ }, /*#__PURE__*/React.createElement("svg", {
+ viewBox: "0 0 20 20",
+ width: "18",
+ height: "18",
+ fill: "currentColor",
+ "aria-hidden": "true"
+ }, /*#__PURE__*/React.createElement("path", {
+ d: "M7 4l-4 6 4 6M13 4l4 6-4 6",
+ stroke: "currentColor",
+ strokeWidth: "2",
+ fill: "none",
+ strokeLinecap: "round",
+ strokeLinejoin: "round"
+ }))), /*#__PURE__*/React.createElement("span", {
+ className: "frbl-before-after__handle-line"
+ })))));
+}
+
+// Register the block.
+registerBlockType('frontblocks/before-after', {
+ title: __('Before After', 'frontblocks'),
+ description: __('Compare two images with a draggable before/after slider.', 'frontblocks'),
+ category: 'media',
+ icon: 'image-flip-horizontal',
+ keywords: [__('before', 'frontblocks'), __('after', 'frontblocks'), __('comparison', 'frontblocks'), __('slider', 'frontblocks')],
+ attributes: {
+ beforeImageId: {
+ type: 'integer'
+ },
+ beforeImageUrl: {
+ type: 'string',
+ default: ''
+ },
+ afterImageId: {
+ type: 'integer'
+ },
+ afterImageUrl: {
+ type: 'string',
+ default: ''
+ },
+ beforeLabel: {
+ type: 'string',
+ default: 'Before'
+ },
+ afterLabel: {
+ type: 'string',
+ default: 'After'
+ },
+ initialPosition: {
+ type: 'number',
+ default: 50
+ },
+ fixedHeight: {
+ type: 'boolean',
+ default: false
+ },
+ blockHeight: {
+ type: 'number',
+ default: 400
+ }
+ },
+ edit: BeforeAfterEdit,
+ save: function save() {
+ return null; // Dynamic block, rendered server-side.
+ }
+});
diff --git a/includes/Frontend/BeforeAfter.php b/includes/Frontend/BeforeAfter.php
new file mode 100644
index 0000000..6a7212b
--- /dev/null
+++ b/includes/Frontend/BeforeAfter.php
@@ -0,0 +1,213 @@
+ 'frontblocks-before-after-option',
+ 'render_callback' => array( $this, 'render_before_after_block' ),
+ 'attributes' => array(
+ 'beforeImageId' => array(
+ 'type' => 'integer',
+ ),
+ 'beforeImageUrl' => array(
+ 'type' => 'string',
+ 'default' => '',
+ ),
+ 'afterImageId' => array(
+ 'type' => 'integer',
+ ),
+ 'afterImageUrl' => array(
+ 'type' => 'string',
+ 'default' => '',
+ ),
+ 'beforeLabel' => array(
+ 'type' => 'string',
+ 'default' => 'Before',
+ ),
+ 'afterLabel' => array(
+ 'type' => 'string',
+ 'default' => 'After',
+ ),
+ 'initialPosition' => array(
+ 'type' => 'number',
+ 'default' => 50,
+ ),
+ 'fixedHeight' => array(
+ 'type' => 'boolean',
+ 'default' => false,
+ ),
+ 'blockHeight' => array(
+ 'type' => 'number',
+ 'default' => 400,
+ ),
+ ),
+ );
+
+ if ( ! WP_Block_Type_Registry::get_instance()->is_registered( 'frontblocks/before-after' ) ) {
+ register_block_type( 'frontblocks/before-after', $args );
+ }
+ }
+
+ /**
+ * Render the Before After block on the frontend.
+ *
+ * @param array $attributes Block attributes.
+ * @return string HTML output.
+ */
+ public function render_before_after_block( $attributes ) {
+ $before_url = isset( $attributes['beforeImageUrl'] ) ? $attributes['beforeImageUrl'] : '';
+ $after_url = isset( $attributes['afterImageUrl'] ) ? $attributes['afterImageUrl'] : '';
+
+ if ( empty( $before_url ) || empty( $after_url ) ) {
+ return '';
+ }
+
+ $initial_position = isset( $attributes['initialPosition'] ) ? (int) $attributes['initialPosition'] : 50;
+ $before_label = isset( $attributes['beforeLabel'] ) ? $attributes['beforeLabel'] : __( 'Before', 'frontblocks' );
+ $after_label = isset( $attributes['afterLabel'] ) ? $attributes['afterLabel'] : __( 'After', 'frontblocks' );
+
+ $fixed_height = ! empty( $attributes['fixedHeight'] );
+ $block_height = isset( $attributes['blockHeight'] ) ? (int) $attributes['blockHeight'] : 400;
+
+ $wrapper_class = 'frbl-before-after';
+ if ( $fixed_height ) {
+ $wrapper_class .= ' frbl-before-after--fixed-height';
+ }
+ if ( ! empty( $attributes['className'] ) ) {
+ $wrapper_class .= ' ' . esc_attr( $attributes['className'] );
+ }
+
+ $wrapper_style = $fixed_height && $block_height ? ' style="height:' . esc_attr( $block_height ) . 'px"' : '';
+
+ ob_start();
+ ?>
+
+ >
+
+
; ?>)
+
+
+
+
+
+
+
+
; ?>)
+
+
+
+
+
+
+
+
+