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 / Agent / workflows / theme / components / SelectThemeVariation.jsx

SelectThemeVariation.jsx in Extendify 3.2.1, at src/Agent/workflows/theme/components/SelectThemeVariation.jsx

206 lines 6.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { usePaletteOverride } from '@agent/hooks/usePaletteOverride';
2 import { usePalettes } from '@agent/hooks/usePalettes';
3 import { useThemeVariations } from '@agent/hooks/useThemeVariations';
4 import { useVariationOverride } from '@agent/hooks/useVariationOverride';
5 import { useChatStore } from '@agent/state/chat';
6 import { paletteDuotone } from '@shared/lib/palette-preview';
7 import { samplePalettes } from '@shared/lib/palettes';
8 import { useEffect, useMemo, useRef, useState } from '@wordpress/element';
9 import { __ } from '@wordpress/i18n';
10
11 const VISIBLE_LIMIT = 8;
12
13 export const SelectThemeVariation = ({ onConfirm, onCancel }) => {
14 const [css, setCss] = useState('');
15 const [selected, setSelected] = useState(null);
16 const [duotoneTheme, setDuotoneTheme] = useState(null);
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;
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
40 const confirmed = useRef(false);
41 const undo = useRef(undoChange);
42 useEffect(() => {
43 undo.current = undoChange;
44 }, [undoChange]);
45 useEffect(() => {
46 return () => {
47 if (!confirmed.current) undo.current();
48 };
49 }, []);
50 const { addMessage, messages } = useChatStore();
51
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],
56 );
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
65 const handleConfirm = () => {
66 if (!selected) return;
67 const option = options.find(({ key }) => key === selected);
68 if (!option) {
69 // translators: A chat message shown to the user when their selected color variation cannot be applied
70 const content = __(
71 'We were unable to apply your selected colors. Please try again.',
72 'extendify-local',
73 );
74 addMessage('message', { role: 'assistant', content, error: true });
75 onCancel();
76 return;
77 }
78 confirmed.current = true;
79 onConfirm({ data: option.data, shouldRefreshPage: true });
80 };
81
82 useEffect(() => {
83 if (isLoading || !noOptions) return;
84 const timer = setTimeout(() => onCancel(), 100);
85 // translators: A chat message shown to the user
86 const content = __(
87 'We were unable to find any colors for your theme',
88 'extendify-local',
89 );
90 const last = messages.at(-1)?.details?.content;
91 if (content === last) return () => clearTimeout(timer);
92 addMessage('message', { role: 'assistant', content, error: true });
93 return () => clearTimeout(timer);
94 }, [addMessage, onCancel, noOptions, messages, isLoading]);
95
96 if (isLoading) {
97 return (
98 <div className="min-h-24 p-2 text-center text-sm">
99 {__('Loading available colors...', 'extendify-local')}
100 </div>
101 );
102 }
103
104 if (noOptions) return null;
105
106 return (
107 <div className="mb-4 ms-2 me-2 flex flex-col rounded-lg border border-gray-300 bg-gray-50">
108 <div className="rounded-lg border-b border-gray-300 bg-white">
109 <div className="grid grid-cols-2 gap-2 p-3">
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 )}
138 </div>
139 </div>
140 <div className="flex justify-start gap-2 p-3">
141 <button
142 type="button"
143 className="w-full rounded-sm border border-gray-500 bg-white p-2 text-sm text-gray-900"
144 onClick={onCancel}
145 >
146 {__('Cancel', 'extendify-local')}
147 </button>
148 <button
149 type="button"
150 className="w-full rounded-sm border border-design-main bg-design-main p-2 text-sm text-white"
151 disabled={!selected}
152 onClick={handleConfirm}
153 >
154 {__('Save', 'extendify-local')}
155 </button>
156 </div>
157 </div>
158 );
159 };
160
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)
200 ?.color;
201
202 const themeSwatches = (settings) =>
203 settings?.color?.palette?.theme
204 ?.filter((item) => item.slug !== 'background')
205 ?.map((item) => item.color) ?? [];
206