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 / panes.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
panes.tsx
1187 lines
1 /**
2 * Style Customizer v2 — panel panes: DesignList, ElementSlate (schema-driven drill-down),
3 * TemplatesPane and CustomCssPane. All read/write the store.
4 */
5 import React from 'react';
6 import { ColorPickerField, ControlRenderer } from './ControlRenderer';
7 import { SECTION_ICONS, SECTION_SUBTITLES, STATE_LABELS } from './constants';
8 import { HoverTip } from './HoverTip';
9 import { useStore } from './store';
10 import { BoxValue, DeviceBag, Section, Template, Token } from './types';
11 // The same "PRO" crown badge the builder's Fields sidebar uses.
12 import proIconUrl from '../../assets/images/icons/everest-form-pro-icon.png';
13
14 const __ = ( window as any ).wp?.i18n?.__ || ( ( s: string ) => s );
15 const apiFetch = ( window as any ).wp?.apiFetch;
16 export const UPGRADE_URL = 'https://everestforms.net/pricing/?utm_source=style-customizer&utm_medium=panel';
17
18 function Icon( { inner }: { inner: string } ) {
19 return (
20 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={ 2 } dangerouslySetInnerHTML={ { __html: inner } } />
21 );
22 }
23
24 /** The small "(i)" glyph the design's advisory banners lead with. */
25 function InfoNoteIcon() {
26 return (
27 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={ 2 } aria-hidden="true">
28 <circle cx="12" cy="12" r="10" />
29 <path d="M12 16v-4" />
30 <path d="M12 8h.01" />
31 </svg>
32 );
33 }
34
35 /** "PRO" marker — the builder's own locked-field crown badge. */
36 export function ProCrown() {
37 return <img className="pro-crown" src={ proIconUrl } alt="" aria-hidden="true" />;
38 }
39
40 /** Human label for a paletteMap slot key — shared by "Your Palette"'s edit rows. */
41 export function paletteSlotLabel( slot: string ): string {
42 switch ( slot ) {
43 case 'form_background':
44 return __( 'Form background', 'everest-forms' );
45 case 'field_background':
46 return __( 'Field background', 'everest-forms' );
47 case 'field_label':
48 return __( 'Label', 'everest-forms' );
49 case 'field_sublabel':
50 return __( 'Sublabel', 'everest-forms' );
51 case 'button_text':
52 return __( 'Button text', 'everest-forms' );
53 case 'button_background':
54 return __( 'Button background', 'everest-forms' );
55 default:
56 return slot;
57 }
58 }
59
60 /** One "Your Palette" edit row — the same swatch + hex + alpha-picker popover every element
61 * color control uses, so editing a slot feels identical to editing any other color. */
62 export function PaletteColorRow( {
63 label,
64 value,
65 onChange,
66 gradientable,
67 }: {
68 label: string;
69 value: string;
70 onChange: ( color: string ) => void;
71 gradientable?: boolean;
72 } ) {
73 return (
74 <div className="pal-edit-row">
75 <span className="pal-edit-label">{ label }</span>
76 <ColorPickerField label={ label } value={ value } onChange={ onChange } gradientable={ gradientable } />
77 </div>
78 );
79 }
80
81 /* --------------------------------------------------------------------- *
82 * Colors (browse) — "Your Palette" (live, editable, Pro-tier) + presets +
83 * conditional "Your palettes" custom list (view/apply/delete only).
84 * --------------------------------------------------------------------- */
85
86 interface ColorsToast {
87 msg: string;
88 kind?: 'success' | 'info';
89 actLabel?: string;
90 onAct?: () => void;
91 }
92
93 export function ColorsPane( {
94 onToast,
95 onPreviewPalette,
96 onClearPreview,
97 }: {
98 onToast: ( t: ColorsToast ) => void;
99 onPreviewPalette: ( colors: Record< string, string > ) => void;
100 onClearPreview: () => void;
101 } ) {
102 const store = useStore();
103 const [ editing, setEditing ] = React.useState( false );
104 const [ editSnapshot, setEditSnapshot ] = React.useState< { palette: string; colors: Record< string, string > } | null >( null );
105 const [ confirmId, setConfirmId ] = React.useState< string | null >( null );
106 const [ busy, setBusy ] = React.useState( false );
107
108 const pro = store.proActive;
109 const slots = Object.keys( store.paletteMap );
110 const custom = store.customPalettes();
111 const builtin = store.builtinPalettes();
112 const palettesBase = store.settings.restBase.replace( /\/styles$/, '/style-palettes' );
113
114 // Re-reads on every store version bump (useStore()), so this always reflects live edits —
115 // the same mechanism that already keeps every other control in sync.
116 const currentColors = store.currentPaletteColors();
117 const appliedPaletteId = store.appliedPaletteId();
118 const originPaletteId = store.originPaletteId();
119 const matchedPaletteId = appliedPaletteId || originPaletteId;
120 const matchedPalette = matchedPaletteId ? store.palettes.find( ( p ) => p.id === matchedPaletteId ) : null;
121 // Also flag "Modified" when the colours never matched a named palette AT ALL but have drifted
122 // from the raw schema defaults (e.g. hand-picked colours, or a Template's own colour set).
123 const paletteModified = ! appliedPaletteId && ( !! originPaletteId || ! store.paletteAtDefault() );
124
125 // Opening the editor snapshots the current state so "Cancel" can restore it exactly — every
126 // other control in the panel is "live, undo to fix", but a dedicated Cancel button here (per
127 // the design) needs a real, precise revert rather than N separate undo steps.
128 const openEditor = () => {
129 setEditSnapshot( { palette: store.palette, colors: { ...currentColors } } );
130 setEditing( true );
131 };
132 const closeEditorKeep = () => {
133 setEditing( false );
134 setEditSnapshot( null );
135 };
136 const cancelEditor = () => {
137 if ( editSnapshot ) {
138 if ( editSnapshot.palette ) {
139 store.applyPalette( editSnapshot.palette );
140 } else {
141 slots.forEach( ( slot ) => {
142 store.setPaletteSlotColor( slot, editSnapshot.colors[ slot ] || '#ffffff', paletteSlotLabel( slot ), false );
143 } );
144 }
145 }
146 setEditing( false );
147 setEditSnapshot( null );
148 };
149
150 const swatch = ( colors: Record< string, string > ) => (
151 <span className="sw" aria-hidden="true">
152 { slots.map( ( slot ) => (
153 <i key={ slot } style={ { background: colors[ slot ] } } />
154 ) ) }
155 </span>
156 );
157
158 const applyPreset = ( p: ReturnType< typeof store.customPalettes >[ number ] ) => {
159 if ( p.is_pro && ! pro ) {
160 window.open( UPGRADE_URL, '_blank' );
161 return;
162 }
163 store.applyPalette( p.id );
164 onToast( {
165 kind: 'success',
166 msg: `${ __( 'Applied palette', 'everest-forms' ) }${ p.name }`,
167 actLabel: __( 'Undo', 'everest-forms' ),
168 onAct: () => store.undo(),
169 } );
170 };
171
172 const deletePalette = async ( id: string ) => {
173 setConfirmId( null );
174 if ( ! apiFetch ) {
175 return;
176 }
177 setBusy( true );
178 try {
179 const res = await apiFetch( { path: `${ palettesBase }/${ id }`, method: 'DELETE' } );
180 store.setCustomPalettes( ( res && res.palettes ) || custom.filter( ( p ) => p.id !== id ) );
181 onToast( { msg: __( 'Custom palette deleted.', 'everest-forms' ) } );
182 } catch ( e ) {
183 onToast( { msg: __( 'Could not delete the palette.', 'everest-forms' ) } );
184 } finally {
185 setBusy( false );
186 }
187 };
188
189 const renderCard = ( p: ReturnType< typeof store.customPalettes >[ number ] ) => {
190 const applied = p.id === appliedPaletteId;
191 const modified = p.id === originPaletteId;
192 const applyLocked = p.is_pro && ! pro;
193 const canDelete = p.is_custom && pro;
194 return (
195 <div
196 key={ p.id }
197 className={
198 'pal-card pal-card--wrap' +
199 ( confirmId === p.id ? ' is-confirming' : '' )
200 }
201 >
202 <button
203 type="button"
204 className="pal-card-apply"
205 role="option"
206 aria-selected={ applied }
207 title={ p.name }
208 onMouseEnter={ () => onPreviewPalette( p.colors ) }
209 onMouseLeave={ onClearPreview }
210 onClick={ () => applyPreset( p ) }
211 >
212 <span className="pal-card-thumb">
213 { swatch( p.colors ) }
214 </span>
215 <span className="cap">
216 <span className="cap-name">{ p.name }</span>
217 { p.is_custom && (
218 <span className="predef-badge predef-badge--custom">{ __( 'Custom', 'everest-forms' ) }</span>
219 ) }
220 { ( applied || modified ) && (
221 <span className="predef-badge predef-badge--base">{ __( 'Base', 'everest-forms' ) }</span>
222 ) }
223 { applyLocked && (
224 <span className="pro" aria-label={ __( 'Pro', 'everest-forms' ) }>
225 <ProCrown />
226 </span>
227 ) }
228 </span>
229 </button>
230 <span className="pal-card-tools">
231 { canDelete && (
232 <button
233 type="button"
234 className="pal-tool pal-tool--danger"
235 aria-label={ `${ __( 'Delete', 'everest-forms' ) } ${ p.name }` }
236 title={ __( 'Delete palette', 'everest-forms' ) }
237 onClick={ () => setConfirmId( p.id ) }
238 >
239 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={ 2 } aria-hidden="true">
240 <path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m2 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
241 </svg>
242 </button>
243 ) }
244 </span>
245 { confirmId === p.id && (
246 <div className="pal-card-confirm" role="alertdialog" aria-label={ __( 'Delete palette?', 'everest-forms' ) }>
247 <span>{ __( 'Delete?', 'everest-forms' ) }</span>
248 <button type="button" className="pal-confirm-yes" onClick={ () => deletePalette( p.id ) } disabled={ busy }>
249 { __( 'Delete', 'everest-forms' ) }
250 </button>
251 <button type="button" className="pal-confirm-no" onClick={ () => setConfirmId( null ) }>
252 { __( 'Cancel', 'everest-forms' ) }
253 </button>
254 </div>
255 ) }
256 </div>
257 );
258 };
259
260 return (
261 <div className="slate-anim">
262 <p className="pane-note">
263 <InfoNoteIcon />
264 <span>{ __( 'For advanced customization, go to Elements or click the live preview.', 'everest-forms' ) }</span>
265 </p>
266
267 <div className="current-block">
268 <div className="block-title">{ __( 'Your Palette', 'everest-forms' ) }</div>
269
270 <div className={ 'pal-editor-card' + ( editing ? ' is-editing' : '' ) }>
271 <div className="pal-editor-head">
272 { swatch( currentColors ) }
273 { pro ? (
274 <button
275 type="button"
276 className="pal-editor-toggle"
277 aria-label={ editing ? __( 'Close palette editor', 'everest-forms' ) : __( 'Edit palette', 'everest-forms' ) }
278 title={ editing ? __( 'Close palette editor', 'everest-forms' ) : __( 'Edit palette', 'everest-forms' ) }
279 onClick={ () => ( editing ? closeEditorKeep() : openEditor() ) }
280 >
281 { editing ? (
282 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={ 2 } aria-hidden="true">
283 <path d="m18 15-6-6-6 6" />
284 </svg>
285 ) : (
286 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={ 2 } aria-hidden="true">
287 <path d="M12 20h9" />
288 <path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" />
289 </svg>
290 ) }
291 </button>
292 ) : (
293 <span className="pro-badge" aria-label={ __( 'Pro', 'everest-forms' ) }>
294 <ProCrown />
295 </span>
296 ) }
297 </div>
298
299 { pro && editing && (
300 <>
301 <div className="pal-edit-rows">
302 { slots.map( ( slot ) => (
303 <PaletteColorRow
304 key={ slot }
305 label={ paletteSlotLabel( slot ) }
306 value={ currentColors[ slot ] || '#ffffff' }
307 onChange={ ( color ) => store.setPaletteSlotColor( slot, color, paletteSlotLabel( slot ) ) }
308 gradientable={ store.slotGradientable( slot ) }
309 />
310 ) ) }
311 </div>
312 <div className="pal-edit-actions">
313 <button type="button" className="pal-edit-save" onClick={ closeEditorKeep }>
314 { __( 'Save', 'everest-forms' ) }
315 </button>
316 <button type="button" className="pal-edit-cancel" onClick={ cancelEditor }>
317 { __( 'Cancel', 'everest-forms' ) }
318 </button>
319 </div>
320 </>
321 ) }
322 </div>
323
324 <div className="pal-current-caption">
325 <span className="pal-current-name">
326 { matchedPalette ? matchedPalette.name : __( 'Default', 'everest-forms' ) }
327 </span>
328 { paletteModified && (
329 <span className="predef-badge">{ __( 'Modified', 'everest-forms' ) }</span>
330 ) }
331 { !! appliedPaletteId && (
332 <span className="predef-badge predef-badge--base">{ __( 'Base', 'everest-forms' ) }</span>
333 ) }
334 </div>
335 </div>
336
337 <div className="block-title">{ __( 'Presets', 'everest-forms' ) }</div>
338 <div className="pal-pop-grid" role="listbox" aria-label={ __( 'Preset palettes', 'everest-forms' ) }>
339 { [ ...custom, ...builtin ].map( renderCard ) }
340 </div>
341 </div>
342 );
343 }
344
345 /* --------------------------------------------------------------------- *
346 * Design list (home)
347 * --------------------------------------------------------------------- */
348
349 export function DesignList( {
350 sections,
351 onOpen,
352 onNavigateTemplates,
353 onNavigateColors,
354 onNavigateCss,
355 onResetAll,
356 onUndo,
357 onRedo,
358 canUndo,
359 canRedo,
360 undoLabel,
361 redoLabel,
362 }: {
363 sections: Section[];
364 onOpen: ( key: string ) => void;
365 onNavigateTemplates: () => void;
366 onNavigateColors: () => void;
367 onNavigateCss: () => void;
368 onResetAll: () => void;
369 onUndo: () => void;
370 onRedo: () => void;
371 canUndo: boolean;
372 canRedo: boolean;
373 undoLabel: string;
374 redoLabel: string;
375 } ) {
376 const store = useStore();
377
378 // "Your Template"/"Your Palette" summary — value-driven off live store state (same pattern
379 // TemplatesPane already uses for its ✓/"Modified" badges), so these never go stale.
380 const appliedId = store.appliedTemplateId();
381 const originId = store.originTemplateId();
382 const matchedTplId = appliedId || originId;
383 const matchedTpl = matchedTplId ? store.allTemplates().find( ( t ) => t.id === matchedTplId ) : null;
384 const templateLabel = matchedTpl ? matchedTpl.name : __( 'Default', 'everest-forms' );
385 // Also flag "Modified" when nothing ever matched a named template but styles have drifted
386 // from the raw schema defaults.
387 const templateModified = ! appliedId && ( !! originId || ! store.isAtSchemaDefault() );
388 const templateIsBase = !! appliedId && ! templateModified;
389
390 const appliedPaletteId = store.appliedPaletteId();
391 const originPaletteId = store.originPaletteId();
392 const matchedPaletteId = appliedPaletteId || originPaletteId;
393 const matchedPalette = matchedPaletteId ? store.palettes.find( ( p ) => p.id === matchedPaletteId ) : null;
394 const paletteLabel = matchedPalette ? matchedPalette.name : __( 'Default', 'everest-forms' );
395 const paletteModified = ! appliedPaletteId && ( !! originPaletteId || ! store.paletteAtDefault() );
396 const paletteIsBase = !! appliedPaletteId && ! paletteModified;
397 const paletteColors = store.currentPaletteColors();
398
399 return (
400 <div className="slate-anim">
401 <div className="uxrow">
402 <div className="uxrow-history">
403 <button
404 type="button"
405 className="uxbtn"
406 disabled={ ! canUndo }
407 aria-label={ canUndo ? `${ __( 'Undo', 'everest-forms' ) }: ${ undoLabel }` : __( 'Undo', 'everest-forms' ) }
408 title={
409 ( canUndo
410 ? `${ __( 'Undo', 'everest-forms' ) }: ${ undoLabel }`
411 : __( 'Undo', 'everest-forms' ) ) + ' (Ctrl+Z)'
412 }
413 onClick={ onUndo }
414 >
415 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={ 2.2 } aria-hidden="true">
416 <path d="M3 10h10a5 5 0 0 1 0 10H7" />
417 <path d="M7 6 3 10l4 4" />
418 </svg>
419 </button>
420 <button
421 type="button"
422 className="uxbtn"
423 disabled={ ! canRedo }
424 aria-label={ canRedo ? `${ __( 'Redo', 'everest-forms' ) }: ${ redoLabel }` : __( 'Redo', 'everest-forms' ) }
425 title={
426 ( canRedo
427 ? `${ __( 'Redo', 'everest-forms' ) }: ${ redoLabel }`
428 : __( 'Redo', 'everest-forms' ) ) + ' (Ctrl+Shift+Z / Ctrl+Y)'
429 }
430 onClick={ onRedo }
431 >
432 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={ 2.2 } aria-hidden="true">
433 <path d="M21 10H11a5 5 0 0 0 0 10h6" />
434 <path d="m17 6 4 4-4 4" />
435 </svg>
436 </button>
437 </div>
438 <span className="uxrow-divider" aria-hidden="true" />
439 <button
440 type="button"
441 className="uxbtn"
442 title={ __( 'Reset all styles', 'everest-forms' ) }
443 aria-label={ __( 'Reset all styles', 'everest-forms' ) }
444 onClick={ onResetAll }
445 >
446 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={ 2 } aria-hidden="true">
447 <path d="M3 12a9 9 0 1 0 3-6.7" />
448 <path d="M3 4v5h5" />
449 </svg>
450 </button>
451 </div>
452 <div className="block-title">{ __( 'Pre-defined', 'everest-forms' ) }</div>
453
454 <div className="predefined-row">
455 <button type="button" className="predef-card" onClick={ onNavigateTemplates }>
456 <span className="predef-body">
457 <span className="predef-kicker">{ __( 'Template', 'everest-forms' ) }</span>
458 <span className="predef-name">
459 { templateLabel }
460 { templateModified && (
461 <span className="predef-badge">{ __( 'Modified', 'everest-forms' ) }</span>
462 ) }
463 { templateIsBase && (
464 <span className="predef-badge predef-badge--base">{ __( 'Base', 'everest-forms' ) }</span>
465 ) }
466 </span>
467 </span>
468 <span className="predef-thumb">
469 <TemplateThumb
470 tpl={ { id: '__current__', name: templateLabel, image: '', palette: store.palette, tokens: store.tokens } }
471 />
472 </span>
473 </button>
474
475 <button type="button" className="predef-card" onClick={ onNavigateColors }>
476 <span className="predef-body">
477 <span className="predef-kicker">{ __( 'Colors', 'everest-forms' ) }</span>
478 <span className="predef-name">
479 { paletteLabel }
480 { paletteModified && (
481 <span className="predef-badge">{ __( 'Modified', 'everest-forms' ) }</span>
482 ) }
483 { paletteIsBase && (
484 <span className="predef-badge predef-badge--base">{ __( 'Base', 'everest-forms' ) }</span>
485 ) }
486 </span>
487 </span>
488 <span className="sw predef-swatch" aria-hidden="true">
489 { Object.keys( store.paletteMap ).map( ( slot ) => (
490 <i key={ slot } style={ { background: paletteColors[ slot ] } } />
491 ) ) }
492 </span>
493 </button>
494 </div>
495
496 <div className="theme-style-row">
497 <span className="tsr-text">
498 <b>
499 { __( 'Apply Theme Style', 'everest-forms' ) }
500 <HoverTip
501 className="info"
502 label={ __( 'About Apply Theme Style', 'everest-forms' ) }
503 tip={ __(
504 'Matches only your active theme’s font. Colors, borders, and spacing always stay as you set them below, either way. Turn off to use your own Font family choice instead.',
505 'everest-forms'
506 ) }
507 >
508 <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
509 <path
510 fillRule="evenodd"
511 clipRule="evenodd"
512 d="M21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12ZM23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12Z"
513 />
514 <path d="M11 16V12C11 11.4477 11.4477 11 12 11C12.5523 11 13 11.4477 13 12V16C13 16.5523 12.5523 17 12 17C11.4477 17 11 16.5523 11 16Z" />
515 <path d="M12.0098 7C12.5621 7 13.0098 7.44772 13.0098 8C13.0098 8.55228 12.5621 9 12.0098 9H12C11.4477 9 11 8.55228 11 8C11 7.44772 11 7 12 7H12.0098Z" />
516 </svg>
517 </HoverTip>
518 </b>
519 </span>
520 <button
521 type="button"
522 className="switch"
523 role="switch"
524 aria-checked={ store.applyThemeStyle }
525 aria-label={ __( 'Apply Theme Style', 'everest-forms' ) }
526 onClick={ () => store.setApplyThemeStyle( ! store.applyThemeStyle ) }
527 />
528 </div>
529
530 <div className="block-title">{ __( 'Elements', 'everest-forms' ) }</div>
531 <p className="hintline">
532 <Icon inner='<path d="M3 3l7 17 2-7 7-2z"/>' />
533 <span>
534 { __( 'Tip: pick an element to style it, or', 'everest-forms' ) }{ ' ' }
535 <b>{ __( 'click it in the live preview.', 'everest-forms' ) }</b>
536 </span>
537 </p>
538 <div className="ellist">
539 { sections.map( ( s ) => {
540 const locked = s.tier === 'pro' && ! store.proActive;
541 return (
542 <button
543 key={ s.key }
544 type="button"
545 className={ 'elrow' + ( store.changedInSection( s.key ) ? ' dirty' : '' ) + ( locked ? ' locked' : '' ) }
546 onClick={ () => onOpen( s.key ) }
547 >
548 <span className="ic">
549 <Icon inner={ SECTION_ICONS[ s.key ] || '' } />
550 </span>
551 <span className="tx">
552 <b>{ s.title }</b>
553 <small>{ SECTION_SUBTITLES[ s.key ] || s.hint }</small>
554 </span>
555 { locked ? (
556 <span className="pro-badge" aria-label={ __( 'Pro feature', 'everest-forms' ) }>
557 <ProCrown />
558 </span>
559 ) : (
560 <span className="dot" aria-hidden="true" />
561 ) }
562 <span className="chev" aria-hidden="true">
563 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={ 2.2 }>
564 <path d="m9 6 6 6-6 6" />
565 </svg>
566 </span>
567 </button>
568 );
569 } ) }
570 </div>
571
572 <div className="block-title">{ __( 'Advanced', 'everest-forms' ) }</div>
573 <div className="ellist">
574 <button type="button" className="elrow" onClick={ onNavigateCss }>
575 <span className="ic">
576 <Icon inner='<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>' />
577 </span>
578 <span className="tx">
579 <b>{ __( 'Custom CSS', 'everest-forms' ) }</b>
580 <small>{ __( 'Add your own CSS styles', 'everest-forms' ) }</small>
581 </span>
582 <span className="chev" aria-hidden="true">
583 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={ 2.2 }>
584 <path d="m9 6 6 6-6 6" />
585 </svg>
586 </span>
587 </button>
588 </div>
589 </div>
590 );
591 }
592
593 /* --------------------------------------------------------------------- *
594 * Element slate (drill-down body)
595 * --------------------------------------------------------------------- */
596
597 interface GroupedTokens {
598 heading: string;
599 tokens: Token[];
600 }
601
602 function groupTokens( tokens: Token[] ): GroupedTokens[] {
603 const out: GroupedTokens[] = [];
604 let current: string | null = null;
605 tokens.forEach( ( t ) => {
606 if ( t.group !== current ) {
607 current = t.group;
608 out.push( { heading: current, tokens: [] } );
609 }
610 out[ out.length - 1 ].tokens.push( t );
611 } );
612 return out;
613 }
614
615 export function ElementSlate( {
616 section,
617 activeState,
618 onChangeState,
619 pulse,
620 }: {
621 section: Section;
622 activeState: string | null;
623 onChangeState: ( id: string ) => void;
624 pulse: number;
625 } ) {
626 const store = useStore();
627 const bodyRef = React.useRef< HTMLDivElement >( null );
628 const tabs = section.states || section.variants || null;
629 const first = tabs ? tabs[ 0 ] : null;
630 const act = activeState || first;
631
632 // When opened via a preview click (pulse changes), scroll the panel up and briefly flash.
633 React.useEffect( () => {
634 const el = bodyRef.current;
635 if ( ! el || ! pulse ) {
636 return;
637 }
638 const scroller = el.closest( '.panel-scroll' );
639 if ( scroller ) {
640 scroller.scrollTop = 0;
641 }
642 el.classList.remove( 'flash' );
643 // Force reflow so the animation restarts on repeated selections.
644 void el.offsetWidth;
645 el.classList.add( 'flash' );
646 }, [ pulse ] );
647
648 // Background sub-options only matter once an image is set.
649 const bgImageSet = !! ( store.tokens[ 'wrap.bgImage' ] && store.tokens[ 'wrap.bgImage' ].desktop );
650
651 const visible = store.schema.filter(
652 ( t ) => t.section === section.key && ! t.hidden && ( ! t.show_when_image || bgImageSet )
653 );
654
655 // On a state/variant tab, show only the controls that belong to that state; shared controls
656 // live on the first tab.
657 const enabledByState = tabs
658 ? visible.filter( ( t ) => ( t.state ? t.state === act : act === first ) )
659 : visible;
660
661 // Dependency dimming: a border-style set to "none" disables its width/colour deps.
662 const dimmedByDep = new Set< string >();
663 const depHints: Record< string, string > = {};
664 visible.forEach( ( t ) => {
665 if ( t.deps && String( store.resolve( t.key ) ) === 'none' ) {
666 t.deps.forEach( ( k ) => dimmedByDep.add( k ) );
667 depHints[ t.key ] = __( 'Width & color have no effect while the border style is None.', 'everest-forms' );
668 }
669 } );
670
671 const locked = ( t: Token ) => t.tier === 'pro' && ! store.proActive;
672 const hasLocked = visible.some( locked );
673
674 // A whole Pro section on a free site: render a locked upgrade teaser instead of an empty slate.
675 const sectionLocked = section.tier === 'pro' && ! store.proActive;
676
677 const renderControl = ( t: Token ) => (
678 <ControlRenderer
679 key={ t.key }
680 token={ t }
681 store={ store }
682 dimmed={ dimmedByDep.has( t.key ) || locked( t ) }
683 depHint={ depHints[ t.key ] }
684 />
685 );
686
687 const renderGroups = ( tokens: Token[] ) =>
688 groupTokens( tokens ).map( ( g, i ) => (
689 <div className="grp" key={ i }>
690 { g.heading && <div className="grp-h">{ g.heading }</div> }
691 { section.key === 'form' && g.heading === 'Background' && (
692 <p className="hintline">
693 <Icon inner='<circle cx="12" cy="12" r="9"/><path d="M12 16v-5M12 8h.01"/>' />
694 { __( 'Background color comes from your color palette.', 'everest-forms' ) }
695 </p>
696 ) }
697 { g.tokens.map( renderControl ) }
698 </div>
699 ) );
700
701 if ( sectionLocked ) {
702 return (
703 <div id="elBody" className="slate-anim" ref={ bodyRef }>
704 <ProSectionTeaser section={ section } />
705 </div>
706 );
707 }
708
709 return (
710 <div id="elBody" className="slate-anim" ref={ bodyRef }>
711 { hasLocked && (
712 <div className="pro-lock">
713 <span className="pro-lock-ic" aria-hidden="true">
714 <Icon inner='<rect x="4" y="11" width="16" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>' />
715 </span>
716 <span>
717 { __( 'This is a Pro feature.', 'everest-forms' ) }{ ' ' }
718 <a href={ UPGRADE_URL } target="_blank" rel="noreferrer">
719 { __( 'Upgrade to Pro', 'everest-forms' ) }
720 </a>{ ' ' }
721 { __( 'to unlock it.', 'everest-forms' ) }
722 </span>
723 </div>
724 ) }
725
726 { tabs && (
727 <div
728 className="state-seg"
729 role="tablist"
730 aria-label={ section.title + ' ' + __( 'variants', 'everest-forms' ) }
731 onKeyDown={ ( e ) => {
732 if ( e.key !== 'ArrowLeft' && e.key !== 'ArrowRight' ) {
733 return;
734 }
735 e.preventDefault();
736 const idx = tabs.indexOf( act as string );
737 const next = tabs[ ( idx + ( e.key === 'ArrowRight' ? 1 : tabs.length - 1 ) ) % tabs.length ];
738 onChangeState( next );
739 document.getElementById( `scv2-state-${ section.key }-${ next }` )?.focus();
740 } }
741 >
742 { tabs.map( ( id ) => (
743 <button
744 key={ id }
745 id={ `scv2-state-${ section.key }-${ id }` }
746 type="button"
747 role="tab"
748 aria-selected={ id === act }
749 aria-controls={ `scv2-state-panel-${ section.key }` }
750 tabIndex={ id === act ? 0 : -1 }
751 onClick={ () => onChangeState( id ) }
752 >
753 { STATE_LABELS[ id ] || id }
754 </button>
755 ) ) }
756 </div>
757 ) }
758
759 <div id={ `scv2-state-panel-${ section.key }` } role={ tabs ? 'tabpanel' : undefined } aria-labelledby={ tabs ? `scv2-state-${ section.key }-${ act }` : undefined }>
760 { renderGroups( enabledByState ) }
761 </div>
762 </div>
763 );
764 }
765
766 /** Locked upgrade teaser — shown wherever a Pro feature is surfaced on a free site. */
767 export function ProTeaser( { title, text }: { title: string; text: string } ) {
768 return (
769 <div className="pro-teaser">
770 <span className="pro-teaser-ic" aria-hidden="true">
771 <Icon inner='<rect x="4" y="11" width="16" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>' />
772 </span>
773 <h4 className="pro-teaser-title">{ title }</h4>
774 <p className="pro-teaser-text">{ text }</p>
775 <a className="pro-teaser-btn" href={ UPGRADE_URL } target="_blank" rel="noreferrer">
776 { __( 'Upgrade to Pro', 'everest-forms' ) }
777 </a>
778 </div>
779 );
780 }
781
782 /** Section-level teaser (a whole Pro design section opened on free). */
783 function ProSectionTeaser( { section }: { section: Section } ) {
784 return (
785 <ProTeaser
786 // translators: %s: section name, e.g. "Messages".
787 title={ ( __( '%s styling is a Pro feature', 'everest-forms' ) as string ).replace( '%s', section.title ) }
788 text={ section.hint }
789 />
790 );
791 }
792
793 /* --------------------------------------------------------------------- *
794 * Templates
795 * --------------------------------------------------------------------- */
796
797 /** The desktop value of a token bag, for hover-preview + thumbnails. */
798 function desktopOf( bag: DeviceBag | undefined ): any {
799 return bag && bag.desktop !== undefined ? bag.desktop : undefined;
800 }
801
802 /**
803 * Flatten a template's per-device token bags into a { key: desktopValue } map covering every
804 * schema key (not just the ones the template sets), mirroring what `store.applyTemplate()` does
805 * on click so hovering previews exactly what clicking would produce.
806 */
807 function flattenForPreview( tokens: Record< string, DeviceBag >, schema: Token[] ): Record< string, any > {
808 const out: Record< string, any > = {};
809 schema.forEach( ( t ) => {
810 out[ t.key ] = tokens && tokens[ t.key ] !== undefined ? desktopOf( tokens[ t.key ] ) : t.default;
811 } );
812 return out;
813 }
814
815 function radiusOf( bag: DeviceBag | undefined, fallback: number ): number {
816 const v = desktopOf( bag ) as BoxValue | undefined;
817 return v && typeof v === 'object' ? Number( v.top ) || 0 : fallback;
818 }
819
820 /** Template thumbnail: real screenshot when it loads, else a live token-driven mini-form. */
821 function TemplateThumb( { tpl }: { tpl: Template } ) {
822 const [ imgOk, setImgOk ] = React.useState( !! tpl.image );
823 const th = templateThumb( tpl );
824 if ( tpl.image && imgOk ) {
825 return (
826 <span className="thumb thumb-has-img">
827 <img src={ tpl.image } alt="" loading="lazy" onError={ () => setImgOk( false ) } />
828 </span>
829 );
830 }
831 return (
832 <span className="thumb" style={ th.thumb }>
833 <span className="l" style={ th.line } />
834 <span className="f" style={ th.field } />
835 <span className="l l2" style={ th.line } />
836 <span className="f f2" style={ th.field } />
837 <span className="b" style={ th.button } />
838 </span>
839 );
840 }
841
842 /** Build a small live thumbnail from a template's migrated tokens — a mini "Name / Message /
843 * Submit" mock-up in the template's own colours, so a custom template reads as recognizably
844 * itself in the grid, the same way a built-in's screenshot does. */
845 function templateThumb( tpl: Template ) {
846 const t = tpl.tokens;
847 const bg = desktopOf( t[ 'wrap.bg' ] ) || '#ffffff';
848 const btnBg = desktopOf( t[ 'btn.bg' ] ) || '#3b82f6';
849 const btnText = desktopOf( t[ 'btn.color' ] ) || '#ffffff';
850 const inputBg = desktopOf( t[ 'input.bg' ] ) || '#ffffff';
851 const border = desktopOf( t[ 'input.borderC' ] ) || '#d8dae2';
852 const label = desktopOf( t[ 'label.color' ] ) || '#1f2433';
853 const fieldRadius = radiusOf( t[ 'input.radius' ], 6 );
854 const btnRadius = radiusOf( t[ 'btn.radius' ], 6 );
855 return {
856 thumb: { background: bg } as React.CSSProperties,
857 line: { background: label } as React.CSSProperties,
858 field: { background: inputBg, borderColor: border, borderRadius: fieldRadius } as React.CSSProperties,
859 button: { background: btnBg, color: btnText, borderRadius: btnRadius } as React.CSSProperties,
860 };
861 }
862
863 /** One template card — thumbnail, name, applied/locked/delete affordances. Used for both the
864 * "Your templates" and built-in grids so the two stay visually identical. */
865 function TemplateCard( {
866 tpl,
867 applied,
868 modified,
869 basedOn,
870 locked,
871 disabled,
872 onPreview,
873 onClearPreview,
874 onApply,
875 confirmingDelete,
876 onRequestDelete,
877 onConfirmDelete,
878 onCancelDelete,
879 }: {
880 tpl: Template;
881 applied: boolean;
882 /** The form was applied FROM this template but has since been edited — show an honest hint
883 * instead of a ✓ (its styles no longer exactly match this template). */
884 modified?: boolean;
885 /** Name of the built-in template this (custom) template exactly derives from, if any. */
886 basedOn?: string;
887 locked: boolean;
888 /** True while a DIFFERENT template is being edited — every action on this card is inert. */
889 disabled?: boolean;
890 onPreview: () => void;
891 onClearPreview: () => void;
892 onApply: () => void;
893 confirmingDelete?: boolean;
894 onRequestDelete?: () => void;
895 onConfirmDelete?: () => void;
896 onCancelDelete?: () => void;
897 } ) {
898 return (
899 <div className={ 'tpl-wrap' + ( confirmingDelete ? ' is-confirming' : '' ) }>
900 <button
901 type="button"
902 className={
903 'tpl' +
904 ( tpl.custom ? ' tpl-user' : '' ) +
905 ( locked ? ' tpl-locked' : '' ) +
906 ( applied ? ' tpl-applied' : '' ) +
907 ( modified ? ' tpl-modified' : '' ) +
908 ( disabled ? ' tpl-disabled' : '' )
909 }
910 aria-pressed={ applied }
911 aria-disabled={ disabled }
912 onMouseEnter={ onPreview }
913 onMouseLeave={ onClearPreview }
914 onFocus={ onPreview }
915 onBlur={ onClearPreview }
916 onClick={ disabled ? undefined : onApply }
917 >
918 <span className="tpl-thumb-box">
919 { locked && (
920 <span className="tpl-pro" aria-label={ __( 'Pro template', 'everest-forms' ) }>
921 <ProCrown />
922 </span>
923 ) }
924 { locked && (
925 <span className="tpl-lock-veil" aria-hidden="true">
926 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={ 2 }>
927 <rect x="4" y="11" width="16" height="9" rx="2" />
928 <path d="M8 11V7a4 4 0 0 1 8 0v4" />
929 </svg>
930 </span>
931 ) }
932 <TemplateThumb tpl={ tpl } />
933 </span>
934 <span className="cap">
935 <span className="cap-name">{ tpl.name }</span>
936 { tpl.custom && <span className="predef-badge predef-badge--custom">{ __( 'Custom', 'everest-forms' ) }</span> }
937 { ( applied || modified ) && <span className="predef-badge predef-badge--base">{ __( 'Base', 'everest-forms' ) }</span> }
938 </span>
939 { basedOn && (
940 <span className="tpl-parent">
941 { /* translators: %s: the built-in template this custom one derives from. */ }
942 { ( __( 'Based on %s', 'everest-forms' ) as string ).replace( '%s', basedOn ) }
943 </span>
944 ) }
945 </button>
946 <span className="tpl-card-tools">
947 { onRequestDelete && (
948 <button
949 type="button"
950 className="tpl-tool tpl-tool--danger"
951 disabled={ disabled }
952 aria-label={ `${ __( 'Delete', 'everest-forms' ) } ${ tpl.name }` }
953 title={ __( 'Delete template', 'everest-forms' ) }
954 onClick={ ( e ) => {
955 e.stopPropagation();
956 onRequestDelete();
957 } }
958 >
959 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={ 2 } aria-hidden="true">
960 <path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m2 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
961 </svg>
962 </button>
963 ) }
964 </span>
965 { confirmingDelete && (
966 <div className="tpl-card-confirm" role="alertdialog" aria-label={ __( 'Delete template?', 'everest-forms' ) }>
967 <span>{ __( 'Delete?', 'everest-forms' ) }</span>
968 <button type="button" className="tpl-confirm-yes" onClick={ onConfirmDelete }>
969 { __( 'Delete', 'everest-forms' ) }
970 </button>
971 <button type="button" className="tpl-confirm-no" onClick={ onCancelDelete }>
972 { __( 'Cancel', 'everest-forms' ) }
973 </button>
974 </div>
975 ) }
976 </div>
977 );
978 }
979
980 export function TemplatesPane( {
981 onPreview,
982 onClearPreview,
983 onApplied,
984 }: {
985 onPreview: ( overrides: Record< string, any > ) => void;
986 onClearPreview: () => void;
987 onApplied: ( name: string ) => void;
988 } ) {
989 const store = useStore();
990 const [ confirmDeleteId, setConfirmDeleteId ] = React.useState< string | null >( null );
991
992 const templates = store.allTemplates();
993 // Memoize the flattened override maps so hover doesn't re-flatten on every mouse event.
994 const flat = React.useMemo(
995 () => Object.fromEntries( templates.map( ( tpl ) => [ tpl.id, flattenForPreview( tpl.tokens, store.schema ) ] ) ),
996 // eslint-disable-next-line react-hooks/exhaustive-deps
997 [ templates, store ]
998 );
999
1000 // Value-driven template state, recomputed whenever the store version bumps.
1001 const ver = store.getVersion();
1002 const appliedId = React.useMemo( () => store.appliedTemplateId(), [ store, ver ] );
1003 const originId = React.useMemo( () => store.originTemplateId(), [ store, ver ] );
1004 const parentNames = React.useMemo( () => {
1005 const map: Record< string, string > = {};
1006 templates.forEach( ( tpl ) => {
1007 const pid = store.templateParentId( tpl );
1008 if ( pid ) {
1009 const parent = store.templates.find( ( b ) => b.id === pid );
1010 if ( parent ) {
1011 map[ tpl.id ] = parent.name;
1012 }
1013 }
1014 } );
1015 return map;
1016 // eslint-disable-next-line react-hooks/exhaustive-deps
1017 }, [ templates, store, ver ] );
1018
1019 // "Your Template" card — same value-driven match as the ✓/"Modified" badges above, fed into
1020 // the existing TemplateThumb live-mockup renderer via a synthetic, always-current object.
1021 const matchedTplId = appliedId || originId;
1022 const matchedTpl = matchedTplId ? templates.find( ( t ) => t.id === matchedTplId ) : null;
1023 const yourTemplateName = matchedTpl ? matchedTpl.name : __( 'Default', 'everest-forms' );
1024 const yourTemplateModified = ! appliedId && ( !! originId || ! store.isAtSchemaDefault() );
1025 const yourTemplateIsBase = !! appliedId && ! yourTemplateModified;
1026
1027 const templatesBase = store.settings.restBase.replace( /\/styles$/, '/style-templates' );
1028
1029 const deleteTemplate = async ( id: string ) => {
1030 setConfirmDeleteId( null );
1031 if ( ! apiFetch ) {
1032 return;
1033 }
1034 store.removeUserTemplate( id ); // optimistic
1035 try {
1036 await apiFetch( { path: `${ templatesBase }/${ id }`, method: 'DELETE' } );
1037 } catch ( e ) {
1038 // The list already reflects the removal; a reload restores it if the server rejected.
1039 }
1040 };
1041
1042 const applyTpl = ( tpl: Template, locked: boolean ) => {
1043 if ( locked ) {
1044 window.open( UPGRADE_URL, '_blank' );
1045 return;
1046 }
1047 store.applyTemplate( tpl.id, tpl.tokens, tpl.palette );
1048 onApplied( tpl.name );
1049 };
1050
1051 const renderGrid = ( list: Template[] ) => (
1052 <div className="tpls">
1053 { list.map( ( tpl ) => {
1054 const locked = !! tpl.is_pro && ! store.proActive;
1055 const withDelete = !! tpl.custom;
1056 return (
1057 <TemplateCard
1058 key={ tpl.id }
1059 tpl={ tpl }
1060 applied={ appliedId === tpl.id }
1061 modified={ originId === tpl.id }
1062 basedOn={ parentNames[ tpl.id ] }
1063 locked={ locked }
1064 onPreview={ () => onPreview( flat[ tpl.id ] ) }
1065 onClearPreview={ onClearPreview }
1066 onApply={ () => applyTpl( tpl, locked ) }
1067 confirmingDelete={ confirmDeleteId === tpl.id }
1068 onRequestDelete={ withDelete ? () => setConfirmDeleteId( tpl.id ) : undefined }
1069 onConfirmDelete={ withDelete ? () => deleteTemplate( tpl.id ) : undefined }
1070 onCancelDelete={ withDelete ? () => setConfirmDeleteId( null ) : undefined }
1071 />
1072 );
1073 } ) }
1074 </div>
1075 );
1076
1077 return (
1078 <div className="slate-anim">
1079 <div className="current-block">
1080 <div className="block-title">{ __( 'Your Template', 'everest-forms' ) }</div>
1081 <div className="tpls">
1082 <div className="tpl-wrap tpl-current">
1083 <span className="tpl tpl-static" aria-label={ __( 'Your current form style', 'everest-forms' ) }>
1084 <span className="tpl-thumb-box">
1085 <TemplateThumb
1086 tpl={ { id: '__current__', name: yourTemplateName, image: '', palette: store.palette, tokens: store.tokens } }
1087 />
1088 </span>
1089 <span className="cap">
1090 <span className="cap-name">{ yourTemplateName }</span>
1091 { yourTemplateModified && <span className="predef-badge">{ __( 'Modified', 'everest-forms' ) }</span> }
1092 { yourTemplateIsBase && <span className="predef-badge predef-badge--base">{ __( 'Base', 'everest-forms' ) }</span> }
1093 </span>
1094 </span>
1095 </div>
1096 </div>
1097 </div>
1098
1099 <div className="block-title">{ __( 'Presets', 'everest-forms' ) }</div>
1100 <p className="pane-note">
1101 <span>
1102 <b>{ __( 'Hover a card to preview it live', 'everest-forms' ) }</b>{ ' ' }
1103 { __( 'click to apply (you can always undo).', 'everest-forms' ) }
1104 </span>
1105 </p>
1106 { /* Legacy set stays in store.templates (see Templates::load_legacy()) for badge matching above; just not offered here. */ }
1107 { renderGrid(
1108 [ ...store.userTemplates, ...store.templates.filter( ( tpl ) => ! tpl.legacy ) ].sort( ( a, b ) => {
1109 if ( !! a.custom !== !! b.custom ) {
1110 return a.custom ? -1 : 1;
1111 }
1112 return ( a.is_pro ? 1 : 0 ) - ( b.is_pro ? 1 : 0 );
1113 } )
1114 ) }
1115 </div>
1116 );
1117 }
1118
1119 /* --------------------------------------------------------------------- *
1120 * Custom CSS
1121 * --------------------------------------------------------------------- */
1122
1123 const CSS_CHIPS = [
1124 '.evf-container',
1125 '.evf-submit-container button',
1126 'input, textarea, select',
1127 'label.evf-field-label',
1128 '.everest-forms-notice--success',
1129 ];
1130
1131 const CSS_EXAMPLE = `.evf-submit-container button {
1132 letter-spacing: .04em;
1133 text-transform: uppercase;
1134 }`;
1135
1136 export function CustomCssPane() {
1137 const store = useStore();
1138 const ref = React.useRef< HTMLTextAreaElement >( null );
1139
1140 const insert = ( snippet: string ) => {
1141 const el = ref.current;
1142 if ( ! el ) {
1143 return;
1144 }
1145 const start = el.selectionStart;
1146 const value = el.value.slice( 0, start ) + snippet + el.value.slice( el.selectionEnd );
1147 store.setCustomCss( value );
1148 el.focus();
1149 };
1150
1151 const len = ( store.customCss || '' ).length;
1152
1153 return (
1154 <div className="slate-anim">
1155 <div className="block-title">{ __( 'Custom CSS', 'everest-forms' ) }</div>
1156 <p className="pane-note">
1157 <span>
1158 { __( 'Applied live as you type —', 'everest-forms' ) } <b>{ __( 'applies to every form on this site', 'everest-forms' ) }</b>{ ' ' }
1159 { __( 'on save (same as the Customizer’s Additional CSS). Click a selector to insert it:', 'everest-forms' ) }
1160 </span>
1161 </p>
1162 <div className="chips">
1163 { CSS_CHIPS.map( ( c ) => (
1164 <button key={ c } type="button" onClick={ () => insert( c + ' {\n\t\n}\n' ) }>
1165 { c }
1166 </button>
1167 ) ) }
1168 </div>
1169 <textarea
1170 ref={ ref }
1171 className="csscode"
1172 spellCheck={ false }
1173 value={ store.customCss || '' }
1174 aria-label={ __( 'Custom CSS', 'everest-forms' ) }
1175 placeholder={ '.evf-submit-container button {\n\tletter-spacing: .04em;\n}' }
1176 onInput={ ( e ) => store.setCustomCss( ( e.target as HTMLTextAreaElement ).value ) }
1177 />
1178 <div className="css-status">
1179 <span>{ len ? `${ len } ${ __( 'characters', 'everest-forms' ) }` : __( 'No custom CSS', 'everest-forms' ) }</span>
1180 <button type="button" className="css-example-btn" onClick={ () => insert( CSS_EXAMPLE ) }>
1181 { __( 'Insert example', 'everest-forms' ) }
1182 </button>
1183 </div>
1184 </div>
1185 );
1186 }
1187