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'] ) ) : ''; + + ?> +
+

+ +
+ + + + +
+

+ + render_notices( $message ); ?> + + + +
+ + + + +
+ $method( $tab_settings ); + } + ?> +
+ + + +
+ + +
+ + render_live_preview(); ?> +
+

' . esc_html__( 'Settings saved.', 'slashed-bricks' ) . '

'; + } elseif ( 'reset' === $message ) { + echo '

' . esc_html__( 'All settings reset to defaults.', 'slashed-bricks' ) . '

'; + } elseif ( 'reset_section' === $message ) { + echo '

' . esc_html__( 'Section reset to defaults.', 'slashed-bricks' ) . '

'; + } + } + + /** + * Render the live preview panel. + * + * Displays sample HTML elements styled with current token values + * that update in real-time as the user adjusts controls via JS. + */ + private function render_live_preview() { + $defaults = Slashed_Bricks_Token_Defaults::get_colors(); + ?> +
+ + +

+ +

+ +

+ +

+ +
+
+
+
+
+
+
+
+ +
+ + + + + + +
+ +
+
+
+
+
+
+
+
+ ' . esc_html__( 'Brand Colors', 'slashed-bricks' ) . ''; + echo '

' . esc_html__( 'Source color tokens in oklch() format. These generate the full color scale.', 'slashed-bricks' ) . '

'; + echo ''; + + foreach ( $defaults['brand'] as $name => $default ) { + $field_name = 'slashed_tokens[brand_' . $name . ']'; + $value = isset( $settings[ 'brand_' . $name ] ) ? $settings[ 'brand_' . $name ] : ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo '
'; + echo ''; + echo '

--sf-color-' . esc_html( $name ) . '-light

'; + echo '
'; + + echo '

' . esc_html__( 'Status Colors', 'slashed-bricks' ) . '

'; + echo ''; + + foreach ( $defaults['status'] as $name => $default ) { + $field_name = 'slashed_tokens[status_' . $name . ']'; + $value = isset( $settings[ 'status_' . $name ] ) ? $settings[ 'status_' . $name ] : ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo '
'; + echo ''; + echo '

--sf-color-' . esc_html( $name ) . '-light

'; + echo '
'; + } + + /** + * Render the Typography tab. + * + * @param array $settings Saved settings for this tab. + */ + private function render_tab_typography( $settings ) { + $defaults = Slashed_Bricks_Token_Defaults::get_typography(); + + // Font families. + echo '

' . esc_html__( 'Font Families', 'slashed-bricks' ) . '

'; + echo ''; + + foreach ( $defaults['font_families'] as $name => $default ) { + $field_name = 'slashed_tokens[font_' . $name . ']'; + $value = isset( $settings[ 'font_' . $name ] ) ? $settings[ 'font_' . $name ] : ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo '
'; + echo ''; + echo '

--sf-font-' . esc_html( $name ) . '

'; + echo '
'; + + // Font sizes. + echo '

' . esc_html__( 'Font Size Scale', 'slashed-bricks' ) . '

'; + echo '

' . esc_html__( 'Min and max values in rem for fluid type scaling via clamp().', 'slashed-bricks' ) . '

'; + echo ''; + + foreach ( $defaults['font_sizes'] as $name => $size_defaults ) { + $min_field = 'slashed_tokens[size_' . $name . '_min]'; + $max_field = 'slashed_tokens[size_' . $name . '_max]'; + $min_value = isset( $settings[ 'size_' . $name . '_min' ] ) ? $settings[ 'size_' . $name . '_min' ] : ''; + $max_value = isset( $settings[ 'size_' . $name . '_max' ] ) ? $settings[ 'size_' . $name . '_max' ] : ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo '
'; + echo ' '; + echo ''; + echo '

--sf-text-' . esc_html( $name ) . '

'; + echo '
'; + + // Scale multipliers. + echo '

' . esc_html__( 'Scale Multipliers', 'slashed-bricks' ) . '

'; + echo ''; + + $text_scale_value = isset( $settings['text_scale'] ) ? $settings['text_scale'] : ''; + $display_scale_value = isset( $settings['text_display_scale'] ) ? $settings['text_display_scale'] : ''; + + echo ''; + echo ''; + echo ''; + echo ''; + + echo ''; + echo ''; + echo ''; + echo ''; + + echo '
'; + echo ''; + echo '

--sf-text-scale

'; + echo '
'; + echo ''; + echo '

--sf-text-display-scale

'; + echo '
'; + } + + /** + * Render the Spacing tab. + * + * @param array $settings Saved settings for this tab. + */ + private function render_tab_spacing( $settings ) { + $defaults = Slashed_Bricks_Token_Defaults::get_spacing(); + + echo '

' . esc_html__( 'Spacing', 'slashed-bricks' ) . '

'; + echo ''; + + // Scale multiplier. + $scale_value = isset( $settings['space_scale'] ) ? $settings['space_scale'] : ''; + echo ''; + echo ''; + echo ''; + echo ''; + + // Spacing aliases. + $aliases = array( + 'gutter' => array( 'label' => 'Gutter', 'var' => '--sf-space-gutter' ), + 'gap' => array( 'label' => 'Gap', 'var' => '--sf-gap' ), + 'content_gap' => array( 'label' => 'Content Gap', 'var' => '--sf-content-gap' ), + 'component_pad' => array( 'label' => 'Component Pad', 'var' => '--sf-component-pad' ), + 'section_pad' => array( 'label' => 'Section Pad', 'var' => '--sf-section-pad' ), + ); + + foreach ( $aliases as $key => $config ) { + $value = isset( $settings[ $key ] ) ? $settings[ $key ] : ''; + $default = isset( $defaults[ $key ] ) ? $defaults[ $key ] : ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo '
'; + echo ''; + echo '

--sf-space-scale

'; + echo '
'; + echo ''; + echo '

' . esc_html( $config['var'] ) . '

'; + echo '
'; + } + + /** + * Render the Radius tab. + * + * @param array $settings Saved settings for this tab. + */ + private function render_tab_radius( $settings ) { + $defaults = Slashed_Bricks_Token_Defaults::get_radius(); + + echo '

' . esc_html__( 'Radius', 'slashed-bricks' ) . '

'; + echo ''; + + $value = isset( $settings['radius_scale'] ) ? $settings['radius_scale'] : ''; + echo ''; + echo ''; + echo ''; + echo ''; + + echo '
'; + echo ''; + echo '

--sf-radius-scale

'; + echo '
'; + } + + /** + * Render the Shadows tab. + * + * @param array $settings Saved settings for this tab. + */ + private function render_tab_shadows( $settings ) { + $defaults = Slashed_Bricks_Token_Defaults::get_shadows(); + + echo '

' . esc_html__( 'Shadows', 'slashed-bricks' ) . '

'; + echo ''; + + $value = isset( $settings['shadow_strength'] ) ? $settings['shadow_strength'] : ''; + echo ''; + echo ''; + echo ''; + echo ''; + + echo '
'; + echo ''; + echo '

' . esc_html__( 'Base opacity value for shadow layers (0-1).', 'slashed-bricks' ) . '

'; + echo '
'; + } + + /** + * Render the Motion tab. + * + * @param array $settings Saved settings for this tab. + */ + private function render_tab_motion( $settings ) { + $defaults = Slashed_Bricks_Token_Defaults::get_motion(); + + echo '

' . esc_html__( 'Motion', 'slashed-bricks' ) . '

'; + echo ''; + + // Scale multiplier. + $scale_value = isset( $settings['motion_scale'] ) ? $settings['motion_scale'] : ''; + echo ''; + echo ''; + echo ''; + echo ''; + + echo '
'; + echo ''; + echo '

--sf-motion-scale

'; + echo '
'; + + // Duration values. + echo '

' . esc_html__( 'Duration Values', 'slashed-bricks' ) . '

'; + echo '

' . esc_html__( 'Base duration values in milliseconds.', 'slashed-bricks' ) . '

'; + echo ''; + + foreach ( $defaults['durations'] as $name => $default ) { + $value = isset( $settings[ 'duration_' . $name ] ) ? $settings[ 'duration_' . $name ] : ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo '
'; + echo ''; + echo ' ms'; + echo '

--sf-duration-' . esc_html( $name ) . '

'; + echo '
'; + } + + /** + * Render the Z-Index tab. + * + * @param array $settings Saved settings for this tab. + */ + private function render_tab_zindex( $settings ) { + $defaults = Slashed_Bricks_Token_Defaults::get_zindex(); + + echo '

' . esc_html__( 'Z-Index', 'slashed-bricks' ) . '

'; + echo '

' . esc_html__( 'Z-index layer values for stacking context management.', 'slashed-bricks' ) . '

'; + echo ''; + + foreach ( $defaults as $name => $default ) { + $value = isset( $settings[ $name ] ) ? $settings[ $name ] : ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo '
'; + echo ''; + echo '

--sf-z-' . esc_html( $name ) . '

'; + echo '
'; + } +} diff --git a/integrations/bricks/includes/class-css-generator.php b/integrations/bricks/includes/class-css-generator.php new file mode 100644 index 00000000..2dc5dcf5 --- /dev/null +++ b/integrations/bricks/includes/class-css-generator.php @@ -0,0 +1,359 @@ + $values ) { + if ( ! is_array( $values ) ) { + continue; + } + foreach ( $values as $value ) { + if ( is_array( $value ) ) { + foreach ( $value as $v ) { + if ( '' !== $v && null !== $v ) { + return true; + } + } + } elseif ( '' !== $value && null !== $value ) { + return true; + } + } + } + + return false; + } + + /** + * Get the full override CSS string. + * + * @return string CSS output or empty string if no overrides. + */ + public static function get_override_css() { + if ( null !== self::$cache ) { + return self::$cache; + } + + $settings = get_option( self::OPTION_NAME, array() ); + + if ( ! is_array( $settings ) || empty( $settings ) ) { + self::$cache = ''; + return self::$cache; + } + + $declarations = array(); + + // Process each section. + if ( ! empty( $settings['colors'] ) && is_array( $settings['colors'] ) ) { + $declarations = array_merge( $declarations, self::generate_color_declarations( $settings['colors'] ) ); + } + + if ( ! empty( $settings['typography'] ) && is_array( $settings['typography'] ) ) { + $declarations = array_merge( $declarations, self::generate_typography_declarations( $settings['typography'] ) ); + } + + if ( ! empty( $settings['spacing'] ) && is_array( $settings['spacing'] ) ) { + $declarations = array_merge( $declarations, self::generate_spacing_declarations( $settings['spacing'] ) ); + } + + if ( ! empty( $settings['radius'] ) && is_array( $settings['radius'] ) ) { + $declarations = array_merge( $declarations, self::generate_radius_declarations( $settings['radius'] ) ); + } + + if ( ! empty( $settings['shadows'] ) && is_array( $settings['shadows'] ) ) { + $declarations = array_merge( $declarations, self::generate_shadow_declarations( $settings['shadows'] ) ); + } + + if ( ! empty( $settings['motion'] ) && is_array( $settings['motion'] ) ) { + $declarations = array_merge( $declarations, self::generate_motion_declarations( $settings['motion'] ) ); + } + + if ( ! empty( $settings['zindex'] ) && is_array( $settings['zindex'] ) ) { + $declarations = array_merge( $declarations, self::generate_zindex_declarations( $settings['zindex'] ) ); + } + + if ( empty( $declarations ) ) { + self::$cache = ''; + return self::$cache; + } + + $css = "@layer slashed.overrides {\n\t:root {\n"; + foreach ( $declarations as $declaration ) { + $css .= "\t\t" . $declaration . "\n"; + } + $css .= "\t}\n}"; + + /** + * Filter the generated override CSS. + * + * @param string $css The generated CSS string. + */ + self::$cache = apply_filters( 'slashed_bricks/override_css', $css ); + + return self::$cache; + } + + /** + * Generate CSS declarations for color tokens. + * + * @param array $settings Color section settings. + * @return array CSS declaration strings. + */ + private static function generate_color_declarations( $settings ) { + $declarations = array(); + + // Brand colors: brand_primary -> --sf-color-primary-light. + $brand_colors = array( 'primary', 'secondary', 'tertiary', 'action', 'neutral', 'base' ); + foreach ( $brand_colors as $color ) { + $key = 'brand_' . $color; + if ( ! empty( $settings[ $key ] ) ) { + $declarations[] = '--sf-color-' . $color . '-light: ' . $settings[ $key ] . ';'; + } + } + + // Status colors: status_success -> --sf-color-success-light. + $status_colors = array( 'success', 'warning', 'error', 'info', 'danger' ); + foreach ( $status_colors as $color ) { + $key = 'status_' . $color; + if ( ! empty( $settings[ $key ] ) ) { + $declarations[] = '--sf-color-' . $color . '-light: ' . $settings[ $key ] . ';'; + } + } + + return $declarations; + } + + /** + * Generate CSS declarations for typography tokens. + * + * @param array $settings Typography section settings. + * @return array CSS declaration strings. + */ + private static function generate_typography_declarations( $settings ) { + $declarations = array(); + + // Font families: font_body -> --sf-font-body. + $font_stacks = array( 'body', 'heading', 'mono', 'display', 'humanist', 'geometric', 'slab' ); + foreach ( $font_stacks as $name ) { + $key = 'font_' . $name; + if ( ! empty( $settings[ $key ] ) ) { + $declarations[] = '--sf-font-' . $name . ': ' . $settings[ $key ] . ';'; + } + } + + // Scale multipliers. + if ( isset( $settings['text_scale'] ) && '' !== $settings['text_scale'] ) { + $declarations[] = '--sf-text-scale: ' . $settings['text_scale'] . ';'; + } + if ( isset( $settings['text_display_scale'] ) && '' !== $settings['text_display_scale'] ) { + $declarations[] = '--sf-text-display-scale: ' . $settings['text_display_scale'] . ';'; + } + + // Font sizes: size_X_min + size_X_max -> --sf-text-X with clamp(). + $sizes = array( '2xs', 'xs', 's', 'm', 'l', 'xl', '2xl', '3xl', '4xl', 'display-s', 'display-m', 'display-l' ); + foreach ( $sizes as $size ) { + $min_key = 'size_' . $size . '_min'; + $max_key = 'size_' . $size . '_max'; + + $min_val = isset( $settings[ $min_key ] ) ? $settings[ $min_key ] : ''; + $max_val = isset( $settings[ $max_key ] ) ? $settings[ $max_key ] : ''; + + // Only generate if both min and max are provided. + if ( '' !== $min_val && '' !== $max_val ) { + $clamp = self::build_clamp( (float) $min_val, (float) $max_val ); + if ( $clamp ) { + $declarations[] = '--sf-text-' . $size . ': ' . $clamp . ';'; + } + } + } + + return $declarations; + } + + /** + * Build a clamp() expression for fluid type scaling. + * + * Formula: clamp(min_rem, calc(slope * (100vw - 22.5rem) + min_rem), max_rem) + * where slope = (max - min) / (95 - 22.5) + * + * @param float $min Minimum size in rem (unitless number). + * @param float $max Maximum size in rem (unitless number). + * @return string|false The clamp() expression or false on invalid input. + */ + private static function build_clamp( $min, $max ) { + if ( $min <= 0 || $max <= 0 ) { + return false; + } + + $viewport_range = self::VIEWPORT_MAX - self::VIEWPORT_MIN; + $slope = ( $max - $min ) / $viewport_range; + + // Round to reasonable precision. + $slope_rounded = round( $slope, 6 ); + $min_rounded = round( $min, 4 ); + $max_rounded = round( $max, 4 ); + + return 'clamp(' . $min_rounded . 'rem, calc(' . $slope_rounded . ' * (100vw - ' . self::VIEWPORT_MIN . 'rem) + ' . $min_rounded . 'rem), ' . $max_rounded . 'rem)'; + } + + /** + * Generate CSS declarations for spacing tokens. + * + * @param array $settings Spacing section settings. + * @return array CSS declaration strings. + */ + private static function generate_spacing_declarations( $settings ) { + $declarations = array(); + + if ( isset( $settings['space_scale'] ) && '' !== $settings['space_scale'] ) { + $declarations[] = '--sf-space-scale: ' . $settings['space_scale'] . ';'; + } + + $aliases = array( + 'gutter' => '--sf-space-gutter', + 'gap' => '--sf-gap', + 'content_gap' => '--sf-content-gap', + 'component_pad' => '--sf-component-pad', + 'section_pad' => '--sf-section-pad', + ); + + foreach ( $aliases as $key => $property ) { + if ( ! empty( $settings[ $key ] ) ) { + $declarations[] = $property . ': ' . $settings[ $key ] . ';'; + } + } + + return $declarations; + } + + /** + * Generate CSS declarations for radius tokens. + * + * @param array $settings Radius section settings. + * @return array CSS declaration strings. + */ + private static function generate_radius_declarations( $settings ) { + $declarations = array(); + + if ( isset( $settings['radius_scale'] ) && '' !== $settings['radius_scale'] ) { + $declarations[] = '--sf-radius-scale: ' . $settings['radius_scale'] . ';'; + } + + return $declarations; + } + + /** + * Generate CSS declarations for shadow tokens. + * + * @param array $settings Shadows section settings. + * @return array CSS declaration strings. + */ + private static function generate_shadow_declarations( $settings ) { + $declarations = array(); + + if ( isset( $settings['shadow_strength'] ) && '' !== $settings['shadow_strength'] ) { + $declarations[] = '--sf-shadow-strength: calc(' . $settings['shadow_strength'] . ' + var(--sf-is-dark) * 0.17);'; + } + + return $declarations; + } + + /** + * Generate CSS declarations for motion tokens. + * + * @param array $settings Motion section settings. + * @return array CSS declaration strings. + */ + private static function generate_motion_declarations( $settings ) { + $declarations = array(); + + if ( isset( $settings['motion_scale'] ) && '' !== $settings['motion_scale'] ) { + $declarations[] = '--sf-motion-scale: ' . $settings['motion_scale'] . ';'; + } + + // Duration values: duration_instant -> --sf-duration-instant: calc(Xms * var(--sf-motion-scale)); + $durations = array( 'instant', 'fast', 'normal', 'slow', 'slower' ); + foreach ( $durations as $name ) { + $key = 'duration_' . $name; + if ( isset( $settings[ $key ] ) && '' !== $settings[ $key ] ) { + $declarations[] = '--sf-duration-' . $name . ': calc(' . $settings[ $key ] . 'ms * var(--sf-motion-scale));'; + } + } + + return $declarations; + } + + /** + * Generate CSS declarations for z-index tokens. + * + * @param array $settings Z-index section settings. + * @return array CSS declaration strings. + */ + private static function generate_zindex_declarations( $settings ) { + $declarations = array(); + + $levels = array( 'below', 'base', 'raised', 'low', 'mid', 'high', 'top', 'max' ); + foreach ( $levels as $name ) { + if ( isset( $settings[ $name ] ) && '' !== $settings[ $name ] ) { + $declarations[] = '--sf-z-' . $name . ': ' . (int) $settings[ $name ] . ';'; + } + } + + return $declarations; + } +} diff --git a/integrations/bricks/includes/class-enqueue.php b/integrations/bricks/includes/class-enqueue.php index 1bdffa0c..8170925a 100644 --- a/integrations/bricks/includes/class-enqueue.php +++ b/integrations/bricks/includes/class-enqueue.php @@ -59,6 +59,11 @@ public function enqueue_frontend_styles() { array(), $version ); + + // Inject token override CSS after the framework stylesheet. + if ( Slashed_Bricks_CSS_Generator::has_overrides() ) { + wp_add_inline_style( 'slashed-framework', Slashed_Bricks_CSS_Generator::get_override_css() ); + } } /** diff --git a/integrations/bricks/includes/class-token-defaults.php b/integrations/bricks/includes/class-token-defaults.php new file mode 100644 index 00000000..bfe68a45 --- /dev/null +++ b/integrations/bricks/includes/class-token-defaults.php @@ -0,0 +1,183 @@ + self::get_colors(), + 'typography' => self::get_typography(), + 'spacing' => self::get_spacing(), + 'radius' => self::get_radius(), + 'shadows' => self::get_shadows(), + 'motion' => self::get_motion(), + 'zindex' => self::get_zindex(), + ); + } + + /** + * Get defaults for a specific section. + * + * @param string $section Section slug. + * @return array Section defaults or empty array. + */ + public static function get_section( $section ) { + $all = self::get_all(); + return isset( $all[ $section ] ) ? $all[ $section ] : array(); + } + + /** + * Get color token defaults. + * + * @return array + */ + public static function get_colors() { + return array( + 'brand' => array( + 'primary' => 'oklch(0.45 0.20 264)', + 'secondary' => 'oklch(0.25 0.03 260)', + 'tertiary' => 'oklch(0.48 0.14 310)', + 'action' => 'oklch(0.60 0.16 210)', + 'neutral' => 'oklch(0.45 0.02 260)', + 'base' => 'oklch(0.98 0.005 260)', + ), + 'status' => array( + 'success' => 'oklch(0.48 0.17 150)', + 'warning' => 'oklch(0.75 0.17 80)', + 'error' => 'oklch(0.62 0.20 35)', + 'info' => 'oklch(0.48 0.15 240)', + 'danger' => 'oklch(0.48 0.24 12)', + ), + ); + } + + /** + * Get typography token defaults. + * + * @return array + */ + public static function get_typography() { + return array( + 'font_families' => array( + 'body' => 'system-ui, sans-serif', + 'heading' => 'system-ui, sans-serif', + 'mono' => 'ui-monospace, monospace', + 'display' => 'system-ui, sans-serif', + 'humanist' => 'system-ui, sans-serif', + 'geometric' => 'system-ui, sans-serif', + 'slab' => 'system-ui, sans-serif', + ), + 'font_sizes' => array( + '2xs' => array( 'min' => 0.51, 'max' => 0.53 ), + 'xs' => array( 'min' => 0.64, 'max' => 0.70 ), + 's' => array( 'min' => 0.80, 'max' => 0.94 ), + 'm' => array( 'min' => 1.00, 'max' => 1.25 ), + 'l' => array( 'min' => 1.25, 'max' => 1.67 ), + 'xl' => array( 'min' => 1.56, 'max' => 2.22 ), + '2xl' => array( 'min' => 1.95, 'max' => 2.96 ), + '3xl' => array( 'min' => 2.44, 'max' => 3.95 ), + '4xl' => array( 'min' => 3.05, 'max' => 5.26 ), + 'display-s' => array( 'min' => 2.40, 'max' => 3.00 ), + 'display-m' => array( 'min' => 3.00, 'max' => 4.00 ), + 'display-l' => array( 'min' => 3.75, 'max' => 5.33 ), + ), + 'scale_multipliers' => array( + 'text_scale' => 1, + 'text_display_scale' => 1, + ), + ); + } + + /** + * Get spacing token defaults. + * + * @return array + */ + public static function get_spacing() { + return array( + 'space_scale' => 1, + 'gutter' => 'var(--sf-space-l)', + 'gap' => 'var(--sf-space-m)', + 'content_gap' => 'var(--sf-space-s)', + 'component_pad' => 'var(--sf-space-m)', + 'section_pad' => 'var(--sf-section-pad--m)', + ); + } + + /** + * Get radius token defaults. + * + * @return array + */ + public static function get_radius() { + return array( + 'radius_scale' => 1, + ); + } + + /** + * Get shadow token defaults. + * + * @return array + */ + public static function get_shadows() { + return array( + 'shadow_strength' => 0.08, + ); + } + + /** + * Get motion token defaults. + * + * @return array + */ + public static function get_motion() { + return array( + 'motion_scale' => 1, + 'durations' => array( + 'instant' => 100, + 'fast' => 150, + 'normal' => 250, + 'slow' => 400, + 'slower' => 600, + ), + ); + } + + /** + * Get z-index token defaults. + * + * @return array + */ + public static function get_zindex() { + return array( + 'below' => -1, + 'base' => 0, + 'raised' => 1, + 'low' => 10, + 'mid' => 100, + 'high' => 500, + 'top' => 900, + 'max' => 9999, + ); + } +} diff --git a/integrations/bricks/slashed-bricks.php b/integrations/bricks/slashed-bricks.php index c08cdd9a..c115a229 100644 --- a/integrations/bricks/slashed-bricks.php +++ b/integrations/bricks/slashed-bricks.php @@ -23,12 +23,28 @@ define( 'SLASHED_BRICKS_PATH', plugin_dir_path( __FILE__ ) ); define( 'SLASHED_BRICKS_URL', plugin_dir_url( __FILE__ ) ); +/** + * Immutable jsDelivr ref for the SLASHED CSS bundle. + * + * Pinned to a specific release tag so the served CSS cannot change outside + * a plugin release. jsDelivr treats commit/tag refs as effectively immutable + * (cached "forever"), whereas branch refs (e.g. @main) have a 12h cache and + * follow the moving branch tip - which is unsafe for production use. + * + * When a new SLASHED framework release is published, bump this constant + * (and verify the tag was not retagged in the upstream repo). + * + * Override per-site with the 'slashed_bricks/css_bundle_url' filter. + */ +define( 'SLASHED_BRICKS_CSS_REF', 'v0.2.12' ); + /** * Get the URL for the SLASHED CSS bundle. * - * Defaults to the jsDelivr CDN (main branch latest) so the plugin works - * without any local file setup. If a local copy is detected (symlink/in-repo - * mode or copy-install mode), the local file takes precedence for faster loads + * Defaults to the jsDelivr CDN pinned to an immutable release tag + * (see SLASHED_BRICKS_CSS_REF) so the plugin works without any local + * file setup. If a local copy is detected (symlink/in-repo mode or + * copy-install mode), the local file takes precedence for faster loads * and offline development. * * Use the 'slashed_bricks/css_bundle_url' filter to override. @@ -36,8 +52,11 @@ * @return string URL to the CSS bundle. */ function slashed_bricks_get_css_url() { - // Default: jsDelivr CDN pointing to main branch latest. - $default_url = 'https://cdn.jsdelivr.net/gh/codeslash-dev/SLASHED@main/dist/slashed.optimal.css'; + // Default: jsDelivr CDN pinned to an immutable release tag. + $default_url = sprintf( + 'https://cdn.jsdelivr.net/gh/codeslash-dev/SLASHED@%s/dist/slashed.optimal.css', + SLASHED_BRICKS_CSS_REF + ); // Prefer local file if available (symlink/in-repo mode). $repo_path = SLASHED_BRICKS_PATH . '../../dist/slashed.optimal.css'; @@ -83,6 +102,22 @@ function slashed_bricks_is_bricks_active() { return false; } +/** + * Initialize the admin page. + * + * The admin page loads regardless of whether Bricks is active so users + * can configure tokens before activating the Bricks theme. + */ +function slashed_bricks_admin_init() { + require_once SLASHED_BRICKS_PATH . 'includes/class-token-defaults.php'; + require_once SLASHED_BRICKS_PATH . 'includes/class-admin-page.php'; + + new Slashed_Bricks_Admin_Page(); +} +if ( is_admin() ) { + add_action( 'plugins_loaded', 'slashed_bricks_admin_init' ); +} + /** * Initialize the plugin. */ @@ -92,6 +127,8 @@ function slashed_bricks_init() { return; } + require_once SLASHED_BRICKS_PATH . 'includes/class-token-defaults.php'; + require_once SLASHED_BRICKS_PATH . 'includes/class-css-generator.php'; require_once SLASHED_BRICKS_PATH . 'includes/class-enqueue.php'; require_once SLASHED_BRICKS_PATH . 'includes/class-variables.php'; require_once SLASHED_BRICKS_PATH . 'includes/class-classes.php'; @@ -105,17 +142,11 @@ function slashed_bricks_init() { add_action( 'after_setup_theme', 'slashed_bricks_init' ); /** - * Activation check: ensure Bricks Builder 1.9.2+ is available. + * Activation check. */ function slashed_bricks_activation_check() { - if ( ! slashed_bricks_is_bricks_active() ) { - deactivate_plugins( plugin_basename( __FILE__ ) ); - wp_die( - esc_html__( 'SLASHED for Bricks requires Bricks Builder 1.9.2 or higher to be installed and active.', 'slashed-bricks' ), - 'Plugin Activation Error', - array( 'back_link' => true ) - ); - } + // Allow activation without Bricks so admin token configuration is available. + // Runtime guards in slashed_bricks_init() handle the Bricks dependency. } register_activation_hook( __FILE__, 'slashed_bricks_activation_check' );