PluginProbe
Extendify / trunk
Extendify vtrunk
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 +121 -49 3.1.4 → trunk 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
11 +const VISIBLE_LIMIT = 8;
12 +
7 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,13 +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 || !noVariations) return;
83 + if (isLoading || !noOptions) return;
47 84 const timer = setTimeout(() => onCancel(), 100);
48 85 // translators: A chat message shown to the user
49 86 const content = __(
50 87 'We were unable to find any colors for your theme',
@@ -53,9 +90,9 @@
53 90 const last = messages.at(-1)?.details?.content;
54 91 if (content === last) return () => clearTimeout(timer);
55 92 addMessage('message', { role: 'assistant', content, error: true });
56 93 return () => clearTimeout(timer);
57 - }, [addMessage, onCancel, noVariations, messages, isLoading]);
94 + }, [addMessage, onCancel, noOptions, messages, isLoading]);
58 95
59 96 if (isLoading) {
60 97 return (
61 98 <div className="min-h-24 p-2 text-center text-sm">
@@ -63,39 +100,42 @@
63 100 </div>
64 101 );
65 102 }
66 103
67 - if (noVariations) return null;
104 + if (noOptions) return null;
68 105
69 106 return (
70 - <div className="mb-4 ms-12 me-2 flex flex-col rounded-lg border border-gray-300 bg-gray-50">
107 + <div className="mb-4 ms-2 me-2 flex flex-col rounded-lg border border-gray-300 bg-gray-50">
71 108 <div className="rounded-lg border-b border-gray-300 bg-white">
72 109 <div className="grid grid-cols-2 gap-2 p-3">
73 - {shuffled?.slice(0, 10)?.map(({ title, css, settings }) => (
74 - <button
75 - key={title}
76 - style={{ backgroundColor: getColor(settings, 'background') }}
77 - type="button"
78 - 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 ${
79 - selected === title ? 'ring ring-design-main ring-wp' : ''
80 - }`}
81 - onClick={() => {
82 - setSelected(title);
83 - setCss(css);
84 - setDuotoneTheme(settings?.color?.duotone?.theme);
85 - }}
86 - >
87 - <div className="flex max-w-fit items-center justify-center -space-x-4 rounded-lg rtl:space-x-reverse">
88 - {getColors(settings)?.map((color, i) => (
89 - <div
90 - key={title + color + i}
91 - style={{ backgroundColor: color }}
92 - className="size-6 shrink-0 overflow-visible rounded-full border border-white md:size-7"
93 - ></div>
94 - ))}
95 - </div>
96 - </button>
97 - ))}
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 + )}
98 138 </div>
99 139 </div>
100 140 <div className="flex justify-start gap-2 p-3">
101 141 <button
@@ -117,17 +157,49 @@
117 157 </div>
118 158 );
119 159 };
120 160
121 -const getColor = (settings, colorName) => {
122 - 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)
123 200 ?.color;
124 -};
125 201
126 -const getColors = (settings) => {
127 - return settings?.color?.palette?.theme
202 +const themeSwatches = (settings) =>
203 + settings?.color?.palette?.theme
128 204 ?.filter((item) => item.slug !== 'background')
129 - ?.reduce((acc, item) => {
130 - acc.push(item.color);
131 - return acc;
132 - }, []);
133 -};
205 + ?.map((item) => item.color) ?? [];