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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"version-sync": "node scripts/version-sync.js",
"lint:css": "stylelint \"**/*.css\"",
"lint:css:fix": "stylelint \"**/*.css\" --fix",
"pretest": "npm run build && node --test tests/element-types.test.js tests/class-hints.test.js",
"pretest": "npm run build && node --test tests/element-types.test.js tests/class-hints.test.js tests/color-model.test.js",
"test": "playwright test",
"test:install": "playwright install --with-deps chromium firefox webkit",
"release": "release-it",
Expand Down
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 @@ -11,6 +11,7 @@ A WordPress plugin that integrates the [SLASHED](https://github.com/codeslash-de
- **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.
- **Color System Panel** - A floating in-builder browser for every `--sf-color-*` token, launched from a pill in the builder corner. Tokens are grouped the way the framework organises colour (the six brand families and five status families, each split into shades/tints, transparent steps, and named semantic aliases, plus a combined Semantic group for page-level tokens like text/bg/border/link). Its differentiator over a single-mode palette: because every SLASHED token is an adaptive `light-dark()` value, each swatch previews **both** variants at once (the "Both" mode splits a swatch on the diagonal — light top-left, dark bottom-right), and the Light/Dark toggle drives the live canvas `[data-theme]` so real elements adapt as you browse. Picking a swatch copies its `var(--sf-color-*)` reference to the clipboard **and** applies it to the selected element's chosen target (background / text / border) — always the variable, never a baked hex, so the element stays theme- and dark-mode-aware. With nothing selected it degrades to copy-only. Light/dark hex previews are server-resolved (`Slashed_Bricks_Color_Resolver`) so no SLASHED stylesheet needs loading in the builder chrome. Toggle with the `slashed_bricks/show_color_panel` filter.

## Requirements

Expand Down Expand Up @@ -105,6 +106,15 @@ Control whether colour swatches are painted next to `--sf-color-*` entries in th
add_filter( 'slashed_bricks/show_color_swatches', '__return_false' );
```

#### `slashed_bricks/show_color_panel`

Control whether the in-builder **Color System Panel** (and its launcher pill) is loaded (see Color System Panel above). Defaults to `true`. Returning `false` skips localising the panel's token list and light/dark hex maps, so no extra data is sent to the builder.

```php
// Hide the Color System panel.
add_filter( 'slashed_bricks/show_color_panel', '__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,23 @@
<script>
/**
* Color System orchestrator.
*
* Owns the open/closed state, renders the always-present launcher pill, and
* mounts the panel on demand. Mounted once by main.js (independent of the
* reBEMer structure-panel pipeline) so the colour browser is reachable from
* anywhere in the builder, not tied to a selected element.
*/
import ColorLauncher from './ColorLauncher.svelte';
import ColorPanel from './ColorPanel.svelte';

/** @type {{ source: { variables: string[], light: object, dark: object } }} */
let { source } = $props();

let open = $state(false);
</script>

<ColorLauncher {open} onToggle={() => (open = !open)} />

{#if open}
<ColorPanel {source} onClose={() => (open = false)} />
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script>
/**
* Floating launcher for the Color System panel.
*
* A small pill pinned to the bottom-right of the builder viewport. Kept
* deliberately unobtrusive (out of the way of Bricks' own toolbars) and
* toggles the panel open/closed. Reflects open state via aria-pressed.
*/
/** @type {{ open: boolean, onToggle: () => void }} */
let { open, onToggle } = $props();
</script>

<button
type="button"
class="slashed-cp-launch"
class:slashed-cp-launch--on={open}
aria-pressed={open}
aria-label="Toggle SLASHED Color System"
title="SLASHED Color System"
onclick={onToggle}
>
<span class="slashed-cp-launch__dot" aria-hidden="true"></span>
<span class="slashed-cp-launch__txt">Colors</span>
</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
<script>
/**
* The Color System panel.
*
* A floating browser for the framework's `--sf-color-*` token system,
* mounted once when the launcher is activated. Inspired by builder colour
* managers, but leaning into what makes SLASHED different: every token is
* an adaptive `light-dark()` value, so the panel previews BOTH variants at
* once (the "Both" mode splits each swatch on the diagonal) and the mode
* toggle drives the live canvas theme so real elements adapt as you browse.
*
* Interaction model:
* - Pick a swatch → its `var(--sf-color-*)` reference is copied to the
* clipboard AND applied to the selected element's chosen target
* (text / background / border). Applying the *variable* (never a baked
* hex) keeps the element adaptive. With nothing selected it degrades to
* copy-only, with a toast telling you to paste.
* - Search filters the whole model live.
*
* The model itself is built by the pure `color-model.js` from data the PHP
* side localises; this component is DOM/Bricks glue + presentation.
*/
import { onMount } from 'svelte';
import * as api from '../lib/bricks-api.js';
import { buildColorModel, filterModel, swatchValue } from '../lib/color-model.js';
import ColorSwatch from './ColorSwatch.svelte';
import Toast from './Toast.svelte';

/** @type {{ source: { variables: string[], light: object, dark: object }, onClose?: () => void }} */
let { source, onClose } = $props();

const TARGETS = [
{ id: 'background', label: 'Background' },
{ id: 'text', label: 'Text' },
{ id: 'border', label: 'Border' },
];
const MODES = [
{ id: 'both', label: 'Both' },
{ id: 'light', label: 'Light' },
{ id: 'dark', label: 'Dark' },
];

let mode = $state('both');
let target = $state('background');
let query = $state('');
let toast = $state(null);

const fullModel = $derived(buildColorModel(source?.variables, source?.light, source?.dark));
const model = $derived(filterModel(fullModel, query));
const totalTokens = $derived(fullModel.groups.reduce((n, g) => n + g.count, 0));
const targetLabel = $derived(TARGETS.find((t) => t.id === target)?.label ?? target);

// --- Live canvas theme preview -------------------------------------------
// Driving the builder canvas iframe's [data-theme] lets the user watch real
// elements flip light/dark as they switch modes. Best-effort and reversible:
// we snapshot the original attribute on mount and restore it on teardown, and
// "Both" restores the original (the canvas can only show one mode at a time).
let _canvasOriginal; // undefined = not captured yet
function canvasRoot() {
if (typeof document === 'undefined') return null;
const iframe = document.querySelector(
'iframe#bricks-builder-iframe, iframe[name="bricks-builder-iframe"], #bricks-builder-area iframe'
);
try {
return iframe?.contentDocument?.documentElement ?? null;
} catch {
return null; // cross-origin or not ready — give up silently
}
}
function applyCanvasTheme(nextMode) {
const root = canvasRoot();
if (!root) return;
try {
if (_canvasOriginal === undefined) {
_canvasOriginal = root.getAttribute('data-theme');
}
if (nextMode === 'light' || nextMode === 'dark') {
root.setAttribute('data-theme', nextMode);
} else if (_canvasOriginal === null) {
root.removeAttribute('data-theme');
} else {
root.setAttribute('data-theme', _canvasOriginal);
}
} catch {
/* fail silently — preview is a bonus, never load-bearing */
}
}
function restoreCanvasTheme() {
const root = canvasRoot();
if (!root || _canvasOriginal === undefined) return;
try {
if (_canvasOriginal === null) root.removeAttribute('data-theme');
else root.setAttribute('data-theme', _canvasOriginal);
} catch { /* ignore */ }
}

function setMode(next) {
mode = next;
applyCanvasTheme(next);
}

onMount(() => () => restoreCanvasTheme());

// --- Clipboard ------------------------------------------------------------
async function copyText(text) {
try {
if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
} catch { /* fall through to legacy path */ }
try {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
const ok = document.execCommand('copy');
ta.remove();
return ok;
} catch {
return false;
}
}

async function pick(swatch) {
const value = swatchValue(swatch);
const copied = await copyText(value);

const id = api.getActiveElementId();
if (id) {
let applied = false;
api.batchMutations(() => {
applied = api.setElementColor(id, target, value);
});
if (applied) {
const label = api.getElementLabel(id) || 'element';
toast = { kind: 'success', message: `${targetLabel} of “${label}” → ${swatch.name}` };
} else {
// We had a selection but couldn't write to it (e.g. a stale id from
// the DOM fallback). Say so rather than implying nothing was selected.
toast = copied
? { kind: 'info', message: `Copied ${value} — couldn't apply to the selected element` }
: { kind: 'error', message: `Couldn't apply or copy ${value}` };
}
return;
}

toast = copied
? { kind: 'info', message: `Copied ${value} — select an element or paste into any Bricks colour field` }
: { kind: 'error', message: `Couldn't copy ${value}` };
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function handleKeydown(e) {
if (e.key === 'Escape') onClose?.();
}
</script>

<svelte:window onkeydown={handleKeydown} />

<div class="slashed-cp" role="dialog" aria-modal="false" aria-label="SLASHED Color System" tabindex="-1">
<header class="slashed-cp__header">
<h2 class="slashed-cp__title">Color System</h2>
<div class="slashed-cp__seg" role="group" aria-label="Preview mode">
{#each MODES as m (m.id)}
<button
type="button"
class="slashed-cp__seg-btn"
class:slashed-cp__seg-btn--on={mode === m.id}
aria-pressed={mode === m.id}
onclick={() => setMode(m.id)}
>{m.label}</button>
{/each}
</div>
<button type="button" class="slashed-cp__close" aria-label="Close Color System panel" onclick={() => onClose?.()}>×</button>
</header>

<div class="slashed-cp__toolbar">
<input
type="text"
class="slashed-cp__search"
placeholder={`Search ${totalTokens} colours…`}
bind:value={query}
aria-label="Search colours"
/>
<div class="slashed-cp__target" role="group" aria-label="Apply to">
<span class="slashed-cp__target-label">Apply to</span>
{#each TARGETS as t (t.id)}
<button
type="button"
class="slashed-cp__chip"
class:slashed-cp__chip--on={target === t.id}
aria-pressed={target === t.id}
onclick={() => (target = t.id)}
>{t.label}</button>
{/each}
</div>
</div>

<div class="slashed-cp__body">
{#if model.groups.length === 0}
<p class="slashed-cp__empty">No colours match “{query}”.</p>
{/if}

{#each model.groups as group (group.id)}
<section class="slashed-cp__group" data-type={group.type}>
<div class="slashed-cp__group-head">
<h3 class="slashed-cp__group-title">
{group.label}
{#if group.tagline}<span class="slashed-cp__group-tag">{group.tagline}</span>{/if}
<span class="slashed-cp__group-count">{group.count}</span>
</h3>
{#if group.use}<p class="slashed-cp__group-use">{group.use}</p>{/if}
</div>

{#each group.sections as section (section.id)}
{#if section.label}
<p class="slashed-cp__section-label">{section.label}</p>
{/if}

{#if group.type === 'semantic'}
<ul class="slashed-cp__list">
{#each section.swatches as swatch (swatch.var)}
<li class="slashed-cp__list-item">
<ColorSwatch {swatch} {mode} onPick={pick} />
<span class="slashed-cp__list-name">{swatch.label}</span>
<code class="slashed-cp__list-var">{swatch.name}</code>
</li>
{/each}
</ul>
{:else}
<div class="slashed-cp__grid">
{#each section.swatches as swatch (swatch.var)}
<div class="slashed-cp__cell">
<ColorSwatch {swatch} {mode} onPick={pick} />
<span class="slashed-cp__cap">{swatch.label}</span>
</div>
{/each}
</div>
{/if}
{/each}
</section>
{/each}
</div>

<footer class="slashed-cp__footer">
{#if mode === 'both'}
<span aria-hidden="true" class="slashed-cp__legend"><span class="slashed-cp__legend-sw"></span> light / dark</span>
{/if}
<span class="slashed-cp__hint">Click a colour to apply &amp; copy <code>var()</code></span>
</footer>
</div>

{#if toast}
<Toast kind={toast.kind} message={toast.message} onDismiss={() => { toast = null; }} />
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script>
/**
* A single colour swatch button.
*
* Renders the framework token's resolved colour. In 'both' mode the square
* is split on the diagonal — light variant top-left, dark variant
* bottom-right — so the adaptive pair reads at a glance (the whole point of
* an adaptive `light-dark()` system). In 'light' / 'dark' mode it paints a
* single solid variant. Translucent tokens (alpha steps, overlays) sit over
* a checkerboard so transparency is visible.
*
* Pure presentation + a single onPick callback; all the apply/copy logic
* lives in ColorPanel.
*/
import { swatchHex } from '../lib/color-model.js';

/** @type {{ swatch: object, mode: 'light'|'dark'|'both', onPick: (s: object) => void }} */
let { swatch, mode, onPick } = $props();

const title = $derived(
`${swatch.name}\nlight ${swatch.light} · dark ${swatch.dark}\nclick: apply + copy var(${swatch.var})`
);
</script>

<button
type="button"
class="slashed-cp-swatch"
class:slashed-cp-swatch--alpha={swatch.alpha}
class:slashed-cp-swatch--split={mode === 'both'}
style="--cp-l:{swatch.light}; --cp-d:{swatch.dark}; --cp-solid:{swatchHex(swatch, mode === 'dark' ? 'dark' : 'light')};"
{title}
aria-label={`${swatch.name} — apply and copy`}
onclick={() => onPick(swatch)}
>
<span class="slashed-cp-swatch__fill" aria-hidden="true"></span>
</button>
Loading