diff --git a/integrations/bricks/README.md b/integrations/bricks/README.md index 648c200f..dfe8cc6d 100644 --- a/integrations/bricks/README.md +++ b/integrations/bricks/README.md @@ -137,11 +137,13 @@ integrations/bricks/ ## CSS Bundle -By default, the plugin loads `dist/slashed.optimal.css` from the jsDelivr CDN (`https://cdn.jsdelivr.net/gh/codeslash-dev/SLASHED@main/dist/slashed.optimal.css`). This means the plugin works out of the box without copying any CSS files locally. +By default, the plugin loads `dist/slashed.optimal.css` from the jsDelivr CDN, pinned to an immutable release tag (e.g. `https://cdn.jsdelivr.net/gh/codeslash-dev/SLASHED@v0.2.12/dist/slashed.optimal.css`). Pinning to a tag (rather than `@main`) ensures the served CSS cannot change outside a plugin release, which is important for reproducibility and supply-chain safety - jsDelivr treats commit/tag refs as effectively immutable, whereas branch refs follow the moving branch tip with a short cache window. The pinned ref lives in the `SLASHED_BRICKS_CSS_REF` constant in `slashed-bricks.php` and is bumped with each plugin release. + +This means the plugin works out of the box without copying any CSS files locally. If a local copy of the bundle is detected (symlink/in-repo mode or a `dist/` folder inside the plugin directory), the local file takes precedence for faster loads and offline development. -To load a different bundle (e.g., the minimal `dist/slashed.essential.css` or the full `dist/slashed.full.css`), use the `slashed_bricks/css_bundle_url` filter. +To load a different bundle (e.g., the minimal `dist/slashed.essential.css` or the full `dist/slashed.full.css`), or to override the pinned ref / point to a commit SHA, use the `slashed_bricks/css_bundle_url` filter. ## License diff --git a/integrations/bricks/assets/admin-page.css b/integrations/bricks/assets/admin-page.css new file mode 100644 index 00000000..4a754c16 --- /dev/null +++ b/integrations/bricks/assets/admin-page.css @@ -0,0 +1,324 @@ +/** + * SLASHED Admin Page Styles + * + * Custom styles for the SLASHED design token settings page. + * Enhances WordPress admin UI with branding and improved UX. + * + * @package SLASHED_Bricks + */ + +/* Page Header */ +.slashed-admin-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; + padding: 15px 20px; + background: #1d2327; + border-radius: 4px; + color: #fff; +} + +.slashed-admin-header h1 { + margin: 0; + padding: 0; + color: #fff; + font-size: 1.6em; + line-height: 1.3; +} + +.slashed-admin-header .slashed-version { + opacity: 0.6; + font-size: 0.5em; + font-weight: normal; + margin-left: 8px; +} + +/* Tab Navigation */ +.slashed-admin-tabs .nav-tab-wrapper { + border-bottom: 2px solid #1d2327; + margin-bottom: 0; + padding-top: 0; +} + +.slashed-admin-tabs .nav-tab { + border: 1px solid transparent; + border-bottom: none; + margin-left: 0; + margin-right: 4px; + padding: 8px 16px; + font-size: 14px; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.slashed-admin-tabs .nav-tab:hover { + background-color: #f0f0f1; + color: #1d2327; +} + +.slashed-admin-tabs .nav-tab-active, +.slashed-admin-tabs .nav-tab-active:hover, +.slashed-admin-tabs .nav-tab-active:focus { + background: #fff; + border-color: #1d2327; + border-bottom: 1px solid #fff; + color: #1d2327; + font-weight: 600; +} + +/* Tab Content */ +.slashed-tab-content { + background: #fff; + border: 1px solid #c3c4c7; + border-top: none; + padding: 20px; + margin-bottom: 20px; + animation: sf-fade-in 0.2s ease-in-out; +} + +@keyframes sf-fade-in { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Section Cards */ +.slashed-section-card { + background: #f9f9f9; + border: 1px solid #e0e0e0; + border-radius: 4px; + padding: 16px 20px; + margin-bottom: 20px; +} + +.slashed-section-card h2 { + margin-top: 0; + padding-top: 0; + font-size: 1.2em; + border-bottom: 1px solid #e0e0e0; + padding-bottom: 8px; + margin-bottom: 12px; +} + +.slashed-section-card .description { + color: #646970; + font-style: italic; +} + +/* Form Field Grid */ +.slashed-field-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 16px; +} + +.slashed-field-grid .form-field { + display: flex; + flex-direction: column; + gap: 4px; +} + +.slashed-field-grid .form-field label { + font-weight: 600; + font-size: 13px; +} + +.slashed-field-grid .form-field .description { + font-size: 12px; + color: #646970; +} + +/* Color Fields */ +.slashed-color-field-wrap { + display: flex; + align-items: center; + gap: 8px; +} + +.slashed-color-field-wrap .slashed-color-field { + flex: 1; +} + +.slashed-color-field-wrap .wp-picker-container { + display: inline-flex; + align-items: center; +} + +.slashed-color-field-wrap .wp-picker-container .wp-color-result { + margin: 0 8px 0 0; +} + +.slashed-color-field-wrap .color-note { + display: block; + font-size: 11px; + color: #646970; + font-style: italic; + margin-top: 4px; +} + +/* Range + Number Input Sync */ +.slashed-range-group { + display: flex; + align-items: center; + gap: 12px; +} + +.slashed-range-group .slashed-range-input { + flex: 1; + height: 6px; + cursor: pointer; + accent-color: #1d2327; +} + +.slashed-range-group .slashed-number-input { + width: 70px; + text-align: center; +} + +.slashed-range-group .slashed-range-label { + min-width: 40px; + text-align: right; + font-size: 12px; + color: #646970; +} + +/* Live Preview Panel */ +.slashed-preview-panel { + border: 2px solid #1d2327; + border-radius: 4px; + padding: 24px; + margin-top: 24px; + background: #fff; + position: relative; +} + +.slashed-preview-panel::before { + content: "Live Preview"; + position: absolute; + top: -10px; + left: 12px; + background: #fff; + padding: 0 8px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: #1d2327; +} + +.slashed-preview-panel .preview-heading { + margin: 0 0 12px; + font-size: 1.5em; + line-height: 1.3; +} + +.slashed-preview-panel .preview-text { + margin: 0 0 16px; + line-height: 1.6; + color: #333; +} + +.slashed-preview-panel .preview-colors { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 16px; +} + +.slashed-preview-panel .preview-color-swatch { + width: 48px; + height: 48px; + border-radius: 4px; + border: 1px solid rgb(0 0 0 / 0.1); +} + +.slashed-preview-panel .preview-button { + display: inline-block; + padding: 10px 24px; + border-radius: 4px; + color: #fff; + font-weight: 600; + font-size: 14px; + margin-right: 8px; + margin-bottom: 12px; +} + +.slashed-preview-panel .preview-spacing { + display: flex; + align-items: flex-end; + gap: 4px; + margin-top: 16px; +} + +.slashed-preview-panel .preview-spacing-box { + background: rgb(29 35 39 / 0.1); + border: 1px solid rgb(29 35 39 / 0.2); + border-radius: 2px; + min-width: 20px; +} + +/* Responsive */ +@media screen and (width <= 782px) { + .slashed-admin-header { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } + + .slashed-field-grid { + grid-template-columns: 1fr; + } + + .slashed-range-group { + flex-wrap: wrap; + } + + .slashed-range-group .slashed-range-input { + width: 100%; + flex: none; + } + + .slashed-preview-panel .preview-colors { + gap: 4px; + } + + .slashed-preview-panel .preview-color-swatch { + width: 36px; + height: 36px; + } +} + +/* WordPress Admin Overrides */ +.wrap .slashed-tab-content .form-table th { + padding-left: 0; + width: 200px; +} + +.wrap .slashed-tab-content .form-table td { + padding-left: 0; +} + +.wrap .slashed-tab-content .form-table td code { + font-size: 12px; + background: #f0f0f1; + padding: 2px 6px; + border-radius: 2px; +} + +/* Reset Button Styling */ +.slashed-reset-section { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid #e0e0e0; +} + +/* Dirty state indicator */ +.slashed-form-dirty .submit .button-primary::after { + content: " *"; + color: #d63638; +} diff --git a/integrations/bricks/assets/admin-page.js b/integrations/bricks/assets/admin-page.js new file mode 100644 index 00000000..d9509fab --- /dev/null +++ b/integrations/bricks/assets/admin-page.js @@ -0,0 +1,298 @@ +/** + * SLASHED Admin Page JavaScript + * + * Handles tab persistence, color picker initialization, range/number sync, + * live preview generation, and form dirty state tracking. + * + * @package SLASHED_Bricks + */ + +( function( $ ) { + 'use strict'; + + var isDirty = false; + var defaults = ( typeof slashedBricksAdmin !== 'undefined' ) ? slashedBricksAdmin.defaults : {}; + var settings = ( typeof slashedBricksAdmin !== 'undefined' ) ? slashedBricksAdmin.settings : {}; + + /** + * Initialize tab persistence via URL hash. + */ + function initTabPersistence() { + var hash = window.location.hash.replace( '#', '' ); + var tabs = $( '.nav-tab-wrapper .nav-tab' ); + + if ( hash && tabs.length ) { + tabs.each( function() { + var href = $( this ).attr( 'href' ); + if ( href && href.indexOf( 'tab=' + hash ) !== -1 ) { + window.location.href = href; + } + } ); + } + + // Set hash on tab click for persistence. + tabs.on( 'click', function() { + var href = $( this ).attr( 'href' ); + if ( href ) { + var match = href.match( /tab=([a-z_-]+)/ ); + if ( match ) { + window.location.hash = match[1]; + } + } + } ); + } + + /** + * Initialize WordPress color pickers on .slashed-color-field inputs. + * + * Note: Since SLASHED uses oklch() color values, the WordPress color picker + * serves as a visual reference and inspiration palette. The actual saved value + * is the oklch() string entered in the text input. + */ + function initColorPickers() { + if ( ! $.fn.wpColorPicker ) { + return; + } + + var palette = [ + '#4338ca', // primary-ish + '#1e293b', // secondary-ish + '#7c3aed', // tertiary-ish + '#0891b2', // action-ish + '#64748b', // neutral-ish + '#16a34a', // success-ish + '#eab308', // warning-ish + '#dc2626' // error-ish + ]; + + $( '.slashed-color-field' ).each( function() { + var $input = $( this ); + + // Create a visual color reference picker alongside the text input. + var $pickerEl = $( '' ); + $pickerEl.insertAfter( $input ); + + $pickerEl.wpColorPicker( { + palettes: palette, + change: function() { + // The color picker is for visual reference only. + // Users should enter oklch() values in the text input. + }, + clear: function() {} + } ); + + // Add a helper note. + if ( ! $input.siblings( '.color-note' ).length ) { + $input.closest( 'td' ).find( '.wp-picker-container' ).after( + 'Use oklch() values in the text field. The color picker is for visual reference only.' + ); + } + } ); + } + + /** + * Sync range inputs with their paired number inputs. + * Elements are linked by matching data-sync attributes. + */ + function initRangeSync() { + $( document ).on( 'input change', '.slashed-range-input', function() { + var syncId = $( this ).data( 'sync' ); + if ( syncId ) { + $( '.slashed-number-input[data-sync="' + syncId + '"]' ).val( $( this ).val() ); + markDirty(); + updateLivePreview(); + } + } ); + + $( document ).on( 'input change', '.slashed-number-input', function() { + var syncId = $( this ).data( 'sync' ); + if ( syncId ) { + $( '.slashed-range-input[data-sync="' + syncId + '"]' ).val( $( this ).val() ); + markDirty(); + updateLivePreview(); + } + } ); + } + + /** + * Generate live preview CSS from current form values. + */ + function updateLivePreview() { + var $style = $( '#slashed-live-preview' ); + if ( ! $style.length ) { + return; + } + + var declarations = []; + + // Collect color values. + var brandColors = [ 'primary', 'secondary', 'tertiary', 'action', 'neutral', 'base' ]; + var statusColors = [ 'success', 'warning', 'error', 'info', 'danger' ]; + + brandColors.forEach( function( color ) { + var val = $( '#brand_' + color ).val(); + if ( val ) { + declarations.push( '--sf-color-' + color + '-light: ' + val ); + } + } ); + + statusColors.forEach( function( color ) { + var val = $( '#status_' + color ).val(); + if ( val ) { + declarations.push( '--sf-color-' + color + '-light: ' + val ); + } + } ); + + // Collect typography values. + var fontStacks = [ 'body', 'heading', 'mono', 'display', 'humanist', 'geometric', 'slab' ]; + fontStacks.forEach( function( name ) { + var val = $( '#font_' + name ).val(); + if ( val ) { + declarations.push( '--sf-font-' + name + ': ' + val ); + } + } ); + + // Scale multipliers. + var textScale = $( '#text_scale' ).val(); + if ( textScale ) { + declarations.push( '--sf-text-scale: ' + textScale ); + } + + var displayScale = $( '#text_display_scale' ).val(); + if ( displayScale ) { + declarations.push( '--sf-text-display-scale: ' + displayScale ); + } + + // Spacing scale. + var spaceScale = $( '#space_scale' ).val(); + if ( spaceScale ) { + declarations.push( '--sf-space-scale: ' + spaceScale ); + } + + // Radius scale. + var radiusScale = $( '#radius_scale' ).val(); + if ( radiusScale ) { + declarations.push( '--sf-radius-scale: ' + radiusScale ); + } + + // Shadow strength. + var shadowStrength = $( '#shadow_strength' ).val(); + if ( shadowStrength ) { + declarations.push( '--sf-shadow-strength: ' + shadowStrength ); + } + + // Motion scale. + var motionScale = $( '#motion_scale' ).val(); + if ( motionScale ) { + declarations.push( '--sf-motion-scale: ' + motionScale ); + } + + // Durations. + var durations = [ 'instant', 'fast', 'normal', 'slow', 'slower' ]; + durations.forEach( function( name ) { + var val = $( '#duration_' + name ).val(); + if ( val ) { + declarations.push( '--sf-duration-' + name + ': calc(' + val + 'ms * var(--sf-motion-scale))' ); + } + } ); + + // Z-index. + var zLevels = [ 'below', 'base', 'raised', 'low', 'mid', 'high', 'top', 'max' ]; + zLevels.forEach( function( name ) { + var val = $( '#z_' + name ).val(); + if ( val ) { + declarations.push( '--sf-z-' + name + ': ' + parseInt( val, 10 ) ); + } + } ); + + // Build CSS string. + var css = ''; + if ( declarations.length ) { + css = '.slashed-preview-panel {\n'; + declarations.forEach( function( decl ) { + css += '\t' + decl + ';\n'; + } ); + css += '}\n'; + } + + $style.text( css ); + } + + /** + * Mark the form as having unsaved changes. + */ + function markDirty() { + if ( ! isDirty ) { + isDirty = true; + $( 'form' ).closest( '.wrap' ).addClass( 'slashed-form-dirty' ); + } + } + + /** + * Initialize form dirty state tracking. + */ + function initDirtyTracking() { + $( '.slashed-tab-content' ).on( 'input change', 'input, select, textarea', function() { + markDirty(); + updateLivePreview(); + } ); + + $( window ).on( 'beforeunload', function() { + if ( isDirty ) { + return 'You have unsaved changes. Are you sure you want to leave?'; + } + } ); + + // Clear dirty state on form submit (save). + $( 'form' ).on( 'submit', function() { + isDirty = false; + } ); + } + + /** + * Enhanced reset section confirmation dialog. + */ + function initResetConfirmation() { + $( document ).on( 'click', '.slashed-reset-btn', function( e ) { + var sectionName = $( this ).data( 'section-name' ) || 'this section'; + var confirmed = window.confirm( + 'Are you sure you want to reset "' + sectionName + '" to default values?\n\nThis cannot be undone.' + ); + + if ( ! confirmed ) { + e.preventDefault(); + return false; + } + + isDirty = false; + } ); + + $( document ).on( 'click', '.slashed-reset-all-btn', function( e ) { + var confirmed = window.confirm( + 'Are you sure you want to reset ALL settings to defaults?\n\nAll customizations will be lost. This cannot be undone.' + ); + + if ( ! confirmed ) { + e.preventDefault(); + return false; + } + + isDirty = false; + } ); + } + + /** + * Initialize all admin page functionality. + */ + $( document ).ready( function() { + initTabPersistence(); + initColorPickers(); + initRangeSync(); + initDirtyTracking(); + initResetConfirmation(); + + // Initial preview render. + updateLivePreview(); + } ); + +} )( jQuery ); diff --git a/integrations/bricks/includes/class-admin-page.php b/integrations/bricks/includes/class-admin-page.php new file mode 100644 index 00000000..c6ed4579 --- /dev/null +++ b/integrations/bricks/includes/class-admin-page.php @@ -0,0 +1,644 @@ +tabs = array( + 'colors' => 'Colors', + 'typography' => 'Typography', + 'spacing' => 'Spacing', + 'radius' => 'Radius', + 'shadows' => 'Shadows', + 'motion' => 'Motion', + 'zindex' => 'Z-Index', + ); + + add_action( 'admin_menu', array( $this, 'register_menu' ) ); + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) ); + add_action( 'admin_post_slashed_bricks_save', array( $this, 'handle_save' ) ); + } + + /** + * Register the top-level admin menu page. + */ + public function register_menu() { + add_menu_page( + __( 'SLASHED Settings', 'slashed-bricks' ), + __( 'SLASHED', 'slashed-bricks' ), + 'manage_options', + 'slashed-bricks', + array( $this, 'render_page' ), + 'dashicons-art', + 59 + ); + } + + /** + * Enqueue admin CSS and JS assets on the SLASHED settings page only. + * + * @param string $hook_suffix The current admin page hook suffix. + */ + public function enqueue_admin_assets( $hook_suffix ) { + if ( 'toplevel_page_slashed-bricks' !== $hook_suffix ) { + return; + } + + $plugin_url = plugin_dir_url( dirname( __FILE__ ) ); + $plugin_dir = plugin_dir_path( dirname( __FILE__ ) ); + + // Enqueue WordPress color picker. + wp_enqueue_style( 'wp-color-picker' ); + + // Enqueue admin page styles. + wp_enqueue_style( + 'slashed-bricks-admin', + $plugin_url . 'assets/admin-page.css', + array( 'wp-color-picker' ), + filemtime( $plugin_dir . 'assets/admin-page.css' ) + ); + + // Enqueue admin page scripts. + wp_enqueue_script( + 'slashed-bricks-admin-js', + $plugin_url . 'assets/admin-page.js', + array( 'jquery', 'wp-color-picker' ), + filemtime( $plugin_dir . 'assets/admin-page.js' ), + true + ); + + // Pass token data to JavaScript for live preview generation. + wp_localize_script( + 'slashed-bricks-admin-js', + 'slashedBricksAdmin', + array( + 'defaults' => Slashed_Bricks_Token_Defaults::get_all(), + 'settings' => $this->get_settings(), + ) + ); + } + + /** + * Get saved token values. + * + * @return array + */ + public function get_settings() { + $settings = get_option( self::OPTION_NAME, array() ); + return is_array( $settings ) ? $settings : array(); + } + + /** + * Get all factory default values. + * + * @return array + */ + public function get_defaults() { + return Slashed_Bricks_Token_Defaults::get_all(); + } + + /** + * Handle form submission for saving settings. + */ + public function handle_save() { + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'Unauthorized access.', 'slashed-bricks' ) ); + } + + if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), self::NONCE_ACTION ) ) { + wp_die( esc_html__( 'Security check failed.', 'slashed-bricks' ) ); + } + + // Handle reset all. + if ( ! empty( $_POST['reset_all'] ) ) { + delete_option( self::OPTION_NAME ); + wp_safe_redirect( admin_url( 'admin.php?page=slashed-bricks&message=reset' ) ); + exit; + } + + // Handle reset section. + if ( ! empty( $_POST['reset_section'] ) ) { + $section = sanitize_key( wp_unslash( $_POST['reset_section'] ) ); + if ( ! isset( $this->tabs[ $section ] ) ) { + wp_safe_redirect( admin_url( 'admin.php?page=slashed-bricks&message=error' ) ); + exit; + } + $settings = $this->get_settings(); + unset( $settings[ $section ] ); + update_option( self::OPTION_NAME, $settings ); + wp_safe_redirect( admin_url( 'admin.php?page=slashed-bricks&tab=' . $section . '&message=reset_section' ) ); + exit; + } + + // Save settings. + $active_tab = isset( $_POST['active_tab'] ) ? sanitize_key( wp_unslash( $_POST['active_tab'] ) ) : 'colors'; + if ( ! isset( $this->tabs[ $active_tab ] ) ) { + $active_tab = 'colors'; + } + $settings = $this->get_settings(); + $section = isset( $_POST['slashed_tokens'] ) ? wp_unslash( $_POST['slashed_tokens'] ) : array(); + + if ( is_array( $section ) ) { + $settings[ $active_tab ] = $this->sanitize_section( $active_tab, $section ); + } + + update_option( self::OPTION_NAME, $settings ); + wp_safe_redirect( admin_url( 'admin.php?page=slashed-bricks&tab=' . $active_tab . '&message=saved' ) ); + exit; + } + + /** + * Sanitize a section's input values. + * + * @param string $section Section slug. + * @param array $data Raw form data. + * @return array Sanitized data. + */ + private function sanitize_section( $section, $data ) { + $sanitized = array(); + + foreach ( $data as $key => $value ) { + $key = sanitize_key( $key ); + if ( is_array( $value ) ) { + $sanitized[ $key ] = array_map( + function ( $v ) { + return $this->sanitize_css_value( sanitize_text_field( $v ) ); + }, + $value + ); + } else { + $sanitized[ $key ] = $this->sanitize_css_value( sanitize_text_field( $value ) ); + } + } + + return $sanitized; + } + + /** + * Sanitize a value for safe use in CSS declarations. + * + * Strips characters that could escape a CSS declaration context or inject + * at-rules. This prevents stored values from breaking out of the + * :root { ... } block when interpolated into generated CSS. + * + * @param string $value The value to sanitize. + * @return string Sanitized value with dangerous characters removed. + */ + private function sanitize_css_value( $value ) { + return str_replace( array( '{', '}', '<', '>', '@', ';' ), '', $value ); + } + + /** + * Render the admin settings page. + */ + public function render_page() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + $active_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'colors'; + if ( ! isset( $this->tabs[ $active_tab ] ) ) { + $active_tab = 'colors'; + } + + $settings = $this->get_settings(); + $message = isset( $_GET['message'] ) ? sanitize_key( wp_unslash( $_GET['message'] ) ) : ''; + + ?> +
' . esc_html__( 'Settings saved.', 'slashed-bricks' ) . '
'; + } elseif ( 'reset' === $message ) { + echo '' . esc_html__( 'All settings reset to defaults.', 'slashed-bricks' ) . '
' . esc_html__( 'Section reset to defaults.', 'slashed-bricks' ) . '
+ +
+ +' . esc_html__( 'Source color tokens in oklch() format. These generate the full color scale.', 'slashed-bricks' ) . '
'; + echo '| '; + echo ' | ';
+ echo '';
+ echo '
| ';
+ echo '
|---|
| '; + echo ' | ';
+ echo '';
+ echo '
| ';
+ echo '
|---|
| '; + echo ' | ';
+ echo '';
+ echo '
| ';
+ echo '
|---|
' . esc_html__( 'Min and max values in rem for fluid type scaling via clamp().', 'slashed-bricks' ) . '
'; + echo '| '; + echo ' | ';
+ echo ' ';
+ echo '';
+ echo '
| ';
+ echo '
|---|
| '; + echo ' | ';
+ echo '';
+ echo '
| ';
+ echo '
|---|---|
| '; + echo ' | ';
+ echo '';
+ echo '
| ';
+ echo '
| '; + echo ' | ';
+ echo '';
+ echo '
| ';
+ echo '
|---|---|
| '; + echo ' | ';
+ echo '';
+ echo '
| ';
+ echo '
| '; + echo ' | ';
+ echo '';
+ echo '
| ';
+ echo '
|---|
| '; + echo ' | ';
+ echo '';
+ echo ' ' . esc_html__( 'Base opacity value for shadow layers (0-1).', 'slashed-bricks' ) . ' '; + echo ' | ';
+ echo '
|---|
| '; + echo ' | ';
+ echo '';
+ echo '
| ';
+ echo '
|---|
' . esc_html__( 'Base duration values in milliseconds.', 'slashed-bricks' ) . '
'; + echo '| '; + echo ' | ';
+ echo '';
+ echo ' ms';
+ echo '
| ';
+ echo '
|---|
' . esc_html__( 'Z-index layer values for stacking context management.', 'slashed-bricks' ) . '
'; + echo '| '; + echo ' | ';
+ echo '';
+ echo '
| ';
+ echo '
|---|