-
Notifications
You must be signed in to change notification settings - Fork 1
feat(bricks): add in-builder Color System panel with light/dark preview #193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
plugins/SLASHED-for-WP/integrations/bricks/assets/editor-app/app.css
Large diffs are not rendered by default.
Oops, something went wrong.
10 changes: 6 additions & 4 deletions
10
plugins/SLASHED-for-WP/integrations/bricks/assets/editor-app/app.js
Large diffs are not rendered by default.
Oops, something went wrong.
23 changes: 23 additions & 0 deletions
23
plugins/SLASHED-for-WP/integrations/bricks/editor-app/src/components/ColorApp.svelte
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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} |
24 changes: 24 additions & 0 deletions
24
plugins/SLASHED-for-WP/integrations/bricks/editor-app/src/components/ColorLauncher.svelte
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
257 changes: 257 additions & 0 deletions
257
plugins/SLASHED-for-WP/integrations/bricks/editor-app/src/components/ColorPanel.svelte
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}` }; | ||
| } | ||
|
|
||
| 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 & copy <code>var()</code></span> | ||
| </footer> | ||
| </div> | ||
|
|
||
| {#if toast} | ||
| <Toast kind={toast.kind} message={toast.message} onDismiss={() => { toast = null; }} /> | ||
| {/if} | ||
36 changes: 36 additions & 0 deletions
36
plugins/SLASHED-for-WP/integrations/bricks/editor-app/src/components/ColorSwatch.svelte
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.