PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
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 0.6.0 All 127 releases
extendify / src / Shared / lib / palette-preview.js

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

250 lines 6.5 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 const cssFromSlots = ({ slots, incoming, themeStyles, themeSettings }) => {
196 const rules = new Map();
197
198 for (const { selector, property, section, read } of slots) {
199 const settings = section === 'settings';
200 const value = read(settings ? incoming?.settings : incoming?.styles);
201 const declarations = rules.get(selector) ?? [];
202
203 declarations.push(
204 `${property}:${
205 value === undefined
206 ? resetValue(read(settings ? themeSettings : themeStyles))
207 : cssValue(value)
208 }`,
209 );
210 rules.set(selector, declarations);
211 }
212
213 let css = '';
214 for (const [selector, declarations] of rules) {
215 css += `${selector}{${declarations.join(';')};}`;
216 }
217
218 return css;
219 };
220
221 // A palette carries no compiled css, unlike a variation or a vibe.
222 export const buildPaletteCss = ({
223 payloads,
224 slug,
225 themeStyles,
226 themeSettings,
227 }) =>
228 cssFromSlots({
229 slots: paletteSlots(payloads).slots,
230 incoming: payloads?.[slug],
231 themeStyles,
232 themeSettings,
233 });
234
235 // One slot pass for the whole set; per-slug calls make it quadratic.
236 export const buildPaletteCssMap = ({
237 payloads,
238 themeStyles,
239 themeSettings,
240 }) => {
241 const { slots } = paletteSlots(payloads);
242
243 return Object.fromEntries(
244 Object.entries(payloads ?? {}).map(([slug, incoming]) => [
245 slug,
246 cssFromSlots({ slots, incoming, themeStyles, themeSettings }),
247 ]),
248 );
249 };
250