PluginProbe ʕ •ᴥ•ʔ
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI / 3.6.0
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI v3.6.0
3.6.0 3.5.3 3.5.2 3.5.1 3.5.0 3.4.8 3.4.7 3.4.6 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.5.1 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.10 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.6.1 1.6.7 1.7.0 1.7.0.1 1.7.0.2 1.7.0.3 1.7.1 1.7.2 1.7.2.1 1.7.2.2 1.7.3 1.7.4 1.7.5 1.7.5.1 1.7.5.2 1.7.6 1.7.7 1.7.7.1 1.7.7.2 1.7.8 1.7.9 1.8.0 1.8.0.1 1.8.1 1.8.2 1.8.2.1 1.8.2.2 1.8.2.3 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.0.1 1.9.1 1.9.2 1.9.3 1.9.4 1.9.4.1 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.0.1 2.0.1 2.0.2 2.0.3 2.0.3.1 2.0.4 2.0.4.1 2.0.5 2.0.6 2.0.7 2.0.8 2.0.8.1 2.0.9 3.0.0 3.0.0.1 3.0.1 3.0.2 3.0.3 3.0.3.1 3.0.4 3.0.4.1 3.0.4.2 3.0.5 3.0.5.1 3.0.5.2 3.0.6 3.0.6.1 3.0.7.1 3.0.8 3.0.8.1 3.0.9 3.0.9.1 3.0.9.2 3.0.9.3 3.0.9.4 3.0.9.5 3.1.0 3.1.1 3.1.2 3.2.0 3.2.1 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.3.0 3.4.0 3.4.1 3.4.2 3.4.2.1 3.4.3 3.4.4 3.4.5 trunk 1.0 1.0.1 1.0.2 1.0.3
everest-forms / src / style-customizer-v2 / App.tsx
everest-forms / src / style-customizer-v2 Last commit date
AiAssistant.tsx 2 days ago App.tsx 2 days ago BuilderSync.ts 2 days ago ControlRenderer.tsx 2 days ago HoverTip.tsx 2 days ago Popover.tsx 2 days ago PreviewBridge.ts 2 days ago PreviewPane.tsx 2 days ago constants.ts 2 days ago cssVars.ts 2 days ago globals.d.ts 2 days ago index.tsx 2 days ago panes.tsx 2 days ago store.ts 2 days ago style.scss 2 days ago types.ts 2 days ago
App.tsx
433 lines
1 /**
2 * Style Customizer v2 — panel app. Renders the sub-tabs (Design / Templates / Custom CSS)
3 * and portals the live preview into the builder's content area.
4 */
5 import React from 'react';
6 import { createPortal } from 'react-dom';
7 import { AiAssistant } from './AiAssistant';
8 import { ColorsPane, CustomCssPane, DesignList, ElementSlate, TemplatesPane } from './panes';
9 import { ConfirmModal, ConfirmState } from './Popover';
10 import { PreviewPane } from './PreviewPane';
11 import { getActiveBridge, SelectionInfo } from './PreviewBridge';
12 import { DEVICE_LABELS, STATE_FORCE } from './constants';
13 import { useStore } from './store';
14 import { Section } from './types';
15
16 const __ = ( window as any ).wp?.i18n?.__ || ( ( s: string ) => s );
17 const apiFetch = ( window as any ).wp?.apiFetch;
18
19 // Temporarily disabled for this release — the AI form-building chat's own styling side effect
20 // is disabled the same way, server-side, via EVF_AI_API::client_supports_style(). Re-enable by
21 // flipping this back to true.
22 const STYLE_AI_ENABLED = false;
23
24 /** Single-list navigation: 'list' is the home screen (PRE-DEFINED + Elements + Advanced);
25 * the rest are drill-downs reached from it, each with its own "← Back" header. */
26 type View = 'list' | 'element' | 'templates' | 'colors' | 'css';
27
28 interface Toast {
29 msg: string;
30 actLabel?: string;
31 onAct?: () => void;
32 kind?: 'success' | 'info';
33 }
34
35 export function App() {
36 const store = useStore();
37 const [ view, setView ] = React.useState< View >( 'list' );
38 const [ curSection, setCurSection ] = React.useState< string | null >( null );
39 const [ activeState, setActiveState ] = React.useState< Record< string, string > >( {} );
40 const [ confirm, setConfirm ] = React.useState< ConfirmState | null >( null );
41 const [ toast, setToast ] = React.useState< Toast | null >( null );
42 const [ saving, setSaving ] = React.useState( false );
43 const [ saveError, setSaveError ] = React.useState( '' );
44 const [ saveErrorConflict, setSaveErrorConflict ] = React.useState( false );
45 const [ selectPulse, setSelectPulse ] = React.useState( 0 );
46 const toastTimer = React.useRef< ReturnType< typeof setTimeout > | null >( null );
47
48 const sections: Section[] = React.useMemo(
49 () => Object.keys( store.sections ).map( ( key ) => ( { key, ...store.sections[ key ] } ) ),
50 [ store.sections ]
51 );
52
53 const dirty = store.isDirty();
54 // Lets the AI assistant panel dismiss itself — a click here may be about to open a native
55 // <select>, which always paints above fixed UI (see EVF-2698).
56 const onPreviewClick = React.useCallback( () => {
57 store.notePreviewInteraction();
58 }, [ store ] );
59
60 const showToast = React.useCallback( ( t: Toast ) => {
61 setToast( t );
62 if ( toastTimer.current ) {
63 clearTimeout( toastTimer.current );
64 }
65 toastTimer.current = setTimeout( () => setToast( null ), 4200 );
66 }, [] );
67
68 const pauseToast = React.useCallback( () => {
69 if ( toastTimer.current ) {
70 clearTimeout( toastTimer.current );
71 toastTimer.current = null;
72 }
73 }, [] );
74
75 const resumeToast = React.useCallback( () => {
76 if ( toastTimer.current ) {
77 clearTimeout( toastTimer.current );
78 }
79 toastTimer.current = setTimeout( () => setToast( null ), 4200 );
80 }, [] );
81
82 React.useEffect( () => {
83 store.onPaletteUnlinked = ( name: string ) => {
84 showToast( {
85 msg: `${ __( 'Unlinked from the', 'everest-forms' ) }${ name }${ __( 'palette after your edit.', 'everest-forms' ) }`,
86 } );
87 };
88 return () => {
89 store.onPaletteUnlinked = null;
90 };
91 }, [ store, showToast ] );
92
93 /* ---- navigation ---- */
94 const openSection = ( key: string ) => {
95 setView( 'element' );
96 setCurSection( key );
97 };
98 const backToList = () => {
99 setCurSection( null );
100 setView( 'list' );
101 getActiveBridge()?.clearSelection();
102 };
103
104 const inSlate = view === 'element' && !! curSection;
105 const section = curSection ? sections.find( ( s ) => s.key === curSection ) : null;
106
107 const tabsFor = ( s: Section | null ) => ( s ? s.states || s.variants || null : null );
108 const currentStateFor = ( s: Section ): string | null => {
109 const tabs = tabsFor( s );
110 if ( ! tabs ) {
111 return null;
112 }
113 return activeState[ s.key ] || tabs[ 0 ];
114 };
115
116 const forceClass = ( () => {
117 if ( ! inSlate || ! section ) {
118 return null;
119 }
120 const st = currentStateFor( section );
121 return st ? STATE_FORCE[ st ] || null : null;
122 } )();
123
124 /* ---- click-to-edit: a preview element was clicked ---- */
125 const onSelectElement = React.useCallback( ( info: SelectionInfo ) => {
126 setView( 'element' );
127 setCurSection( info.section );
128 if ( info.variant ) {
129 setActiveState( ( m ) => ( { ...m, [ info.section ]: info.variant as string } ) );
130 }
131 setSelectPulse( ( n ) => n + 1 );
132 }, [] );
133
134 /* ---- save (invoked by the builder's Save button) ---- */
135 const save = React.useCallback( async () => {
136 // A just-migrated record is, by definition, not "dirty" (nothing has been edited yet) —
137 // but it still needs one real save to actually persist as v2, or the migration banner's
138 // own "hit Save to keep it" is a lie: the record stays legacy-shaped forever.
139 if ( ! apiFetch || ( ! store.isDirty() && ! store.migration.just_migrated ) ) {
140 return;
141 }
142 setSaving( true );
143 setSaveError( '' );
144 setSaveErrorConflict( false );
145 try {
146 const data: Record< string, unknown > = {
147 record: store.toRecord(),
148 base_updated_at: store.baseUpdatedAt,
149 };
150 // Only sent when touched this session, to avoid clobbering the legacy preview page's own toggle for the same meta.
151 if ( store.applyThemeStyleTouched ) {
152 data.apply_theme_style = store.applyThemeStyle;
153 }
154 const res = await apiFetch( {
155 path: `${ store.settings.restBase }/${ store.settings.formId }`,
156 method: 'POST',
157 data,
158 } );
159 store.markSaved( res.record );
160 } catch ( e: any ) {
161 const status = e && e.data && e.data.status;
162 setSaveErrorConflict( status === 409 );
163 setSaveError(
164 status === 409
165 ? __( 'These styles changed elsewhere — reload before saving.', 'everest-forms' )
166 : ( e && e.message ) || __( 'Failed to save styles.', 'everest-forms' )
167 );
168 } finally {
169 setSaving( false );
170 }
171 }, [ store ] );
172
173 const saveRef = React.useRef( save );
174 saveRef.current = save;
175
176 React.useEffect( () => {
177 const onClick = ( e: MouseEvent ) => {
178 const target = e.target as HTMLElement;
179 if ( target && target.closest && target.closest( '.everest-forms-save-button' ) ) {
180 saveRef.current();
181 }
182 };
183 document.addEventListener( 'click', onClick, true );
184 return () => document.removeEventListener( 'click', onClick, true );
185 }, [] );
186
187 React.useEffect( () => {
188 const handler = ( e: BeforeUnloadEvent ) => {
189 if ( store.isDirty() ) {
190 e.preventDefault();
191 e.returnValue = '';
192 }
193 };
194 window.addEventListener( 'beforeunload', handler );
195 return () => window.removeEventListener( 'beforeunload', handler );
196 }, [ store ] );
197
198 /* ---- undo/redo: confirm what just happened, with a one-click reverse (mirrors the
199 * "Applied palette X [Undo]" pattern already used for apply-palette/reset-all/apply-template) ---- */
200 const handleUndo = React.useCallback( () => {
201 if ( ! store.canUndo() ) {
202 return;
203 }
204 const label = store.undoLabel();
205 store.undo();
206 showToast( {
207 msg: `${ __( 'Undid:', 'everest-forms' ) } ${ label }`,
208 actLabel: __( 'Redo', 'everest-forms' ),
209 onAct: () => store.redo(),
210 } );
211 }, [ store, showToast ] );
212
213 const handleRedo = React.useCallback( () => {
214 if ( ! store.canRedo() ) {
215 return;
216 }
217 const label = store.redoLabel();
218 store.redo();
219 showToast( {
220 msg: `${ __( 'Redid:', 'everest-forms' ) } ${ label }`,
221 actLabel: __( 'Undo', 'everest-forms' ),
222 onAct: () => store.undo(),
223 } );
224 }, [ store, showToast ] );
225
226 React.useEffect( () => {
227 const onKey = ( e: KeyboardEvent ) => {
228 const target = e.target as HTMLElement;
229 if ( target && /^(INPUT|TEXTAREA|SELECT)$/.test( target.tagName ) ) {
230 return;
231 }
232 if ( ( e.ctrlKey || e.metaKey ) && e.key.toLowerCase() === 'z' ) {
233 e.preventDefault();
234 if ( e.shiftKey ) {
235 handleRedo();
236 } else {
237 handleUndo();
238 }
239 return;
240 }
241 // Ctrl+Y: the conventional Windows/Linux redo shortcut, alongside Ctrl+Shift+Z. Not
242 // Cmd+Y (macOS binds that to the browser's own History, which this shouldn't swallow).
243 if ( e.ctrlKey && ! e.metaKey && e.key.toLowerCase() === 'y' ) {
244 e.preventDefault();
245 handleRedo();
246 }
247 };
248 window.addEventListener( 'keydown', onKey );
249 return () => window.removeEventListener( 'keydown', onKey );
250 }, [ handleUndo, handleRedo ] );
251
252 /* ---- render ---- */
253 const previewHost = document.getElementById( 'evf-scv2-preview' );
254
255 const browseMeta: Record< string, { title: string } > = {
256 templates: { title: __( 'Templates', 'everest-forms' ) },
257 colors: { title: __( 'Colors', 'everest-forms' ) },
258 css: { title: __( 'Custom CSS', 'everest-forms' ) },
259 };
260 const resetLabels: Record< string, string > = {
261 templates: __( 'Reset templates', 'everest-forms' ),
262 colors: __( 'Reset colors', 'everest-forms' ),
263 css: __( 'Reset Custom CSS', 'everest-forms' ),
264 };
265
266 return (
267 <div className="scv2-panel">
268 { inSlate && section && (
269 <div className="navback">
270 <button type="button" className="bk" onClick={ backToList }>
271 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={ 2.2 }>
272 <path d="m15 18-6-6 6-6" />
273 </svg>
274 <span>{ section.title }</span>
275 </button>
276 { ! ( section.tier === 'pro' && ! store.proActive ) && (
277 <button
278 type="button"
279 className="uxbtn"
280 title={ __( 'Reset this section', 'everest-forms' ) }
281 aria-label={ __( 'Reset this section', 'everest-forms' ) + '' + section.title }
282 onClick={ () => store.resetSection( section.key ) }
283 >
284 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={ 2 } aria-hidden="true">
285 <path d="M3 12a9 9 0 1 0 3-6.7" />
286 <path d="M3 4v5h5" />
287 </svg>
288 </button>
289 ) }
290 </div>
291 ) }
292
293 { ( view === 'templates' || view === 'colors' || view === 'css' ) && (
294 <div className="navback">
295 <button type="button" className="bk" onClick={ () => setView( 'list' ) }>
296 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={ 2.2 }>
297 <path d="m15 18-6-6 6-6" />
298 </svg>
299 <span>{ browseMeta[ view ].title }</span>
300 </button>
301 { ( view === 'templates' || view === 'colors' || view === 'css' ) && (
302 <button
303 type="button"
304 className="uxbtn"
305 title={ resetLabels[ view ] }
306 aria-label={ resetLabels[ view ] }
307 onClick={ () => {
308 if ( view === 'templates' ) {
309 store.resetTemplate();
310 } else if ( view === 'colors' ) {
311 store.resetPalette();
312 } else {
313 store.resetCustomCss();
314 }
315 } }
316 >
317 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={ 2 } aria-hidden="true">
318 <path d="M3 12a9 9 0 1 0 3-6.7" />
319 <path d="M3 4v5h5" />
320 </svg>
321 </button>
322 ) }
323 </div>
324 ) }
325
326 { store.device !== 'desktop' && (
327 <div className="ctxbar">
328 <span>
329 { __( 'Editing', 'everest-forms' ) }{ ' ' }
330 <b>
331 { DEVICE_LABELS[ store.device ] } · ≤ { store.breakpoints[ store.device ] }px
332 </b>{ ' ' }
333 { __( 'inherits Desktop', 'everest-forms' ) }
334 </span>
335 <button type="button" className="back" onClick={ () => store.setDevice( 'desktop' ) }>
336 { __( 'Back to Desktop', 'everest-forms' ) }
337 </button>
338 </div>
339 ) }
340
341 <div className="panel-scroll">
342 { view === 'element' && section ? (
343 <ElementSlate
344 key={ section.key }
345 section={ section }
346 activeState={ currentStateFor( section ) }
347 onChangeState={ ( id ) => setActiveState( ( m ) => ( { ...m, [ section.key ]: id } ) ) }
348 pulse={ selectPulse }
349 />
350 ) : view === 'list' ? (
351 <DesignList
352 sections={ sections }
353 onOpen={ openSection }
354 onNavigateTemplates={ () => setView( 'templates' ) }
355 onNavigateColors={ () => setView( 'colors' ) }
356 onNavigateCss={ () => setView( 'css' ) }
357 onUndo={ handleUndo }
358 onRedo={ handleRedo }
359 canUndo={ store.canUndo() }
360 canRedo={ store.canRedo() }
361 undoLabel={ store.undoLabel() }
362 redoLabel={ store.redoLabel() }
363 onResetAll={ () =>
364 setConfirm( {
365 title: __( 'Reset all styles?', 'everest-forms' ),
366 message: __(
367 'Resets every element to default — palette, fonts, spacing, and more. You can undo right after.',
368 'everest-forms'
369 ),
370 confirmLabel: __( 'Reset all', 'everest-forms' ),
371 danger: true,
372 onConfirm: () => {
373 store.resetAll();
374 showToast( {
375 msg: __( 'All styles reset to default.', 'everest-forms' ),
376 actLabel: __( 'Undo', 'everest-forms' ),
377 onAct: () => store.undo(),
378 } );
379 },
380 } )
381 }
382 />
383 ) : view === 'templates' ? (
384 <TemplatesPane
385 onPreview={ ( ov ) => getActiveBridge()?.previewValues( ov ) }
386 onClearPreview={ () => getActiveBridge()?.revert() }
387 onApplied={ ( name ) =>
388 showToast( {
389 kind: 'success',
390 msg: `${ __( 'Applied template', 'everest-forms' ) }${ name }`,
391 actLabel: __( 'Undo', 'everest-forms' ),
392 onAct: () => store.undo(),
393 } )
394 }
395 />
396 ) : view === 'colors' ? (
397 <ColorsPane
398 onToast={ showToast }
399 onPreviewPalette={ ( colors ) => getActiveBridge()?.previewPalette( colors ) }
400 onClearPreview={ () => getActiveBridge()?.revert() }
401 />
402 ) : (
403 <CustomCssPane />
404 ) }
405 </div>
406
407 { confirm && <ConfirmModal state={ confirm } onClose={ () => setConfirm( null ) } /> }
408
409 { previewHost &&
410 createPortal(
411 <PreviewPane
412 forceClass={ forceClass }
413 saving={ saving }
414 dirty={ dirty }
415 saveError={ saveError }
416 saveErrorConflict={ saveErrorConflict }
417 onSelect={ onSelectElement }
418 onIframeClick={ onPreviewClick }
419 onUndo={ handleUndo }
420 onRedo={ handleRedo }
421 toast={ toast }
422 onToastPause={ pauseToast }
423 onToastResume={ resumeToast }
424 />,
425 previewHost
426 ) }
427
428 { /* Portaled to <body> so position:fixed isn't trapped by a transformed ancestor. */ }
429 { STYLE_AI_ENABLED && createPortal( <AiAssistant />, document.body ) }
430 </div>
431 );
432 }
433