diff --git a/integrations/bricks/README.md b/integrations/bricks/README.md index dfe8cc6d..043e9a75 100644 --- a/integrations/bricks/README.md +++ b/integrations/bricks/README.md @@ -5,9 +5,10 @@ A WordPress plugin that integrates the [SLASHED](https://github.com/codeslash-de ## Features - **CSS Loading** - Automatically enqueues the SLASHED CSS bundle on the frontend and within the Bricks editor iframe -- **Variable Pickers** - Registers all 587+ SLASHED CSS custom properties for use in Bricks variable pickers and the code editor autocomplete -- **Class Autocomplete** - All `.sf-*` layout classes and `.is-*` state classes appear in the Bricks class input with organized categories -- **Color Palette** - Synchronizes SLASHED color tokens (brand scales, status, and semantic colors) with the Bricks global color palette +- **Variable Pickers** - Registers every `--sf-*` CSS custom property declared in the active bundle (~600 in `optimal`, ~700 in `full`) with the Bricks variable pickers and code editor autocomplete, organized into category groups +- **Class Autocomplete** - Registers every `.sf-*` layout/utility class and `.is-*` state class declared in the active bundle with the Bricks class input, organized into "SLASHED Layout" and "SLASHED State" categories +- **Color Palette** - Synchronizes every `--sf-color-*` token (brand scales including alpha steps, status, and semantic colors) with the Bricks global color palette +- **Dynamic Detection** - The integration parses the loaded CSS bundle at runtime, so registrations stay in sync with whichever bundle (`essential` / `optimal` / `full`) and SLASHED release is active. There is no hand-curated list to drift out of date. ## Requirements @@ -96,6 +97,32 @@ add_filter( 'slashed_bricks/registered_variables', function( $variables ) { } ); ``` +#### `slashed_bricks/inventory` + +Replace the resolved inventory wholesale. Useful for tests, custom forks of the framework, or sites that want to ship their own token list. Expects an array shaped `['variables' => string[], 'sf_classes' => string[], 'is_classes' => string[]]`. + +```php +add_filter( 'slashed_bricks/inventory', function( $inventory ) { + $inventory['variables'][] = '--sf-color-my-custom'; + return $inventory; +} ); +``` + +#### `slashed_bricks/inventory_local_path` + +Override the local CSS path the inventory parses. The filter is authoritative: + +- Return a string to use a specific path (skipping the default candidates). +- Return `false` to skip local resolution entirely (forcing a CDN fetch). +- Return `null` (the default) to use the bundled candidate paths. + +```php +// Use a child-theme copy of the bundle. +add_filter( 'slashed_bricks/inventory_local_path', function() { + return get_stylesheet_directory() . '/assets/slashed.optimal.css'; +} ); +``` + #### `slashed_bricks/color_categories` Filter which color categories to include in the palette. @@ -115,6 +142,13 @@ add_filter( 'slashed_bricks/color_categories', function( $categories ) { ``` integrations/bricks/ slashed-bricks.php Main plugin bootstrap (guards, constants, loader) + data/inventory.json Built-in fallback inventory (used when no + local CSS or CDN is reachable) + includes/class-css-parser.php Pure parser: declared --sf-* properties + + .sf-/.is- class selectors from a CSS string + includes/class-inventory.php Resolves the active CSS bundle (local file, + then CDN with transient cache, then fallback) + and categorizes variables by prefix family includes/class-enqueue.php CSS enqueue for frontend + editor iframe includes/class-variables.php Variable registration for builder pickers includes/class-classes.php Class registration for autocomplete @@ -129,11 +163,32 @@ integrations/bricks/ 2. **Enqueue** (`class-enqueue.php`) - Hooks into `wp_enqueue_scripts` to load the SLASHED CSS bundle. Since Bricks fires `wp_enqueue_scripts` in its editor iframe context, this single hook covers both frontend and builder preview. -3. **Variables** (`class-variables.php`) - Registers organized variable groups via `bricks/builder/i18n` and provides code editor autocomplete data via `bricks/code/get_code_signatures`. +3. **Inventory** (`class-inventory.php`) - The single source of truth for "what does the framework actually ship?" It locates the active CSS bundle (preferring a local file, falling back to the CDN, then to a built-in `data/inventory.json`), parses it once via `class-css-parser.php`, and caches the result via WordPress transients (keyed by file mtime or URL). All three registration classes share one process-local cache so a single page load incurs at most one parse. + +4. **Variables** (`class-variables.php`) - Pulls the categorized variable list from the inventory and registers organized variable groups via `bricks/builder/i18n` and code editor autocomplete data via `bricks/code/get_code_signatures`. + +5. **Classes** (`class-classes.php`) - Pulls `.sf-*` and `.is-*` class lists from the inventory and registers them as locked global classes via `bricks/setup/control_options`, tagged "SLASHED Layout" and "SLASHED State" respectively. + +6. **Colors** (`class-colors.php`) - Pulls every `--sf-color-*` from the inventory, splits them into brand-family categories (one per brand: Primary, Secondary, Tertiary, Action, Neutral, Base), Status, and Semantic groups, and injects them via `bricks/setup/control_options`. Swatches reference CSS variables (`var(--sf-color-*)`) rather than hardcoded values, so they adapt to theme customization and dark mode. + +### Inventory Resolution Order + +The inventory class tries these sources in order, stopping at the first one that succeeds: -4. **Classes** (`class-classes.php`) - Registers all layout and state classes as locked global classes via `bricks/setup/control_options`, making them available in the class picker with categorized organization. +1. **Local file** at `dist/slashed.optimal.css` (relative to the plugin) - covers symlink/in-repo development and copy-installs that include the `dist/` folder. Cached as a transient keyed by file mtime, so edits invalidate automatically. +2. **CDN URL** - whatever `slashed_bricks_get_css_url()` returns, fetched via `wp_remote_get` and cached as a transient for one day. +3. **Built-in fallback** - `data/inventory.json`, generated at release time from `dist/slashed.optimal.css` by `scripts/gen-bricks-inventory.js`. This keeps the plugin functional on hosts that block outbound HTTP. -5. **Colors** (`class-colors.php`) - Injects color entries into the Bricks global color palette via `bricks/setup/control_options`. Colors reference CSS variables (`var(--sf-color-*)`) rather than hardcoded values, so they adapt to theme customization and dark mode. +You can short-circuit step 1 with the `slashed_bricks/inventory_local_path` filter, or replace the resolved inventory entirely with the `slashed_bricks/inventory` filter. + +### Regenerating the Fallback Inventory + +The fallback JSON ships with the plugin and must be regenerated whenever the framework adds or removes tokens or classes. The build script does this automatically: + +```bash +npm run build # rebuilds dist/ AND regenerates inventory.json +npm run bricks:inventory # only regenerate inventory.json +``` ## CSS Bundle diff --git a/integrations/bricks/assets/admin-page.css b/integrations/bricks/assets/admin-page.css index 4a754c16..e81157c7 100644 --- a/integrations/bricks/assets/admin-page.css +++ b/integrations/bricks/assets/admin-page.css @@ -133,7 +133,62 @@ color: #646970; } -/* Color Fields */ +/* Color Fields - paired HEX picker + Advanced raw input */ +.slashed-color-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 12px; +} + +.slashed-color-row .slashed-color-input { + display: flex; + align-items: center; +} + +.slashed-color-row .slashed-color-input[hidden] { + display: none; +} + +.slashed-color-row .slashed-color-raw { + min-width: 280px; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 12px; +} + +.slashed-color-row .slashed-color-toggle { + font-size: 12px; + color: #2271b1; + cursor: pointer; + background: none; + border: 0; + padding: 4px 0; + text-decoration: underline; +} + +.slashed-color-row .slashed-color-toggle:hover, +.slashed-color-row .slashed-color-toggle:focus { + color: #135e96; +} + +/* Toggle button label visibility - driven by data-mode on the row */ +.slashed-color-row .slashed-color-toggle__label-hex, +.slashed-color-row .slashed-color-toggle__label-raw { + display: none; +} +.slashed-color-row[data-mode="hex"] .slashed-color-toggle__label-hex { + display: inline; +} +.slashed-color-row[data-mode="raw"] .slashed-color-toggle__label-raw { + display: inline; +} + +.slashed-color-row .description { + flex-basis: 100%; + margin: 0; +} + +/* Legacy single-input colors (kept for any custom integrations) */ .slashed-color-field-wrap { display: flex; align-items: center; @@ -187,6 +242,16 @@ color: #646970; } +.slashed-range-group .slashed-range-unit { + min-width: 24px; + font-size: 12px; + color: #646970; +} + +.wrap .slashed-tab-content select { + min-width: 180px; +} + /* Live Preview Panel */ .slashed-preview-panel { border: 2px solid #1d2327; diff --git a/integrations/bricks/assets/admin-page.js b/integrations/bricks/assets/admin-page.js index d9509fab..609f21b0 100644 --- a/integrations/bricks/assets/admin-page.js +++ b/integrations/bricks/assets/admin-page.js @@ -43,50 +43,85 @@ } /** - * Initialize WordPress color pickers on .slashed-color-field inputs. + * Initialize the color picker on every paired HEX/Advanced color row. * - * 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. + * Each row contains: + * - .slashed-color-hex - the HEX input wired to wpColorPicker + * - .slashed-color-raw - the Advanced (oklch / any CSS color) input + * - .slashed-color-toggle - swaps which one is active/visible + * + * The active mode is recorded on the row's data-mode attribute, set + * server-side based on the saved value's shape (HEX vs anything else). */ function initColorPickers() { if ( ! $.fn.wpColorPicker ) { return; } + // Curated palette of the rough HEX equivalents of SLASHED's + // default oklch tokens - shown as quick-pick chips in the picker + // dropdown. Real chosen values are still written to the input. 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 + '#4338ca', '#1e293b', '#7c3aed', '#0891b2', + '#64748b', '#fafafa', '#16a34a', '#ca8a04', + '#dc2626', '#2563eb' ]; - $( '.slashed-color-field' ).each( function() { + $( '.slashed-color-hex' ).each( function() { var $input = $( this ); - // Create a visual color reference picker alongside the text input. - var $pickerEl = $( '' ); - $pickerEl.insertAfter( $input ); - - $pickerEl.wpColorPicker( { + $input.wpColorPicker( { palettes: palette, change: function() { - // The color picker is for visual reference only. - // Users should enter oklch() values in the text input. + // Defer one tick so wpColorPicker has finished writing + // the new value into the underlying input, then mirror + // the change into our dirty/preview pipeline. + window.setTimeout( function() { + markDirty(); + updateLivePreview(); + }, 0 ); }, - clear: function() {} + clear: function() { + markDirty(); + updateLivePreview(); + } } ); + } ); - // 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.' - ); + // Toggle button: swap which input is active for a given row. + $( document ).on( 'click', '.slashed-color-toggle', function( e ) { + e.preventDefault(); + var $btn = $( this ); + var $row = $( '#' + $btn.data( 'row' ) ); + if ( ! $row.length ) { + return; } + var current = $row.attr( 'data-mode' ) || 'hex'; + var next = current === 'hex' ? 'raw' : 'hex'; + + $row.attr( 'data-mode', next ); + $row.find( '.slashed-color-input--hex' ).attr( 'hidden', next !== 'hex' ? true : null ); + $row.find( '.slashed-color-input--raw' ).attr( 'hidden', next !== 'raw' ? true : null ); + + // Clear the inactive input so the merged save logic resolves + // unambiguously to the user's chosen value. + if ( next === 'hex' ) { + var $raw = $row.find( '.slashed-color-raw' ); + if ( $raw.val() !== '' ) { + $raw.val( '' ); + } + } else { + // Switching to raw: clear the picker via its API so the + // saved hex doesn't compete with what the user types. + var $hex = $row.find( '.slashed-color-hex' ); + if ( $hex.val() !== '' && $hex.wpColorPicker ) { + $hex.wpColorPicker( 'color', '' ); + } + $row.find( '.slashed-color-raw' ).trigger( 'focus' ); + } + + markDirty(); + updateLivePreview(); } ); } @@ -125,19 +160,31 @@ var declarations = []; + // Resolve active color value per row: prefer the raw input when + // it has content, otherwise use the HEX input. Mirrors what the + // PHP sanitiser does on save, so the preview always matches. + function resolveColor( baseKey ) { + var raw = $( '#' + baseKey + '_raw' ).val(); + if ( raw && raw.length ) { + return raw; + } + var hex = $( '#' + baseKey + '_hex' ).val(); + return hex && hex.length ? hex : ''; + } + // 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(); + var val = resolveColor( 'brand_' + color ); if ( val ) { declarations.push( '--sf-color-' + color + '-light: ' + val ); } } ); statusColors.forEach( function( color ) { - var val = $( '#status_' + color ).val(); + var val = resolveColor( 'status_' + color ); if ( val ) { declarations.push( '--sf-color-' + color + '-light: ' + val ); } @@ -205,6 +252,28 @@ } } ); + // Contrast tab knobs (numeric, no unit). + [ 'contrast_bias', 'contrast_threshold', 'opacity_disabled' ].forEach( function( key ) { + var val = $( '#' + key ).val(); + if ( val !== undefined && val !== '' ) { + declarations.push( '--sf-' + key.replace( /_/g, '-' ) + ': ' + val ); + } + } ); + + // Focus ring metrics (px-suffixed). + [ 'focus_ring_width', 'focus_ring_offset' ].forEach( function( key ) { + var val = $( '#' + key ).val(); + if ( val !== undefined && val !== '' ) { + declarations.push( '--sf-' + key.replace( /_/g, '-' ) + ': ' + val + 'px' ); + } + } ); + + // Focus ring style (enum). + var ringStyle = $( '#focus_ring_style' ).val(); + if ( ringStyle ) { + declarations.push( '--sf-focus-ring-style: ' + ringStyle ); + } + // Build CSS string. var css = ''; if ( declarations.length ) { diff --git a/integrations/bricks/data/inventory.json b/integrations/bricks/data/inventory.json new file mode 100644 index 00000000..91b66aef --- /dev/null +++ b/integrations/bricks/data/inventory.json @@ -0,0 +1,783 @@ +{ + "_meta": { + "source": "dist/slashed.optimal.css", + "generated_at": "2026-05-24T19:41:29.118Z", + "counts": { + "variables": 603, + "sf_classes": 123, + "is_classes": 40 + } + }, + "variables": [ + "--sf-animation-blink", + "--sf-animation-color-pulse", + "--sf-animation-delay-1", + "--sf-animation-delay-2", + "--sf-animation-delay-3", + "--sf-animation-delay-4", + "--sf-animation-delay-5", + "--sf-animation-fade-in", + "--sf-animation-fade-out", + "--sf-animation-float", + "--sf-animation-ping", + "--sf-animation-scale-down", + "--sf-animation-scale-up", + "--sf-animation-slide-in-down", + "--sf-animation-slide-in-left", + "--sf-animation-slide-in-right", + "--sf-animation-slide-in-up", + "--sf-aspect", + "--sf-bento-cols", + "--sf-bento-cols-default", + "--sf-bento-gap", + "--sf-bento-row", + "--sf-bento-row-compact", + "--sf-bento-row-default", + "--sf-bento-row-tall", + "--sf-blur-l", + "--sf-blur-m", + "--sf-blur-s", + "--sf-blur-xl", + "--sf-blur-xs", + "--sf-body-color", + "--sf-body-em-style", + "--sf-body-font-family", + "--sf-body-font-size", + "--sf-body-font-weight", + "--sf-body-line-height", + "--sf-body-strong-weight", + "--sf-body-text-wrap", + "--sf-border-style", + "--sf-border-style-dotted", + "--sf-border-style-soft", + "--sf-border-style-strong", + "--sf-border-width-1", + "--sf-border-width-2", + "--sf-border-width-3", + "--sf-border-width-4", + "--sf-border-width-hairline", + "--sf-box-border-color", + "--sf-box-border-width", + "--sf-box-padding", + "--sf-breakout-width", + "--sf-caret-color", + "--sf-center-gutter", + "--sf-center-max", + "--sf-cluster-align", + "--sf-cluster-gap", + "--sf-cluster-justify", + "--sf-code-font-size", + "--sf-color-action", + "--sf-color-action-100", + "--sf-color-action-200", + "--sf-color-action-300", + "--sf-color-action-400", + "--sf-color-action-50", + "--sf-color-action-500", + "--sf-color-action-600", + "--sf-color-action-700", + "--sf-color-action-800", + "--sf-color-action-900", + "--sf-color-action-950", + "--sf-color-action-a10", + "--sf-color-action-a20", + "--sf-color-action-a30", + "--sf-color-action-a40", + "--sf-color-action-a5", + "--sf-color-action-a50", + "--sf-color-action-a60", + "--sf-color-action-a70", + "--sf-color-action-a80", + "--sf-color-action-a90", + "--sf-color-action-a95", + "--sf-color-action-active", + "--sf-color-action-darker", + "--sf-color-action-ghost", + "--sf-color-action-hover", + "--sf-color-action-lighter", + "--sf-color-action-muted", + "--sf-color-action-subtle", + "--sf-color-action-superdark", + "--sf-color-action-superlight", + "--sf-color-action-xdark", + "--sf-color-action-xlight", + "--sf-color-base", + "--sf-color-base-100", + "--sf-color-base-200", + "--sf-color-base-300", + "--sf-color-base-400", + "--sf-color-base-50", + "--sf-color-base-500", + "--sf-color-base-600", + "--sf-color-base-700", + "--sf-color-base-800", + "--sf-color-base-900", + "--sf-color-base-950", + "--sf-color-base-a10", + "--sf-color-base-a20", + "--sf-color-base-a30", + "--sf-color-base-a40", + "--sf-color-base-a5", + "--sf-color-base-a50", + "--sf-color-base-a60", + "--sf-color-base-a70", + "--sf-color-base-a80", + "--sf-color-base-a90", + "--sf-color-base-a95", + "--sf-color-base-active", + "--sf-color-base-darker", + "--sf-color-base-ghost", + "--sf-color-base-hover", + "--sf-color-base-lighter", + "--sf-color-base-muted", + "--sf-color-base-subtle", + "--sf-color-base-superdark", + "--sf-color-base-superlight", + "--sf-color-base-xdark", + "--sf-color-base-xlight", + "--sf-color-bg", + "--sf-color-bg--active", + "--sf-color-bg--disabled", + "--sf-color-bg--focus", + "--sf-color-bg--hover", + "--sf-color-bg--selected", + "--sf-color-border", + "--sf-color-border--disabled", + "--sf-color-border--focus", + "--sf-color-border--strong", + "--sf-color-border--subtle", + "--sf-color-border--translucent", + "--sf-color-code-bg", + "--sf-color-code-text", + "--sf-color-danger", + "--sf-color-danger-muted", + "--sf-color-danger-strong", + "--sf-color-danger-subtle", + "--sf-color-dim", + "--sf-color-error", + "--sf-color-error-muted", + "--sf-color-error-strong", + "--sf-color-error-subtle", + "--sf-color-heading", + "--sf-color-info", + "--sf-color-info-muted", + "--sf-color-info-strong", + "--sf-color-info-subtle", + "--sf-color-inverse", + "--sf-color-link", + "--sf-color-link--active", + "--sf-color-link--disabled", + "--sf-color-link--hover", + "--sf-color-link--underline", + "--sf-color-link--visited", + "--sf-color-mark-bg", + "--sf-color-mark-text", + "--sf-color-neutral", + "--sf-color-neutral-100", + "--sf-color-neutral-200", + "--sf-color-neutral-300", + "--sf-color-neutral-400", + "--sf-color-neutral-50", + "--sf-color-neutral-500", + "--sf-color-neutral-600", + "--sf-color-neutral-700", + "--sf-color-neutral-800", + "--sf-color-neutral-900", + "--sf-color-neutral-950", + "--sf-color-neutral-a10", + "--sf-color-neutral-a20", + "--sf-color-neutral-a30", + "--sf-color-neutral-a40", + "--sf-color-neutral-a5", + "--sf-color-neutral-a50", + "--sf-color-neutral-a60", + "--sf-color-neutral-a70", + "--sf-color-neutral-a80", + "--sf-color-neutral-a90", + "--sf-color-neutral-a95", + "--sf-color-neutral-active", + "--sf-color-neutral-darker", + "--sf-color-neutral-ghost", + "--sf-color-neutral-hover", + "--sf-color-neutral-lighter", + "--sf-color-neutral-muted", + "--sf-color-neutral-subtle", + "--sf-color-neutral-superdark", + "--sf-color-neutral-superlight", + "--sf-color-neutral-xdark", + "--sf-color-neutral-xlight", + "--sf-color-overlay", + "--sf-color-primary", + "--sf-color-primary-100", + "--sf-color-primary-200", + "--sf-color-primary-300", + "--sf-color-primary-400", + "--sf-color-primary-50", + "--sf-color-primary-500", + "--sf-color-primary-600", + "--sf-color-primary-700", + "--sf-color-primary-800", + "--sf-color-primary-900", + "--sf-color-primary-950", + "--sf-color-primary-a10", + "--sf-color-primary-a20", + "--sf-color-primary-a30", + "--sf-color-primary-a40", + "--sf-color-primary-a5", + "--sf-color-primary-a50", + "--sf-color-primary-a60", + "--sf-color-primary-a70", + "--sf-color-primary-a80", + "--sf-color-primary-a90", + "--sf-color-primary-a95", + "--sf-color-primary-active", + "--sf-color-primary-darker", + "--sf-color-primary-ghost", + "--sf-color-primary-hover", + "--sf-color-primary-light", + "--sf-color-primary-lighter", + "--sf-color-primary-muted", + "--sf-color-primary-subtle", + "--sf-color-primary-superdark", + "--sf-color-primary-superlight", + "--sf-color-primary-xdark", + "--sf-color-primary-xlight", + "--sf-color-raised", + "--sf-color-scheme", + "--sf-color-secondary", + "--sf-color-secondary-100", + "--sf-color-secondary-200", + "--sf-color-secondary-300", + "--sf-color-secondary-400", + "--sf-color-secondary-50", + "--sf-color-secondary-500", + "--sf-color-secondary-600", + "--sf-color-secondary-700", + "--sf-color-secondary-800", + "--sf-color-secondary-900", + "--sf-color-secondary-950", + "--sf-color-secondary-a10", + "--sf-color-secondary-a20", + "--sf-color-secondary-a30", + "--sf-color-secondary-a40", + "--sf-color-secondary-a5", + "--sf-color-secondary-a50", + "--sf-color-secondary-a60", + "--sf-color-secondary-a70", + "--sf-color-secondary-a80", + "--sf-color-secondary-a90", + "--sf-color-secondary-a95", + "--sf-color-secondary-active", + "--sf-color-secondary-darker", + "--sf-color-secondary-ghost", + "--sf-color-secondary-hover", + "--sf-color-secondary-lighter", + "--sf-color-secondary-muted", + "--sf-color-secondary-subtle", + "--sf-color-secondary-superdark", + "--sf-color-secondary-superlight", + "--sf-color-secondary-xdark", + "--sf-color-secondary-xlight", + "--sf-color-selection-bg", + "--sf-color-selection-text", + "--sf-color-success", + "--sf-color-success-muted", + "--sf-color-success-strong", + "--sf-color-success-subtle", + "--sf-color-surface", + "--sf-color-tertiary", + "--sf-color-tertiary-100", + "--sf-color-tertiary-200", + "--sf-color-tertiary-300", + "--sf-color-tertiary-400", + "--sf-color-tertiary-50", + "--sf-color-tertiary-500", + "--sf-color-tertiary-600", + "--sf-color-tertiary-700", + "--sf-color-tertiary-800", + "--sf-color-tertiary-900", + "--sf-color-tertiary-950", + "--sf-color-tertiary-a10", + "--sf-color-tertiary-a20", + "--sf-color-tertiary-a30", + "--sf-color-tertiary-a40", + "--sf-color-tertiary-a5", + "--sf-color-tertiary-a50", + "--sf-color-tertiary-a60", + "--sf-color-tertiary-a70", + "--sf-color-tertiary-a80", + "--sf-color-tertiary-a90", + "--sf-color-tertiary-a95", + "--sf-color-tertiary-active", + "--sf-color-tertiary-darker", + "--sf-color-tertiary-ghost", + "--sf-color-tertiary-hover", + "--sf-color-tertiary-lighter", + "--sf-color-tertiary-muted", + "--sf-color-tertiary-subtle", + "--sf-color-tertiary-superdark", + "--sf-color-tertiary-superlight", + "--sf-color-tertiary-xdark", + "--sf-color-tertiary-xlight", + "--sf-color-text", + "--sf-color-text--disabled", + "--sf-color-text--inverse", + "--sf-color-text--muted", + "--sf-color-text--on-action", + "--sf-color-text--on-base", + "--sf-color-text--on-danger", + "--sf-color-text--on-error", + "--sf-color-text--on-info", + "--sf-color-text--on-inverse", + "--sf-color-text--on-neutral", + "--sf-color-text--on-primary", + "--sf-color-text--on-secondary", + "--sf-color-text--on-success", + "--sf-color-text--on-tertiary", + "--sf-color-text--on-warning", + "--sf-color-text--placeholder", + "--sf-color-text--secondary", + "--sf-color-warning", + "--sf-color-warning-muted", + "--sf-color-warning-strong", + "--sf-color-warning-subtle", + "--sf-color-well", + "--sf-component-pad", + "--sf-container-default", + "--sf-container-full", + "--sf-container-narrow", + "--sf-container-prose", + "--sf-container-wide", + "--sf-content-gap", + "--sf-content-width", + "--sf-contrast-bias", + "--sf-contrast-threshold", + "--sf-cover-min-height", + "--sf-cover-padding", + "--sf-current-font-weight", + "--sf-divider-color", + "--sf-divider-style", + "--sf-divider-width", + "--sf-drop-shadow-l", + "--sf-drop-shadow-m", + "--sf-drop-shadow-s", + "--sf-duration-fast", + "--sf-duration-instant", + "--sf-duration-none", + "--sf-duration-normal", + "--sf-duration-slow", + "--sf-duration-slower", + "--sf-ease-bounce", + "--sf-ease-elastic", + "--sf-ease-in", + "--sf-ease-in-out", + "--sf-ease-linear", + "--sf-ease-out", + "--sf-ease-overshoot", + "--sf-ease-spring", + "--sf-field-block", + "--sf-field-border-color", + "--sf-field-required-marker", + "--sf-field-text-color", + "--sf-flow-space", + "--sf-focus-ring-color", + "--sf-focus-ring-offset", + "--sf-focus-ring-shadow", + "--sf-focus-ring-style", + "--sf-focus-ring-width", + "--sf-font-body", + "--sf-font-display", + "--sf-font-features", + "--sf-font-geometric", + "--sf-font-heading", + "--sf-font-humanist", + "--sf-font-mono", + "--sf-font-slab", + "--sf-font-variation", + "--sf-font-weight-black", + "--sf-font-weight-body", + "--sf-font-weight-bold", + "--sf-font-weight-display", + "--sf-font-weight-extrabold", + "--sf-font-weight-extralight", + "--sf-font-weight-heading", + "--sf-font-weight-light", + "--sf-font-weight-medium", + "--sf-font-weight-normal", + "--sf-font-weight-semibold", + "--sf-font-weight-thin", + "--sf-frame-ratio", + "--sf-gap", + "--sf-gradient-brand", + "--sf-gradient-fade--b", + "--sf-gradient-fade--l", + "--sf-gradient-fade--r", + "--sf-gradient-fade--t", + "--sf-gradient-primary", + "--sf-gradient-secondary", + "--sf-gradient-surface", + "--sf-gradient-tertiary", + "--sf-grid-gap", + "--sf-grid-min", + "--sf-grid-min-default", + "--sf-grid-min-l", + "--sf-grid-min-m", + "--sf-grid-min-s", + "--sf-grid-min-xl", + "--sf-grid-min-xs", + "--sf-h1-font-weight", + "--sf-h1-letter-spacing", + "--sf-h1-line-height", + "--sf-h1-size", + "--sf-h2-font-weight", + "--sf-h2-letter-spacing", + "--sf-h2-line-height", + "--sf-h2-size", + "--sf-h3-font-weight", + "--sf-h3-letter-spacing", + "--sf-h3-line-height", + "--sf-h3-size", + "--sf-h4-font-weight", + "--sf-h4-letter-spacing", + "--sf-h4-line-height", + "--sf-h4-size", + "--sf-h5-font-weight", + "--sf-h5-letter-spacing", + "--sf-h5-line-height", + "--sf-h5-size", + "--sf-h6-font-weight", + "--sf-h6-letter-spacing", + "--sf-h6-line-height", + "--sf-h6-size", + "--sf-header-height", + "--sf-heading-color", + "--sf-heading-font-family", + "--sf-heading-text-wrap", + "--sf-icon-box-bg", + "--sf-icon-box-border", + "--sf-icon-box-pad", + "--sf-icon-box-radius", + "--sf-icon-l", + "--sf-icon-m", + "--sf-icon-s", + "--sf-icon-size", + "--sf-icon-xl", + "--sf-icon-xs", + "--sf-imposter-margin", + "--sf-is-active", + "--sf-is-current", + "--sf-is-dark", + "--sf-is-open", + "--sf-is-pressed", + "--sf-leading-normal", + "--sf-leading-relaxed", + "--sf-leading-snug", + "--sf-leading-tight", + "--sf-line-clamp", + "--sf-mask-scrim-end", + "--sf-mask-scrim-start", + "--sf-motion-scale", + "--sf-opacity-0", + "--sf-opacity-10", + "--sf-opacity-100", + "--sf-opacity-25", + "--sf-opacity-50", + "--sf-opacity-75", + "--sf-opacity-disabled", + "--sf-optical-sizing", + "--sf-perspective-far", + "--sf-perspective-near", + "--sf-perspective-normal", + "--sf-print-base-size", + "--sf-print-page-margin", + "--sf-print-page-size", + "--sf-prose-paragraph", + "--sf-radius-2xl", + "--sf-radius-3xl", + "--sf-radius-4xl", + "--sf-radius-full", + "--sf-radius-l", + "--sf-radius-m", + "--sf-radius-none", + "--sf-radius-s", + "--sf-radius-scale", + "--sf-radius-xl", + "--sf-radius-xs", + "--sf-ratio-cinema", + "--sf-ratio-golden", + "--sf-ratio-photo", + "--sf-ratio-portrait", + "--sf-ratio-square", + "--sf-ratio-video", + "--sf-reel-gap", + "--sf-reel-height", + "--sf-reel-item-width", + "--sf-safe-bottom", + "--sf-safe-left", + "--sf-safe-right", + "--sf-safe-top", + "--sf-scroll-shadow-size", + "--sf-scroll-timeline-range-end", + "--sf-scroll-timeline-range-start", + "--sf-scrollbar-thumb", + "--sf-scrollbar-track", + "--sf-section-pad", + "--sf-section-pad--l", + "--sf-section-pad--m", + "--sf-section-pad--s", + "--sf-section-pad--xl", + "--sf-shadow-2xl", + "--sf-shadow-color", + "--sf-shadow-glow", + "--sf-shadow-glow-color", + "--sf-shadow-inner", + "--sf-shadow-l", + "--sf-shadow-m", + "--sf-shadow-none", + "--sf-shadow-s", + "--sf-shadow-strength", + "--sf-shadow-xl", + "--sf-shadow-xs", + "--sf-sidebar-gap", + "--sf-sidebar-min-width", + "--sf-sidebar-width", + "--sf-sidebar-width-default", + "--sf-size-l", + "--sf-size-m", + "--sf-size-s", + "--sf-size-xl", + "--sf-size-xs", + "--sf-space-2xl", + "--sf-space-2xs", + "--sf-space-3xl", + "--sf-space-4xl", + "--sf-space-content", + "--sf-space-gap", + "--sf-space-gutter", + "--sf-space-l", + "--sf-space-m", + "--sf-space-none", + "--sf-space-px", + "--sf-space-s", + "--sf-space-scale", + "--sf-space-xl", + "--sf-space-xs", + "--sf-stack-gap", + "--sf-sticky-offset", + "--sf-stroke-bold", + "--sf-stroke-heavy", + "--sf-stroke-regular", + "--sf-stroke-thin", + "--sf-switcher-gap", + "--sf-switcher-threshold", + "--sf-text-2xl", + "--sf-text-2xs", + "--sf-text-3xl", + "--sf-text-4xl", + "--sf-text-display-l", + "--sf-text-display-m", + "--sf-text-display-s", + "--sf-text-display-scale", + "--sf-text-l", + "--sf-text-m", + "--sf-text-s", + "--sf-text-scale", + "--sf-text-shadow-l", + "--sf-text-shadow-m", + "--sf-text-shadow-none", + "--sf-text-shadow-s", + "--sf-text-xl", + "--sf-text-xs", + "--sf-touch-target", + "--sf-tracking-normal", + "--sf-tracking-tight", + "--sf-tracking-wide", + "--sf-tracking-wider", + "--sf-tracking-widest", + "--sf-transition-all", + "--sf-transition-colors", + "--sf-transition-enter", + "--sf-transition-exit", + "--sf-transition-fast", + "--sf-transition-opacity", + "--sf-transition-shadow", + "--sf-transition-slow", + "--sf-transition-transform", + "--sf-truncate-suffix", + "--sf-z-base", + "--sf-z-below", + "--sf-z-high", + "--sf-z-low", + "--sf-z-max", + "--sf-z-mid", + "--sf-z-raised", + "--sf-z-top" + ], + "sf_classes": [ + "sf-alternate", + "sf-aspect", + "sf-bento", + "sf-bento--2", + "sf-bento--4", + "sf-bento--compact", + "sf-bento--tall", + "sf-box", + "sf-breakout", + "sf-center", + "sf-center--intrinsic", + "sf-clickable-parent", + "sf-cluster", + "sf-cluster--2xs", + "sf-cluster--between", + "sf-cluster--center", + "sf-cluster--end", + "sf-cluster--l", + "sf-cluster--m", + "sf-cluster--no-wrap", + "sf-cluster--s", + "sf-cluster--xl", + "sf-cluster--xs", + "sf-color-pulse", + "sf-container", + "sf-container--full", + "sf-container--narrow", + "sf-container--prose", + "sf-container--wide", + "sf-content-grid", + "sf-cover", + "sf-cover--max", + "sf-cover--min", + "sf-cover--padding-l", + "sf-cover--padding-s", + "sf-cover__center", + "sf-divider", + "sf-divider--vertical", + "sf-equal-height", + "sf-fade-in", + "sf-fade-out", + "sf-flow", + "sf-frame", + "sf-frame--3-2", + "sf-frame--4-3", + "sf-frame--cinema", + "sf-frame--golden", + "sf-frame--portrait", + "sf-frame--square", + "sf-frame--video", + "sf-full-bleed", + "sf-grid", + "sf-grid--dense", + "sf-grid--fit", + "sf-grid--l", + "sf-grid--m", + "sf-grid--s", + "sf-grid--xl", + "sf-grid--xs", + "sf-grid-1", + "sf-grid-1-2", + "sf-grid-1-3", + "sf-grid-2", + "sf-grid-2-1", + "sf-grid-3", + "sf-grid-3-1", + "sf-grid-4", + "sf-grid-6", + "sf-icon", + "sf-icon--boxed", + "sf-icon--l", + "sf-icon--m", + "sf-icon--s", + "sf-icon--xl", + "sf-icon--xs", + "sf-imposter", + "sf-imposter--contain", + "sf-imposter--fixed", + "sf-line-clamp-2", + "sf-line-clamp-3", + "sf-line-clamp-N", + "sf-no-tap-highlight", + "sf-not-prose", + "sf-overflow-fade", + "sf-pancake", + "sf-prose", + "sf-reel", + "sf-scale-down", + "sf-scale-up", + "sf-scroll-shadow", + "sf-scroll-snap", + "sf-section", + "sf-section--l", + "sf-section--m", + "sf-section--s", + "sf-section--xl", + "sf-section-group", + "sf-sidebar", + "sf-sidebar--narrow", + "sf-sidebar--right", + "sf-sidebar--wide", + "sf-slide-in-down", + "sf-slide-in-left", + "sf-slide-in-right", + "sf-slide-in-up", + "sf-stack", + "sf-stack--2xl", + "sf-stack--2xs", + "sf-stack--3xl", + "sf-stack--center", + "sf-stack--end", + "sf-stack--l", + "sf-stack--m", + "sf-stack--s", + "sf-stack--stretch", + "sf-stack--xl", + "sf-stack--xs", + "sf-subgrid", + "sf-subgrid-rows", + "sf-switcher", + "sf-switcher--no-wrap", + "sf-switcher--vertical", + "sf-truncate" + ], + "is_classes": [ + "is-active", + "is-busy", + "is-clickable", + "is-clipped", + "is-collapsed", + "is-current", + "is-danger", + "is-disabled", + "is-draggable", + "is-dragging", + "is-drop-target", + "is-empty", + "is-error", + "is-expanded", + "is-fixed", + "is-focused", + "is-fullscreen", + "is-hidden", + "is-highlighted", + "is-info", + "is-invalid", + "is-invisible", + "is-loading", + "is-open", + "is-overlay", + "is-pending", + "is-pinned", + "is-pressed", + "is-readonly", + "is-resizable", + "is-scrollable", + "is-selected", + "is-skeleton", + "is-sticky", + "is-success", + "is-truncated", + "is-unselectable", + "is-valid", + "is-visible", + "is-warning" + ] +} diff --git a/integrations/bricks/includes/class-admin-page.php b/integrations/bricks/includes/class-admin-page.php index c6ed4579..d98e8da8 100644 --- a/integrations/bricks/includes/class-admin-page.php +++ b/integrations/bricks/includes/class-admin-page.php @@ -45,6 +45,7 @@ class Slashed_Bricks_Admin_Page { public function __construct() { $this->tabs = array( 'colors' => 'Colors', + 'contrast' => 'Contrast', 'typography' => 'Typography', 'spacing' => 'Spacing', 'radius' => 'Radius', @@ -194,6 +195,13 @@ public function handle_save() { * @return array Sanitized data. */ private function sanitize_section( $section, $data ) { + // Colors get a specialised merger because each token has two paired + // inputs in the form (HEX picker + raw advanced) that resolve to a + // single stored value. + if ( 'colors' === $section ) { + return $this->sanitize_color_section( $data ); + } + $sanitized = array(); foreach ( $data as $key => $value ) { @@ -213,6 +221,102 @@ function ( $v ) { return $sanitized; } + /** + * Merge paired HEX/raw color inputs into a single stored value per token. + * + * The Colors tab renders two inputs per color: a HEX picker (suffix + * "_hex") and an Advanced raw input (suffix "_raw") that accepts any + * CSS color string (oklch, rgb, hsl, etc.). The raw value wins when + * non-empty; otherwise the HEX value is used. The merged result is + * stored under the base key (e.g. "brand_primary"), preserving the + * storage shape the CSS generator already expects. + * + * Legacy direct writes (key without suffix) are still accepted, so + * imports or pre-upgrade saved values keep working. + * + * @param array $data Raw form data for the colors section. + * @return array Sanitized color settings keyed by base token name. + */ + private function sanitize_color_section( $data ) { + $grouped = array(); + + foreach ( $data as $key => $value ) { + $key = (string) $key; + if ( '' === $key || ! is_string( $value ) ) { + continue; + } + + $clean_value = $this->sanitize_css_value( sanitize_text_field( $value ) ); + $clean_value = trim( $clean_value ); + + if ( $this->key_has_suffix( $key, '_hex' ) ) { + $base = sanitize_key( substr( $key, 0, -4 ) ); + if ( '' !== $base ) { + $grouped[ $base ]['hex'] = $clean_value; + } + } elseif ( $this->key_has_suffix( $key, '_raw' ) ) { + $base = sanitize_key( substr( $key, 0, -4 ) ); + if ( '' !== $base ) { + $grouped[ $base ]['raw'] = $clean_value; + } + } else { + $base = sanitize_key( $key ); + if ( '' !== $base ) { + $grouped[ $base ]['direct'] = $clean_value; + } + } + } + + $sanitized = array(); + foreach ( $grouped as $base => $parts ) { + if ( ! empty( $parts['raw'] ) ) { + $sanitized[ $base ] = $parts['raw']; + } elseif ( ! empty( $parts['hex'] ) ) { + $sanitized[ $base ] = $parts['hex']; + } elseif ( isset( $parts['direct'] ) && '' !== $parts['direct'] ) { + $sanitized[ $base ] = $parts['direct']; + } + // All-empty rows are omitted so the framework default applies. + } + + return $sanitized; + } + + /** + * Polyfill for str_ends_with() since the plugin supports PHP 7.4. + * + * @param string $haystack Subject string. + * @param string $needle Suffix to test for. + * @return bool + */ + private function key_has_suffix( $haystack, $needle ) { + $nl = strlen( $needle ); + if ( $nl === 0 ) { + return true; + } + $hl = strlen( $haystack ); + if ( $hl < $nl ) { + return false; + } + return substr( $haystack, -$nl ) === $needle; + } + + /** + * Determine whether a stored value looks like a HEX color. + * + * Accepts 3, 4, 6, and 8-digit forms. Anything else (oklch, rgb, hsl, + * named color, var()...) is treated as a raw advanced value so the + * UI starts in the Advanced input and won't truncate it via the + * HEX-only color picker. + * + * @param string $value Stored color value. + * @return bool + */ + private function is_hex_color( $value ) { + return is_string( $value ) + && 1 === preg_match( '/^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/', trim( $value ) ); + } + /** * Sanitize a value for safe use in CSS declarations. * @@ -365,25 +469,37 @@ private function render_live_preview() { /** * Render the Colors tab. * + * Each color exposes two paired inputs: + * - A HEX picker (real WP color picker) - the primary path. + * - An "Advanced (raw value)" input that accepts any CSS color + * string (oklch, rgb, hsl, var(), ...) - used when the user + * wants to keep an authoring format the picker can't represent. + * + * The active mode is auto-detected on render: if the saved value + * is a HEX literal we start in HEX mode; if it's anything else + * (oklch, named, etc.) we start in Advanced mode. + * * @param array $settings Saved settings for this tab. */ private function render_tab_colors( $settings ) { $defaults = Slashed_Bricks_Token_Defaults::get_colors(); echo '

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

'; - echo '

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

'; + echo '

' . esc_html__( 'Pick a color via the HEX picker, or switch to "Advanced" to paste an oklch() / rgb() / any other CSS color value. Whatever you save is fed straight into the framework as the source of the brand 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 ''; + foreach ( $defaults['brand'] as $name => $default_oklch ) { + $base_key = 'brand_' . $name; + $saved = isset( $settings[ $base_key ] ) ? (string) $settings[ $base_key ] : ''; + $hex_default_hint = isset( $defaults['brand_hex_hints'][ $name ] ) ? $defaults['brand_hex_hints'][ $name ] : ''; + $this->render_color_row( + $base_key, + ucfirst( $name ), + $saved, + $hex_default_hint, + $default_oklch, + '--sf-color-' . $name . '-light' + ); } echo '
'; - echo ''; - echo '

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

'; - echo '
'; @@ -391,21 +507,274 @@ private function render_tab_colors( $settings ) { 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 ''; + foreach ( $defaults['status'] as $name => $default_oklch ) { + $base_key = 'status_' . $name; + $saved = isset( $settings[ $base_key ] ) ? (string) $settings[ $base_key ] : ''; + $hex_default_hint = isset( $defaults['status_hex_hints'][ $name ] ) ? $defaults['status_hex_hints'][ $name ] : ''; + $this->render_color_row( + $base_key, + ucfirst( $name ), + $saved, + $hex_default_hint, + $default_oklch, + '--sf-color-' . $name . '-light' + ); } echo '
'; - echo ''; - echo '

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

'; - echo '
'; } + /** + * Render a single color row with paired HEX and Advanced inputs. + * + * @param string $base_key Stored token key (e.g. "brand_primary"). + * @param string $label Display label (e.g. "Primary"). + * @param string $saved Currently saved value (any CSS color string, or empty). + * @param string $hex_hint HEX placeholder shown when nothing is saved. + * @param string $oklch_hint OKLCH placeholder shown in the Advanced input. + * @param string $css_var Underlying CSS custom property name (informational only). + */ + private function render_color_row( $base_key, $label, $saved, $hex_hint, $oklch_hint, $css_var ) { + $is_hex = $this->is_hex_color( $saved ); + $start_mode = ( $is_hex || '' === $saved ) ? 'hex' : 'raw'; + $hex_value = $is_hex ? $saved : ''; + $raw_value = $is_hex ? '' : $saved; + $row_id = 'slashed-color-row-' . $base_key; + + ?> + + + + + +
+ +
> + +
+ +
> + +
+ + + +

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

' . esc_html__( 'Fine-tune how the framework derives shades and picks readable text colors against your brand colors.', 'slashed-bricks' ) . '

'; + echo ''; + + $this->render_range_field( + $settings, + 'contrast_bias', + __( 'Contrast bias', 'slashed-bricks' ), + __( 'Shifts every derived shade up (positive) or down (negative). Useful to brighten or dim the entire scale without re-picking source colors.', 'slashed-bricks' ), + -0.2, + 0.2, + 0.01, + $defaults['contrast_bias'], + '--sf-contrast-bias' + ); + + $this->render_range_field( + $settings, + 'contrast_threshold', + __( 'Contrast threshold', 'slashed-bricks' ), + __( 'Lightness threshold used to pick text-on-color (white vs. black). Lower values prefer dark text on more colors; higher values prefer white text.', 'slashed-bricks' ), + 0, + 1, + 0.01, + $defaults['contrast_threshold'], + '--sf-contrast-threshold' + ); + + echo '
'; + + echo '

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

'; + echo ''; + + $this->render_range_field( + $settings, + 'opacity_disabled', + __( 'Disabled opacity', 'slashed-bricks' ), + __( 'Opacity applied to disabled UI elements (buttons, inputs, links).', 'slashed-bricks' ), + 0, + 1, + 0.01, + $defaults['opacity_disabled'], + '--sf-opacity-disabled' + ); + + echo '
'; + + echo '

' . esc_html__( 'Focus ring', 'slashed-bricks' ) . '

'; + echo '

' . esc_html__( 'Visual indicator drawn around keyboard-focused elements. Lower the offset to tighten the ring, raise it to give the element more breathing room.', 'slashed-bricks' ) . '

'; + echo ''; + + $this->render_range_field( + $settings, + 'focus_ring_width', + __( 'Ring width', 'slashed-bricks' ), + '', + 0, + 8, + 0.5, + $defaults['focus_ring_width'], + '--sf-focus-ring-width', + 'px' + ); + + $this->render_range_field( + $settings, + 'focus_ring_offset', + __( 'Ring offset', 'slashed-bricks' ), + '', + 0, + 8, + 0.5, + $defaults['focus_ring_offset'], + '--sf-focus-ring-offset', + 'px' + ); + + // Focus ring style is a small enum, so render a select instead of a slider. + $style_value = isset( $settings['focus_ring_style'] ) ? (string) $settings['focus_ring_style'] : ''; + $style_default = $defaults['focus_ring_style']; + $style_options = array( 'solid', 'dashed', 'dotted', 'double', 'none' ); + echo ''; + echo ''; + echo ''; + echo ''; + + echo '
'; + echo ''; + echo '

--sf-focus-ring-style

'; + echo '
'; + } + + /** + * Render a paired range + number input that share a sync id. + * + * Markup matches what initRangeSync() in admin-page.js looks for: + * .slashed-range-input and .slashed-number-input sharing data-sync. + * + * Only the number input carries the form `name`, so submission is + * unambiguous regardless of which control the user touched last. + * + * @param array $settings Saved settings for the section. + * @param string $key Token key (also used as data-sync id). + * @param string $label Field label. + * @param string $description Help text shown beneath the inputs. + * @param float|int $min Minimum allowed value. + * @param float|int $max Maximum allowed value. + * @param float $step Step granularity. + * @param float|int $default Factory default (used as placeholder). + * @param string $css_var Underlying CSS custom property (informational only). + * @param string $unit Optional unit label rendered after the inputs (e.g. 'px'). + */ + private function render_range_field( $settings, $key, $label, $description, $min, $max, $step, $default, $css_var, $unit = '' ) { + $value = isset( $settings[ $key ] ) ? $settings[ $key ] : ''; + $slider_val = '' !== $value ? $value : $default; + + echo ''; + echo ''; + echo ''; + echo '
'; + + printf( + '', + esc_attr( $key ), + esc_attr( (string) $min ), + esc_attr( (string) $max ), + esc_attr( (string) $step ), + esc_attr( (string) $slider_val ) + ); + + printf( + '', + esc_attr( $key ), + esc_attr( (string) $min ), + esc_attr( (string) $max ), + esc_attr( (string) $step ), + esc_attr( (string) $value ), + esc_attr( (string) $default ) + ); + + if ( '' !== $unit ) { + echo '' . esc_html( $unit ) . ''; + } + + echo '
'; + + if ( '' !== $description ) { + echo '

' . esc_html( $description ) . '

'; + } + echo '

' . esc_html( $css_var ) . '

'; + echo ''; + echo ''; + } + /** * Render the Typography tab. * diff --git a/integrations/bricks/includes/class-classes.php b/integrations/bricks/includes/class-classes.php index aa3fe5c5..79013e2e 100644 --- a/integrations/bricks/includes/class-classes.php +++ b/integrations/bricks/includes/class-classes.php @@ -12,8 +12,13 @@ /** * Class Slashed_Bricks_Classes * - * Registers SLASHED utility and layout classes with Bricks Builder + * Registers SLASHED utility, layout, and state classes with Bricks Builder * for autocomplete and the global classes panel. + * + * The class list is derived from the active CSS bundle via + * Slashed_Bricks_Inventory, so every selector the framework actually ships + * shows up in the Bricks class picker - no hand-curated list to drift out + * of sync with releases. */ class Slashed_Bricks_Classes { @@ -31,9 +36,13 @@ public function __construct() { * @return array Modified control options. */ public function register_global_classes( $control_options ) { + if ( ! is_array( $control_options ) ) { + $control_options = array(); + } + $classes = $this->get_classes(); - if ( ! isset( $control_options['globalClassesLocked'] ) ) { + if ( ! isset( $control_options['globalClassesLocked'] ) || ! is_array( $control_options['globalClassesLocked'] ) ) { $control_options['globalClassesLocked'] = array(); } @@ -47,28 +56,27 @@ public function register_global_classes( $control_options ) { /** * Get all SLASHED classes formatted for Bricks. * + * Layout (.sf-*) classes are tagged "SLASHED Layout"; state (.is-*) + * classes are tagged "SLASHED State". + * * @return array Array of class entries. */ public function get_classes() { $classes = array(); - // Layout classes. - $layout = $this->get_layout_classes(); - foreach ( $layout as $class_name ) { + foreach ( Slashed_Bricks_Inventory::get_sf_classes() as $name ) { $classes[] = array( - 'id' => $class_name, - 'name' => $class_name, + 'id' => $name, + 'name' => $name, 'settings' => array( 'locked' => true ), 'category' => 'SLASHED Layout', ); } - // State classes. - $states = $this->get_state_classes(); - foreach ( $states as $class_name ) { + foreach ( Slashed_Bricks_Inventory::get_is_classes() as $name ) { $classes[] = array( - 'id' => $class_name, - 'name' => $class_name, + 'id' => $name, + 'name' => $name, 'settings' => array( 'locked' => true ), 'category' => 'SLASHED State', ); @@ -81,229 +89,4 @@ public function get_classes() { */ return apply_filters( 'slashed_bricks/registered_classes', $classes ); } - - /** - * Get all SLASHED layout classes. - * - * @return array - */ - private function get_layout_classes() { - return array( - // Section. - 'sf-section', - 'sf-section--s', - 'sf-section--m', - 'sf-section--l', - 'sf-section--xl', - 'sf-section-group', - - // Divider. - 'sf-divider', - 'sf-divider--vertical', - - // Container. - 'sf-container', - 'sf-container--narrow', - 'sf-container--prose', - 'sf-container--wide', - 'sf-container--full', - - // Stack. - 'sf-stack', - 'sf-stack--2xs', - 'sf-stack--xs', - 'sf-stack--s', - 'sf-stack--m', - 'sf-stack--l', - 'sf-stack--xl', - 'sf-stack--2xl', - 'sf-stack--3xl', - 'sf-stack--center', - 'sf-stack--end', - 'sf-stack--stretch', - - // Box. - 'sf-box', - - // Center. - 'sf-center', - 'sf-center--intrinsic', - - // Cluster. - 'sf-cluster', - 'sf-cluster--2xs', - 'sf-cluster--xs', - 'sf-cluster--s', - 'sf-cluster--m', - 'sf-cluster--l', - 'sf-cluster--xl', - 'sf-cluster--no-wrap', - 'sf-cluster--center', - 'sf-cluster--end', - 'sf-cluster--between', - - // Sidebar. - 'sf-sidebar', - 'sf-sidebar--right', - 'sf-sidebar--narrow', - 'sf-sidebar--wide', - - // Switcher. - 'sf-switcher', - 'sf-switcher--no-wrap', - 'sf-switcher--vertical', - - // Grid. - 'sf-grid', - 'sf-grid--fit', - 'sf-grid--xs', - 'sf-grid--s', - 'sf-grid--m', - 'sf-grid--l', - 'sf-grid--xl', - 'sf-grid--dense', - 'sf-grid-1', - 'sf-grid-2', - 'sf-grid-3', - 'sf-grid-4', - 'sf-grid-6', - 'sf-grid-1-2', - 'sf-grid-2-1', - 'sf-grid-1-3', - 'sf-grid-3-1', - - // Icon. - 'sf-icon', - 'sf-icon--xs', - 'sf-icon--s', - 'sf-icon--m', - 'sf-icon--l', - 'sf-icon--xl', - - // Cover. - 'sf-cover', - 'sf-cover__center', - 'sf-cover--min', - 'sf-cover--max', - 'sf-cover--padding-s', - 'sf-cover--padding-l', - - // Frame. - 'sf-frame', - 'sf-frame--square', - 'sf-frame--video', - 'sf-frame--cinema', - 'sf-frame--portrait', - 'sf-frame--4-3', - 'sf-frame--3-2', - 'sf-frame--golden', - - // Reel. - 'sf-reel', - - // Imposter. - 'sf-imposter', - 'sf-imposter--fixed', - 'sf-imposter--contain', - - // Alternate. - 'sf-alternate', - - // Pancake. - 'sf-pancake', - - // Bento. - 'sf-bento', - 'sf-bento--2', - 'sf-bento--4', - 'sf-bento--compact', - 'sf-bento--tall', - - // Subgrid. - 'sf-subgrid', - 'sf-subgrid-rows', - - // Prose. - 'sf-prose', - 'sf-not-prose', - - // Content Grid. - 'sf-content-grid', - 'sf-breakout', - 'sf-full-bleed', - ); - } - - /** - * Get all SLASHED state classes. - * - * @return array - */ - private function get_state_classes() { - return array( - // Visibility. - 'is-hidden', - 'is-invisible', - 'is-visible', - - // Interactivity. - 'is-disabled', - 'is-readonly', - - // Loading. - 'is-loading', - 'is-busy', - 'is-pending', - 'is-skeleton', - - // Active. - 'is-active', - 'is-selected', - 'is-current', - 'is-highlighted', - 'is-pressed', - - // Disclosure. - 'is-open', - 'is-collapsed', - 'is-expanded', - - // Validation. - 'is-valid', - 'is-invalid', - 'is-warning', - 'is-success', - 'is-error', - 'is-info', - 'is-danger', - - // Position. - 'is-sticky', - 'is-pinned', - 'is-fixed', - 'is-fullscreen', - 'is-resizable', - - // Overflow. - 'is-clipped', - 'is-scrollable', - 'is-truncated', - - // Drag. - 'is-dragging', - 'is-drop-target', - 'is-draggable', - - // Overlay. - 'is-overlay', - - // Focus. - 'is-clickable', - 'is-unselectable', - 'is-focused', - - // Empty. - 'is-empty', - ); - } } diff --git a/integrations/bricks/includes/class-colors.php b/integrations/bricks/includes/class-colors.php index 94607b5b..c7bb2662 100644 --- a/integrations/bricks/includes/class-colors.php +++ b/integrations/bricks/includes/class-colors.php @@ -12,152 +12,329 @@ /** * Class Slashed_Bricks_Colors * - * Registers SLASHED color tokens with Bricks Builder's global color palette. + * Registers SLASHED color tokens with Bricks Builder as a set of separate, + * named color palettes that appear under the "Color palettes" dropdown of + * the Bricks color picker - distinct from the site's global colors. * - * Note: The 'raw' color type (used instead of 'hex' for var() references) - * requires Bricks 1.9.2+. Older versions may not render swatches correctly. + * Strategy + * -------- + * Bricks stores user-managed color palettes in the wp_options row + * `bricks_color_palette`. We treat SLASHED palettes as managed/virtual: + * + * 1. On every read of that option (option_bricks_color_palette / + * default_option_bricks_color_palette), we inject our palettes into + * the array Bricks sees. + * 2. On every write (pre_update_option_bricks_color_palette), we strip + * our palettes back out so the database never persists them. That + * way the integration is the single source of truth - bumping the + * framework or changing the active bundle automatically updates + * what Bricks shows, without leaving stale rows behind on the site. + * + * Each palette's swatch references the framework variable directly via + * var(--sf-color-X). Modern browsers resolve var() inside the picker + * preview because the SLASHED bundle is loaded into the editor iframe. + * This keeps swatches in sync with theme customization and dark mode. + * + * Note: the 'raw' field is included alongside 'hex' for forward + * compatibility with Bricks 1.9.2+, which prefers 'raw' when present. */ class Slashed_Bricks_Colors { + /** + * Prefix used on every palette and color id this integration injects. + * Used to identify our entries when stripping on save. + */ + const PALETTE_ID_PREFIX = 'slashed-'; + + /** + * Brand family slugs in canonical display order. + * + * @var string[] + */ + private static $brands = array( 'primary', 'secondary', 'tertiary', 'action', 'neutral', 'base' ); + + /** + * Status family slugs. + * + * @var string[] + */ + private static $statuses = array( 'success', 'warning', 'error', 'info', 'danger' ); + /** * Constructor. Register hooks. */ public function __construct() { - add_filter( 'bricks/setup/control_options', array( $this, 'register_global_colors' ) ); + // Inject SLASHED palettes when Bricks (or anything else) reads the + // bricks_color_palette option. Run late so any other plugin's + // additions are preserved. + add_filter( 'option_bricks_color_palette', array( $this, 'inject_palettes' ), 20 ); + add_filter( 'default_option_bricks_color_palette', array( $this, 'inject_palettes' ), 20 ); + + // Strip SLASHED palettes before they are persisted back to the DB. + // pre_update_option_* signature is ($value, $old_value, $option). + add_filter( 'pre_update_option_bricks_color_palette', array( $this, 'strip_palettes' ), 10, 1 ); } /** - * Register SLASHED colors with Bricks global color palette. + * Inject SLASHED palettes into the Bricks palette list. + * + * Idempotent: any existing SLASHED-prefixed palettes are removed first + * so multiple read passes don't create duplicates. * - * @param array $control_options Existing control options. - * @return array Modified control options. + * @param mixed $palettes Existing value of bricks_color_palette option. + * @return array */ - public function register_global_colors( $control_options ) { - $colors = $this->get_colors(); + public function inject_palettes( $palettes ) { + if ( ! is_array( $palettes ) ) { + $palettes = array(); + } + + $palettes = $this->strip_palettes( $palettes ); - if ( ! isset( $control_options['globalColors'] ) ) { - $control_options['globalColors'] = array(); + foreach ( $this->build_palettes() as $palette ) { + $palettes[] = $palette; + } + + return $palettes; + } + + /** + * Remove SLASHED-prefixed palettes from a palette array. + * + * Always returns a clean array, even when the option is malformed + * (e.g. a fresh install with no row, a serialized scalar from a + * misbehaving migration, or a corrupted value). pre_update_option_* + * hooks must never widen the stored type from array to scalar - that + * would break Bricks' own loader, which iterates over the option. + * + * @param mixed $palettes Value of bricks_color_palette option. + * @return array + */ + public function strip_palettes( $palettes ) { + if ( ! is_array( $palettes ) ) { + return array(); } - foreach ( $colors as $color_entry ) { - $control_options['globalColors'][] = $color_entry; + $kept = array(); + foreach ( $palettes as $palette ) { + if ( is_array( $palette ) + && isset( $palette['id'] ) + && is_string( $palette['id'] ) + && 0 === strpos( $palette['id'], self::PALETTE_ID_PREFIX ) + ) { + continue; + } + $kept[] = $palette; } - return $control_options; + return array_values( $kept ); } /** - * Get all SLASHED colors formatted for Bricks. + * Build the SLASHED palette set from the current inventory. * - * @return array Array of color entries. + * Returns one palette per brand family plus combined Status and + * Semantic palettes - so users get a small, navigable picker rather + * than one 275-swatch wall of color. + * + * @return array */ - public function get_colors() { - $colors = array(); - $categories = $this->get_color_categories(); + public function build_palettes() { + $vars = Slashed_Bricks_Inventory::get_color_variables(); - /** - * Filter which color categories to include. - * - * @param array $categories Associative array of category => color definitions. - */ - $categories = apply_filters( 'slashed_bricks/color_categories', $categories ); + $by_brand = array_fill_keys( self::$brands, array() ); + $status = array(); + $semantic = array(); - foreach ( $categories as $category => $category_colors ) { - foreach ( $category_colors as $color ) { - $color['category'] = $category; - $colors[] = $color; + foreach ( $vars as $var ) { + $key = substr( $var, strlen( '--sf-color-' ) ); + if ( '' === $key ) { + continue; + } + $first_dash = strpos( $key, '-' ); + $first = false === $first_dash ? $key : substr( $key, 0, $first_dash ); + + if ( in_array( $first, self::$brands, true ) ) { + $by_brand[ $first ][] = $var; + } elseif ( in_array( $first, self::$statuses, true ) ) { + $status[] = $var; + } else { + $semantic[] = $var; } } + $palettes = array(); + + foreach ( self::$brands as $brand ) { + if ( empty( $by_brand[ $brand ] ) ) { + continue; + } + $palettes[] = $this->build_palette( + $brand, + 'SLASHED · ' . ucfirst( $brand ), + $by_brand[ $brand ] + ); + } + + if ( ! empty( $status ) ) { + $palettes[] = $this->build_palette( 'status', 'SLASHED · Status', $status ); + } + + if ( ! empty( $semantic ) ) { + $palettes[] = $this->build_palette( 'semantic', 'SLASHED · Semantic', $semantic ); + } + /** - * Filter the registered colors before passing to Bricks. + * Filter the SLASHED palettes before injection. * - * @param array $colors Array of color entry arrays. + * @param array $palettes List of palette arrays. */ - return apply_filters( 'slashed_bricks/registered_colors', $colors ); + return apply_filters( 'slashed_bricks/registered_palettes', $palettes ); } /** - * Get color categories with their color definitions. + * Build a single Bricks-shaped palette entry. * - * @return array + * @param string $palette_slug Internal slug (used for id). + * @param string $palette_name Display name. + * @param string[] $vars Variable names (e.g. "--sf-color-primary-50"). + * @return array{id:string,name:string,colors:array} */ - private function get_color_categories() { - $categories = array(); - - // Brand colors with palette scales. - $brands = array( 'primary', 'secondary', 'tertiary', 'action', 'neutral', 'base' ); - $scale = array( '50', '100', '200', '300', '400', '500', '600', '700', '800', '900', '950' ); - - foreach ( $brands as $brand ) { - $category_name = 'SLASHED ' . ucfirst( $brand ); - $brand_colors = array(); + private function build_palette( $palette_slug, $palette_name, $vars ) { + return array( + 'id' => self::PALETTE_ID_PREFIX . $palette_slug, + 'name' => $palette_name, + 'colors' => $this->vars_to_palette_colors( $palette_slug, $vars ), + ); + } - // Base color. - $brand_colors[] = array( - 'id' => 'sf-' . $brand, - 'name' => 'SF ' . ucfirst( $brand ), - 'color' => array( 'raw' => 'var(--sf-color-' . $brand . ')' ), + /** + * Convert a list of --sf-color-* variable names into Bricks palette + * color entries. + * + * Each entry uses var(--sf-color-X) for both 'hex' (the legacy field + * Bricks reads in older versions) and 'raw' (the preferred field in + * Bricks 1.9.2+). The picker resolves these via the SLASHED bundle + * loaded in the editor iframe, so swatches always reflect the live + * theme - including dark mode and any user token overrides from the + * SLASHED admin page. + * + * @param string $palette_slug Internal slug. + * @param string[] $vars Variable names. + * @return array> + */ + private function vars_to_palette_colors( $palette_slug, $vars ) { + $colors = array(); + foreach ( $vars as $var ) { + $key = substr( $var, strlen( '--sf-color-' ) ); + if ( '' === $key ) { + continue; + } + $reference = 'var(' . $var . ')'; + $colors[] = array( + 'id' => self::PALETTE_ID_PREFIX . $palette_slug . '-' . $this->slugify( $key ), + 'name' => $this->humanize_key( $key ), + 'hex' => $reference, + 'raw' => $reference, ); + } + return $colors; + } - // Scale colors. - foreach ( $scale as $step ) { - $brand_colors[] = array( - 'id' => 'sf-' . $brand . '-' . $step, - 'name' => 'SF ' . ucfirst( $brand ) . ' ' . $step, - 'color' => array( 'raw' => 'var(--sf-color-' . $brand . '-' . $step . ')' ), - ); - } + /** + * Convert a color key into a human-readable label. + * + * Examples: + * "primary" -> "Primary" + * "primary-50" -> "Primary 50" + * "primary-a20" -> "Primary A20" + * "text--secondary" -> "Text Secondary" + * + * @param string $key Color key. + * @return string + */ + private function humanize_key( $key ) { + $normalized = str_replace( '--', '-', $key ); + $parts = array_filter( explode( '-', $normalized ), 'strlen' ); - $categories[ $category_name ] = $brand_colors; + $label_parts = array(); + foreach ( $parts as $part ) { + if ( preg_match( '/^[0-9]+$/', $part ) ) { + $label_parts[] = $part; + } elseif ( preg_match( '/^a[0-9]+$/i', $part ) ) { + $label_parts[] = strtoupper( $part ); + } else { + $label_parts[] = ucfirst( $part ); + } } - // Status colors. - $status_colors = array(); - $statuses = array( 'success', 'warning', 'error', 'info', 'danger' ); + return implode( ' ', $label_parts ); + } - foreach ( $statuses as $status ) { - $status_colors[] = array( - 'id' => 'sf-' . $status, - 'name' => 'SF ' . ucfirst( $status ), - 'color' => array( 'raw' => 'var(--sf-color-' . $status . ')' ), - ); - } + /** + * Convert a color key into a stable id-safe slug. + * + * @param string $key Color key. + * @return string + */ + private function slugify( $key ) { + $normalized = str_replace( '--', '-', $key ); + $normalized = preg_replace( '/[^a-zA-Z0-9-]/', '', $normalized ); + return strtolower( trim( $normalized, '-' ) ); + } - $categories['SLASHED Status'] = $status_colors; - - // Semantic colors. - $semantic_colors = array(); - $semantic_map = array( - 'text' => 'Text', - 'text--secondary' => 'Text Secondary', - 'text--muted' => 'Text Muted', - 'heading' => 'Heading', - 'bg' => 'Background', - 'surface' => 'Surface', - 'well' => 'Well', - 'raised' => 'Raised', - 'overlay' => 'Overlay', - 'inverse' => 'Inverse', - 'border' => 'Border', - 'border--subtle' => 'Border Subtle', - 'border--strong' => 'Border Strong', - 'link' => 'Link', - 'link--hover' => 'Link Hover', - 'link--active' => 'Link Active', - 'link--visited' => 'Link Visited', - ); + // --------------------------------------------------------------- + // Backward-compatible flat color list (kept for filters/tests). + // --------------------------------------------------------------- - foreach ( $semantic_map as $token => $label ) { - $semantic_colors[] = array( - 'id' => 'sf-' . str_replace( '--', '-', $token ), - 'name' => 'SF ' . $label, - 'color' => array( 'raw' => 'var(--sf-color-' . $token . ')' ), - ); + /** + * Get every SLASHED color as a flat array. + * + * Mirrors the pre-palette shape so existing callers and the + * 'slashed_bricks/registered_colors' / 'slashed_bricks/color_categories' + * filters keep working. + * + * @return array> + */ + public function get_colors() { + $palettes = $this->build_palettes(); + + // Re-shape into the legacy "categories" map. + $categories = array(); + foreach ( $palettes as $palette ) { + $cat = $palette['name']; + if ( ! isset( $categories[ $cat ] ) ) { + $categories[ $cat ] = array(); + } + foreach ( $palette['colors'] as $color ) { + $categories[ $cat ][] = array( + 'id' => $color['id'], + 'name' => $color['name'], + 'color' => array( 'raw' => isset( $color['raw'] ) ? $color['raw'] : $color['hex'] ), + ); + } } - $categories['SLASHED Semantic'] = $semantic_colors; + /** + * Filter which palette categories to include. + * + * @param array $categories Map of palette name => color entries. + */ + $categories = apply_filters( 'slashed_bricks/color_categories', $categories ); + + $colors = array(); + foreach ( $categories as $category => $entries ) { + foreach ( $entries as $entry ) { + $entry['category'] = $category; + $colors[] = $entry; + } + } - return $categories; + /** + * Filter the registered colors before passing to Bricks. + * + * @param array $colors Flat list of color entries. + */ + return apply_filters( 'slashed_bricks/registered_colors', $colors ); } } diff --git a/integrations/bricks/includes/class-css-generator.php b/integrations/bricks/includes/class-css-generator.php index 2dc5dcf5..47c75c7b 100644 --- a/integrations/bricks/includes/class-css-generator.php +++ b/integrations/bricks/includes/class-css-generator.php @@ -127,6 +127,10 @@ public static function get_override_css() { $declarations = array_merge( $declarations, self::generate_zindex_declarations( $settings['zindex'] ) ); } + if ( ! empty( $settings['contrast'] ) && is_array( $settings['contrast'] ) ) { + $declarations = array_merge( $declarations, self::generate_contrast_declarations( $settings['contrast'] ) ); + } + if ( empty( $declarations ) ) { self::$cache = ''; return self::$cache; @@ -356,4 +360,71 @@ private static function generate_zindex_declarations( $settings ) { return $declarations; } + + /** + * Generate CSS declarations for the Contrast tab tokens. + * + * Each control writes one custom property. Numeric inputs are + * formatted with a guarded float cast so locale settings can't + * smuggle commas into the output. Pixel-unit fields suffix 'px' + * automatically. The focus ring style is restricted to a known + * enum so we never emit garbage like "javascript:"-style values. + * + * @param array $settings Contrast section settings. + * @return array CSS declaration strings. + */ + private static function generate_contrast_declarations( $settings ) { + $declarations = array(); + + // Plain unitless numerics. + $numerics = array( + 'contrast_bias' => '--sf-contrast-bias', + 'contrast_threshold' => '--sf-contrast-threshold', + 'opacity_disabled' => '--sf-opacity-disabled', + ); + foreach ( $numerics as $key => $property ) { + if ( isset( $settings[ $key ] ) && '' !== $settings[ $key ] ) { + $declarations[] = $property . ': ' . self::format_float( $settings[ $key ] ) . ';'; + } + } + + // Pixel-typed focus ring metrics. + $pixel_metrics = array( + 'focus_ring_width' => '--sf-focus-ring-width', + 'focus_ring_offset' => '--sf-focus-ring-offset', + ); + foreach ( $pixel_metrics as $key => $property ) { + if ( isset( $settings[ $key ] ) && '' !== $settings[ $key ] ) { + $declarations[] = $property . ': ' . self::format_float( $settings[ $key ] ) . 'px;'; + } + } + + // Focus ring style: restricted enum so we never echo arbitrary input. + if ( isset( $settings['focus_ring_style'] ) && '' !== $settings['focus_ring_style'] ) { + $style = (string) $settings['focus_ring_style']; + $allowed = array( 'solid', 'dashed', 'dotted', 'double', 'none' ); + if ( in_array( $style, $allowed, true ) ) { + $declarations[] = '--sf-focus-ring-style: ' . $style . ';'; + } + } + + return $declarations; + } + + /** + * Locale-safe float formatter. + * + * Casts to float, then formats with '.' decimal and no trailing + * zeros. Avoids surprises from setlocale() shifting the decimal + * separator to ','. + * + * @param mixed $value Raw numeric input. + * @return string + */ + private static function format_float( $value ) { + $num = (float) $value; + // Up to 6 decimals, then trim trailing zeros and dot. + $out = rtrim( rtrim( number_format( $num, 6, '.', '' ), '0' ), '.' ); + return '' === $out ? '0' : $out; + } } diff --git a/integrations/bricks/includes/class-css-parser.php b/integrations/bricks/includes/class-css-parser.php new file mode 100644 index 00000000..f678635e --- /dev/null +++ b/integrations/bricks/includes/class-css-parser.php @@ -0,0 +1,112 @@ + string[] // declared --sf-* custom property names + * 'sf_classes' => string[] // .sf-* class selectors found in the file + * 'is_classes' => string[] // .is-* class selectors found in the file + * ) + * + * The "variables" list only includes properties that are actually declared + * (left-hand side of a CSS declaration), not arbitrary mentions inside + * comments or var() calls. This avoids polluting the inventory with + * documentation strings like "--sf-space-*". + */ +class Slashed_Bricks_CSS_Parser { + + /** + * Parse a CSS string into an inventory of variables and class selectors. + * + * @param string $css Raw CSS source. + * @return array{variables: string[], sf_classes: string[], is_classes: string[]} + */ + public static function parse( $css ) { + if ( ! is_string( $css ) || '' === $css ) { + return self::empty_inventory(); + } + + // Strip /* ... */ block comments first so documentation strings + // like "--sf-space-*" don't bleed into the inventory. + $stripped = preg_replace( '#/\*[\s\S]*?\*/#', '', $css ); + if ( null === $stripped ) { + $stripped = $css; + } + + return array( + 'variables' => self::extract_declared_variables( $stripped ), + 'sf_classes' => self::extract_class_names( $stripped, 'sf-' ), + 'is_classes' => self::extract_class_names( $stripped, 'is-' ), + ); + } + + /** + * Extract custom property names that are declared (LHS of a colon). + * + * Matches patterns like "--sf-color-primary:", " --sf-space-m :" but + * not bare mentions like "see --sf-color-*" inside comments (already + * stripped) or "var(--sf-color-primary)" usages. + * + * @param string $css CSS with comments removed. + * @return string[] Sorted unique list of declared property names. + */ + private static function extract_declared_variables( $css ) { + $matches = array(); + if ( ! preg_match_all( '/(--sf-[a-zA-Z0-9_-]+)\s*:/', $css, $matches ) ) { + return array(); + } + + $names = array_values( array_unique( $matches[1] ) ); + sort( $names ); + return $names; + } + + /** + * Extract class selectors with the given prefix. + * + * Matches ".sf-something" or ".is-something" anywhere in the CSS. + * The prefix should include the trailing dash, e.g. "sf-" or "is-". + * + * @param string $css CSS with comments removed. + * @param string $prefix Class prefix without the leading dot. + * @return string[] Sorted unique list of class names (without leading dot). + */ + private static function extract_class_names( $css, $prefix ) { + $pattern = '/\.(' . preg_quote( $prefix, '/' ) . '[a-zA-Z0-9_-]+)/'; + $matches = array(); + if ( ! preg_match_all( $pattern, $css, $matches ) ) { + return array(); + } + + $names = array_values( array_unique( $matches[1] ) ); + sort( $names ); + return $names; + } + + /** + * Return an empty inventory shape. Helps callers avoid undefined keys. + * + * @return array{variables: string[], sf_classes: string[], is_classes: string[]} + */ + public static function empty_inventory() { + return array( + 'variables' => array(), + 'sf_classes' => array(), + 'is_classes' => array(), + ); + } +} diff --git a/integrations/bricks/includes/class-inventory.php b/integrations/bricks/includes/class-inventory.php new file mode 100644 index 00000000..72030265 --- /dev/null +++ b/integrations/bricks/includes/class-inventory.php @@ -0,0 +1,575 @@ + + * - get_sf_classes() sorted unique .sf-* names + * - get_is_classes() sorted unique .is-* names + * - get_color_variables() every --sf-color-* token + */ +class Slashed_Bricks_Inventory { + + /** + * Transient key prefix. + */ + const TRANSIENT_PREFIX = 'slashed_bricks_inv_'; + + /** + * Transient TTL for remote-fetched bundles. + */ + const TRANSIENT_TTL = DAY_IN_SECONDS; + + /** + * Per-request cache so multiple registration classes don't each + * pay the resolution cost. + * + * @var array|null + */ + private static $cache = null; + + /** + * Get the full inventory, resolving and caching on first access. + * + * @return array{variables: string[], sf_classes: string[], is_classes: string[]} + */ + public static function get() { + if ( null !== self::$cache ) { + return self::$cache; + } + + // Prime the cache with the resolved inventory BEFORE applying the + // filter. A filter callback is allowed to query inventory data via + // the public Slashed_Bricks_Inventory::get_*() helpers; without a + // pre-primed cache that re-entry would call resolve() recursively + // and never terminate. With the cache primed, recursive get() calls + // short-circuit on the first line above. + self::$cache = self::sanitize_inventory( self::resolve() ); + + /** + * Filter the resolved inventory before it's used to register + * variables, classes, and colors with Bricks. + * + * Filter callbacks may safely call Slashed_Bricks_Inventory::get_*() + * - the cache is primed before this filter fires, so re-entrant + * calls are bounded by the per-request cache instead of re-running + * resolve(). + * + * @param array $inventory ['variables', 'sf_classes', 'is_classes']. + */ + self::$cache = self::sanitize_inventory( + apply_filters( 'slashed_bricks/inventory', self::$cache ) + ); + + return self::$cache; + } + + /** + * Normalise an inventory array to the canonical shape. + * + * Defensive coercion for arbitrary inputs: filter callbacks may + * legitimately want to extend, prune, or replace inventory entries, + * but they could also return null, a partial array, or non-string + * entries. This normaliser guarantees the consumers downstream + * (Variables / Classes / Colors registration) always see the same + * shape: three keys, sorted unique string lists. + * + * @param mixed $inventory Possibly malformed inventory data. + * @return array{variables: string[], sf_classes: string[], is_classes: string[]} + */ + private static function sanitize_inventory( $inventory ) { + $base = Slashed_Bricks_CSS_Parser::empty_inventory(); + if ( ! is_array( $inventory ) ) { + return $base; + } + foreach ( array_keys( $base ) as $key ) { + $list = isset( $inventory[ $key ] ) && is_array( $inventory[ $key ] ) + ? array_filter( $inventory[ $key ], 'is_string' ) + : array(); + $list = array_values( array_unique( $list ) ); + sort( $list ); + $base[ $key ] = $list; + } + return $base; + } + + /** + * Reset the per-request cache. Mostly useful for tests. + */ + public static function flush() { + self::$cache = null; + } + + /** + * Get the flat list of all declared --sf-* variables. + * + * @return string[] + */ + public static function get_variables() { + $inv = self::get(); + return $inv['variables']; + } + + /** + * Get only --sf-color-* variables. + * + * @return string[] + */ + public static function get_color_variables() { + $vars = self::get_variables(); + return array_values( + array_filter( + $vars, + static function ( $v ) { + return 0 === strpos( $v, '--sf-color-' ); + } + ) + ); + } + + /** + * Get .sf-* class names declared in the bundle. + * + * @return string[] + */ + public static function get_sf_classes() { + $inv = self::get(); + return $inv['sf_classes']; + } + + /** + * Get .is-* class names declared in the bundle. + * + * @return string[] + */ + public static function get_is_classes() { + $inv = self::get(); + return $inv['is_classes']; + } + + /** + * Get variables grouped by category label. + * + * Categories appear in canonical display order. Empty categories are + * dropped. Names within each category are sorted. + * + * @return array + */ + public static function get_variables_by_category() { + $grouped = array(); + foreach ( self::get_variables() as $var ) { + $cat = self::categorize_variable( $var ); + if ( ! isset( $grouped[ $cat ] ) ) { + $grouped[ $cat ] = array(); + } + $grouped[ $cat ][] = $var; + } + + $ordered = array(); + foreach ( self::category_order() as $cat ) { + if ( ! empty( $grouped[ $cat ] ) ) { + sort( $grouped[ $cat ] ); + $ordered[ $cat ] = $grouped[ $cat ]; + } + } + + // Append any uncategorized buckets at the end (defensive). + foreach ( $grouped as $cat => $list ) { + if ( ! isset( $ordered[ $cat ] ) ) { + sort( $list ); + $ordered[ $cat ] = $list; + } + } + + return $ordered; + } + + /** + * Categorize a CSS variable based on its name. + * + * Matches against the first segment after "--sf-" (e.g. "color", + * "space", "duration"). Falls back to "Misc" for unknown families. + * + * @param string $name Full variable name including leading "--". + * @return string Category label (without the "SLASHED " prefix). + */ + public static function categorize_variable( $name ) { + $key = $name; + if ( 0 === strpos( $key, '--sf-' ) ) { + $key = substr( $key, 5 ); + } + + $dash = strpos( $key, '-' ); + $first = false === $dash ? $key : substr( $key, 0, $dash ); + + $map = self::category_map(); + return isset( $map[ $first ] ) ? $map[ $first ] : 'Misc'; + } + + /** + * Display order for variable categories. Categories not in this list + * are appended after the canonical ones. + * + * @return string[] + */ + public static function category_order() { + return array( + 'Colors', + 'Typography', + 'Spacing', + 'Sizing', + 'Layout', + 'Borders', + 'Radius', + 'Shadows', + 'Effects', + 'Motion', + 'Icons', + 'Z-Index', + 'States', + 'Focus', + 'Scroll', + 'Print', + 'Misc', + ); + } + + /** + * First-segment -> category-label mapping. Drives categorize_variable(). + * + * @return array + */ + private static function category_map() { + return array( + // Colors. + 'color' => 'Colors', + // Typography. + 'text' => 'Typography', + 'font' => 'Typography', + 'leading' => 'Typography', + 'tracking' => 'Typography', + 'body' => 'Typography', + 'heading' => 'Typography', + 'h1' => 'Typography', + 'h2' => 'Typography', + 'h3' => 'Typography', + 'h4' => 'Typography', + 'h5' => 'Typography', + 'h6' => 'Typography', + 'prose' => 'Typography', + 'code' => 'Typography', + 'optical' => 'Typography', + 'line' => 'Typography', + // Spacing. + 'space' => 'Spacing', + 'gap' => 'Spacing', + 'gutter' => 'Spacing', + 'component' => 'Spacing', + 'section' => 'Spacing', + 'flow' => 'Spacing', + 'safe' => 'Spacing', + 'header' => 'Spacing', + 'sticky' => 'Spacing', + // Sizing. + 'size' => 'Sizing', + 'aspect' => 'Sizing', + 'ratio' => 'Sizing', + 'touch' => 'Sizing', + // Layout. + 'container' => 'Layout', + 'stack' => 'Layout', + 'cluster' => 'Layout', + 'sidebar' => 'Layout', + 'switcher' => 'Layout', + 'grid' => 'Layout', + 'cover' => 'Layout', + 'frame' => 'Layout', + 'reel' => 'Layout', + 'imposter' => 'Layout', + 'bento' => 'Layout', + 'box' => 'Layout', + 'center' => 'Layout', + 'content' => 'Layout', + 'breakout' => 'Layout', + 'divider' => 'Layout', + 'field' => 'Layout', + // Borders. + 'border' => 'Borders', + 'stroke' => 'Borders', + // Radius. + 'radius' => 'Radius', + // Shadows. + 'shadow' => 'Shadows', + // Effects. + 'blur' => 'Effects', + 'opacity' => 'Effects', + 'gradient' => 'Effects', + 'mask' => 'Effects', + 'perspective' => 'Effects', + 'drop' => 'Effects', + 'contrast' => 'Effects', + // Motion. + 'duration' => 'Motion', + 'ease' => 'Motion', + 'transition' => 'Motion', + 'motion' => 'Motion', + 'animation' => 'Motion', + // Icons. + 'icon' => 'Icons', + // Z-Index. + 'z' => 'Z-Index', + // States. + 'is' => 'States', + 'current' => 'States', + // Focus. + 'focus' => 'Focus', + 'caret' => 'Focus', + // Scroll. + 'scroll' => 'Scroll', + 'scrollbar' => 'Scroll', + // Print. + 'print' => 'Print', + // Misc explicit assignments. + 'truncate' => 'Misc', + ); + } + + // --------------------------------------------------------------- + // Resolution. + // --------------------------------------------------------------- + + /** + * Resolve the active CSS bundle into a parsed inventory. + * + * @return array{variables: string[], sf_classes: string[], is_classes: string[]} + */ + private static function resolve() { + // 1. Local file - cheapest, also handles offline development. + $local_path = self::find_local_bundle_path(); + if ( '' !== $local_path ) { + $inventory = self::parse_path_with_cache( $local_path ); + if ( ! empty( $inventory['variables'] ) ) { + return $inventory; + } + } + + // 2. Remote URL (CDN). Cached as a transient. + $url = function_exists( 'slashed_bricks_get_css_url' ) + ? slashed_bricks_get_css_url() + : ''; + + if ( '' !== $url && self::is_remote_url( $url ) ) { + $inventory = self::parse_url_with_cache( $url ); + if ( ! empty( $inventory['variables'] ) ) { + return $inventory; + } + } + + // 3. Built-in fallback - keeps the plugin functional even when + // no CSS is reachable (hosts blocking outbound HTTP, etc). + return self::fallback_inventory(); + } + + /** + * Find a local path to the configured CSS bundle, if any. + * + * Aligns the inventory source with whichever CSS bundle is actually + * enqueued: the active URL from slashed_bricks_get_css_url() drives + * the choice, so a slashed_bricks/css_bundle_url filter that swaps + * 'optimal' for 'essential' or 'full' is reflected in what tokens + * the Bricks UI shows. + * + * The 'slashed_bricks/inventory_local_path' filter is authoritative: + * - return a string -> use that path (or empty if it doesn't exist) + * - return false -> skip local resolution entirely + * - return null -> derive the path from the active CSS URL + * + * @return string Absolute path, or '' when no local copy is available. + */ + private static function find_local_bundle_path() { + $override = apply_filters( 'slashed_bricks/inventory_local_path', null ); + + if ( false === $override ) { + return ''; + } + + if ( is_string( $override ) ) { + return ( '' !== $override && file_exists( $override ) ) ? $override : ''; + } + + // Derive the local path from the active CSS URL. This keeps the + // parsed inventory aligned with whichever bundle the + // slashed_bricks/css_bundle_url filter has selected, so we never + // register tokens from 'optimal' while the site loads 'full'. + if ( function_exists( 'slashed_bricks_get_css_url' ) ) { + $derived = self::derive_local_path_from_url( slashed_bricks_get_css_url() ); + if ( '' !== $derived && file_exists( $derived ) ) { + return $derived; + } + } + + return ''; + } + + /** + * Map a plugin-served CSS URL back to its filesystem path. + * + * Returns a path only when the URL clearly maps to a file inside the + * plugin's URL space (covers both copy-install mode where dist/ lives + * under the plugin and symlink-in-repo mode where dist/ lives at + * SLASHED_BRICKS_PATH . '../../dist/'). For any other URL (CDN, + * third-party host) returns '' so the caller can fall through to + * remote fetching. + * + * @param string $url Active CSS bundle URL. + * @return string Absolute filesystem path candidate, or '' when not a local URL. + */ + private static function derive_local_path_from_url( $url ) { + if ( ! is_string( $url ) || '' === $url ) { + return ''; + } + if ( 0 !== strpos( $url, SLASHED_BRICKS_URL ) ) { + return ''; + } + $relative = substr( $url, strlen( SLASHED_BRICKS_URL ) ); + return SLASHED_BRICKS_PATH . $relative; + } + + /** + * Parse a CSS file from disk, with a transient cache keyed by mtime. + * + * @param string $path Absolute path to a CSS file. + * @return array Inventory shape (may be empty on read failure). + */ + private static function parse_path_with_cache( $path ) { + $mtime = @filemtime( $path ); + if ( false === $mtime ) { + return Slashed_Bricks_CSS_Parser::empty_inventory(); + } + + $key = self::TRANSIENT_PREFIX . md5( 'path:' . $path . ':' . $mtime ); + $cached = get_transient( $key ); + if ( is_array( $cached ) && isset( $cached['variables'] ) ) { + return $cached; + } + + $css = @file_get_contents( $path ); + if ( false === $css || '' === $css ) { + return Slashed_Bricks_CSS_Parser::empty_inventory(); + } + + $inventory = Slashed_Bricks_CSS_Parser::parse( $css ); + set_transient( $key, $inventory, self::TRANSIENT_TTL ); + return $inventory; + } + + /** + * Parse a CSS file fetched from a URL, with a transient cache keyed by URL. + * + * @param string $url HTTPS URL to a CSS file. + * @return array Inventory shape (may be empty on network failure). + */ + private static function parse_url_with_cache( $url ) { + $key = self::TRANSIENT_PREFIX . md5( 'url:' . $url ); + $cached = get_transient( $key ); + if ( is_array( $cached ) && isset( $cached['variables'] ) ) { + return $cached; + } + + if ( ! function_exists( 'wp_remote_get' ) ) { + return Slashed_Bricks_CSS_Parser::empty_inventory(); + } + + $response = wp_remote_get( + $url, + array( + 'timeout' => 10, + 'user-agent' => 'SLASHED-Bricks/' . SLASHED_BRICKS_VERSION, + ) + ); + + if ( is_wp_error( $response ) ) { + return Slashed_Bricks_CSS_Parser::empty_inventory(); + } + + if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { + return Slashed_Bricks_CSS_Parser::empty_inventory(); + } + + $css = wp_remote_retrieve_body( $response ); + if ( ! is_string( $css ) || '' === $css ) { + return Slashed_Bricks_CSS_Parser::empty_inventory(); + } + + $inventory = Slashed_Bricks_CSS_Parser::parse( $css ); + set_transient( $key, $inventory, self::TRANSIENT_TTL ); + return $inventory; + } + + /** + * Whether a URL points to a remote (http/https) resource. + * + * @param string $url URL to check. + * @return bool + */ + private static function is_remote_url( $url ) { + return 0 === strpos( $url, 'http://' ) || 0 === strpos( $url, 'https://' ); + } + + /** + * Hardcoded fallback inventory used when neither a local file nor a + * remote fetch succeed. Pulled from the shipped optimal bundle of the + * release that this plugin version targets, so coverage is still + * complete even with no network or filesystem access. + * + * The list is generated from dist/slashed.optimal.css and bumped + * alongside SLASHED_BRICKS_CSS_REF. + * + * @return array{variables: string[], sf_classes: string[], is_classes: string[]} + */ + private static function fallback_inventory() { + $path = SLASHED_BRICKS_PATH . 'data/inventory.json'; + if ( file_exists( $path ) ) { + $json = @file_get_contents( $path ); + if ( is_string( $json ) && '' !== $json ) { + $decoded = json_decode( $json, true ); + if ( is_array( $decoded ) + && isset( $decoded['variables'], $decoded['sf_classes'], $decoded['is_classes'] ) + ) { + return array( + 'variables' => array_values( $decoded['variables'] ), + 'sf_classes' => array_values( $decoded['sf_classes'] ), + 'is_classes' => array_values( $decoded['is_classes'] ), + ); + } + } + } + + return Slashed_Bricks_CSS_Parser::empty_inventory(); + } +} diff --git a/integrations/bricks/includes/class-token-defaults.php b/integrations/bricks/includes/class-token-defaults.php index bfe68a45..3114db85 100644 --- a/integrations/bricks/includes/class-token-defaults.php +++ b/integrations/bricks/includes/class-token-defaults.php @@ -25,6 +25,7 @@ class Slashed_Bricks_Token_Defaults { public static function get_all() { return array( 'colors' => self::get_colors(), + 'contrast' => self::get_contrast(), 'typography' => self::get_typography(), 'spacing' => self::get_spacing(), 'radius' => self::get_radius(), @@ -48,6 +49,15 @@ public static function get_section( $section ) { /** * Get color token defaults. * + * Brand and status defaults are stored as oklch() strings (the + * authoring format the framework was designed around). The + * accompanying *_hex_hints maps provide approximate HEX equivalents + * used purely for the admin color picker preview - so the picker + * has something concrete to display when no override is saved. + * + * Hex hints are intentionally rough sRGB approximations; they are + * never written to CSS unless the user explicitly picks them. + * * @return array */ public static function get_colors() { @@ -67,6 +77,21 @@ public static function get_colors() { 'info' => 'oklch(0.48 0.15 240)', 'danger' => 'oklch(0.48 0.24 12)', ), + 'brand_hex_hints' => array( + 'primary' => '#4338ca', + 'secondary' => '#1e293b', + 'tertiary' => '#7c3aed', + 'action' => '#0891b2', + 'neutral' => '#64748b', + 'base' => '#fafafa', + ), + 'status_hex_hints' => array( + 'success' => '#16a34a', + 'warning' => '#ca8a04', + 'error' => '#dc2626', + 'info' => '#2563eb', + 'danger' => '#dc2626', + ), ); } @@ -180,4 +205,25 @@ public static function get_zindex() { 'max' => 9999, ); } + + /** + * Get contrast / opacity / focus-ring token defaults. + * + * Cross-cutting visual fine-tuning knobs that don't belong to any + * one token family. Defaults mirror what core/tokens.css declares + * so the admin placeholders match what the framework would do + * without any overrides at all. + * + * @return array + */ + public static function get_contrast() { + return array( + 'contrast_bias' => 0, + 'contrast_threshold' => 0.6, + 'opacity_disabled' => 0.45, + 'focus_ring_width' => 2, + 'focus_ring_offset' => 2, + 'focus_ring_style' => 'solid', + ); + } } diff --git a/integrations/bricks/includes/class-variables.php b/integrations/bricks/includes/class-variables.php index 2abc71e1..e90c9bc4 100644 --- a/integrations/bricks/includes/class-variables.php +++ b/integrations/bricks/includes/class-variables.php @@ -12,14 +12,17 @@ /** * Class Slashed_Bricks_Variables * - * Registers SLASHED CSS custom properties with Bricks Builder - * for variable pickers and code editor autocomplete. + * Registers SLASHED CSS custom properties with Bricks Builder for variable + * pickers and code editor autocomplete. * - * Bricks 1.9.2+ automatically detects CSS custom properties from stylesheets - * loaded in the editor iframe. This class provides additional organization - * (category labels via i18n) and code editor autocomplete (via code signatures). - * The primary variable picker population happens through CSS loading in - * class-enqueue.php. + * The variable list is derived from the active CSS bundle via + * Slashed_Bricks_Inventory, so anything declared in the framework is + * picked up automatically - no hand-curated list to fall behind a release. + * + * Bricks 1.9.2+ also auto-detects custom properties from stylesheets + * loaded into the editor iframe; this class adds organized category labels + * (via bricks/builder/i18n) and code editor autocomplete entries (via + * bricks/code/get_code_signatures) on top of that. */ class Slashed_Bricks_Variables { @@ -34,19 +37,14 @@ public function __construct() { /** * Get all SLASHED CSS variables organized by category. * - * @return array Associative array of category => variables. + * Sourced from the inventory and grouped by prefix family. The shape + * matches the previous hand-curated version so existing filters + * keep working. + * + * @return array Associative array of category => variable names. */ public function get_variables() { - $variables = array( - 'Colors' => $this->get_color_variables(), - 'Spacing' => $this->get_spacing_variables(), - 'Typography' => $this->get_typography_variables(), - 'Layout' => $this->get_layout_variables(), - 'Radius' => $this->get_radius_variables(), - 'Shadows' => $this->get_shadow_variables(), - 'Motion' => $this->get_motion_variables(), - 'Z-Index' => $this->get_z_index_variables(), - ); + $variables = Slashed_Bricks_Inventory::get_variables_by_category(); /** * Filter the registered CSS variables. @@ -63,9 +61,11 @@ public function get_variables() { * @return array Modified i18n strings. */ public function register_variable_groups( $i18n ) { - $variables = $this->get_variables(); + if ( ! is_array( $i18n ) ) { + $i18n = array(); + } - foreach ( $variables as $category => $vars ) { + foreach ( $this->get_variables() as $category => $vars ) { $i18n[ 'slashed_' . sanitize_key( $category ) ] = 'SLASHED ' . $category; } @@ -79,10 +79,12 @@ public function register_variable_groups( $i18n ) { * @return array Modified code signatures. */ public function register_code_signatures( $signatures ) { - $variables = $this->get_variables(); + if ( ! is_array( $signatures ) ) { + $signatures = array(); + } $css_vars = array(); - foreach ( $variables as $category => $vars ) { + foreach ( $this->get_variables() as $category => $vars ) { foreach ( $vars as $var ) { $css_vars[] = array( 'label' => $var, @@ -92,7 +94,7 @@ public function register_code_signatures( $signatures ) { } } - if ( ! isset( $signatures['css'] ) ) { + if ( ! isset( $signatures['css'] ) || ! is_array( $signatures['css'] ) ) { $signatures['css'] = array(); } @@ -100,278 +102,4 @@ public function register_code_signatures( $signatures ) { return $signatures; } - - /** - * Get color-related CSS variables. - * - * @return array - */ - private function get_color_variables() { - $colors = array(); - $brands = array( 'primary', 'secondary', 'tertiary', 'action', 'neutral', 'base' ); - $status = array( 'success', 'warning', 'error', 'info', 'danger' ); - - // Brand colors - base + scale. - foreach ( $brands as $brand ) { - $colors[] = '--sf-color-' . $brand; - foreach ( array( '50', '100', '200', '300', '400', '500', '600', '700', '800', '900', '950' ) as $step ) { - $colors[] = '--sf-color-' . $brand . '-' . $step; - } - - // Functional aliases. - $colors[] = '--sf-color-' . $brand . '-hover'; - $colors[] = '--sf-color-' . $brand . '-active'; - $colors[] = '--sf-color-' . $brand . '-subtle'; - $colors[] = '--sf-color-' . $brand . '-muted'; - $colors[] = '--sf-color-' . $brand . '-ghost'; - - // Shade aliases. - $colors[] = '--sf-color-' . $brand . '-superlight'; - $colors[] = '--sf-color-' . $brand . '-xlight'; - $colors[] = '--sf-color-' . $brand . '-lighter'; - $colors[] = '--sf-color-' . $brand . '-darker'; - $colors[] = '--sf-color-' . $brand . '-xdark'; - $colors[] = '--sf-color-' . $brand . '-superdark'; - } - - // Status colors. - foreach ( $status as $s ) { - $colors[] = '--sf-color-' . $s; - $colors[] = '--sf-color-' . $s . '-subtle'; - $colors[] = '--sf-color-' . $s . '-strong'; - $colors[] = '--sf-color-' . $s . '-muted'; - } - - // Semantic colors. - $semantic = array( - 'text', 'text--secondary', 'text--muted', 'heading', - 'text--placeholder', 'text--disabled', 'text--inverse', - 'text--on-primary', 'text--on-secondary', 'text--on-tertiary', - 'text--on-action', 'text--on-neutral', 'text--on-base', - 'bg', 'surface', 'well', 'raised', 'overlay', 'inverse', - 'bg--hover', 'bg--active', 'bg--selected', 'bg--focus', 'bg--disabled', - 'border', 'border--subtle', 'border--strong', - 'border--focus', 'border--disabled', 'border--translucent', - 'link', 'link--hover', 'link--active', 'link--visited', - 'link--underline', 'link--disabled', - 'code-bg', 'code-text', - 'selection-bg', 'mark-bg', 'dim', - ); - foreach ( $semantic as $s ) { - $colors[] = '--sf-color-' . $s; - } - - return $colors; - } - - /** - * Get spacing-related CSS variables. - * - * @return array - */ - private function get_spacing_variables() { - $sizes = array( 'none', 'px', '2xs', 'xs', 's', 'm', 'l', 'xl', '2xl', '3xl', '4xl' ); - $vars = array(); - - foreach ( $sizes as $size ) { - $vars[] = '--sf-space-' . $size; - } - - $vars[] = '--sf-gap'; - $vars[] = '--sf-content-gap'; - $vars[] = '--sf-component-pad'; - $vars[] = '--sf-space-gutter'; - $vars[] = '--sf-section-pad'; - - return $vars; - } - - /** - * Get typography-related CSS variables. - * - * @return array - */ - private function get_typography_variables() { - $sizes = array( '2xs', 'xs', 's', 'm', 'l', 'xl', '2xl', '3xl', '4xl' ); - $vars = array(); - - foreach ( $sizes as $size ) { - $vars[] = '--sf-text-' . $size; - } - - // Display sizes. - $vars[] = '--sf-text-display-s'; - $vars[] = '--sf-text-display-m'; - $vars[] = '--sf-text-display-l'; - - // Font families. - $vars[] = '--sf-font-body'; - $vars[] = '--sf-font-heading'; - $vars[] = '--sf-font-mono'; - $vars[] = '--sf-font-display'; - $vars[] = '--sf-font-humanist'; - $vars[] = '--sf-font-geometric'; - $vars[] = '--sf-font-slab'; - - // Font weights. - $vars[] = '--sf-font-weight-thin'; - $vars[] = '--sf-font-weight-extralight'; - $vars[] = '--sf-font-weight-light'; - $vars[] = '--sf-font-weight-normal'; - $vars[] = '--sf-font-weight-medium'; - $vars[] = '--sf-font-weight-semibold'; - $vars[] = '--sf-font-weight-bold'; - $vars[] = '--sf-font-weight-extrabold'; - $vars[] = '--sf-font-weight-black'; - - // Leading. - $vars[] = '--sf-leading-tight'; - $vars[] = '--sf-leading-snug'; - $vars[] = '--sf-leading-normal'; - $vars[] = '--sf-leading-relaxed'; - - // Tracking. - $vars[] = '--sf-tracking-tight'; - $vars[] = '--sf-tracking-normal'; - $vars[] = '--sf-tracking-wide'; - $vars[] = '--sf-tracking-wider'; - $vars[] = '--sf-tracking-widest'; - - return $vars; - } - - /** - * Get layout-related CSS variables. - * - * @return array - */ - private function get_layout_variables() { - return array( - '--sf-container-narrow', - '--sf-container-default', - '--sf-container-wide', - '--sf-container-full', - '--sf-stack-gap', - '--sf-cluster-gap', - '--sf-cluster-align', - '--sf-cluster-justify', - '--sf-sidebar-gap', - '--sf-sidebar-min-width', - '--sf-sidebar-width-default', - '--sf-switcher-threshold', - '--sf-switcher-gap', - '--sf-grid-min', - '--sf-grid-gap', - '--sf-cover-min-height', - '--sf-cover-padding', - '--sf-frame-ratio', - '--sf-reel-item-width', - '--sf-reel-gap', - '--sf-reel-height', - '--sf-imposter-margin', - '--sf-bento-gap', - '--sf-breakout-width', - '--sf-content-width', - '--sf-prose-paragraph', - '--sf-box-padding', - '--sf-box-border-width', - '--sf-box-border-color', - '--sf-center-max', - '--sf-center-gutter', - ); - } - - /** - * Get radius-related CSS variables. - * - * @return array - */ - private function get_radius_variables() { - return array( - '--sf-radius-none', - '--sf-radius-xs', - '--sf-radius-s', - '--sf-radius-m', - '--sf-radius-l', - '--sf-radius-xl', - '--sf-radius-2xl', - '--sf-radius-3xl', - '--sf-radius-4xl', - '--sf-radius-full', - ); - } - - /** - * Get shadow-related CSS variables. - * - * @return array - */ - private function get_shadow_variables() { - return array( - '--sf-shadow-none', - '--sf-shadow-xs', - '--sf-shadow-s', - '--sf-shadow-m', - '--sf-shadow-l', - '--sf-shadow-xl', - '--sf-shadow-2xl', - '--sf-shadow-inner', - ); - } - - /** - * Get motion/transition-related CSS variables. - * - * @return array - */ - private function get_motion_variables() { - return array( - // Durations. - '--sf-duration-none', - '--sf-duration-instant', - '--sf-duration-fast', - '--sf-duration-normal', - '--sf-duration-slow', - '--sf-duration-slower', - - // Easings. - '--sf-ease-linear', - '--sf-ease-out', - '--sf-ease-in', - '--sf-ease-in-out', - '--sf-ease-spring', - '--sf-ease-elastic', - '--sf-ease-bounce', - '--sf-ease-overshoot', - - // Transitions. - '--sf-transition-all', - '--sf-transition-colors', - '--sf-transition-transform', - '--sf-transition-opacity', - '--sf-transition-shadow', - '--sf-transition-fast', - '--sf-transition-slow', - '--sf-transition-enter', - '--sf-transition-exit', - ); - } - - /** - * Get z-index CSS variables. - * - * @return array - */ - private function get_z_index_variables() { - return array( - '--sf-z-below', - '--sf-z-base', - '--sf-z-raised', - '--sf-z-low', - '--sf-z-mid', - '--sf-z-high', - '--sf-z-top', - '--sf-z-max', - ); - } } diff --git a/integrations/bricks/slashed-bricks.php b/integrations/bricks/slashed-bricks.php index c115a229..796bf5a9 100644 --- a/integrations/bricks/slashed-bricks.php +++ b/integrations/bricks/slashed-bricks.php @@ -129,6 +129,8 @@ function slashed_bricks_init() { 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-css-parser.php'; + require_once SLASHED_BRICKS_PATH . 'includes/class-inventory.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'; diff --git a/package.json b/package.json index edae4e6b..b5297e34 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,10 @@ ], "scripts": { "prepare": "git rev-parse --is-inside-work-tree >/dev/null 2>&1 && git config core.hooksPath .githooks && chmod +x .githooks/* || true", - "build": "node scripts/bundle.js", + "build": "node scripts/bundle.js && node scripts/gen-bricks-inventory.js", "watch": "node scripts/bundle.js --watch", "docs:tokens": "node scripts/gen-token-reference.js", + "bricks:inventory": "node scripts/gen-bricks-inventory.js", "lint:css": "stylelint \"**/*.css\"", "lint:css:fix": "stylelint \"**/*.css\" --fix", "test": "playwright test", diff --git a/scripts/gen-bricks-inventory.js b/scripts/gen-bricks-inventory.js new file mode 100644 index 00000000..c7ed3fa9 --- /dev/null +++ b/scripts/gen-bricks-inventory.js @@ -0,0 +1,74 @@ +#!/usr/bin/env node +/** + * Generate the Bricks integration's fallback inventory.json from the + * built optimal CSS bundle. + * + * The integration parses the active CSS bundle at runtime to keep the + * Bricks UI registry in sync with whatever the framework actually ships. + * When neither a local file nor a CDN fetch is reachable (e.g. hosts + * blocking outbound HTTP), the integration falls back to this JSON file - + * so this script must run as part of every release that bumps + * SLASHED_BRICKS_CSS_REF in the plugin bootstrap. + * + * Usage: + * node scripts/gen-bricks-inventory.js + */ + +const fs = require('node:fs'); +const path = require('node:path'); + +const ROOT = path.resolve(__dirname, '..'); +const SOURCE = path.join(ROOT, 'dist', 'slashed.optimal.css'); +const OUT = path.join(ROOT, 'integrations', 'bricks', 'data', 'inventory.json'); + +if (!fs.existsSync(SOURCE)) { + console.error(`[gen-bricks-inventory] source not found: ${SOURCE}`); + console.error(' run the bundle build first (npm run build).'); + process.exit(1); +} + +const raw = fs.readFileSync(SOURCE, 'utf8'); + +// Strip block comments so documentation strings like "--sf-space-*" +// don't pollute the inventory. +const css = raw.replace(/\/\*[\s\S]*?\*\//g, ''); + +function unique(list) { + return Array.from(new Set(list)).sort(); +} + +function matchAll(pattern) { + const out = []; + let m; + while ((m = pattern.exec(css)) !== null) { + out.push(m[1]); + } + return unique(out); +} + +const variables = matchAll(/(--sf-[a-zA-Z0-9_-]+)\s*:/g); +const sfClasses = matchAll(/\.(sf-[a-zA-Z0-9_-]+)/g); +const isClasses = matchAll(/\.(is-[a-zA-Z0-9_-]+)/g); + +const inventory = { + _meta: { + source: 'dist/slashed.optimal.css', + generated_at: new Date().toISOString(), + counts: { + variables: variables.length, + sf_classes: sfClasses.length, + is_classes: isClasses.length, + }, + }, + variables, + sf_classes: sfClasses, + is_classes: isClasses, +}; + +fs.mkdirSync(path.dirname(OUT), { recursive: true }); +fs.writeFileSync(OUT, JSON.stringify(inventory, null, 2) + '\n'); + +console.log( + `[gen-bricks-inventory] wrote ${path.relative(ROOT, OUT)} ` + + `(${variables.length} vars, ${sfClasses.length} .sf-, ${isClasses.length} .is-)` +);