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
← All changes | src/Agent/workflows/theme/components/SelectThemeVariation.jsx +122 -55 3.1.23.2.1 View file →
@@ -1,35 +1,72 @@
1 +import { usePaletteOverride } from '@agent/hooks/usePaletteOverride';
2 +import { usePalettes } from '@agent/hooks/usePalettes';
1 3 import { useThemeVariations } from '@agent/hooks/useThemeVariations';
2 4 import { useVariationOverride } from '@agent/hooks/useVariationOverride';
3 5 import { useChatStore } from '@agent/state/chat';
6 +import { paletteDuotone } from '@shared/lib/palette-preview';
7 +import { samplePalettes } from '@shared/lib/palettes';
4 8 import { useEffect, useMemo, useRef, useState } from '@wordpress/element';
5 9 import { __ } from '@wordpress/i18n';
6 10
7 -export const SelectThemeVariation = ({ onConfirm, onCancel, onLoad }) => {
11 +const VISIBLE_LIMIT = 8;
12 +
13 +export const SelectThemeVariation = ({ onConfirm, onCancel }) => {
8 14 const [css, setCss] = useState('');
9 15 const [selected, setSelected] = useState(null);
10 16 const [duotoneTheme, setDuotoneTheme] = useState(null);
11 - const { undoChange } = useVariationOverride({ css, duotoneTheme });
17 + const {
18 + palettes,
19 + css: paletteCss,
20 + preferred,
21 + isLoading: palettesLoading,
22 + } = usePalettes();
23 + const onPalettes = Boolean(palettes?.length);
24 + const { variations, isLoading: variationsLoading } = useThemeVariations({
25 + enabled: !palettesLoading && !onPalettes,
26 + });
27 + const isLoading = palettesLoading || variationsLoading;
12 28
29 + // A palette layers over core's global styles; a variation replaces them.
30 + const { undoChange: undoPalette } = usePaletteOverride({
31 + css: onPalettes ? css : '',
32 + duotoneTheme: onPalettes ? duotoneTheme : null,
33 + });
34 + const { undoChange: undoVariation } = useVariationOverride({
35 + css: onPalettes ? '' : css,
36 + duotoneTheme: onPalettes ? null : duotoneTheme,
37 + });
38 + const undoChange = onPalettes ? undoPalette : undoVariation;
39 +
13 40 const confirmed = useRef(false);
41 + const undo = useRef(undoChange);
14 42 useEffect(() => {
43 + undo.current = undoChange;
44 + }, [undoChange]);
45 + useEffect(() => {
15 46 return () => {
16 - if (!confirmed.current) undoChange();
47 + if (!confirmed.current) undo.current();
17 48 };
18 49 }, []);
19 - const { variations, isLoading } = useThemeVariations();
20 - const noVariations = !variations || variations.length === 0;
21 50 const { addMessage, messages } = useChatStore();
22 51
23 - const shuffled = useMemo(
24 - () => (variations ? variations.sort(() => Math.random() - 0.5) : []),
25 - [variations],
52 + // Drawn per mount, so the immutable SWR cache cannot freeze one set.
53 + const shown = useMemo(
54 + () => samplePalettes(palettes, preferred, VISIBLE_LIMIT),
55 + [palettes, preferred],
26 56 );
27 57
58 + const options = useMemo(
59 + () =>
60 + onPalettes ? paletteOptions(shown, paletteCss) : themeOptions(variations),
61 + [onPalettes, shown, paletteCss, variations],
62 + );
63 + const noOptions = options.length === 0;
64 +
28 65 const handleConfirm = () => {
29 66 if (!selected) return;
30 - const variation = variations.find((v) => v.title === selected);
31 - if (!variation) {
67 + const option = options.find(({ key }) => key === selected);
68 + if (!option) {
32 69 // translators: A chat message shown to the user when their selected color variation cannot be applied
33 70 const content = __(
34 71 'We were unable to apply your selected colors. Please try again.',
35 72 'extendify-local',
@@ -38,18 +75,13 @@
38 75 onCancel();
39 76 return;
40 77 }
41 78 confirmed.current = true;
42 - onConfirm({ data: { variation }, shouldRefreshPage: true });
79 + onConfirm({ data: option.data, shouldRefreshPage: true });
43 80 };
44 81
45 82 useEffect(() => {
46 - if (isLoading) return;
47 - onLoad();
48 - }, [isLoading, onLoad]);
49 -
50 - useEffect(() => {
51 - if (isLoading || !noVariations) return;
83 + if (isLoading || !noOptions) return;
52 84 const timer = setTimeout(() => onCancel(), 100);
53 85 // translators: A chat message shown to the user
54 86 const content = __(
55 87 'We were unable to find any colors for your theme',
@@ -58,9 +90,9 @@
58 90 const last = messages.at(-1)?.details?.content;
59 91 if (content === last) return () => clearTimeout(timer);
60 92 addMessage('message', { role: 'assistant', content, error: true });
61 93 return () => clearTimeout(timer);
62 - }, [addMessage, onCancel, noVariations, messages, isLoading]);
94 + }, [addMessage, onCancel, noOptions, messages, isLoading]);
63 95
64 96 if (isLoading) {
65 97 return (
66 98 <div className="min-h-24 p-2 text-center text-sm">
@@ -68,39 +100,42 @@
68 100 </div>
69 101 );
70 102 }
71 103
72 - if (noVariations) return null;
104 + if (noOptions) return null;
73 105
74 106 return (
75 - <div className="mb-4 ml-10 mr-2 flex flex-col rounded-lg border border-gray-300 bg-gray-50 rtl:ml-2 rtl:mr-10">
107 + <div className="mb-4 ms-2 me-2 flex flex-col rounded-lg border border-gray-300 bg-gray-50">
76 108 <div className="rounded-lg border-b border-gray-300 bg-white">
77 109 <div className="grid grid-cols-2 gap-2 p-3">
78 - {shuffled?.slice(0, 10)?.map(({ title, css, settings }) => (
79 - <button
80 - key={title}
81 - style={{ backgroundColor: getColor(settings, 'background') }}
82 - type="button"
83 - className={`relative flex w-full items-center justify-center overflow-hidden rounded-lg border border-gray-300 bg-none p-2 text-center text-sm ${
84 - selected === title ? 'ring ring-design-main ring-wp' : ''
85 - }`}
86 - onClick={() => {
87 - setSelected(title);
88 - setCss(css);
89 - setDuotoneTheme(settings?.color?.duotone?.theme);
90 - }}
91 - >
92 - <div className="flex max-w-fit items-center justify-center -space-x-4 rounded-lg rtl:space-x-reverse">
93 - {getColors(settings)?.map((color, i) => (
94 - <div
95 - key={title + color + i}
96 - style={{ backgroundColor: color }}
97 - className="size-6 shrink-0 overflow-visible rounded-full border border-white md:size-7"
98 - ></div>
99 - ))}
100 - </div>
101 - </button>
102 - ))}
110 + {options.map(
111 + ({ key, title, background, swatches, previewCss, duotone }) => (
112 + <button
113 + key={key}
114 + style={{ backgroundColor: background }}
115 + type="button"
116 + aria-label={title}
117 + className={`relative flex w-full items-center justify-center overflow-hidden rounded-lg border border-gray-300 bg-none p-2 text-center text-sm ${
118 + selected === key ? 'ring ring-design-main ring-wp' : ''
119 + }`}
120 + onClick={() => {
121 + setSelected(key);
122 + setCss(previewCss);
123 + setDuotoneTheme(duotone);
124 + }}
125 + >
126 + <div className="flex max-w-fit items-center justify-center -space-x-4 rounded-lg rtl:space-x-reverse">
127 + {swatches.map((color, i) => (
128 + <div
129 + key={key + color + i}
130 + style={{ backgroundColor: color }}
131 + className="size-6 shrink-0 overflow-visible rounded-full border border-white md:size-7"
132 + ></div>
133 + ))}
134 + </div>
135 + </button>
136 + ),
137 + )}
103 138 </div>
104 139 </div>
105 140 <div className="flex justify-start gap-2 p-3">
106 141 <button
@@ -122,17 +157,49 @@
122 157 </div>
123 158 );
124 159 };
125 160
126 -const getColor = (settings, colorName) => {
127 - return settings?.color?.palette?.theme.find((item) => item.slug === colorName)
161 +// `colors` also carries tint1-3 card backgrounds, never swatches.
162 +const SWATCH_ROLES = [
163 + 'foreground',
164 + 'primary',
165 + 'secondary',
166 + 'tertiary',
167 + 'foregroundAlt',
168 +];
169 +
170 +// Only `colors` is guaranteed hex; settings.color.palette carries var() refs.
171 +const paletteOptions = (palettes, css) =>
172 + palettes.map((palette) => ({
173 + key: palette.slug,
174 + title: palette.title || palette.slug,
175 + background: palette.colors?.background,
176 + swatches: SWATCH_ROLES.map((role) => palette.colors?.[role]).filter(
177 + Boolean,
178 + ),
179 + previewCss: css?.[palette.slug] ?? '',
180 + duotone: paletteDuotone(palette),
181 + data: { colorPalette: palette.slug },
182 + }));
183 +
184 +const themeOptions = (variations) =>
185 + [...(variations ?? [])]
186 + .sort(() => Math.random() - 0.5)
187 + .slice(0, VISIBLE_LIMIT)
188 + .map((variation) => ({
189 + key: variation.title,
190 + title: variation.title,
191 + background: themeColor(variation.settings, 'background'),
192 + swatches: themeSwatches(variation.settings),
193 + previewCss: variation.css,
194 + duotone: variation.settings?.color?.duotone?.theme,
195 + data: { variation },
196 + }));
197 +
198 +const themeColor = (settings, colorName) =>
199 + settings?.color?.palette?.theme?.find((item) => item.slug === colorName)
128 200 ?.color;
129 -};
130 201
131 -const getColors = (settings) => {
132 - return settings?.color?.palette?.theme
202 +const themeSwatches = (settings) =>
203 + settings?.color?.palette?.theme
133 204 ?.filter((item) => item.slug !== 'background')
134 - ?.reduce((acc, item) => {
135 - acc.push(item.color);
136 - return acc;
137 - }, []);
138 -};
205 + ?.map((item) => item.color) ?? [];