/** * Style Customizer v2 — builder panel entry. Mounts into the "Style" builder tab and renders * the two-pane panel (controls + live preview), initializing the store from the localized * payload (`evfStyleV2.payload`) with a REST `apiFetch` fallback. */ import React from 'react'; import { createRoot } from 'react-dom/client'; import { createPortal } from 'react-dom'; import './style.scss'; import { App } from './App'; import { PreviewSkeleton } from './PreviewPane'; import { initStore, getStore } from './store'; import { BootstrapSettings, StylePayload } from './types'; const apiFetch = ( window as any ).wp?.apiFetch; const __ = ( window as any ).wp?.i18n?.__ || ( ( s: string ) => s ); const rawSettings: BootstrapSettings | undefined = ( window as any ).evfStyleV2; const settings: BootstrapSettings = rawSettings || { restBase: '', formId: 0, formTitle: '', previewUrl: '', frontendCssUrl: '', wrapperId: '', markerClass: 'evf-style-v2', previewSession: '', aiEnabled: false, aiDisabled: false, ajaxUrl: '', aiNonce: '', aiHintDismissed: true, }; // Bridge so the (legacy jQuery) Fields tab can flag unsaved changes without importing the bundle. ( window as any ).evfScv2SetFormDirty = ( dirty: boolean ) => { try { getStore().setUnsavedFieldChanges( !! dirty ); } catch ( e ) { // Store not initialized yet — the builder will call again on the next interaction. } }; // Reverse bridge: lets the (legacy jQuery) tab-switcher ask "would leaving the Style tab // right now lose anything?" before it switches away — the existing `beforeunload` guard // only protects a real page close/reload, not this in-app SPA tab switch. ( window as any ).evfScv2IsDirty = (): boolean => { try { return getStore().isDirty(); } catch ( e ) { return false; // Store not initialized yet — nothing to lose. } }; const MIGRATION_SEEN_PREFIX = 'evf_scv2_migration_seen_'; /** Whether this browser has already sat through the migrating transition for this form once. */ function hasSeenMigration( formId: number ): boolean { try { return window.localStorage.getItem( MIGRATION_SEEN_PREFIX + formId ) === '1'; } catch ( e ) { return false; } } function markMigrationSeen( formId: number ) { try { window.localStorage.setItem( MIGRATION_SEEN_PREFIX + formId, '1' ); } catch ( e ) { // Best-effort only — worst case the transition shows again next visit. } } // Synchronous init from the localized payload, at module load — before Bootstrap even mounts. // Except the very first time this form is seen freshly migrated, which shows a one-time // "Migrating your styles…" transition and re-fetches via REST instead. let readyFromPayload = false; let migrationPending = false; if ( rawSettings && rawSettings.payload && settings.formId ) { const justMigrated = !! ( rawSettings.payload.migration && rawSettings.payload.migration.just_migrated ); if ( justMigrated && ! hasSeenMigration( settings.formId ) ) { migrationPending = true; } else { try { initStore( rawSettings.payload, settings ); readyFromPayload = true; } catch ( e ) { readyFromPayload = false; } } } /** Long enough to read as a deliberate, reassuring step rather than a flash. */ const MIGRATION_MIN_VISIBLE_MS = 900; function Bootstrap() { const [ state, setState ] = React.useState< 'loading' | 'migrating' | 'ready' | 'error' >( readyFromPayload ? 'ready' : migrationPending ? 'migrating' : 'loading' ); const [ message, setMessage ] = React.useState( '' ); React.useEffect( () => { if ( readyFromPayload ) { return; // Already initialized synchronously above — no fetch needed. } const finish = ( payload: StylePayload ) => { try { initStore( payload, settings ); } catch ( e ) { setMessage( __( 'The style customizer could not start.', 'everest-forms' ) ); setState( 'error' ); return; } setState( 'ready' ); }; if ( migrationPending ) { markMigrationSeen( settings.formId ); // Never show this transition again for this form. const startedAt = Date.now(); const fresh = apiFetch && settings.formId ? apiFetch( { path: `${ settings.restBase }/${ settings.formId }` } ) : Promise.reject( new Error( 'apiFetch unavailable' ) ); // On failure, fall back to the already-embedded payload rather than blocking the panel. fresh .catch( () => ( rawSettings as { payload: StylePayload } ).payload ) .then( ( payload: StylePayload ) => { const remaining = MIGRATION_MIN_VISIBLE_MS - ( Date.now() - startedAt ); if ( remaining > 0 ) { window.setTimeout( () => finish( payload ), remaining ); } else { finish( payload ); } } ); return; } if ( ! apiFetch || ! settings.formId ) { setMessage( __( 'The style customizer could not start.', 'everest-forms' ) ); setState( 'error' ); return; } apiFetch( { path: `${ settings.restBase }/${ settings.formId }` } ) .then( ( payload: StylePayload ) => finish( payload ) ) .catch( ( e: any ) => { setMessage( ( e && e.message ) || __( 'Failed to load styles.', 'everest-forms' ) ); setState( 'error' ); } ); }, [] ); if ( state === 'loading' ) { return ; } if ( state === 'migrating' ) { return ; } if ( state === 'error' ) { return (

{ message }

); } return ; } /** Sidebar controls skeleton (subtabs + a few shimmering rows), shown while the schema loads. */ function SidebarSkeleton() { return (