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 / Launch / components / SmallPreview.jsx

SmallPreview.jsx in Extendify 3.2.1, at src/Launch/components/SmallPreview.jsx

238 lines 7.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import themeJSON from '@launch/_data/theme-processed.json';
2 import { usePreviewIframe } from '@launch/hooks/usePreviewIframe';
3 import { useVibeCss } from '@launch/hooks/useVibeCss';
4 import { getFontOverrides } from '@launch/lib/preview-helpers';
5 import { hexTomatrixValues, lowerImageQuality } from '@launch/lib/util';
6 import { pageNames } from '@shared/lib/pages';
7 import { BlockPreview } from '@wordpress/block-editor';
8 import { rawHandler } from '@wordpress/blocks';
9 import {
10 useCallback,
11 useEffect,
12 useMemo,
13 useRef,
14 useState,
15 } from '@wordpress/element';
16 import { __ } from '@wordpress/i18n';
17 import classNames from 'classnames';
18 import { colord } from 'colord';
19 import { AnimatePresence, motion } from 'framer-motion';
20
21 export const SmallPreview = ({
22 style,
23 onSelect,
24 selected,
25 siteTitle,
26 showNav = true,
27 }) => {
28 const previewContainer = useRef(null);
29 const blockRef = useRef(null);
30 const observer = useRef(null);
31 const [ready, setReady] = useState(false);
32 const variation = style?.variation;
33 const theme = variation?.settings?.color?.palette?.theme;
34 const vibe = useMemo(() => style?.siteStyle?.vibe, [style?.siteStyle?.vibe]);
35 const { data: vibeCss } = useVibeCss();
36 const blockVariationCSS = useMemo(
37 () => vibeCss?.[vibe] ?? vibeCss?.['natural-1'] ?? '',
38 [vibe, vibeCss],
39 );
40
41 const onLoad = useCallback(
42 (frame) => {
43 // Run this 150 times at an interval of 100ms (15s)
44 // This is a brute force check that the styles are there
45 let lastRun = performance.now();
46 let counter = 0;
47 const variationTitle = variation?.slug
48 ?.split('-')
49 .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
50 .join(' ');
51
52 const variationStyles = themeJSON[variationTitle];
53 const { customFontLinks, fontOverrides } = getFontOverrides(variation);
54
55 const checkOnStyles = () => {
56 if (counter >= 150) return;
57 const now = performance.now();
58 // Don't pass here until we've waited 100ms
59 if (now - lastRun < 100) return requestAnimationFrame(checkOnStyles);
60 lastRun = now;
61 const content = frame?.contentDocument;
62 if (content) {
63 content.querySelector('[href*=load-styles]')?.remove();
64 const siteTitleElement = content.querySelector('[href*=site-title]');
65 if (siteTitleElement.textContent !== siteTitle) {
66 siteTitleElement.textContent = siteTitle;
67 }
68 }
69 const primaryColor = theme?.find(
70 ({ slug }) => slug === 'primary',
71 )?.color;
72 const [r, g, b] = primaryColor
73 ? hexTomatrixValues(primaryColor)
74 : [0, 0, 0];
75
76 // Add custom font links if not already present
77 if (
78 customFontLinks &&
79 !frame.contentDocument?.querySelector('[id^="ext-custom-font"]')
80 ) {
81 frame.contentDocument?.head?.insertAdjacentHTML(
82 'beforeend',
83 customFontLinks,
84 );
85 }
86
87 if (!frame.contentDocument?.getElementById('ext-tj')) {
88 frame.contentDocument?.body?.insertAdjacentHTML(
89 'beforeend',
90 `<style id="ext-tj">
91 ${variationStyles}
92 ${fontOverrides}
93 ${blockVariationCSS}
94 .wp-block-missing { display: none !important }
95 img.custom-logo, [class*=wp-duotone-] img[src^="data"] {
96 filter: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"><filter id="solid-color"><feColorMatrix color-interpolation-filters="sRGB" type="matrix" values="0 0 0 0 ${r} 0 0 0 0 ${g} 0 0 0 0 ${b} 0 0 0 1 0"/></filter></svg>#solid-color') !important;
97 }
98 </style>`,
99 );
100 }
101
102 counter++;
103 requestAnimationFrame(checkOnStyles); // recursive
104 };
105 checkOnStyles();
106 },
107 [variation, theme, siteTitle, blockVariationCSS],
108 );
109
110 const { loading, ready: show } = usePreviewIframe({
111 container: blockRef.current,
112 ready,
113 onLoad,
114 // Additional time to wait after we think we are ready
115 loadDelay: 750,
116 });
117 const blocks = useMemo(() => {
118 const links = [
119 pageNames.about.title,
120 pageNames.blog.title,
121 pageNames.contact.title,
122 ];
123
124 const code = [
125 style?.headerCode,
126 style?.patterns
127 .flatMap(({ code }) => code)
128 .slice(0, 3)
129 .join('\n'),
130 style?.footerCode,
131 ]
132 .filter(Boolean)
133 .join('')
134 .replace(
135 // Replace natural-1 with dynamic vibe value
136 /natural-1/g,
137 vibe || 'natural-1',
138 )
139 .replace(
140 // <!-- wp:navigation --> <!-- /wp:navigation -->
141 /<!-- wp:navigation[.\S\s]*?\/wp:navigation -->/g,
142 showNav
143 ? `<!-- wp:paragraph {"className":"tmp-nav"} --><p class="tmp-nav" style="display: flex; gap: 2rem; margin:0;">${links.map((link) => `<span>${link}</span>`).join('')}</p ><!-- /wp:paragraph -->`
144 : '',
145 )
146 .replace(
147 // <!-- wp:navigation /-->
148 /<!-- wp:navigation.*\/-->/g,
149 showNav
150 ? `<!-- wp:paragraph {"className":"tmp-nav"} --><p class="tmp-nav" style="display: flex; gap: 2rem; margin:0;">${links.map((link) => `<span>${link}</span>`).join('')}</p ><!-- /wp:paragraph -->`
151 : '',
152 )
153 .replace(
154 /<!--\s*wp:social-links\b[^>]*>.*?<!--\s*\/wp:social-links\s*-->/gis,
155 // dont replace if showNav is true
156 (match) => (showNav ? match : ''),
157 )
158 .replace(
159 /<!-- wp:site-logo.*\/-->/g,
160 '<!-- wp:paragraph {"className":"custom-logo"} --><p class="custom-logo" style="display:flex; align-items: center; margin:0;"><img alt="" class="custom-logo" style="height: 32px;" src="https://images.extendify-cdn.com/demo-content/logos/ext-custom-logo-default.webp"></p ><!-- /wp:paragraph -->',
161 );
162 return rawHandler({ HTML: lowerImageQuality(code) });
163 }, [style?.headerCode, style?.patterns, style?.footerCode, vibe, showNav]);
164
165 useEffect(() => {
166 if (observer.current) return;
167 observer.current = new IntersectionObserver((entries) => {
168 entries[0].isIntersecting && setReady(true);
169 });
170 observer.current.observe(blockRef.current);
171 return () => observer.current.disconnect();
172 }, []);
173
174 return (
175 // biome-ignore lint: keep the button role until a refactor is done
176 <div
177 data-test="layout-preview"
178 className="relative h-full w-full overflow-hidden"
179 ref={blockRef}
180 role={onSelect ? 'button' : undefined}
181 tabIndex={onSelect ? 0 : undefined}
182 aria-label={
183 onSelect ? __('Press to select', 'extendify-local') : undefined
184 }
185 aria-selected={onSelect ? selected : undefined}
186 onKeyDown={(e) => {
187 if (!['Enter', 'Space', ' '].includes(e.key)) return;
188 onSelect?.({ ...style, variation });
189 }}
190 onClick={() => onSelect?.({ ...style, variation })}
191 >
192 {ready ? (
193 <motion.div
194 ref={previewContainer}
195 className={classNames('absolute inset-0 z-20', {
196 'opacity-0': !show,
197 })}
198 initial={{ opacity: 0 }}
199 animate={{ opacity: loading ? 0 : 1 }}
200 >
201 <BlockPreview
202 blocks={blocks}
203 viewportWidth={1400}
204 additionalStyles={[
205 // TODO: { css: themeJSON[style.variation.title] },
206 {
207 css: '.rich-text [data-rich-text-placeholder]:after { content: "" }',
208 },
209 ]}
210 />
211 </motion.div>
212 ) : null}
213 <AnimatePresence>
214 {show || (
215 <motion.div
216 initial={{ opacity: 0.7 }}
217 animate={{ opacity: 1 }}
218 exit={{ opacity: 0 }}
219 transition={{ duration: 0.5 }}
220 className="absolute inset-0 z-30"
221 style={{
222 backgroundColor: colord(
223 theme?.find(({ slug }) => slug === 'primary')?.color ?? '#ccc',
224 )
225 .alpha(0.25)
226 .toRgbString(),
227 backgroundImage:
228 'linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.5) 50%, rgba(255,255,255,0) 100%)',
229 backgroundSize: '600% 600%',
230 animation: 'extendify-loading-skeleton 10s ease-in-out infinite',
231 }}
232 />
233 )}
234 </AnimatePresence>
235 </div>
236 );
237 };
238