/**
* Style Customizer v2 — panel panes: DesignList, ElementSlate (schema-driven drill-down),
* TemplatesPane and CustomCssPane. All read/write the store.
*/
import React from 'react';
import { ColorPickerField, ControlRenderer } from './ControlRenderer';
import { SECTION_ICONS, SECTION_SUBTITLES, STATE_LABELS } from './constants';
import { HoverTip } from './HoverTip';
import { useStore } from './store';
import { BoxValue, DeviceBag, Section, Template, Token } from './types';
// The same "PRO" crown badge the builder's Fields sidebar uses.
import proIconUrl from '../../assets/images/icons/everest-form-pro-icon.png';
const __ = ( window as any ).wp?.i18n?.__ || ( ( s: string ) => s );
const apiFetch = ( window as any ).wp?.apiFetch;
export const UPGRADE_URL = 'https://everestforms.net/pricing/?utm_source=style-customizer&utm_medium=panel';
function Icon( { inner }: { inner: string } ) {
return (
);
}
/** The small "(i)" glyph the design's advisory banners lead with. */
function InfoNoteIcon() {
return (
);
}
/** "PRO" marker — the builder's own locked-field crown badge. */
export function ProCrown() {
return ;
}
/** Human label for a paletteMap slot key — shared by "Your Palette"'s edit rows. */
export function paletteSlotLabel( slot: string ): string {
switch ( slot ) {
case 'form_background':
return __( 'Form background', 'everest-forms' );
case 'field_background':
return __( 'Field background', 'everest-forms' );
case 'field_label':
return __( 'Label', 'everest-forms' );
case 'field_sublabel':
return __( 'Sublabel', 'everest-forms' );
case 'button_text':
return __( 'Button text', 'everest-forms' );
case 'button_background':
return __( 'Button background', 'everest-forms' );
default:
return slot;
}
}
/** One "Your Palette" edit row — the same swatch + hex + alpha-picker popover every element
* color control uses, so editing a slot feels identical to editing any other color. */
export function PaletteColorRow( {
label,
value,
onChange,
gradientable,
}: {
label: string;
value: string;
onChange: ( color: string ) => void;
gradientable?: boolean;
} ) {
return (
{ label }
);
}
/* --------------------------------------------------------------------- *
* Colors (browse) — "Your Palette" (live, editable, Pro-tier) + presets +
* conditional "Your palettes" custom list (view/apply/delete only).
* --------------------------------------------------------------------- */
interface ColorsToast {
msg: string;
kind?: 'success' | 'info';
actLabel?: string;
onAct?: () => void;
}
export function ColorsPane( {
onToast,
onPreviewPalette,
onClearPreview,
}: {
onToast: ( t: ColorsToast ) => void;
onPreviewPalette: ( colors: Record< string, string > ) => void;
onClearPreview: () => void;
} ) {
const store = useStore();
const [ editing, setEditing ] = React.useState( false );
const [ editSnapshot, setEditSnapshot ] = React.useState< { palette: string; colors: Record< string, string > } | null >( null );
const [ confirmId, setConfirmId ] = React.useState< string | null >( null );
const [ busy, setBusy ] = React.useState( false );
const pro = store.proActive;
const slots = Object.keys( store.paletteMap );
const custom = store.customPalettes();
const builtin = store.builtinPalettes();
const palettesBase = store.settings.restBase.replace( /\/styles$/, '/style-palettes' );
// Re-reads on every store version bump (useStore()), so this always reflects live edits —
// the same mechanism that already keeps every other control in sync.
const currentColors = store.currentPaletteColors();
const appliedPaletteId = store.appliedPaletteId();
const originPaletteId = store.originPaletteId();
const matchedPaletteId = appliedPaletteId || originPaletteId;
const matchedPalette = matchedPaletteId ? store.palettes.find( ( p ) => p.id === matchedPaletteId ) : null;
// Also flag "Modified" when the colours never matched a named palette AT ALL but have drifted
// from the raw schema defaults (e.g. hand-picked colours, or a Template's own colour set).
const paletteModified = ! appliedPaletteId && ( !! originPaletteId || ! store.paletteAtDefault() );
// Opening the editor snapshots the current state so "Cancel" can restore it exactly — every
// other control in the panel is "live, undo to fix", but a dedicated Cancel button here (per
// the design) needs a real, precise revert rather than N separate undo steps.
const openEditor = () => {
setEditSnapshot( { palette: store.palette, colors: { ...currentColors } } );
setEditing( true );
};
const closeEditorKeep = () => {
setEditing( false );
setEditSnapshot( null );
};
const cancelEditor = () => {
if ( editSnapshot ) {
if ( editSnapshot.palette ) {
store.applyPalette( editSnapshot.palette );
} else {
slots.forEach( ( slot ) => {
store.setPaletteSlotColor( slot, editSnapshot.colors[ slot ] || '#ffffff', paletteSlotLabel( slot ), false );
} );
}
}
setEditing( false );
setEditSnapshot( null );
};
const swatch = ( colors: Record< string, string > ) => (
{ slots.map( ( slot ) => (
) ) }
);
const applyPreset = ( p: ReturnType< typeof store.customPalettes >[ number ] ) => {
if ( p.is_pro && ! pro ) {
window.open( UPGRADE_URL, '_blank' );
return;
}
store.applyPalette( p.id );
onToast( {
kind: 'success',
msg: `${ __( 'Applied palette', 'everest-forms' ) } “${ p.name }”`,
actLabel: __( 'Undo', 'everest-forms' ),
onAct: () => store.undo(),
} );
};
const deletePalette = async ( id: string ) => {
setConfirmId( null );
if ( ! apiFetch ) {
return;
}
setBusy( true );
try {
const res = await apiFetch( { path: `${ palettesBase }/${ id }`, method: 'DELETE' } );
store.setCustomPalettes( ( res && res.palettes ) || custom.filter( ( p ) => p.id !== id ) );
onToast( { msg: __( 'Custom palette deleted.', 'everest-forms' ) } );
} catch ( e ) {
onToast( { msg: __( 'Could not delete the palette.', 'everest-forms' ) } );
} finally {
setBusy( false );
}
};
const renderCard = ( p: ReturnType< typeof store.customPalettes >[ number ] ) => {
const applied = p.id === appliedPaletteId;
const modified = p.id === originPaletteId;
const applyLocked = p.is_pro && ! pro;
const canDelete = p.is_custom && pro;
return (
onPreviewPalette( p.colors ) }
onMouseLeave={ onClearPreview }
onClick={ () => applyPreset( p ) }
>
{ swatch( p.colors ) }
{ p.name }
{ p.is_custom && (
{ __( 'Custom', 'everest-forms' ) }
) }
{ ( applied || modified ) && (
{ __( 'Base', 'everest-forms' ) }
) }
{ applyLocked && (
) }
{ canDelete && (
setConfirmId( p.id ) }
>
) }
{ confirmId === p.id && (
{ __( 'Delete?', 'everest-forms' ) }
deletePalette( p.id ) } disabled={ busy }>
{ __( 'Delete', 'everest-forms' ) }
setConfirmId( null ) }>
{ __( 'Cancel', 'everest-forms' ) }
) }
);
};
return (
{ __( 'For advanced customization, go to Elements or click the live preview.', 'everest-forms' ) }
{ __( 'Your Palette', 'everest-forms' ) }
{ swatch( currentColors ) }
{ pro ? (
( editing ? closeEditorKeep() : openEditor() ) }
>
{ editing ? (
) : (
) }
) : (
) }
{ pro && editing && (
<>
{ slots.map( ( slot ) => (
store.setPaletteSlotColor( slot, color, paletteSlotLabel( slot ) ) }
gradientable={ store.slotGradientable( slot ) }
/>
) ) }
{ __( 'Save', 'everest-forms' ) }
{ __( 'Cancel', 'everest-forms' ) }
>
) }
{ matchedPalette ? matchedPalette.name : __( 'Default', 'everest-forms' ) }
{ paletteModified && (
{ __( 'Modified', 'everest-forms' ) }
) }
{ !! appliedPaletteId && (
{ __( 'Base', 'everest-forms' ) }
) }
{ __( 'Presets', 'everest-forms' ) }
{ [ ...custom, ...builtin ].map( renderCard ) }
);
}
/* --------------------------------------------------------------------- *
* Design list (home)
* --------------------------------------------------------------------- */
export function DesignList( {
sections,
onOpen,
onNavigateTemplates,
onNavigateColors,
onNavigateCss,
onResetAll,
onUndo,
onRedo,
canUndo,
canRedo,
undoLabel,
redoLabel,
}: {
sections: Section[];
onOpen: ( key: string ) => void;
onNavigateTemplates: () => void;
onNavigateColors: () => void;
onNavigateCss: () => void;
onResetAll: () => void;
onUndo: () => void;
onRedo: () => void;
canUndo: boolean;
canRedo: boolean;
undoLabel: string;
redoLabel: string;
} ) {
const store = useStore();
// "Your Template"/"Your Palette" summary — value-driven off live store state (same pattern
// TemplatesPane already uses for its ✓/"Modified" badges), so these never go stale.
const appliedId = store.appliedTemplateId();
const originId = store.originTemplateId();
const matchedTplId = appliedId || originId;
const matchedTpl = matchedTplId ? store.allTemplates().find( ( t ) => t.id === matchedTplId ) : null;
const templateLabel = matchedTpl ? matchedTpl.name : __( 'Default', 'everest-forms' );
// Also flag "Modified" when nothing ever matched a named template but styles have drifted
// from the raw schema defaults.
const templateModified = ! appliedId && ( !! originId || ! store.isAtSchemaDefault() );
const templateIsBase = !! appliedId && ! templateModified;
const appliedPaletteId = store.appliedPaletteId();
const originPaletteId = store.originPaletteId();
const matchedPaletteId = appliedPaletteId || originPaletteId;
const matchedPalette = matchedPaletteId ? store.palettes.find( ( p ) => p.id === matchedPaletteId ) : null;
const paletteLabel = matchedPalette ? matchedPalette.name : __( 'Default', 'everest-forms' );
const paletteModified = ! appliedPaletteId && ( !! originPaletteId || ! store.paletteAtDefault() );
const paletteIsBase = !! appliedPaletteId && ! paletteModified;
const paletteColors = store.currentPaletteColors();
return (
{ __( 'Pre-defined', 'everest-forms' ) }
{ __( 'Template', 'everest-forms' ) }
{ templateLabel }
{ templateModified && (
{ __( 'Modified', 'everest-forms' ) }
) }
{ templateIsBase && (
{ __( 'Base', 'everest-forms' ) }
) }
{ __( 'Colors', 'everest-forms' ) }
{ paletteLabel }
{ paletteModified && (
{ __( 'Modified', 'everest-forms' ) }
) }
{ paletteIsBase && (
{ __( 'Base', 'everest-forms' ) }
) }
{ Object.keys( store.paletteMap ).map( ( slot ) => (
) ) }
{ __( 'Apply Theme Style', 'everest-forms' ) }
store.setApplyThemeStyle( ! store.applyThemeStyle ) }
/>
{ __( 'Elements', 'everest-forms' ) }
{ __( 'Tip: pick an element to style it, or', 'everest-forms' ) }{ ' ' }
{ __( 'click it in the live preview.', 'everest-forms' ) }
{ sections.map( ( s ) => {
const locked = s.tier === 'pro' && ! store.proActive;
return (
onOpen( s.key ) }
>
{ s.title }
{ SECTION_SUBTITLES[ s.key ] || s.hint }
{ locked ? (
) : (
) }
);
} ) }
{ __( 'Advanced', 'everest-forms' ) }
{ __( 'Custom CSS', 'everest-forms' ) }
{ __( 'Add your own CSS styles', 'everest-forms' ) }
);
}
/* --------------------------------------------------------------------- *
* Element slate (drill-down body)
* --------------------------------------------------------------------- */
interface GroupedTokens {
heading: string;
tokens: Token[];
}
function groupTokens( tokens: Token[] ): GroupedTokens[] {
const out: GroupedTokens[] = [];
let current: string | null = null;
tokens.forEach( ( t ) => {
if ( t.group !== current ) {
current = t.group;
out.push( { heading: current, tokens: [] } );
}
out[ out.length - 1 ].tokens.push( t );
} );
return out;
}
export function ElementSlate( {
section,
activeState,
onChangeState,
pulse,
}: {
section: Section;
activeState: string | null;
onChangeState: ( id: string ) => void;
pulse: number;
} ) {
const store = useStore();
const bodyRef = React.useRef< HTMLDivElement >( null );
const tabs = section.states || section.variants || null;
const first = tabs ? tabs[ 0 ] : null;
const act = activeState || first;
// When opened via a preview click (pulse changes), scroll the panel up and briefly flash.
React.useEffect( () => {
const el = bodyRef.current;
if ( ! el || ! pulse ) {
return;
}
const scroller = el.closest( '.panel-scroll' );
if ( scroller ) {
scroller.scrollTop = 0;
}
el.classList.remove( 'flash' );
// Force reflow so the animation restarts on repeated selections.
void el.offsetWidth;
el.classList.add( 'flash' );
}, [ pulse ] );
// Background sub-options only matter once an image is set.
const bgImageSet = !! ( store.tokens[ 'wrap.bgImage' ] && store.tokens[ 'wrap.bgImage' ].desktop );
const visible = store.schema.filter(
( t ) => t.section === section.key && ! t.hidden && ( ! t.show_when_image || bgImageSet )
);
// On a state/variant tab, show only the controls that belong to that state; shared controls
// live on the first tab.
const enabledByState = tabs
? visible.filter( ( t ) => ( t.state ? t.state === act : act === first ) )
: visible;
// Dependency dimming: a border-style set to "none" disables its width/colour deps.
const dimmedByDep = new Set< string >();
const depHints: Record< string, string > = {};
visible.forEach( ( t ) => {
if ( t.deps && String( store.resolve( t.key ) ) === 'none' ) {
t.deps.forEach( ( k ) => dimmedByDep.add( k ) );
depHints[ t.key ] = __( 'Width & color have no effect while the border style is None.', 'everest-forms' );
}
} );
const locked = ( t: Token ) => t.tier === 'pro' && ! store.proActive;
const hasLocked = visible.some( locked );
// A whole Pro section on a free site: render a locked upgrade teaser instead of an empty slate.
const sectionLocked = section.tier === 'pro' && ! store.proActive;
const renderControl = ( t: Token ) => (
);
const renderGroups = ( tokens: Token[] ) =>
groupTokens( tokens ).map( ( g, i ) => (
{ g.heading &&
{ g.heading }
}
{ section.key === 'form' && g.heading === 'Background' && (
{ __( 'Background color comes from your color palette.', 'everest-forms' ) }
) }
{ g.tokens.map( renderControl ) }
) );
if ( sectionLocked ) {
return (
);
}
return (
{ hasLocked && (
) }
{ tabs && (
{
if ( e.key !== 'ArrowLeft' && e.key !== 'ArrowRight' ) {
return;
}
e.preventDefault();
const idx = tabs.indexOf( act as string );
const next = tabs[ ( idx + ( e.key === 'ArrowRight' ? 1 : tabs.length - 1 ) ) % tabs.length ];
onChangeState( next );
document.getElementById( `scv2-state-${ section.key }-${ next }` )?.focus();
} }
>
{ tabs.map( ( id ) => (
onChangeState( id ) }
>
{ STATE_LABELS[ id ] || id }
) ) }
) }
{ renderGroups( enabledByState ) }
);
}
/** Locked upgrade teaser — shown wherever a Pro feature is surfaced on a free site. */
export function ProTeaser( { title, text }: { title: string; text: string } ) {
return (
);
}
/** Section-level teaser (a whole Pro design section opened on free). */
function ProSectionTeaser( { section }: { section: Section } ) {
return (
);
}
/* --------------------------------------------------------------------- *
* Templates
* --------------------------------------------------------------------- */
/** The desktop value of a token bag, for hover-preview + thumbnails. */
function desktopOf( bag: DeviceBag | undefined ): any {
return bag && bag.desktop !== undefined ? bag.desktop : undefined;
}
/**
* Flatten a template's per-device token bags into a { key: desktopValue } map covering every
* schema key (not just the ones the template sets), mirroring what `store.applyTemplate()` does
* on click so hovering previews exactly what clicking would produce.
*/
function flattenForPreview( tokens: Record< string, DeviceBag >, schema: Token[] ): Record< string, any > {
const out: Record< string, any > = {};
schema.forEach( ( t ) => {
out[ t.key ] = tokens && tokens[ t.key ] !== undefined ? desktopOf( tokens[ t.key ] ) : t.default;
} );
return out;
}
function radiusOf( bag: DeviceBag | undefined, fallback: number ): number {
const v = desktopOf( bag ) as BoxValue | undefined;
return v && typeof v === 'object' ? Number( v.top ) || 0 : fallback;
}
/** Template thumbnail: real screenshot when it loads, else a live token-driven mini-form. */
function TemplateThumb( { tpl }: { tpl: Template } ) {
const [ imgOk, setImgOk ] = React.useState( !! tpl.image );
const th = templateThumb( tpl );
if ( tpl.image && imgOk ) {
return (
setImgOk( false ) } />
);
}
return (
);
}
/** Build a small live thumbnail from a template's migrated tokens — a mini "Name / Message /
* Submit" mock-up in the template's own colours, so a custom template reads as recognizably
* itself in the grid, the same way a built-in's screenshot does. */
function templateThumb( tpl: Template ) {
const t = tpl.tokens;
const bg = desktopOf( t[ 'wrap.bg' ] ) || '#ffffff';
const btnBg = desktopOf( t[ 'btn.bg' ] ) || '#3b82f6';
const btnText = desktopOf( t[ 'btn.color' ] ) || '#ffffff';
const inputBg = desktopOf( t[ 'input.bg' ] ) || '#ffffff';
const border = desktopOf( t[ 'input.borderC' ] ) || '#d8dae2';
const label = desktopOf( t[ 'label.color' ] ) || '#1f2433';
const fieldRadius = radiusOf( t[ 'input.radius' ], 6 );
const btnRadius = radiusOf( t[ 'btn.radius' ], 6 );
return {
thumb: { background: bg } as React.CSSProperties,
line: { background: label } as React.CSSProperties,
field: { background: inputBg, borderColor: border, borderRadius: fieldRadius } as React.CSSProperties,
button: { background: btnBg, color: btnText, borderRadius: btnRadius } as React.CSSProperties,
};
}
/** One template card — thumbnail, name, applied/locked/delete affordances. Used for both the
* "Your templates" and built-in grids so the two stay visually identical. */
function TemplateCard( {
tpl,
applied,
modified,
basedOn,
locked,
disabled,
onPreview,
onClearPreview,
onApply,
confirmingDelete,
onRequestDelete,
onConfirmDelete,
onCancelDelete,
}: {
tpl: Template;
applied: boolean;
/** The form was applied FROM this template but has since been edited — show an honest hint
* instead of a ✓ (its styles no longer exactly match this template). */
modified?: boolean;
/** Name of the built-in template this (custom) template exactly derives from, if any. */
basedOn?: string;
locked: boolean;
/** True while a DIFFERENT template is being edited — every action on this card is inert. */
disabled?: boolean;
onPreview: () => void;
onClearPreview: () => void;
onApply: () => void;
confirmingDelete?: boolean;
onRequestDelete?: () => void;
onConfirmDelete?: () => void;
onCancelDelete?: () => void;
} ) {
return (
{ locked && (
) }
{ locked && (
) }
{ tpl.name }
{ tpl.custom && { __( 'Custom', 'everest-forms' ) } }
{ ( applied || modified ) && { __( 'Base', 'everest-forms' ) } }
{ basedOn && (
{ /* translators: %s: the built-in template this custom one derives from. */ }
{ ( __( 'Based on %s', 'everest-forms' ) as string ).replace( '%s', basedOn ) }
) }
{ onRequestDelete && (
{
e.stopPropagation();
onRequestDelete();
} }
>
) }
{ confirmingDelete && (
{ __( 'Delete?', 'everest-forms' ) }
{ __( 'Delete', 'everest-forms' ) }
{ __( 'Cancel', 'everest-forms' ) }
) }
);
}
export function TemplatesPane( {
onPreview,
onClearPreview,
onApplied,
}: {
onPreview: ( overrides: Record< string, any > ) => void;
onClearPreview: () => void;
onApplied: ( name: string ) => void;
} ) {
const store = useStore();
const [ confirmDeleteId, setConfirmDeleteId ] = React.useState< string | null >( null );
const templates = store.allTemplates();
// Memoize the flattened override maps so hover doesn't re-flatten on every mouse event.
const flat = React.useMemo(
() => Object.fromEntries( templates.map( ( tpl ) => [ tpl.id, flattenForPreview( tpl.tokens, store.schema ) ] ) ),
// eslint-disable-next-line react-hooks/exhaustive-deps
[ templates, store ]
);
// Value-driven template state, recomputed whenever the store version bumps.
const ver = store.getVersion();
const appliedId = React.useMemo( () => store.appliedTemplateId(), [ store, ver ] );
const originId = React.useMemo( () => store.originTemplateId(), [ store, ver ] );
const parentNames = React.useMemo( () => {
const map: Record< string, string > = {};
templates.forEach( ( tpl ) => {
const pid = store.templateParentId( tpl );
if ( pid ) {
const parent = store.templates.find( ( b ) => b.id === pid );
if ( parent ) {
map[ tpl.id ] = parent.name;
}
}
} );
return map;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ templates, store, ver ] );
// "Your Template" card — same value-driven match as the ✓/"Modified" badges above, fed into
// the existing TemplateThumb live-mockup renderer via a synthetic, always-current object.
const matchedTplId = appliedId || originId;
const matchedTpl = matchedTplId ? templates.find( ( t ) => t.id === matchedTplId ) : null;
const yourTemplateName = matchedTpl ? matchedTpl.name : __( 'Default', 'everest-forms' );
const yourTemplateModified = ! appliedId && ( !! originId || ! store.isAtSchemaDefault() );
const yourTemplateIsBase = !! appliedId && ! yourTemplateModified;
const templatesBase = store.settings.restBase.replace( /\/styles$/, '/style-templates' );
const deleteTemplate = async ( id: string ) => {
setConfirmDeleteId( null );
if ( ! apiFetch ) {
return;
}
store.removeUserTemplate( id ); // optimistic
try {
await apiFetch( { path: `${ templatesBase }/${ id }`, method: 'DELETE' } );
} catch ( e ) {
// The list already reflects the removal; a reload restores it if the server rejected.
}
};
const applyTpl = ( tpl: Template, locked: boolean ) => {
if ( locked ) {
window.open( UPGRADE_URL, '_blank' );
return;
}
store.applyTemplate( tpl.id, tpl.tokens, tpl.palette );
onApplied( tpl.name );
};
const renderGrid = ( list: Template[] ) => (
{ list.map( ( tpl ) => {
const locked = !! tpl.is_pro && ! store.proActive;
const withDelete = !! tpl.custom;
return (
onPreview( flat[ tpl.id ] ) }
onClearPreview={ onClearPreview }
onApply={ () => applyTpl( tpl, locked ) }
confirmingDelete={ confirmDeleteId === tpl.id }
onRequestDelete={ withDelete ? () => setConfirmDeleteId( tpl.id ) : undefined }
onConfirmDelete={ withDelete ? () => deleteTemplate( tpl.id ) : undefined }
onCancelDelete={ withDelete ? () => setConfirmDeleteId( null ) : undefined }
/>
);
} ) }
);
return (
{ __( 'Your Template', 'everest-forms' ) }
{ yourTemplateName }
{ yourTemplateModified && { __( 'Modified', 'everest-forms' ) } }
{ yourTemplateIsBase && { __( 'Base', 'everest-forms' ) } }
{ __( 'Presets', 'everest-forms' ) }
{ __( 'Hover a card to preview it live', 'everest-forms' ) } { ' ' }
— { __( 'click to apply (you can always undo).', 'everest-forms' ) }
{ /* Legacy set stays in store.templates (see Templates::load_legacy()) for badge matching above; just not offered here. */ }
{ renderGrid(
[ ...store.userTemplates, ...store.templates.filter( ( tpl ) => ! tpl.legacy ) ].sort( ( a, b ) => {
if ( !! a.custom !== !! b.custom ) {
return a.custom ? -1 : 1;
}
return ( a.is_pro ? 1 : 0 ) - ( b.is_pro ? 1 : 0 );
} )
) }
);
}
/* --------------------------------------------------------------------- *
* Custom CSS
* --------------------------------------------------------------------- */
const CSS_CHIPS = [
'.evf-container',
'.evf-submit-container button',
'input, textarea, select',
'label.evf-field-label',
'.everest-forms-notice--success',
];
const CSS_EXAMPLE = `.evf-submit-container button {
letter-spacing: .04em;
text-transform: uppercase;
}`;
export function CustomCssPane() {
const store = useStore();
const ref = React.useRef< HTMLTextAreaElement >( null );
const insert = ( snippet: string ) => {
const el = ref.current;
if ( ! el ) {
return;
}
const start = el.selectionStart;
const value = el.value.slice( 0, start ) + snippet + el.value.slice( el.selectionEnd );
store.setCustomCss( value );
el.focus();
};
const len = ( store.customCss || '' ).length;
return (
{ __( 'Custom CSS', 'everest-forms' ) }
{ __( 'Applied live as you type —', 'everest-forms' ) } { __( 'applies to every form on this site', 'everest-forms' ) } { ' ' }
{ __( 'on save (same as the Customizer’s Additional CSS). Click a selector to insert it:', 'everest-forms' ) }
{ CSS_CHIPS.map( ( c ) => (
insert( c + ' {\n\t\n}\n' ) }>
{ c }
) ) }
);
}