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 +122 -55 3.1.3 → 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
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) ?? [];