# extendify/trunk/src/Shared/lib/palette-preview.js

Extendify, version trunk. 250 lines.

- Page: https://pluginprobe.com/plugins/extendify/trunk/code/src/Shared/lib/palette-preview.js
- Raw: https://pluginprobe.com/plugins/extendify/trunk/raw/src/Shared/lib/palette-preview.js
- Modified: 2026-09-09T18:23:40+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/extendify/trunk/code/src/Shared/lib/palette-preview.js#L10-L20`.

```javascript
import {
	ownedPaletteSettingPaths,
	ownedPaletteStylePaths,
} from '@shared/lib/palette-globals';
import { isObject } from '@shared/lib/utils';

const headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];

const elementSelectors = {
	body: 'body',
	button: '.wp-element-button, .wp-block-button__link',
	caption: '.wp-element-caption',
	cite: 'cite',
	link: 'a:where(:not(.wp-element-button))',
};

const cssProperties = {
	'color.text': 'color',
	'color.background': 'background-color',
	'color.gradient': 'background',
	'border.color': 'border-color',
};

const kebab = (property) =>
	property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);

// Core names the vars via _wp_to_kebab_case, which drops the ':' from :hover.
const wpKebab = (key) =>
	kebab(key)
		.replace(/[^a-z0-9-]+/gi, '-')
		.replace(/-+/g, '-')
		.replace(/^-|-$/g, '');

const valueAt = (source, path) =>
	path.reduce(
		(value, key) => (isObject(value) ? value[key] : undefined),
		source,
	);

// theme.json stores preset references as var:preset|color|primary.
const cssValue = (value) =>
	typeof value === 'string' && value.startsWith('var:')
		? `var(--wp--${value.slice(4).split('|').map(kebab).join('--')})`
		: value;

// revert alone would drop the theme's own value with the outgoing palette's.
const resetValue = (value) =>
	typeof value === 'string' || typeof value === 'number'
		? cssValue(value)
		: 'revert';

const blockSelector = (block) => {
	const [namespace, name] = block.split('/');
	return namespace === 'core'
		? `.wp-block-${name}`
		: `.wp-block-${namespace}-${name}`;
};

const presetGroups = {
	palette: { var: 'color', value: 'color' },
	gradients: { var: 'gradient', value: 'gradient' },
};

// A palette lists its presets plainly; WP keys its own theme data by origin.
const presetList = (settings, group) => {
	const presets = valueAt(settings, ['color', group]);
	return Array.isArray(presets) ? presets : (presets?.theme ?? []);
};

const presetValue = (settings, group, slug, key) =>
	presetList(settings, group).find((entry) => entry?.slug === slug)?.[key];

const presetSlugs = (palettes, group) => {
	const slugs = new Set();

	for (const palette of Object.values(palettes ?? {})) {
		for (const entry of presetList(palette?.settings, group)) {
			if (entry?.slug) slugs.add(entry.slug);
		}
	}

	return [...slugs];
};

// Returning [] rather than null would hide an unmapped leaf from the guard.
const settingSlots = (path, palettes) => {
	const [root, group] = path;

	// The svg filter carries the duotone, so there is no var to write.
	if (root === 'color' && group === 'duotone') return [];

	if (root === 'color' && presetGroups[group]) {
		const { var: name, value } = presetGroups[group];
		return presetSlugs(palettes, group).map((slug) => ({
			selector: ':root',
			property: `--wp--preset--${name}--${wpKebab(slug)}`,
			read: (settings) => presetValue(settings, group, slug, value),
		}));
	}

	if (root !== 'custom') return null;

	return [
		{
			selector: ':root',
			property: `--wp--${path.map(wpKebab).join('--')}`,
			read: (settings) => valueAt(settings, path),
		},
	];
};

const styleSlots = (path) => {
	if (path[0] === 'elements') {
		const [, element, ...rest] = path;
		const pseudo = rest[0]?.startsWith(':') ? rest[0] : '';
		const property = cssProperties[rest.slice(pseudo ? 1 : 0).join('.')];
		if (!property) return null;

		if (element !== 'heading') {
			return [
				{
					selector: `:root :where(${elementSelectors[element] ?? element})${pseudo}`,
					property,
					read: (styles) => valueAt(styles, path),
				},
			];
		}

		// Resolved per level so a theme's own h5 colour survives the reset.
		return headings.map((level) => ({
			selector: `:root :where(${level})${pseudo}`,
			property,
			read: (styles) =>
				valueAt(styles, ['elements', level, ...rest]) ??
				valueAt(styles, ['elements', 'heading', ...rest]),
		}));
	}

	if (path[0] === 'blocks') {
		const [, block, ...rest] = path;
		const property = cssProperties[rest.join('.')];
		if (!property) return null;

		return [
			{
				selector: `:root :where(${blockSelector(block)})`,
				property,
				read: (styles) => valueAt(styles, path),
			},
		];
	}

	const property = cssProperties[path.join('.')];
	if (!property) return null;

	return [
		{
			selector: ':root :where(body)',
			property,
			read: (styles) => valueAt(styles, path),
		},
	];
};

const paletteSlots = (palettes) => {
	const slots = [];
	const covered = [];

	for (const path of ownedPaletteSettingPaths(palettes)) {
		const built = settingSlots(path, palettes);
		if (built === null) continue;
		covered.push(`settings.${path.join('.')}`);
		slots.push(...built.map((slot) => ({ ...slot, section: 'settings' })));
	}

	for (const path of ownedPaletteStylePaths(palettes)) {
		const built = styleSlots(path);
		if (built === null) continue;
		covered.push(`styles.${path.join('.')}`);
		slots.push(...built.map((slot) => ({ ...slot, section: 'styles' })));
	}

	return { slots, covered };
};

export const paletteResetScope = (palettes) =>
	paletteSlots(palettes).covered.sort();

// An older backend wraps this list by origin.
export const paletteDuotone = (palette) => {
	const duotone = palette?.settings?.color?.duotone;
	return Array.isArray(duotone) ? duotone : duotone?.theme;
};

const cssFromSlots = ({ slots, incoming, themeStyles, themeSettings }) => {
	const rules = new Map();

	for (const { selector, property, section, read } of slots) {
		const settings = section === 'settings';
		const value = read(settings ? incoming?.settings : incoming?.styles);
		const declarations = rules.get(selector) ?? [];

		declarations.push(
			`${property}:${
				value === undefined
					? resetValue(read(settings ? themeSettings : themeStyles))
					: cssValue(value)
			}`,
		);
		rules.set(selector, declarations);
	}

	let css = '';
	for (const [selector, declarations] of rules) {
		css += `${selector}{${declarations.join(';')};}`;
	}

	return css;
};

// A palette carries no compiled css, unlike a variation or a vibe.
export const buildPaletteCss = ({
	payloads,
	slug,
	themeStyles,
	themeSettings,
}) =>
	cssFromSlots({
		slots: paletteSlots(payloads).slots,
		incoming: payloads?.[slug],
		themeStyles,
		themeSettings,
	});

// One slot pass for the whole set; per-slug calls make it quadratic.
export const buildPaletteCssMap = ({
	payloads,
	themeStyles,
	themeSettings,
}) => {
	const { slots } = paletteSlots(payloads);

	return Object.fromEntries(
		Object.entries(payloads ?? {}).map(([slug, incoming]) => [
			slug,
			cssFromSlots({ slots, incoming, themeStyles, themeSettings }),
		]),
	);
};

```
