Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions plugins/SLASHED-for-WP/integrations/bricks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ A WordPress plugin that integrates the [SLASHED](https://github.com/codeslash-de
- **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. Disabled automatically on Bricks 2.2+ (the new Color Manager bakes palette colors into `:root` as static hex, which would override the framework's adaptive `light-dark()` tokens and break dark mode) — use the Variable Manager to reach the tokens there
- **Variable-Picker Swatches** - Paints a colour square next to each `--sf-color-*` entry in the Bricks variable-picker dropdown, builder-side only. Because the variables stay empty-valued, Bricks has no value to draw a swatch from; the square is rendered from a server-resolved hex map so dark/light stays 100% framework-driven and nothing is written to `:root`. Purely additive and fail-silent: if Bricks ever changes the picker markup you lose the squares, never the picker. Restores the swatch affordance lost when the Color Palette is disabled on Bricks 2.2+. Toggle with the `slashed_bricks/show_color_swatches` filter
- **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.
- **reBEMer** - Subtree-scoped BEM class manager inside the Bricks builder structure panel: add / rename / replace classes for an element and its children in one transaction, with reference-count preflight (REST), snapshot+rollback, reserved-name guard against SLASHED utilities, and Cmd/Ctrl-Z undo. See [docs/rebemer.md](../../docs/rebemer.md) for the full design.

Expand Down Expand Up @@ -95,6 +96,15 @@ Control whether SLASHED palettes are injected into the Bricks color palette (`br
add_filter( 'slashed_bricks/inject_color_palette', '__return_true' );
```

#### `slashed_bricks/show_color_swatches`

Control whether colour swatches are painted next to `--sf-color-*` entries in the Bricks variable-picker dropdown (builder-side only — see Variable-Picker Swatches above). Defaults to `true`. Returning `false` also skips localising the hex map, so no extra data is sent to the builder.

```php
// Hide the variable-picker swatches.
add_filter( 'slashed_bricks/show_color_swatches', '__return_false' );
```

#### `slashed_bricks/registered_variables`

Filter the CSS variables array before registration with Bricks.
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
/**
* Color swatches for the Bricks variable-picker dropdown.
*
* SLASHED color tokens are registered with Bricks' Variable Manager as
* empty-valued variables (see includes/class-variables.php) so the
* framework's adaptive `light-dark()` definitions in `:root` stay the
* single source of truth and nothing the plugin injects competes with
* them. The cost of that fidelity is that Bricks has no value to render
* a colour swatch from in the variable picker — entries show as plain
* text.
*
* This module paints the swatch back in, builder-side only, without ever
* touching `:root`. Each `--sf-color-*` entry in the picker dropdown gets
* a small coloured square using a server-resolved hex map (localised from
* `Slashed_Bricks_Inventory::get_color_hex_map()`), because the builder
* panel does not load the SLASHED stylesheet and so cannot resolve
* `var(--sf-color-*)` itself.
*
* Design notes — mirrors the rest of the integration's philosophy:
*
* - **Additive and fail-silent.** Bricks renders the list, names and
* the canvas hover-preview natively; we only prepend a swatch. If the
* markup ever changes or anything throws, you lose the square — never
* the picker.
*
* - **Reconcile, don't stamp-once.** Bricks (Vue) reuses `<li>` nodes
* when the list is filtered via the search box, so a row that was
* "sf-color-action" can become "sf-color-primary" — or a non-colour
* variable — while keeping the same DOM node. Every pass recomputes
* each row's desired colour and adds / updates / removes its swatch
* accordingly, which is reuse-safe where a one-time marker would go
* stale.
*
* - **Scoped to colour tokens only.** `resolveSwatchColor()` returns a
* colour only for `--sf-color-*` names present in the hex map, so the
* same observer is harmless in the spacing / typography pickers.
*
* @module color-swatches
*/

const ITEM_SELECTOR = 'li.variable-picker-item';
const SWATCH_CLASS = 'slashed-var-swatch';
const DEBOUNCE_MS = 50;

const log = (level, ...args) => console[level]('[slashed-swatches]', ...args);

/**
* Resolve the swatch colour for a variable label, or null.
*
* Pure and DOM-free so it can be unit tested with `node --test`. Accepts
* the variable name as shown in the picker (Bricks omits the leading
* `--`, e.g. "sf-color-primary") and returns the resolved hex from the
* map, or null when the name is not a single known `--sf-color-*` token.
*
* @param {string} rawName - e.g. "sf-color-primary" or "--sf-color-primary".
* @param {Record<string, string>} hexMap - Map keyed by "--sf-color-*".
* @returns {string | null}
*/
export function resolveSwatchColor(rawName, hexMap) {
if (!rawName || !hexMap) return null;

let name = String(rawName).trim();
if (!name || /\s/.test(name)) return null;

// The picker renders names without the leading `--`; normalise so the
// lookup key matches the hex map (which is keyed by the full property).
if (name.slice(0, 2) !== '--') name = '--' + name.replace(/^-+/, '');

if (name.indexOf('--sf-color-') !== 0) return null;

const hex = hexMap[name];
return typeof hex === 'string' && hex ? hex : null;
}

// ----- DOM glue (only runs in the browser) ---------------------------

let _enabled = false;
let _hexMap = {};
let _observer = null;
let _debounce = null;
let _controller = null;

/**
* Read the variable name a picker row represents.
*
* The row layout is:
* <li class="variable-picker-item">
* <span title="sf-color-primary">sf-color-primary</span>
* <span class="option-value"></span>
* </li>
* The first span's `title` holds the canonical name; fall back to its
* text, then the row text, so a minor markup shift still resolves.
*
* @param {Element} li
* @returns {string}
*/
function itemName(li) {
const span =
li.querySelector(':scope > span[title]') || li.querySelector(':scope > span');
if (span) {
const title = span.getAttribute('title');
if (title && title.trim()) return title.trim();
return (span.textContent || '').trim();
}
return (li.textContent || '').trim();
}

/**
* Add, update, or remove a single row's swatch to match its current
* variable. Idempotent and safe to run on every pass.
*
* @param {Element} li
*/
function reconcile(li) {
// Category / group headers (`.title`, `.category`) are not variables.
if (li.classList.contains('title') || li.classList.contains('category')) {
const stale = li.querySelector(':scope > .' + SWATCH_CLASS);
if (stale) stale.remove();
return;
}

const color = resolveSwatchColor(itemName(li), _hexMap);
let swatch = li.querySelector(':scope > .' + SWATCH_CLASS);

if (!color) {
if (swatch) swatch.remove();
return;
}

if (!swatch) {
swatch = document.createElement('span');
swatch.className = SWATCH_CLASS;
swatch.setAttribute('aria-hidden', 'true');
li.insertBefore(swatch, li.firstChild);
}

if (swatch.dataset.color !== color) {
swatch.style.setProperty('--slashed-swatch-color', color);
swatch.dataset.color = color;
}
}

function runPass() {
try {
const items = document.querySelectorAll(ITEM_SELECTOR);
for (const li of items) reconcile(li);
} catch (err) {
log('warn', 'swatch pass failed', err);
}
}

function schedule() {
if (_debounce !== null) return;
_debounce = setTimeout(() => {
_debounce = null;
runPass();
}, DEBOUNCE_MS);
}

/**
* Cheap pre-filter for the body-level observer: only schedule a pass when
* a mutation actually touches the variable picker.
*
* We observe `document.body` (the picker dropdown is created, destroyed,
* and re-rendered by Bricks, with no container that's stable across its
* whole lifecycle), but the vast majority of builder mutations — canvas
* edits, structure-panel rebuilds — are irrelevant. Bailing here keeps
* those from waking the reconciler at all.
*
* - A text/markup patch inside an existing row (Bricks reuses `<li>`
* nodes when filtering) shows up as a mutation whose target is within
* a `.variable-picker-item`.
* - Opening the dropdown adds a subtree that contains the rows.
*
* @param {MutationRecord[]} records
* @returns {boolean}
*/
function touchesPicker(records) {
for (const m of records) {
const t = m.target;
if (t && t.nodeType === 1 && t.closest && t.closest(ITEM_SELECTOR)) {
return true;
}
for (const node of m.addedNodes) {
if (node.nodeType !== 1) continue;
if (
(node.matches && node.matches(ITEM_SELECTOR)) ||
(node.querySelector && node.querySelector(ITEM_SELECTOR))
) {
return true;
}
}
}
return false;
}

/**
* Initialise variable-picker swatches.
*
* Safe to call when disabled (no-ops) and idempotent (a second call
* tears down the first). Pass an `AbortSignal` to have the observer
* disconnected automatically when the host aborts, matching how the
* reBEMer entry point manages its own lifecycle.
*
* @param {boolean} enabled
* @param {Record<string, string>} hexMap - Map keyed by "--sf-color-*".
* @param {{ signal?: AbortSignal }} [options]
*/
export function init(enabled, hexMap, options = {}) {
destroy();

_enabled = !!enabled;
_hexMap = hexMap && typeof hexMap === 'object' ? hexMap : {};
if (!_enabled || Object.keys(_hexMap).length === 0) return;

_controller = new AbortController();

// The picker dropdown is created, destroyed, and re-rendered (on search)
// by Bricks, so a persistent observer is simpler and more robust than
// trying to hook open/close. We watch `document.body` because there's no
// container that stays mounted across the picker's whole lifecycle, but
// `touchesPicker()` filters out unrelated builder churn so canvas edits
// and structure-panel rebuilds never wake the reconciler. When a
// relevant mutation lands we schedule a single cheap debounced pass;
// our own swatch insertions trigger at most one extra no-op pass before
// settling.
_observer = new MutationObserver((records) => {
if (touchesPicker(records)) schedule();
});
_observer.observe(document.body, { childList: true, subtree: true });

runPass();

if (options.signal) {
if (options.signal.aborted) destroy();
else options.signal.addEventListener('abort', destroy, { once: true });
}
}

/**
* Tear down the observer and remove every swatch we injected.
*/
export function destroy() {
if (_debounce !== null) {
clearTimeout(_debounce);
_debounce = null;
}
if (_observer) {
_observer.disconnect();
_observer = null;
}
if (_controller) {
_controller.abort();
_controller = null;
}
try {
document.querySelectorAll('.' + SWATCH_CLASS).forEach((node) => node.remove());
} catch {
/* ignore */
}
_enabled = false;
_hexMap = {};
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import { mount, unmount } from 'svelte';
import * as api from './lib/bricks-api.js';
import * as classHints from './lib/class-hints.js';
import * as colorSwatches from './lib/color-swatches.js';
import BemBadge from './components/BemBadge.svelte';
import BemPanel from './components/BemPanel.svelte';
import './styles/panel.css';
Expand Down Expand Up @@ -59,6 +60,7 @@ function start() {
const cfg = window.slashedBricksEditor;
if (cfg && typeof cfg === 'object') {
classHints.init(cfg.showClassHints, cfg.classHints, { signal });
colorSwatches.init(cfg.showColorSwatches, cfg.colorHexMap, { signal });
}

let attempts = 0;
Expand Down Expand Up @@ -237,6 +239,7 @@ window.addEventListener('beforeunload', () => {
controller.abort();
closePanel();
classHints.destroy();
colorSwatches.destroy();
for (const { instance } of badgeInstances.values()) {
try { unmount(instance); } catch { /* ignore */ }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,3 +353,29 @@ li[data-id]:focus-within > .rebemer-badge-host {
}
.rebemer-class-hint__cat[hidden] { display: none; }
.rebemer-class-hint__desc { margin: 4px 0 0; color: var(--rebemer-fg); }

/* Variable-picker colour swatch — a small square prepended to each
`--sf-color-*` row in the Bricks variable dropdown by
color-swatches.js. The colour is set inline via the
`--slashed-swatch-color` custom property; everything else lives here.
The checkerboard underlay shows through translucent (alpha) tokens. */
.slashed-var-swatch {
flex: 0 0 auto;
display: inline-block;
width: 12px;
height: 12px;
margin-right: 7px;
vertical-align: middle;
border-radius: 3px;
border: 1px solid rgba(127, 127, 127, .45);
/* Layer order: the resolved colour sits on top (opaque tokens hide the
checkerboard entirely); translucent/alpha tokens let the checkerboard
show through so transparency reads at a glance. */
background-color: #fff;
background-image:
linear-gradient(var(--slashed-swatch-color, transparent), var(--slashed-swatch-color, transparent)),
linear-gradient(45deg, rgba(127, 127, 127, .35) 25%, transparent 25%, transparent 75%, rgba(127, 127, 127, .35) 75%),
linear-gradient(45deg, rgba(127, 127, 127, .35) 25%, transparent 25%, transparent 75%, rgba(127, 127, 127, .35) 75%);
background-position: 0 0, 0 0, 4px 4px;
background-size: auto, 8px 8px, 8px 8px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,31 @@ public function enqueue() {
wp_enqueue_script( self::SCRIPT_HANDLE, $base_url . 'app.js', array(), $js_ver, true );

$plugin_settings = Slashed_Token_Store::get_plugin_settings();

/**
* Toggle the variable-picker colour swatches.
*
* Swatches are painted builder-side onto each `--sf-color-*` entry
* in the Bricks variable dropdown (see editor-app color-swatches.js)
* and never touch `:root`, so dark/light stays framework-driven.
* Filter to false to disable them.
*
* @param bool $enabled Default true.
*/
$show_color_swatches = (bool) apply_filters( 'slashed_bricks/show_color_swatches', true );

$color_hex_map = ( $show_color_swatches && class_exists( 'Slashed_Bricks_Inventory' ) )
? Slashed_Bricks_Inventory::get_color_hex_map()
: array();

wp_localize_script(
self::SCRIPT_HANDLE,
'slashedBricksEditor',
array(
'showClassHints' => ! empty( $plugin_settings['show_class_hints'] ),
'classHints' => Slashed_Token_Page::get_class_hints(),
'showClassHints' => ! empty( $plugin_settings['show_class_hints'] ),
'classHints' => Slashed_Token_Page::get_class_hints(),
'showColorSwatches' => $show_color_swatches,
'colorHexMap' => $color_hex_map,
)
);
}
Expand Down