PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 All 128 releases
extendify / src / Shared / lib / palette-preview.js

palette-preview.js in Extendify 3.2.2, at src/Shared/lib/palette-preview.js

275 lines 7.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import {
2 ownedPaletteSettingPaths,
3 ownedPaletteStylePaths,
4 } from '@shared/lib/palette-globals';
5 import { isObject } from '@shared/lib/utils';
6
7 const headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
8
9 const elementSelectors = {
10 body: 'body',
11 button: '.wp-element-button, .wp-block-button__link',
12 caption: '.wp-element-caption',
13 cite: 'cite',
14 link: 'a:where(:not(.wp-element-button))',
15 };
16
17 const cssProperties = {
18 'color.text': 'color',
19 'color.background': 'background-color',
20 'color.gradient': 'background',
21 'border.color': 'border-color',
22 };
23
24 const kebab = (property) =>
25 property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
26
27 // Core names the vars via _wp_to_kebab_case, which drops the ':' from :hover.
28 const wpKebab = (key) =>
29 kebab(key)
30 .replace(/[^a-z0-9-]+/gi, '-')
31 .replace(/-+/g, '-')
32 .replace(/^-|-$/g, '');
33
34 const valueAt = (source, path) =>
35 path.reduce(
36 (value, key) => (isObject(value) ? value[key] : undefined),
37 source,
38 );
39
40 // theme.json stores preset references as var:preset|color|primary.
41 const cssValue = (value) =>
42 typeof value === 'string' && value.startsWith('var:')
43 ? `var(--wp--${value.slice(4).split('|').map(kebab).join('--')})`
44 : value;
45
46 // revert alone would drop the theme's own value with the outgoing palette's.
47 const resetValue = (value) =>
48 typeof value === 'string' || typeof value === 'number'
49 ? cssValue(value)
50 : 'revert';
51
52 const blockSelector = (block) => {
53 const [namespace, name] = block.split('/');
54 return namespace === 'core'
55 ? `.wp-block-${name}`
56 : `.wp-block-${namespace}-${name}`;
57 };
58
59 const presetGroups = {
60 palette: { var: 'color', value: 'color' },
61 gradients: { var: 'gradient', value: 'gradient' },
62 };
63
64 // A palette lists its presets plainly; WP keys its own theme data by origin.
65 const presetList = (settings, group) => {
66 const presets = valueAt(settings, ['color', group]);
67 return Array.isArray(presets) ? presets : (presets?.theme ?? []);
68 };
69
70 const presetValue = (settings, group, slug, key) =>
71 presetList(settings, group).find((entry) => entry?.slug === slug)?.[key];
72
73 const presetSlugs = (palettes, group) => {
74 const slugs = new Set();
75
76 for (const palette of Object.values(palettes ?? {})) {
77 for (const entry of presetList(palette?.settings, group)) {
78 if (entry?.slug) slugs.add(entry.slug);
79 }
80 }
81
82 return [...slugs];
83 };
84
85 // Returning [] rather than null would hide an unmapped leaf from the guard.
86 const settingSlots = (path, palettes) => {
87 const [root, group] = path;
88
89 // The svg filter carries the duotone, so there is no var to write.
90 if (root === 'color' && group === 'duotone') return [];
91
92 if (root === 'color' && presetGroups[group]) {
93 const { var: name, value } = presetGroups[group];
94 return presetSlugs(palettes, group).map((slug) => ({
95 selector: ':root',
96 property: `--wp--preset--${name}--${wpKebab(slug)}`,
97 read: (settings) => presetValue(settings, group, slug, value),
98 }));
99 }
100
101 if (root !== 'custom') return null;
102
103 return [
104 {
105 selector: ':root',
106 property: `--wp--${path.map(wpKebab).join('--')}`,
107 read: (settings) => valueAt(settings, path),
108 },
109 ];
110 };
111
112 const styleSlots = (path) => {
113 if (path[0] === 'elements') {
114 const [, element, ...rest] = path;
115 const pseudo = rest[0]?.startsWith(':') ? rest[0] : '';
116 const property = cssProperties[rest.slice(pseudo ? 1 : 0).join('.')];
117 if (!property) return null;
118
119 if (element !== 'heading') {
120 return [
121 {
122 selector: `:root :where(${elementSelectors[element] ?? element})${pseudo}`,
123 property,
124 read: (styles) => valueAt(styles, path),
125 },
126 ];
127 }
128
129 // Resolved per level so a theme's own h5 colour survives the reset.
130 return headings.map((level) => ({
131 selector: `:root :where(${level})${pseudo}`,
132 property,
133 read: (styles) =>
134 valueAt(styles, ['elements', level, ...rest]) ??
135 valueAt(styles, ['elements', 'heading', ...rest]),
136 }));
137 }
138
139 if (path[0] === 'blocks') {
140 const [, block, ...rest] = path;
141 const property = cssProperties[rest.join('.')];
142 if (!property) return null;
143
144 return [
145 {
146 selector: `:root :where(${blockSelector(block)})`,
147 property,
148 read: (styles) => valueAt(styles, path),
149 },
150 ];
151 }
152
153 const property = cssProperties[path.join('.')];
154 if (!property) return null;
155
156 return [
157 {
158 selector: ':root :where(body)',
159 property,
160 read: (styles) => valueAt(styles, path),
161 },
162 ];
163 };
164
165 const paletteSlots = (palettes) => {
166 const slots = [];
167 const covered = [];
168
169 for (const path of ownedPaletteSettingPaths(palettes)) {
170 const built = settingSlots(path, palettes);
171 if (built === null) continue;
172 covered.push(`settings.${path.join('.')}`);
173 slots.push(...built.map((slot) => ({ ...slot, section: 'settings' })));
174 }
175
176 for (const path of ownedPaletteStylePaths(palettes)) {
177 const built = styleSlots(path);
178 if (built === null) continue;
179 covered.push(`styles.${path.join('.')}`);
180 slots.push(...built.map((slot) => ({ ...slot, section: 'styles' })));
181 }
182
183 return { slots, covered };
184 };
185
186 export const paletteResetScope = (palettes) =>
187 paletteSlots(palettes).covered.sort();
188
189 // An older backend wraps this list by origin.
190 export const paletteDuotone = (palette) => {
191 const duotone = palette?.settings?.color?.duotone;
192 return Array.isArray(duotone) ? duotone : duotone?.theme;
193 };
194
195 // Roles can share a hex; the palette's own order breaks the tie.
196 const roleByColor = (colors) => {
197 const roles = {};
198
199 for (const [role, hex] of Object.entries(colors ?? {})) {
200 const key = hex?.toLowerCase();
201 if (key && !(key in roles)) roles[key] = role;
202 }
203
204 return roles;
205 };
206
207 export const remapPaletteDuotone = (palette, colors) => {
208 const duotone = paletteDuotone(palette);
209 if (!Array.isArray(duotone)) return undefined;
210
211 const roles = roleByColor(palette?.colors);
212 const recolor = (pole) => colors?.[roles[pole?.toLowerCase()]] ?? pole;
213
214 return duotone.map((preset) => ({
215 ...preset,
216 colors: preset?.colors?.map(recolor),
217 }));
218 };
219
220 const cssFromSlots = ({ slots, incoming, themeStyles, themeSettings }) => {
221 const rules = new Map();
222
223 for (const { selector, property, section, read } of slots) {
224 const settings = section === 'settings';
225 const value = read(settings ? incoming?.settings : incoming?.styles);
226 const declarations = rules.get(selector) ?? [];
227
228 declarations.push(
229 `${property}:${
230 value === undefined
231 ? resetValue(read(settings ? themeSettings : themeStyles))
232 : cssValue(value)
233 }`,
234 );
235 rules.set(selector, declarations);
236 }
237
238 let css = '';
239 for (const [selector, declarations] of rules) {
240 css += `${selector}{${declarations.join(';')};}`;
241 }
242
243 return css;
244 };
245
246 // A palette carries no compiled css, unlike a variation or a vibe.
247 export const buildPaletteCss = ({
248 payloads,
249 slug,
250 themeStyles,
251 themeSettings,
252 }) =>
253 cssFromSlots({
254 slots: paletteSlots(payloads).slots,
255 incoming: payloads?.[slug],
256 themeStyles,
257 themeSettings,
258 });
259
260 // One slot pass for the whole set; per-slug calls make it quadratic.
261 export const buildPaletteCssMap = ({
262 payloads,
263 themeStyles,
264 themeSettings,
265 }) => {
266 const { slots } = paletteSlots(payloads);
267
268 return Object.fromEntries(
269 Object.entries(payloads ?? {}).map(([slug, incoming]) => [
270 slug,
271 cssFromSlots({ slots, incoming, themeStyles, themeSettings }),
272 ]),
273 );
274 };
275