← All changes
|
src/Agent/workflows/theme/components/SelectThemeVariation.jsx
+122
-55
3.1.2
→
3.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) ?? []; | |