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 '';
@@ -391,21 +507,274 @@ private function render_tab_colors( $settings ) {
echo '' . esc_html__( 'Status Colors', 'slashed-bricks' ) . ' ';
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 '';
+
+ echo '' . esc_html__( 'Opacity', 'slashed-bricks' ) . ' ';
+ 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 '';
+ }
+
+ /**
+ * 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 '' . esc_html( $label ) . ' ';
+ 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-)`
+);