Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/twentig/dist/blocks/button/view.asset.php
+++ b/twentig/dist/blocks/button/view.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array('@wordpress/interactivity'), 'version' => '1d08482c3dd40db963a0', 'type' => 'module');
--- a/twentig/dist/blocks/gallery/view.asset.php
+++ b/twentig/dist/blocks/gallery/view.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array('@wordpress/interactivity'), 'version' => '67af92bc0d582ebd1706', 'type' => 'module');
--- a/twentig/dist/blocks/template-part/view.asset.php
+++ b/twentig/dist/blocks/template-part/view.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array('@wordpress/interactivity'), 'version' => '9afd96133fc6584379e1', 'type' => 'module');
--- a/twentig/dist/blocks/video/view.asset.php
+++ b/twentig/dist/blocks/video/view.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array('@wordpress/interactivity'), 'version' => 'dfd5e30c08217a3852fc', 'type' => 'module');
--- a/twentig/dist/index.asset.php
+++ b/twentig/dist/index.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives', 'wp-token-list', 'wp-url'), 'version' => 'd5575cd466f335729583');
+<?php return array('dependencies' => array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives', 'wp-token-list', 'wp-url'), 'version' => 'ce811dae13b4fcbf7a9a');
--- a/twentig/inc/block-patterns.php
+++ b/twentig/inc/block-patterns.php
@@ -11,7 +11,8 @@
* Registers the block pattern categories.
*/
function twentig_register_block_pattern_categories() {
- register_block_pattern_category( 'posts', array( 'label' => _x( 'Posts', 'Block pattern category', 'default' ) ) );
+ // Categories using 'default' text domain intentionally reuse WordPress core translations.
+ register_block_pattern_category( 'posts', array( 'label' => esc_html_x( 'Posts', 'Block pattern category', 'default' ) ) );
register_block_pattern_category( 'text', array( 'label' => esc_html_x( 'Text', 'Block pattern category', 'default' ) ) );
register_block_pattern_category( 'text-image', array( 'label' => esc_html_x( 'Text and Image', 'Block pattern category', 'twentig' ) ) );
register_block_pattern_category( 'hero', array( 'label' => esc_html_x( 'Hero', 'Block pattern category', 'twentig' ) ) );
@@ -29,8 +30,8 @@
register_block_pattern_category( 'pricing', array( 'label' => esc_html_x( 'Pricing', 'Block pattern category', 'twentig' ) ) );
register_block_pattern_category( 'faq', array( 'label' => esc_html_x( 'FAQs', 'Block pattern category', 'twentig' ) ) );
register_block_pattern_category( 'events', array( 'label' => esc_html_x( 'Events, Schedule', 'Block pattern category', 'twentig' ) ) );
- register_block_pattern_category( 'page', array( 'label' => _x( 'Pages', 'Block pattern category', 'default' ) ) );
- register_block_pattern_category( 'page-single', array( 'label' => _x( 'Single Pages', 'Block pattern category', 'twentig' ) ) );
+ register_block_pattern_category( 'page', array( 'label' => esc_html_x( 'Pages', 'Block pattern category', 'default' ) ) );
+ register_block_pattern_category( 'page-single', array( 'label' => esc_html_x( 'Single Pages', 'Block pattern category', 'twentig' ) ) );
}
add_action( 'init', 'twentig_register_block_pattern_categories', 9 );
@@ -220,9 +221,9 @@
foreach ( $font_sizes as $old_size => $new_size ) {
$content = str_replace( ""fontSize":"$old_size"", ""fontSize":"$new_size"", $content );
- $formatted_old_size = preg_replace('/([a-zA-Z])(d)/', '$1-$2', $old_size);
- $formatted_new_size = preg_replace('/([a-zA-Z])(d)/', '$1-$2', $new_size);
- $content = str_replace("has-$formatted_old_size-font-size", "has-$formatted_new_size-font-size", $content);
+ $formatted_old_size = preg_replace( '/([a-zA-Z])(d)/', '$1-$2', $old_size );
+ $formatted_new_size = preg_replace( '/([a-zA-Z])(d)/', '$1-$2', $new_size );
+ $content = str_replace( "has-$formatted_old_size-font-size", "has-$formatted_new_size-font-size", $content );
}
break;
}
--- a/twentig/inc/block-presets.php
+++ b/twentig/inc/block-presets.php
@@ -16,13 +16,14 @@
$classes = array(
- 'core/paragraph' => array(
+ 'core/paragraph' => array(
'tw-text-balance' => __( 'Apply balanced text wrapping.', 'twentig' ),
'tw-text-pretty' => __( 'Apply pretty text wrapping.', 'twentig' ),
- 'tw-text-shadow' => __( 'Add shadow to text.', 'twentig' ),
- 'tw-text-gradient' => __( 'Apply background gradient to text.', 'twentig' ),
'tw-link-hover-underline' => __( 'Underline link only on hover.', 'twentig' ),
'tw-link-no-underline' => __( 'Remove underline from link.', 'twentig' ),
+ 'tw-text-shadow' => __( 'Add shadow to text.', 'twentig' ),
+ 'tw-text-gradient' => __( 'Apply background gradient to text.', 'twentig' ),
+ 'tw-whitespace-nowrap' => __( 'Prevent text from wrapping.', 'twentig' ),
'tw-highlight-padding' => __( 'Add padding to the highlighted text’s background.', 'twentig' ),
'tw-md-text-left' => __( 'Align text left on tablet and mobile.', 'twentig' ),
'tw-md-text-center' => __( 'Align text center on tablet and mobile.', 'twentig' ),
@@ -34,10 +35,13 @@
'core/heading' => array(
'tw-text-balance' => __( 'Apply balanced text wrapping.', 'twentig' ),
'tw-text-pretty' => __( 'Apply pretty text wrapping.', 'twentig' ),
- 'tw-text-shadow' => __( 'Add shadow to text.', 'twentig' ),
- 'tw-text-gradient' => __( 'Apply background gradient to text.', 'twentig' ),
+ 'tw-text-wrap' => __( 'Reset text wrapping to default.', 'twentig' ),
'tw-link-hover-underline' => __( 'Underline link only on hover.', 'twentig' ),
'tw-link-no-underline' => __( 'Remove underline from link.', 'twentig' ),
+ 'tw-link-hover-fade' => __( 'Fade link on hover.', 'twentig' ),
+ 'tw-text-shadow' => __( 'Add shadow to text.', 'twentig' ),
+ 'tw-text-gradient' => __( 'Apply background gradient to text.', 'twentig' ),
+ 'screen-reader-text' => __( 'Visually hide the block, but make it available for screen readers.', 'twentig' ),
'tw-highlight-padding' => __( 'Add padding to the highlighted text’s background.', 'twentig' ),
'tw-md-text-left' => __( 'Align text left on tablet and mobile.', 'twentig' ),
'tw-md-text-center' => __( 'Align text center on tablet and mobile.', 'twentig' ),
@@ -49,8 +53,10 @@
'core/post-title' => array(
'tw-text-balance' => __( 'Apply balanced text wrapping.', 'twentig' ),
'tw-text-pretty' => __( 'Apply pretty text wrapping.', 'twentig' ),
+ 'tw-text-wrap' => __( 'Reset text wrapping to default.', 'twentig' ),
'tw-link-hover-underline' => __( 'Underline link only on hover.', 'twentig' ),
'tw-link-no-underline' => __( 'Remove underline from link.', 'twentig' ),
+ 'tw-link-hover-fade' => __( 'Fade link on hover', 'twentig' ),
'tw-text-shadow' => __( 'Add shadow to text.', 'twentig' ),
'tw-text-gradient' => __( 'Apply background gradient to text.', 'twentig' ),
'tw-md-text-left' => __( 'Align text left on tablet and mobile.', 'twentig' ),
@@ -60,6 +66,12 @@
'tw-sm-text-center' => __( 'Align text center on mobile.', 'twentig' ),
'tw-sm-text-right' => __( 'Align text right on mobile.', 'twentig' ),
),
+ 'core/pullquote' => array(
+ 'tw-text-balance' => __( 'Apply balance text wrapping.', 'twentig' ),
+ ),
+ 'core/quote' => array(
+ 'tw-text-balance' => __( 'Apply balance text wrapping.', 'twentig' ),
+ ),
'core/list' => array(
'has-text-align-center' => __( 'Align text center.', 'twentig' ),
'tw-list-spacing-medium' => __( 'Set a medium spacing between the list items.', 'twentig' ),
@@ -129,6 +141,10 @@
'tw-sm-justify-center' => __( 'Justify items center on mobile.', 'twentig' ),
'tw-sm-justify-end' => __( 'Justify items from the end on mobile.', 'twentig' ),
),
+ 'core/button' => array(
+ 'tw-lightbox-dark' => __( 'Apply a dark theme to the lightbox.', 'twentig' ),
+ 'tw-lightbox-full' => __( 'Display the video in a full-screen lightbox.', 'twentig' ),
+ ),
'core/social-links' => array(
'tw-md-justify-start' => __( 'Justify items from the start on tablet.', 'twentig' ),
'tw-md-justify-center' => __( 'Justify items center on tablet.', 'twentig' ),
@@ -173,10 +189,14 @@
'core/categories' => array(
'tw-link-hover-underline' => __( 'Underline link only on hover.', 'twentig' ),
'tw-no-bullet' => __( 'Remove bullet from list.', 'twentig' ),
+ 'tw-list-spacing-medium' => __( 'Set a medium spacing between the list items.', 'twentig' ),
+ 'tw-list-spacing-loose' => __( 'Set a loose spacing between the list items.', 'twentig' ),
),
'core/archives' => array(
'tw-link-hover-underline' => __( 'Underline link only on hover.', 'twentig' ),
'tw-no-bullet' => __( 'Remove bullet from list.', 'twentig' ),
+ 'tw-list-spacing-medium' => __( 'Set a medium spacing between the list items.', 'twentig' ),
+ 'tw-list-spacing-loose' => __( 'Set a loose spacing between the list items.', 'twentig' ),
),
'core/site-title' => array(
'tw-link-hover-underline' => __( 'Underline link only on hover.', 'twentig' ),
@@ -205,6 +225,20 @@
'core/post-comments-form' => array(
'tw-form-rounded' => __( 'Make the corners of the input and textarea rounded.', 'twentig' ),
),
+ 'core/post-template' => array(
+ 'tw-alt-columns' => __( 'Alternate column sides on every other post.', 'twentig' ),
+ 'tw-alt-spacer' => __( 'Alternate spacer visibility on every other post to create an offset layout.', 'twentig' ),
+ 'tw-alt-grid' => __( 'Alternate between one and two grid columns.', 'twentig' ),
+ ),
+ 'core/navigation-submenu' => array(
+ 'tw-submenu-rounded' => __( 'Make the corners rounded.', 'twentig' ),
+ 'tw-submenu-shadow' => __( 'Add shadow.', 'twentig' ),
+ 'tw-submenu-noborder' => __( 'Remove border.', 'twentig' ),
+ 'tw-submenu-align-right' => __( 'Align submenu to the right of its parent.', 'twentig' ),
+ ),
+ 'core/accordion-heading' => array(
+ 'tw-no-underline' => __( 'Remove underline on hover.', 'twentig' ),
+ ),
);
return apply_filters( 'twentig_block_classes', $classes );
--- a/twentig/inc/block-styles.php
+++ b/twentig/inc/block-styles.php
@@ -35,6 +35,15 @@
);
/* List */
+
+ register_block_style(
+ 'core/list',
+ array(
+ 'name' => 'tw-square',
+ 'label' => esc_html__( 'Square', 'twentig' ),
+ )
+ );
+
register_block_style(
'core/list',
array(
@@ -158,7 +167,7 @@
'core/separator',
array(
'name' => 'tw-asterisks',
- 'label' => __( 'Asterisks', 'twentig' ),
+ 'label' => esc_html__( 'Asterisks', 'twentig' ),
)
);
@@ -166,7 +175,7 @@
'core/separator',
array(
'name' => 'tw-dotted',
- 'label' => __( 'Dotted', 'twentig' ),
+ 'label' => esc_html__( 'Dotted', 'twentig' ),
)
);
@@ -174,7 +183,7 @@
'core/separator',
array(
'name' => 'tw-dashed',
- 'label' => __( 'Dashed', 'twentig' ),
+ 'label' => esc_html__( 'Dashed', 'twentig' ),
)
);
@@ -184,7 +193,7 @@
'core/tag-cloud',
array(
'name' => 'tw-outline-rounded',
- 'label' => __( 'Rounded', 'twentig' ),
+ 'label' => esc_html__( 'Rounded', 'twentig' ),
)
);
@@ -192,7 +201,7 @@
'core/tag-cloud',
array(
'name' => 'tw-outline-pill',
- 'label' => __( 'Pill', 'twentig' ),
+ 'label' => esc_html__( 'Pill', 'twentig' ),
)
);
@@ -200,7 +209,7 @@
'core/tag-cloud',
array(
'name' => 'tw-plain',
- 'label' => __( 'Plain', 'twentig' ),
+ 'label' => esc_html__( 'Plain', 'twentig' ),
)
);
@@ -208,7 +217,7 @@
'core/tag-cloud',
array(
'name' => 'tw-list',
- 'label' => __( 'List', 'twentig' ),
+ 'label' => esc_html__( 'List', 'twentig' ),
)
);
@@ -218,7 +227,7 @@
'core/post-terms',
array(
'name' => 'tw-outline',
- 'label' => __( 'Outline', 'twentig' ),
+ 'label' => esc_html__( 'Outline', 'twentig' ),
)
);
@@ -226,7 +235,7 @@
'core/post-terms',
array(
'name' => 'tw-outline-rounded',
- 'label' => __( 'Rounded', 'twentig' ),
+ 'label' => esc_html__( 'Rounded', 'twentig' ),
)
);
@@ -234,7 +243,7 @@
'core/post-terms',
array(
'name' => 'tw-outline-pill',
- 'label' => __( 'Pill', 'twentig' ),
+ 'label' => esc_html__( 'Pill', 'twentig' ),
)
);
@@ -242,7 +251,7 @@
'core/post-terms',
array(
'name' => 'tw-hashtag',
- 'label' => __( 'Hashtag', 'twentig' ),
+ 'label' => esc_html__( 'Hashtag', 'twentig' ),
)
);
@@ -250,7 +259,7 @@
'core/post-terms',
array(
'name' => 'tw-plain',
- 'label' => __( 'Plain', 'twentig' ),
+ 'label' => esc_html__( 'Plain', 'twentig' ),
)
);
@@ -258,7 +267,7 @@
'core/post-terms',
array(
'name' => 'tw-list',
- 'label' => __( 'List', 'twentig' ),
+ 'label' => esc_html__( 'List', 'twentig' ),
)
);
@@ -267,7 +276,7 @@
'core/search',
array(
'name' => 'tw-underline',
- 'label' => __( 'Underline', 'twentig' ),
+ 'label' => esc_html__( 'Underline', 'twentig' ),
)
);
@@ -276,7 +285,7 @@
'core/post-navigation-link',
array(
'name' => 'tw-nav-stack',
- 'label' => __( 'Stack', 'twentig' ),
+ 'label' => esc_html__( 'Stack', 'twentig' ),
)
);
@@ -287,7 +296,7 @@
'core/query-pagination-numbers',
array(
'name' => 'tw-square',
- 'label' => __( 'Square', 'twentig' ),
+ 'label' => esc_html__( 'Square', 'twentig' ),
)
);
@@ -295,7 +304,7 @@
'core/query-pagination-numbers',
array(
'name' => 'tw-rounded',
- 'label' => __( 'Rounded', 'twentig' ),
+ 'label' => esc_html__( 'Rounded', 'twentig' ),
)
);
@@ -303,7 +312,7 @@
'core/query-pagination-numbers',
array(
'name' => 'tw-circle',
- 'label' => __( 'Circle', 'twentig' ),
+ 'label' => esc_html__( 'Circle', 'twentig' ),
)
);
@@ -311,7 +320,7 @@
'core/query-pagination-numbers',
array(
'name' => 'tw-plain',
- 'label' => __( 'Plain', 'twentig' ),
+ 'label' => esc_html__( 'Plain', 'twentig' ),
)
);
@@ -319,7 +328,7 @@
'core/query-pagination-previous',
array(
'name' => 'tw-btn-square',
- 'label' => __( 'Square', 'twentig' ),
+ 'label' => esc_html__( 'Square', 'twentig' ),
)
);
@@ -327,7 +336,7 @@
'core/query-pagination-previous',
array(
'name' => 'tw-btn-rounded',
- 'label' => __( 'Rounded', 'twentig' ),
+ 'label' => esc_html__( 'Rounded', 'twentig' ),
)
);
@@ -335,7 +344,7 @@
'core/query-pagination-previous',
array(
'name' => 'tw-btn-pill',
- 'label' => __( 'Pill', 'twentig' ),
+ 'label' => esc_html__( 'Pill', 'twentig' ),
)
);
@@ -343,7 +352,7 @@
'core/query-pagination-next',
array(
'name' => 'tw-btn-square',
- 'label' => __( 'Square', 'twentig' ),
+ 'label' => esc_html__( 'Square', 'twentig' ),
)
);
@@ -351,7 +360,7 @@
'core/query-pagination-next',
array(
'name' => 'tw-btn-rounded',
- 'label' => __( 'Rounded', 'twentig' ),
+ 'label' => esc_html__( 'Rounded', 'twentig' ),
)
);
@@ -359,7 +368,15 @@
'core/query-pagination-next',
array(
'name' => 'tw-btn-pill',
- 'label' => __( 'Pill', 'twentig' ),
+ 'label' => esc_html__( 'Pill', 'twentig' ),
+ )
+ );
+
+ register_block_style(
+ 'core/navigation-link',
+ array(
+ 'name' => 'tw-external-link',
+ 'label' => esc_html__( 'External', 'twentig' ),
)
);
--- a/twentig/inc/block-themes.php
+++ b/twentig/inc/block-themes.php
@@ -1,57 +0,0 @@
-<?php
-/**
- * Additional functionalities for block themes.
- *
- * @package twentig
- */
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * Enqueue styles for block themes: spacing, layout.
- */
-function twentig_block_theme_enqueue_scripts() {
- if ( twentig_theme_supports_spacing() ) {
- wp_enqueue_style(
- 'twentig-global-spacing',
- TWENTIG_ASSETS_URI . "/blocks/tw-spacing.css",
- array(),
- TWENTIG_VERSION
- );
- }
-}
-add_action( 'wp_enqueue_scripts', 'twentig_block_theme_enqueue_scripts', 11 );
-
-/**
- * Enqueue styles inside the editor.
- */
-function twentig_block_theme_editor_styles() {
-
- $fse_blocks = array(
- 'columns',
- 'latest-posts',
- );
-
- foreach ( $fse_blocks as $block_name ) {
- add_editor_style( TWENTIG_ASSETS_URI . "/blocks/{$block_name}/block.css" );
- }
-
- if ( twentig_theme_supports_spacing() ) {
- add_editor_style( TWENTIG_ASSETS_URI . "/blocks/tw-spacing.css" );
- add_editor_style( TWENTIG_ASSETS_URI . "/blocks/tw-spacing-editor.css" );
- }
-}
-add_action( 'admin_init', 'twentig_block_theme_editor_styles' );
-
-/**
- * Adds support for Twentig features.
- */
-function twentig_block_theme_support() {
-
- if ( current_theme_supports( 'twentig-v2' ) ) {
- return;
- }
-
- add_theme_support( 'tw-spacing' );
-}
-add_action( 'after_setup_theme', 'twentig_block_theme_support', 11 );
--- a/twentig/inc/blocks.php
+++ b/twentig/inc/blocks.php
@@ -7,7 +7,7 @@
defined( 'ABSPATH' ) || exit;
-foreach ( (array) glob( wp_normalize_path( TWENTIG_PATH . '/inc/blocks/*.php' ) ) as $twentig_block_file ) {
+foreach ( (array) glob( wp_normalize_path( TWENTIG_PATH . 'inc/blocks/*.php' ) ) as $twentig_block_file ) {
require_once $twentig_block_file;
}
@@ -22,18 +22,27 @@
wp_enqueue_style(
'twentig-blocks',
- plugins_url( 'dist/' . $block_library_filename . '.css', dirname( __FILE__ ) ),
+ TWENTIG_ASSETS_URI . '/' . $block_library_filename . '.css',
array(),
$asset_file['version']
);
+ if ( ! current_theme_supports( 'twentig-theme' ) ) {
+ wp_enqueue_style(
+ 'twentig-blocks-compat',
+ TWENTIG_ASSETS_URI . '/blocks/compat.css',
+ array(),
+ $asset_file['version']
+ );
+ }
+
if ( ! is_admin() ) {
return;
}
wp_enqueue_script(
'twentig-blocks-editor',
- plugins_url( '/dist/index.js', dirname( __FILE__ ) ),
+ TWENTIG_ASSETS_URI . '/index.js',
$asset_file['dependencies'],
$asset_file['version'],
array( 'in_footer' => false )
@@ -46,26 +55,65 @@
'spacingSizes' => function_exists( 'twentig_get_spacing_sizes' ) ? twentig_get_spacing_sizes() : array(),
'cssClasses' => twentig_get_block_css_classes(),
'portfolioType' => post_type_exists( 'portfolio' ) ? 'portfolio' : '',
+ 'buttonIcons' => twentig_get_icons(),
);
- wp_localize_script( 'twentig-blocks-editor', 'twentigEditorConfig', $config );
+ wp_add_inline_script(
+ 'twentig-blocks-editor',
+ 'var twentigEditorConfig = ' . wp_json_encode( $config ) . ';',
+ 'before'
+ );
wp_set_script_translations( 'twentig-blocks-editor', 'twentig' );
wp_enqueue_style(
'twentig-editor',
- plugins_url( 'dist/index.css', dirname( __FILE__ ) ),
+ TWENTIG_ASSETS_URI . '/index.css',
array( 'wp-edit-blocks' ),
$asset_file['version']
);
+
+ $font_url = TWENTIG_ASSETS_URI . '/css/symbols.woff2';
+ $css = "@font-face{font-family:'Material Symbols';font-style:normal;font-weight:400;src:url('{$font_url}') format('woff2');}";
+ wp_add_inline_style( 'wp-block-library', $css );
}
add_action( 'enqueue_block_assets', 'twentig_block_assets' );
/**
+ * Adds visibility classes to the global styles.
+ */
+function twentig_enqueue_class_styles() {
+ $breakpoints = apply_filters( 'twentig_breakpoints', array( 'mobile' => 768, 'tablet' => 1024 ) );
+ $mobile = (int) ( $breakpoints['mobile'] ?? 768 );
+ $tablet = (int) ( $breakpoints['tablet'] ?? 1024 );
+
+ $css = sprintf(
+ '@media (width < %1$dpx) { .tw-sm-hidden { display: none !important; }}' .
+ '@media (%1$dpx <= width < %2$dpx) { .tw-md-hidden { display: none !important; }}' .
+ '@media (width >= %2$dpx) { .tw-lg-hidden { display: none !important; }}',
+ $mobile,
+ $tablet
+ );
+
+ wp_add_inline_style( 'twentig-blocks', $css );
+}
+
+/**
* Override block styles.
*/
function twentig_override_block_styles() {
+ twentig_enqueue_class_styles();
+
+ if ( twentig_theme_supports_spacing() ) {
+ wp_enqueue_style(
+ 'twentig-global-spacing',
+ TWENTIG_ASSETS_URI . "/blocks/tw-spacing.css",
+ array(),
+ TWENTIG_VERSION
+ );
+ }
+
if ( ! wp_should_load_separate_core_block_assets() || ! wp_is_block_theme() ) {
return;
}
@@ -73,7 +121,6 @@
// Override core blocks style.
$overridden_blocks = array(
'columns',
- 'gallery',
'media-text',
'post-template',
'latest-posts',
@@ -121,79 +168,35 @@
add_action( 'init', 'twentig_enqueue_block_styles' );
/**
- * Adds visibility classes to the global styles.
+ * Enqueue styles inside the editor.
*/
-function twentig_enqueue_class_styles() {
- $breakpoints = apply_filters( 'twentig_breakpoints', array( 'mobile' => 768, 'tablet' => 1024 ) );
- $mobile = (int) ( $breakpoints['mobile'] ?? 768 );
- $tablet = (int) ( $breakpoints['tablet'] ?? 1024 );
+function twentig_block_theme_editor_styles() {
- $css = sprintf(
- '@media (width < %1$dpx) { .tw-sm-hidden { display: none !important; }}' .
- '@media (%1$dpx <= width < %2$dpx) { .tw-md-hidden { display: none !important; }}' .
- '@media (width >= %2$dpx) { .tw-lg-hidden { display: none !important; }}',
- $mobile,
- $tablet
+ $blocks = array(
+ 'columns',
+ 'latest-posts',
);
- wp_add_inline_style( 'twentig-blocks', $css );
+ foreach ( $blocks as $block_name ) {
+ add_editor_style( TWENTIG_ASSETS_URI . "/blocks/{$block_name}/block.css" );
+ }
+
+ if ( twentig_theme_supports_spacing() ) {
+ add_editor_style( TWENTIG_ASSETS_URI . "/blocks/tw-spacing.css" );
+ add_editor_style( TWENTIG_ASSETS_URI . "/blocks/tw-spacing-editor.css" );
+ }
}
-add_action( 'wp_enqueue_scripts', 'twentig_enqueue_class_styles' );
+add_action( 'admin_init', 'twentig_block_theme_editor_styles' );
/**
- * Filters the blocks to add animation.
- *
- * @param string $block_content The block content about to be appended.
- * @param array $block The full block, including name and attributes.
- * @return string Modified block content with animation classes and attributes.
+ * Adds support for Twentig features.
*/
-function twentig_add_block_animation( $block_content, $block ) {
+function twentig_block_theme_support() {
- if ( ! empty( $block['attrs']['twAnimation'] ) ) {
-
- wp_enqueue_script(
- 'tw-block-animation',
- plugins_url( '/dist/js/block-animation.js', dirname( __FILE__ ) ),
- array(),
- '1.0',
- array(
- 'in_footer' => false,
- 'strategy' => 'defer',
- )
- );
-
- $attributes = $block['attrs'];
- $animation = $attributes['twAnimation'];
- $duration = $attributes['twAnimationDuration'] ?? '';
- $delay = $attributes['twAnimationDelay'] ?? 0;
-
- $tag_processor = new WP_HTML_Tag_Processor( $block_content );
- $tag_processor->next_tag();
- $tag_processor->add_class( 'tw-block-animation' );
- $tag_processor->add_class( sanitize_html_class( 'tw-animation-' . $animation ) );
-
- if ( $duration ) {
- $tag_processor->add_class( sanitize_html_class( 'tw-duration-' . $duration ) );
- }
-
- if ( $delay ) {
- $style_attr = $tag_processor->get_attribute( 'style' );
- $style = '--tw-animation-delay:' . esc_attr( $delay ) . 's;' . $style_attr;
- $tag_processor->set_attribute( 'style', esc_attr( $style ) );
- }
-
- return $tag_processor->get_updated_html();
+ if ( current_theme_supports( 'twentig-theme' ) || current_theme_supports( 'twentig-v2' ) ) {
+ return;
}
- return $block_content;
-}
-add_filter( 'render_block', 'twentig_add_block_animation', 10, 2 );
-
-/**
- * Handles no JavaScript detection.
- * Adds a style tag element when no JavaScript is detected.
- */
-function twentig_support_no_script() {
- echo "<noscript><style>.tw-block-animation{opacity:1;transform:none;clip-path:none;}</style></noscript>n";
+ add_theme_support( 'tw-spacing' );
}
-add_action( 'wp_head', 'twentig_support_no_script' );
+add_action( 'after_setup_theme', 'twentig_block_theme_support', 11 );
--- a/twentig/inc/blocks/accordion.php
+++ b/twentig/inc/blocks/accordion.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ * Server-side customizations for the `core/accordion` block.
+ *
+ * @package twentig
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Filters the accordion block output.
+ *
+ * @param string $block_content Rendered block content.
+ * @param array $block Block object.
+ * @return string Filtered block content.
+ */
+function twentig_filter_accordion_block( $block_content, $block ) {
+ $icon_type = $block['attrs']['twIcon'] ?? '';
+ $show_icon = $block['attrs']['showIcon'] ?? true;
+
+ if ( empty( $icon_type ) || ! $show_icon ) {
+ return $block_content;
+ }
+
+ $tag_processor = new WP_HTML_Tag_Processor( $block_content );
+ if ( $tag_processor->next_tag( array( 'class_name' => 'wp-block-accordion' ) ) ) {
+ $tag_processor->add_class( 'tw-has-icon' );
+ $tag_processor->add_class( 'tw-icon-' . sanitize_html_class( $icon_type ) );
+ $block_content = $tag_processor->get_updated_html();
+
+ $icon = '<svg class="accordion-arrow" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" version="1.1" aria-hidden="true" focusable="false"><path d="m12 15.4-6-6L7.4 8l4.6 4.6L16.6 8 18 9.4z"></path></svg>';
+
+ if ( 'plus' === $icon_type ) {
+ $icon = '<svg class="accordion-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" version="1.1" aria-hidden="true" focusable="false"><path class="plus-vertical" d="M11 6h2v12h-2z"/><path d="M6 11h12v2H6z"/></svg>';
+ } elseif ( 'plus-circle' === $icon_type ) {
+ $icon = '<svg class="accordion-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" version="1.1" aria-hidden="true" focusable="false"><path d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2m0 1.75a8.25 8.25 0 1 0 0 16.5 8.25 8.25 0 0 0 0-16.5" /><path d="M11.125 7.5h1.75v9h-1.75z" class="plus-vertical" /><path d="M7.5 11.125h9v1.75h-9z" /></svg>';
+ }
+
+ $block_content = preg_replace(
+ '/(<span class="wp-block-accordion-heading__toggle-icon"[^>]*>)+(</span>)/',
+ '$1' . $icon . '$2',
+ $block_content
+ );
+ }
+
+ return $block_content;
+}
+add_filter( 'render_block_core/accordion', 'twentig_filter_accordion_block', 10, 2 );
--- a/twentig/inc/blocks/animation.php
+++ b/twentig/inc/blocks/animation.php
@@ -0,0 +1,69 @@
+<?php
+/**
+ * Server-side customizations for the core blocks.
+ *
+ * @package twentig
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Filters the blocks to add animation.
+ *
+ * @param string $block_content The block content about to be appended.
+ * @param array $block The full block, including name and attributes.
+ * @return string Modified block content with animation classes and attributes.
+ */
+function twentig_add_block_animation( $block_content, $block ) {
+
+ if ( empty( $block['attrs']['twAnimation'] ) ) {
+ return $block_content;
+ }
+
+ static $script_enqueued = false;
+ if ( ! $script_enqueued ) {
+ wp_enqueue_script(
+ 'tw-block-animation',
+ TWENTIG_ASSETS_URI . '/js/block-animation.js',
+ array(),
+ TWENTIG_VERSION,
+ array(
+ 'in_footer' => false,
+ 'strategy' => 'defer',
+ )
+ );
+ $script_enqueued = true;
+ }
+
+ $attributes = $block['attrs'];
+ $animation = $attributes['twAnimation'];
+ $duration = $attributes['twAnimationDuration'] ?? '';
+ $delay = $attributes['twAnimationDelay'] ?? 0;
+
+ $tag_processor = new WP_HTML_Tag_Processor( $block_content );
+ $tag_processor->next_tag();
+ $tag_processor->add_class( 'tw-block-animation' );
+ $tag_processor->add_class( sanitize_html_class( 'tw-animation-' . $animation ) );
+
+ if ( $duration ) {
+ $tag_processor->add_class( sanitize_html_class( 'tw-duration-' . $duration ) );
+ }
+
+ if ( $delay ) {
+ $style_attr = $tag_processor->get_attribute( 'style' );
+ $style = '--tw-animation-delay:' . esc_attr( $delay ) . 's;' . $style_attr;
+ $tag_processor->set_attribute( 'style', $style );
+ }
+
+ return $tag_processor->get_updated_html();
+}
+add_filter( 'render_block', 'twentig_add_block_animation', 10, 2 );
+
+/**
+ * Handles no JavaScript detection.
+ * Adds a style tag element when no JavaScript is detected.
+ */
+function twentig_support_no_script() {
+ echo "<noscript><style>.tw-block-animation{opacity:1;transform:none;clip-path:none;}</style></noscript>n";
+}
+add_action( 'wp_head', 'twentig_support_no_script' );
--- a/twentig/inc/blocks/button.php
+++ b/twentig/inc/blocks/button.php
@@ -0,0 +1,284 @@
+<?php
+/**
+ * Server-side customizations for the `core/button` block.
+ *
+ * @package twentig
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Filters the button block output.
+ *
+ * @param string $block_content Rendered block content.
+ * @param array $block Block object.
+ * @return string Filtered block content.
+ */
+function twentig_filter_button_block( $block_content, $block ) {
+
+ $attributes = $block['attrs'] ?? array();
+ $icon = $attributes['twIcon'] ?? '';
+ $show_label = $attributes['twShowLabel'] ?? true;
+ $open_dialog = $attributes['twOpenDialog'] ?? false;
+
+ if ( empty( $icon ) && $show_label && ! $open_dialog ) {
+ return $block_content;
+ }
+
+ if ( $icon ) {
+ $button_icon = twentig_get_icon( $icon );
+ if ( $button_icon ) {
+ $position = $attributes['twIconPosition'] ?? 'right';
+ $tag_name = $attributes['tagName'] ?? 'a';
+ if ( ! in_array( $tag_name, array( 'a', 'button' ), true ) ) {
+ $tag_name = 'a';
+ }
+
+ if ( 'left' === $position ) {
+ $replacement = $show_label
+ ? '$1' . $button_icon . '$2$3'
+ : '$1' . $button_icon . '<span class="tw-button-text screen-reader-text">$2</span>$3';
+ } else {
+ $replacement = $show_label
+ ? '$1$2' . $button_icon . '$3'
+ : '$1<span class="tw-button-text screen-reader-text">$2</span>' . $button_icon . '$3';
+ }
+
+ $pattern = sprintf( '/(<%1$s[^>]*>)(.*?)(</%1$s>)/is', preg_quote( $tag_name, '/' ) );
+ $result = preg_replace( $pattern, $replacement, $block_content, 1 );
+
+ if ( null !== $result ) {
+ $block_content = $result;
+ }
+
+ $tag_processor = new WP_HTML_Tag_Processor( $block_content );
+ if ( $tag_processor->next_tag() ) {
+ $tag_processor->add_class( 'tw-has-icon' );
+ $tag_processor->add_class( 'has-icon__' . sanitize_html_class( $icon ) );
+ $block_content = $tag_processor->get_updated_html();
+ }
+ }
+ }
+
+ if ( $open_dialog ) {
+ $tag_processor = new WP_HTML_Tag_Processor( $block_content );
+
+ if ( ! $tag_processor->next_tag( 'a' ) ) {
+ return $block_content;
+ }
+
+ $url = $tag_processor->get_attribute( 'href' );
+
+ if ( empty( $url ) ) {
+ return $block_content;
+ }
+
+ $iframe_src = '';
+ $video_src = '';
+ $class_names = $attributes['className'] ?? '';
+ $lightbox_class_names = 'tw-lightbox-video';
+
+ if ( preg_match( '/(?:youtube.com/(?:(?:v|e(?:mbed)?|shorts|live)/|S*?[?&]v=)|youtu.be/)([a-zA-Z0-9_-]{11})/', $url, $match ) ) {
+ $iframe_src = 'https://www.youtube-nocookie.com/embed/' . esc_attr( $match[1] ) . '?autoplay=1&rel=0';
+ if ( str_contains( $url, 'controls=0' ) ) {
+ $iframe_src .= '&controls=0';
+ }
+ if ( str_contains( $url, '/shorts' ) ) {
+ $lightbox_class_names .= ' tw-lightbox-9-16';
+ }
+ } elseif ( preg_match( '/(?:player.)?vimeo.com/(?:channels/(?:w+/)?|groups/[^/]+/videos/|album/d+/video/|video/|)(d+)/i', $url, $match ) ) {
+ $iframe_src = 'https://player.vimeo.com/video/' . esc_attr( $match[1] ) . '?autoplay=1';
+ } elseif ( preg_match( '/.(?:mp4|webm|ogv)(?:[?#]|$)/i', $url ) ) {
+ $video_src = $url;
+ }
+
+ if ( empty( $iframe_src ) && empty( $video_src ) ) {
+ return $block_content;
+ }
+
+ if ( str_contains( $class_names, 'tw-lightbox-dark' ) ) {
+ $lightbox_class_names .= ' tw-lightbox-dark';
+ }
+
+ if ( str_contains( $class_names, 'tw-lightbox-full' ) ) {
+ $lightbox_class_names .= ' tw-lightbox-full';
+ }
+
+ $tag_processor->remove_attribute( 'href' );
+ $tag_processor->set_attribute( 'data-wp-interactive', 'twentig/modal' );
+
+ $tag_processor->set_attribute(
+ 'data-wp-context',
+ wp_json_encode(
+ array(
+ 'videoSrc' => esc_url( $video_src ),
+ 'iframeSrc' => esc_url_raw( $iframe_src ),
+ 'lightboxClassNames' => $lightbox_class_names,
+ ),
+ JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
+ )
+ );
+ $tag_processor->set_attribute( 'data-wp-on--click', 'actions.openVideoModal' );
+ $block_content = $tag_processor->get_updated_html();
+
+ $block_content = str_replace( array( '<a ', '</a>' ), array( '<button ', '</button>' ), $block_content );
+
+ add_action( 'wp_footer', 'twentig_video_print_lightbox_overlay' );
+
+ wp_enqueue_script_module(
+ 'tw-block-button',
+ TWENTIG_ASSETS_URI . '/blocks/button/view.js',
+ array( '@wordpress/interactivity' ),
+ TWENTIG_VERSION
+ );
+ }
+
+ return $block_content;
+}
+add_filter( 'render_block_core/button', 'twentig_filter_button_block', 10, 2 );
+
+/**
+ * Renders the video lightbox markup.
+ */
+function twentig_video_print_lightbox_overlay() {
+ $close_button_label = __( 'Close', 'twentig' );
+ ?>
+ <dialog
+ id="tw-modal-video"
+ class="tw-lightbox-video"
+ data-wp-interactive="twentig/modal"
+ data-wp-context='{}'
+ data-wp-on--close="actions.onCloseVideoModal"
+ data-wp-on--click="actions.closeVideoModal"
+ data-wp-bind--class="state.lightboxClassNames"
+ >
+ <button type="button" aria-label="<?php echo esc_attr( $close_button_label ); ?>" data-wp-on--click="actions.closeVideoModal" class="tw-lightbox-close-button">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false"><path d="M13 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z"></path></svg>
+ </button>
+ <div class="tw-lightbox-video-container">
+ <iframe
+ width="1000"
+ allow="autoplay"
+ allowfullscreen
+ data-wp-bind--src="state.iframe"
+ data-wp-bind--hidden="!state.isIframePlaying"
+ hidden
+ ></iframe>
+ <video
+ autoplay
+ controls
+ playsinline
+ data-wp-bind--src="state.video"
+ data-wp-bind--hidden="!state.isVideoPlaying"
+ hidden
+ ></video>
+ </div>
+ </dialog>
+ <?php
+}
+
+/**
+ * Gets button icons.
+ *
+ * @return array Array of icon data with label and SVG markup.
+ */
+function twentig_get_icons() {
+ $icons = array(
+ 'arrow-left' => array(
+ 'label' => __( 'Arrow Left', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="m7.825 13 5.6 5.6L12 20l-8-8 8-8 1.425 1.4-5.6 5.6H20v2z"></path></svg>',
+ ),
+ 'arrow-right' => array(
+ 'label' => __( 'Arrow Right', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="M16.175 13H4v-2h12.175l-5.6-5.6L12 4l8 8-8 8-1.425-1.4z"></path></svg>',
+ ),
+ 'arrow-up' => array(
+ 'label' => __( 'Arrow Up', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="M11 20V7.825l-5.6 5.6L4 12l8-8 8 8-1.4 1.425-5.6-5.6V20z"></path></svg>',
+ ),
+ 'arrow-down' => array(
+ 'label' => __( 'Arrow Down', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="M11 4v12.175l-5.6-5.6L4 12l8 8 8-8-1.4-1.425-5.6 5.6V4z"></path></svg>',
+ ),
+ 'arrow-outward' => array(
+ 'label' => __( 'Arrow Outward', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="M6.4 18 5 16.6 14.6 7H6V5h12v12h-2V8.4z"></path></svg>',
+ ),
+ 'external' => array(
+ 'label' => __( 'External', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="M5 21q-.824 0-1.412-.587A1.93 1.93 0 0 1 3 19V5q0-.824.587-1.412A1.93 1.93 0 0 1 5 3h7v2H5v14h14v-7h2v7q0 .824-.587 1.413A1.93 1.93 0 0 1 19 21zm4.7-5.3-1.4-1.4L17.6 5H14V3h7v7h-2V6.4z"></path></svg>',
+ ),
+ 'arrow-alt-left' => array(
+ 'label' => __( 'Arrow Left Alt', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="m10 18-6-6 6-6 1.4 1.45L7.85 11H20v2H7.85l3.55 3.55z"></path></svg>',
+ ),
+ 'arrow-alt-right' => array(
+ 'label' => __( 'Arrow Right Alt', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="m14 18-1.4-1.45L16.15 13H4v-2h12.15L12.6 7.45 14 6l6 6z"></path></svg>',
+ ),
+ 'arrow-alt-up' => array(
+ 'label' => __( 'Arrow Up Alt', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="M11 20V7.8l-3.6 3.6L6 10l6-6 6 6-1.4 1.4L13 7.8V20z"></path></svg>',
+ ),
+ 'arrow-alt-down' => array(
+ 'label' => __( 'Arrow Down Alt', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="m12 20-6-6 1.4-1.4 3.6 3.6V4h2v12.2l3.6-3.6L18 14z"></path></svg>',
+ ),
+ 'arrow-alt-outward' => array(
+ 'label' => __( 'Arrow Outward Alt', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="M16 14.45h2V6H9.55v2h5.05L6 16.6 7.4 18 16 9.4z"></path></svg>',
+ ),
+ 'download' => array(
+ 'label' => __( 'Download', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="m12 16-5-5 1.4-1.45 2.6 2.6V4h2v8.15l2.6-2.6L17 11zm-6 4q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 18v-3h2v3h12v-3h2v3q0 .824-.587 1.413A1.93 1.93 0 0 1 18 20z"></path></svg>',
+ ),
+ 'chevron-left' => array(
+ 'label' => __( 'Chevron Left', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="m13.6 18-6-6 6-6L15 7.4 10.4 12l4.6 4.6z"></path></svg>',
+ ),
+ 'chevron-right' => array(
+ 'label' => __( 'Chevron Right', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="M13.6 12 9 7.4 10.4 6l6 6-6 6L9 16.6z"></path></svg>',
+ ),
+ 'chevron-up' => array(
+ 'label' => __( 'Chevron Up', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="M12 10.4 7.4 15 6 13.6l6-6 6 6-1.4 1.4z"></path></svg>',
+ ),
+ 'chevron-down' => array(
+ 'label' => __( 'Chevron Down', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="m12 16.4-6-6L7.4 9l4.6 4.6L16.6 9l1.4 1.4z"></path></svg>',
+ ),
+ 'play' => array(
+ 'label' => __( 'Play', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="M8 19V5l11 7z"></path></svg>',
+ ),
+ 'play-circle' => array(
+ 'label' => __( 'Play Circle', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="m9.5 16.5 7-4.5-7-4.5zM12 22a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"></path></svg>',
+ ),
+ 'mail' => array(
+ 'label' => __( 'Mail', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="M20 4q.825 0 1.412.588Q22 5.176 22 6v12q0 .825-.588 1.412A1.93 1.93 0 0 1 20 20H4q-.824 0-1.412-.588A1.93 1.93 0 0 1 2 18V6q0-.824.588-1.412A1.93 1.93 0 0 1 4 4zm-8 11L4 9v9h16V9zM4 7l8 6 8-6V6H4z"></path></svg>',
+ ),
+ 'phone' => array(
+ 'label' => __( 'Phone', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="M19.95 21q.45 0 .75-.3t.3-.75V15.9a.88.88 0 0 0-.225-.588 1.16 1.16 0 0 0-.575-.362l-3.45-.7a1.6 1.6 0 0 0-.712.063 1.4 1.4 0 0 0-.588.337L13.1 17a16 16 0 0 1-1.8-1.213 18 18 0 0 1-1.625-1.437 18 18 0 0 1-1.513-1.662A12 12 0 0 1 6.975 10.9L9.4 8.45q.2-.2.275-.475T9.7 7.3l-.65-3.5a.9.9 0 0 0-.325-.562A.93.93 0 0 0 8.1 3H4.05q-.45 0-.75.3t-.3.75q0 3.125 1.362 6.175t3.863 5.55 5.55 3.862T19.95 21"></path></svg>',
+ ),
+ 'heart' => array(
+ 'label' => __( 'Heart', 'twentig' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="m12 19.25-1.042-.937a104 104 0 0 1-3.437-3.178q-1.355-1.322-2.136-2.354-.78-1.031-1.083-1.885A5.2 5.2 0 0 1 4 9.146Q4 7.292 5.27 6.02q1.273-1.27 3.127-1.27 1.02 0 1.979.438.958.435 1.625 1.229a4.6 4.6 0 0 1 1.625-1.23 4.7 4.7 0 0 1 1.98-.437q1.853 0 3.124 1.27Q20 7.293 20 9.147q0 .895-.292 1.729-.291.834-1.073 1.854-.78 1.02-2.145 2.365a112 112 0 0 1-3.49 3.26z"></path></svg>',
+ ),
+ );
+ return $icons;
+}
+
+/**
+ * Gets a specific icon's SVG markup.
+ *
+ * @param string $icon_name Icon identifier.
+ * @return string Icon SVG markup or empty string if not found.
+ */
+function twentig_get_icon( $icon_name ) {
+ $icons = twentig_get_icons();
+ return $icons[ $icon_name ]['icon'] ?? '';
+}
--- a/twentig/inc/blocks/columns.php
+++ b/twentig/inc/blocks/columns.php
@@ -1,19 +1,19 @@
-<?php
-/**
- * Fix columns core spacing.
- * @see https://github.com/WordPress/gutenberg/issues/45277
- *
- * @package twentig
- */
-
-defined( 'ABSPATH' ) || exit;
-
-function twentig_fix_columns_default_gap( $metadata ) {
- if ( isset( $metadata['name'] ) && $metadata['name'] === 'core/columns' ) {
- if ( isset( $metadata['supports']['spacing']['blockGap'] ) && is_array( $metadata['supports']['spacing']['blockGap'] ) ) {
- $metadata['supports']['spacing']['blockGap']['__experimentalDefault'] = 'var(--wp--style--columns-gap-default,2em)';
- }
- }
- return $metadata;
-}
-add_filter( 'block_type_metadata', 'twentig_fix_columns_default_gap' );
+<?php
+/**
+ * Fix columns core spacing.
+ * @see https://github.com/WordPress/gutenberg/issues/45277
+ *
+ * @package twentig
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+function twentig_fix_columns_default_gap( $metadata ) {
+ if ( isset( $metadata['name'] ) && $metadata['name'] === 'core/columns' ) {
+ if ( isset( $metadata['supports']['spacing']['blockGap'] ) && is_array( $metadata['supports']['spacing']['blockGap'] ) ) {
+ $metadata['supports']['spacing']['blockGap']['__experimentalDefault'] = 'var(--wp--style--columns-gap-default,2em)';
+ }
+ }
+ return $metadata;
+}
+add_filter( 'block_type_metadata', 'twentig_fix_columns_default_gap' );
--- a/twentig/inc/blocks/cover.php
+++ b/twentig/inc/blocks/cover.php
@@ -1,63 +1,63 @@
-<?php
-/**
- * Server-side customizations for the `core/cover` block.
- *
- * @package twentig
- */
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * Filters the cover block output to add responsive image sizes.
- *
- * @param string $block_content Rendered block content.
- * @param array $block Block object.
- * @return string Filtered block content.
- */
-function twentig_filter_cover_block( $block_content, $block ) {
-
- $attributes = $block['attrs'] ?? array();
- $image_id = $attributes['id'] ?? null;
-
- if ( ! $image_id && ! empty( $attributes['useFeaturedImage'] ) ) {
- $image_id = get_post_thumbnail_id();
- }
-
- if ( ! $image_id ) {
- return $block_content;
- }
-
- $image_meta = wp_get_attachment_metadata( $image_id );
-
- if ( ! $image_meta || empty( $image_meta['width'] ) ) {
- return $block_content;
- }
-
- $width = absint( $image_meta['width'] );
-
- if ( ! $width ) {
- return $block_content;
- }
-
- $sizes = sprintf(
- '(max-width: 799px) 200vw, (max-width: %1$dpx) 100vw, %1$dpx',
- $width
- );
-
- if ( ! empty( $attributes['style']['dimensions']['aspectRatio'] ) ) {
- $sizes = sprintf(
- '(max-width: 799px) 125vw, (max-width: %1$dpx) 100vw, %1$dpx',
- $width
- );
- }
-
- $tag_processor = new WP_HTML_Tag_Processor( $block_content );
-
- if ( $tag_processor->next_tag( 'img' ) ) {
- $tag_processor->set_attribute( 'sizes', $sizes );
- $block_content = $tag_processor->get_updated_html();
- }
-
- return $block_content;
-}
-add_filter( 'render_block_core/cover', 'twentig_filter_cover_block', 10, 2 );
+<?php
+/**
+ * Server-side customizations for the `core/cover` block.
+ *
+ * @package twentig
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Filters the cover block output to add responsive image sizes.
+ *
+ * @param string $block_content Rendered block content.
+ * @param array $block Block object.
+ * @return string Filtered block content.
+ */
+function twentig_filter_cover_block( $block_content, $block ) {
+
+ $attributes = $block['attrs'] ?? array();
+ $image_id = $attributes['id'] ?? null;
+
+ if ( ! $image_id && ! empty( $attributes['useFeaturedImage'] ) ) {
+ $image_id = get_post_thumbnail_id();
+ }
+
+ if ( ! $image_id ) {
+ return $block_content;
+ }
+
+ $image_meta = wp_get_attachment_metadata( $image_id );
+
+ if ( ! $image_meta || empty( $image_meta['width'] ) ) {
+ return $block_content;
+ }
+
+ $width = absint( $image_meta['width'] );
+
+ if ( ! $width ) {
+ return $block_content;
+ }
+
+ $sizes = sprintf(
+ '(max-width: 799px) 200vw, (max-width: %1$dpx) 100vw, %1$dpx',
+ $width
+ );
+
+ if ( ! empty( $attributes['style']['dimensions']['aspectRatio'] ) ) {
+ $sizes = sprintf(
+ '(max-width: 799px) 125vw, (max-width: %1$dpx) 100vw, %1$dpx',
+ $width
+ );
+ }
+
+ $tag_processor = new WP_HTML_Tag_Processor( $block_content );
+
+ if ( $tag_processor->next_tag( 'img' ) ) {
+ $tag_processor->set_attribute( 'sizes', $sizes );
+ $block_content = $tag_processor->get_updated_html();
+ }
+
+ return $block_content;
+}
+add_filter( 'render_block_core/cover', 'twentig_filter_cover_block', 10, 2 );
--- a/twentig/inc/blocks/details.php
+++ b/twentig/inc/blocks/details.php
@@ -1,43 +1,44 @@
-<?php
-/**
- * Server-side customizations for the `core/details` block.
- *
- * @package twentig
- */
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * Filters the details block output.
- *
- * @param string $block_content Rendered block content.
- * @param array $block Block object.
- * @return string Filtered block content.
- */
-function twentig_filter_details_block( $block_content, $block ) {
- $icon_type = $block['attrs']['twIcon'] ?? '';
-
- if ( empty( $icon_type ) ) {
- return $block_content;
- }
-
- $icon_position = $block['attrs']['twIconPosition'] ?? 'right';
- $tag_processor = new WP_HTML_Tag_Processor( $block_content );
- $tag_processor->next_tag();
- $tag_processor->add_class( 'tw-has-icon' );
-
- if ( 'left' === $icon_position ) {
- $tag_processor->add_class( 'tw-has-icon-left' );
- }
-
- $block_content = $tag_processor->get_updated_html();
-
- $icon_svg = '<svg class="details-arrow" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" version="1.1" aria-hidden="true" focusable="false"><path d="m12 15.375-6-6 1.4-1.4 4.6 4.6 4.6-4.6 1.4 1.4-6 6Z"></path></svg>';
- if ( 'plus' === $icon_type ) {
- $icon_svg = '<svg class="details-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" version="1.1" aria-hidden="true" focusable="false"><path class="plus-vertical" d="M11 6h2v12h-2z"/><path d="M6 11h12v2H6z"/></svg>';
- } elseif ( 'plus-circle' === $icon_type ) {
- $icon_svg = '<svg class="details-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" version="1.1" aria-hidden="true" focusable="false"><path d="M12 3.75c4.55 0 8.25 3.7 8.25 8.25s-3.7 8.25-8.25 8.25-8.25-3.7-8.25-8.25S7.45 3.75 12 3.75M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Z" /><path d="M11.125 7.5h1.75v9h-1.75z" class="plus-vertical" /><path d="M7.5 11.125h9v1.75h-9z" /></svg>';
- }
- return str_replace( '</summary>', $icon_svg . '</summary>', $block_content );
-}
-add_filter( 'render_block_core/details', 'twentig_filter_details_block', 10, 2 );
+<?php
+/**
+ * Server-side customizations for the `core/details` block.
+ *
+ * @package twentig
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Filters the details block output.
+ *
+ * @param string $block_content Rendered block content.
+ * @param array $block Block object.
+ * @return string Filtered block content.
+ */
+function twentig_filter_details_block( $block_content, $block ) {
+ $icon_type = $block['attrs']['twIcon'] ?? '';
+
+ if ( empty( $icon_type ) ) {
+ return $block_content;
+ }
+
+ $icon_position = $block['attrs']['twIconPosition'] ?? 'right';
+ $tag_processor = new WP_HTML_Tag_Processor( $block_content );
+ $tag_processor->next_tag();
+ $tag_processor->add_class( 'tw-has-icon' );
+ $tag_processor->add_class( 'tw-icon-' . sanitize_html_class( $icon_type ) );
+
+ if ( 'left' === $icon_position ) {
+ $tag_processor->add_class( 'tw-has-icon-left' );
+ }
+
+ $block_content = $tag_processor->get_updated_html();
+
+ $icon_svg = '<svg class="details-arrow" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" version="1.1" aria-hidden="true" focusable="false"><path d="m12 15.4-6-6L7.4 8l4.6 4.6L16.6 8 18 9.4z"></path></svg>';
+ if ( 'plus' === $icon_type ) {
+ $icon_svg = '<svg class="details-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" version="1.1" aria-hidden="true" focusable="false"><path class="plus-vertical" d="M11 6h2v12h-2z"/><path d="M6 11h12v2H6z"/></svg>';
+ } elseif ( 'plus-circle' === $icon_type ) {
+ $icon_svg = '<svg class="details-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" version="1.1" aria-hidden="true" focusable="false"><path d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2m0 1.75a8.25 8.25 0 1 0 0 16.5 8.25 8.25 0 0 0 0-16.5" /><path d="M11.125 7.5h1.75v9h-1.75z" class="plus-vertical" /><path d="M7.5 11.125h9v1.75h-9z" /></svg>';
+ }
+ return str_replace( '</summary>', $icon_svg . '</summary>', $block_content );
+}
+add_filter( 'render_block_core/details', 'twentig_filter_details_block', 10, 2 );
--- a/twentig/inc/blocks/gallery.php
+++ b/twentig/inc/blocks/gallery.php
@@ -1,61 +1,234 @@
-<?php
-/**
- * Server-side customizations for the `core/gallery` block.
- *
- * @package twentig
- */
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * Filters the gallery block output.
- *
- * @param string $block_content Rendered block content.
- * @param array $block Block object.
- * @return string Filtered block content.
- */
-function twentig_filter_gallery_block( $block_content, $block ) {
-
- $attributes = $block['attrs'] ?? array();
- $layout = $attributes['twLayout'] ?? null;
- $animation = $attributes['twAnimation'] ?? '';
-
- $scale_contain = isset( $block['attrs']['twImageRatio'] ) && false === ( $block['attrs']['imageCrop'] ?? true );
- if ( $scale_contain ) {
- $block_content = str_replace( 'scaleAttr":false', 'scaleAttr":"contain"', $block_content );
- }
-
- if ( ! $animation ) {
- return $block_content;
- }
-
- $tag_processor = new WP_HTML_Tag_Processor( $block_content );
- $tag_processor->next_tag();
-
- $duration = $attributes['twAnimationDuration'] ?? '';
- $delay = $attributes['twAnimationDelay'] ?? 0;
-
- $tag_processor->set_bookmark( 'tw-gallery' );
- $tag_processor->remove_class( 'tw-block-animation' );
-
- while ( $tag_processor->next_tag( 'figure' ) ) {
- if ( ! $tag_processor->has_class( 'tw-block-animation' ) ) {
- $tag_processor->add_class( 'tw-block-animation' );
- $tag_processor->add_class( 'tw-animation-' . $animation );
-
- if ( $duration ) {
- $tag_processor->add_class( 'tw-duration-' . $duration );
- }
-
- if ( $delay ) {
- $style_attr = $tag_processor->get_attribute( 'style' );
- $style = '--tw-animation-delay:' . esc_attr( $delay ) . 's;' . $style_attr;
- $tag_processor->set_attribute( 'style', $style );
- }
- }
- }
- $tag_processor->seek( 'tw-gallery' );
- $block_content = $tag_processor->get_updated_html();
- return $block_content;
-}
-add_filter( 'render_block_core/gallery', 'twentig_filter_gallery_block', 10, 2 );
+<?php
+/**
+ * Server-side customizations for the `core/gallery` block.
+ *
+ * @package twentig
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Sets the lightbox scale attribute to "contain" for images inside a gallery
+ * that has an aspect ratio with image crop disabled.
+ *
+ * @param string $block_content The block content.
+ * @param array $block The full block, including name and attributes.
+ * @return string The unmodified block content.
+ */
+function twentig_gallery_image_scale_attr( $block_content, $block ) {
+ $attributes = $block['attrs'] ?? array();
+ $scale_contain = isset( $attributes['aspectRatio'] ) && false === ( $attributes['imageCrop'] ?? true );
+ if ( ! $scale_contain ) {
+ return $block_content;
+ }
+
+ $processor = new WP_HTML_Tag_Processor( $block_content );
+ $metadata = array();
+ while ( $processor->next_tag( array( 'tag_name' => 'figure', 'class_name' => 'wp-block-image' ) ) ) {
+ $context = $processor->get_attribute( 'data-wp-context' );
+ if ( $context ) {
+ $data = json_decode( html_entity_decode( $context ), true );
+ if ( ! empty( $data['imageId'] ) ) {
+ $metadata[ $data['imageId'] ] = array( 'scaleAttr' => 'contain' );
+ }
+ }
+ }
+ if ( $metadata ) {
+ wp_interactivity_state( 'core/image', array( 'metadata' => $metadata ) );
+ }
+ return $block_content;
+}
+add_filter( 'render_block_core/gallery', 'twentig_gallery_image_scale_attr', 10, 2 );
+
+/**
+ * Filters the gallery block output.
+ *
+ * @param string $block_content Rendered block content.
+ * @param array $block Block object.
+ * @return string Filtered block content.
+ */
+function twentig_filter_gallery_block( $block_content, $block ) {
+
+ $attributes = $block['attrs'] ?? array();
+ $layout = $attributes['twLayout'] ?? null;
+ $animation = $attributes['twAnimation'] ?? '';
+
+ if ( ! $layout && ! $animation ) {
+ return $block_content;
+ }
+
+ $tag_processor = new WP_HTML_Tag_Processor( $block_content );
+ $tag_processor->next_tag();
+
+ if ( $animation && 'carousel' !== $layout ) {
+ $duration = $attributes['twAnimationDuration'] ?? '';
+ $delay = $attributes['twAnimationDelay'] ?? 0;
+
+ $tag_processor->set_bookmark( 'tw-gallery' );
+ $tag_processor->remove_class( 'tw-block-animation' );
+
+ while ( $tag_processor->next_tag( 'figure' ) ) {
+ if ( ! $tag_processor->has_class( 'tw-block-animation' ) ) {
+ $tag_processor->add_class( 'tw-block-animation' );
+ $tag_processor->add_class( 'tw-animation-' . sanitize_html_class( $animation ) );
+
+ if ( $duration ) {
+ $tag_processor->add_class( 'tw-duration-' . sanitize_html_class( $duration ) );
+ }
+
+ if ( $delay ) {
+ $style_attr = $tag_processor->get_attribute( 'style' );
+ $style = '--tw-animation-delay:' . esc_attr( $delay ) . 's;' . $style_attr;
+ $tag_processor->set_attribute( 'style', $style );
+ }
+ }
+ }
+ $tag_processor->seek( 'tw-gallery' );
+ $block_content = $tag_processor->get_updated_html();
+ }
+
+ if ( 'justified' === $layout ) {
+ $rowHeight = (int) ( $attributes['twRowHeight'] ?? 250 );
+ $crop = $attributes['imageCrop'] ?? true;
+ $tag_processor->remove_class( 'columns-default' );
+
+ $style_attr = $tag_processor->get_attribute( 'style' );
+ $style = '--tw-row-height:' . esc_attr( $rowHeight ) . 'px;' . $style_attr;
+ $tag_processor->set_attribute( 'style', $style );
+
+ if ( $crop ) {
+ while ( $tag_processor->next_tag( 'figure' ) ) {
+ $tag_processor->set_bookmark( 'figure' );
+ $tag_processor->next_tag( 'img' );
+
+ $ratio = '';
+ $attachment_id = absint( $tag_processor->get_attribute( 'data-id' ) );
+ $tag_processor->set_attribute( 'decoding', 'auto' );
+
+ if ( ! $attachment_id ) {
+ $class = $tag_processor->get_attribute( 'class' );
+ if ( ! empty( $class ) && preg_match( '/wp-image-([0-9]+)/i', $class, $class_id ) ) {
+ $attachment_id = absint( $class_id[1] );
+ }
+ }
+
+ if ( $attachment_id ) {
+ $metadata = wp_get_attachment_metadata( $attachment_id );
+ if ( is_array( $metadata ) && isset( $metadata['width'], $metadata['height'] ) && (int) $metadata['height'] !== 0 ) {
+ $ratio = round( (int) $metadata['width'] / (int) $metadata['height'], 6 );
+ }
+ } else {
+ $image_src = $tag_processor->get_attribute( 'src' );
+ if ( $image_src ) {
+ $query = wp_parse_url( str_replace( '&', '&', $image_src ), PHP_URL_QUERY );
+ if ( $query ) {
+ $query_params = wp_parse_args( $query );
+ if ( isset( $query_params['w'], $query_params['h'] ) && is_numeric( $query_params['w'] ) && is_numeric( $query_params['h'] ) && (int) $query_params['h'] !== 0 ) {
+ $ratio = round( (int) $query_params['w'] / (int) $query_params['h'], 6 );
+ }
+ }
+ }
+ }
+
+ if ( $ratio ) {
+ $tag_processor->seek( 'figure' );
+ $style_attr = $tag_processor->get_attribute( 'style' );
+ $style = '--tw-ratio:' . esc_attr( $ratio ) . ';' . $style_attr;
+ $tag_processor->set_attribute( 'style', $style );
+ }
+ }
+ }
+
+ $block_content = $tag_processor->get_updated_html();
+
+ } elseif ( 'carousel' === $layout ) {
+
+ wp_enqueue_script_module(
+ 'tw-block-gallery',
+ TWENTIG_ASSETS_URI . '/blocks/gallery/view.js',
+ array( '@wordpress/interactivity' ),
+ TWENTIG_VERSION
+ );
+
+ $slides_count = count( $block['innerBlocks'] );
+ $settings = $attributes['twCarousel'] ?? array();
+ $arrows_position = $settings['arrowsPosition'] ?? 'overlay';
+ $markers_position = $settings['markersPosition'] ?? 'below';
+ $columns = (int) ( $attributes['columns'] ?? 3 );
+ $view = $settings['view'] ?? '';
+ $show_edges = 'adjacent-images' === $view;
+
+ if ( 1 !== $columns || str_contains( $arrows_position, 'below' ) ) {
+ $markers_position = 'none';
+ }
+
+ $tag_processor->set_attribute( 'role', 'region' );
+ $tag_processor->set_attribute( 'aria-roledescription', __( 'carousel', 'twentig' ) );
+ $tag_processor->set_attribute( 'aria-label', __( 'Image gallery', 'twentig' ) );
+ $tag_processor->set_attribute( 'data-wp-interactive', 'twentig/carousel' );
+ $tag_processor->set_attribute( 'data-wp-init', 'callbacks.initCarousel' );
+ $tag_process