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 / addons / StyleCustomizer / V2 / Schema.php
everest-forms / addons / StyleCustomizer / V2 Last commit date
assets 1 week ago BuilderPanel.php 1 week ago Compiler.php 1 week ago Engine.php 1 week ago FrontendEnqueue.php 1 week ago Migrator.php 1 week ago Palettes.php 1 week ago PreviewDraft.php 1 week ago RestController.php 1 week ago Sanitizer.php 1 week ago Schema.php 1 week ago Templates.php 1 week ago
Schema.php
853 lines
1 <?php
2 /**
3 * Style Customizer v2 — token Schema (single source of truth for tokens, controls, CSS vars,
4 * the sanitizer, and legacy migration). Strict subset of the bundled v1 customizer: 0 net-new
5 * settings; responsive Desktop/Tablet/Mobile applies to margin/padding only.
6 *
7 * @package EverestForms\Addons\StyleCustomizer\V2
8 * @since x.x.x
9 */
10
11 namespace EverestForms\Addons\StyleCustomizer\V2;
12
13 defined( 'ABSPATH' ) || exit;
14
15 /**
16 * Style schema registry.
17 */
18 final class Schema {
19
20 /**
21 * Schema version. Persisted on each saved record as `schema_version`; the presence of
22 * that key is what flips a form onto the v2 engine (no separate option/meta).
23 */
24 const VERSION = 1;
25
26 /**
27 * Memoized, normalized + filtered token list.
28 *
29 * @var array|null
30 */
31 protected static $tokens = null;
32
33 /**
34 * Memoized key => token index.
35 *
36 * @var array|null
37 */
38 protected static $index = null;
39
40 /**
41 * Current schema version.
42 *
43 * @return int
44 */
45 public static function version() {
46 return self::VERSION;
47 }
48
49 /**
50 * Reset the memoized cache (tests / after a runtime filter change).
51 */
52 public static function flush() {
53 self::$tokens = null;
54 self::$index = null;
55 }
56
57 /* --------------------------------------------------------------------- *
58 * Public accessors
59 * --------------------------------------------------------------------- */
60
61 /**
62 * Section metadata — drives the drill-down list order, titles and variant/state tabs.
63 *
64 * @return array
65 */
66 public static function sections() {
67 $sections = array(
68 'form' => array(
69 'title' => __( 'Form', 'everest-forms' ),
70 'hint' => __( 'The canvas everything sits on — plus the form-wide font.', 'everest-forms' ),
71 ),
72 'text' => array(
73 'title' => __( 'Text', 'everest-forms' ),
74 'hint' => __( 'One typography system, four roles.', 'everest-forms' ),
75 'variants' => array( 'label', 'sublabel', 'desc', 'title' ),
76 ),
77 'fields' => array(
78 'title' => __( 'Inputs', 'everest-forms' ),
79 'hint' => __( 'Text boxes, dropdowns, textareas, date pickers — and the upload area.', 'everest-forms' ),
80 'states' => array( 'normal', 'focus' ),
81 ),
82 'choice' => array(
83 'title' => __( 'Choices', 'everest-forms' ),
84 'hint' => __( 'One style set for every choice field: radios, checkboxes, Yes/No, image choices, ratings, Likert rows, and payment pickers.', 'everest-forms' ),
85 ),
86 'button' => array(
87 'title' => __( 'Button', 'everest-forms' ),
88 'hint' => __( 'Styles Submit — Multi-Part and file-upload buttons follow automatically.', 'everest-forms' ),
89 'states' => array( 'normal', 'hover' ),
90 ),
91 'messages' => array(
92 'title' => __( 'Messages', 'everest-forms' ),
93 'hint' => __( 'The success, error and validation messages shown to visitors.', 'everest-forms' ),
94 'states' => array( 'success', 'error', 'validation' ),
95 ),
96 );
97
98 // Product policy: every design section is Pro; only the palette picker + Custom CSS are free.
99 foreach ( $sections as &$section ) {
100 $section['tier'] = 'pro';
101 }
102 unset( $section );
103
104 /**
105 * Filter the style-customizer section metadata (order, titles, variant/state tabs, tier).
106 * Pro/add-ons can add sections or adjust an existing section's tier here.
107 *
108 * @param array $sections Section definitions keyed by section id.
109 */
110 return apply_filters( 'evf_style_sections', $sections );
111 }
112
113 /**
114 * The full, normalized, filterable token list. Free/Pro tiers and any add-on sections are
115 * layered in via the `evf_style_schema` filter.
116 *
117 * @return array List of token definition arrays.
118 */
119 public static function tokens() {
120 if ( null === self::$tokens ) {
121 /**
122 * Filter the v2 style token registry.
123 *
124 * @param array $tokens Raw token definitions.
125 * @param int $version Schema version.
126 */
127 $tokens = apply_filters( 'evf_style_schema', self::raw_tokens(), self::VERSION );
128 $tokens = array_map( array( __CLASS__, 'normalize' ), $tokens );
129 self::$tokens = self::apply_tier_policy( $tokens );
130 }
131 return self::$tokens;
132 }
133
134 /**
135 * Stamp each token's tier: only the palette-driven tokens ({@see free_token_keys()}) are
136 * tier=free, everything else (including Pro/add-on contributions) is tier=pro. The sanitizer
137 * and compiler refuse to persist/emit a pro token unless {@see Engine::pro_active()}.
138 *
139 * @param array $tokens Normalized tokens.
140 * @return array
141 */
142 protected static function apply_tier_policy( $tokens ) {
143 $free = self::free_token_keys();
144 foreach ( $tokens as &$token ) {
145 $token['tier'] = isset( $free[ $token['key'] ] ) ? 'free' : 'pro';
146 }
147 unset( $token );
148 return $tokens;
149 }
150
151 /**
152 * Token keys that stay FREE — the palette-driven colours plus the derived button-hover shade.
153 *
154 * @return array Map of free token key => true.
155 */
156 protected static function free_token_keys() {
157 $keys = array();
158 foreach ( self::palette_map() as $token_keys ) {
159 foreach ( (array) $token_keys as $key ) {
160 $keys[ $key ] = true;
161 }
162 }
163 $keys['btn.bgHover'] = true;
164
165 /**
166 * Filter the token keys that stay in the FREE tier (all others are Pro).
167 *
168 * @param array $keys Map of free token key => true.
169 */
170 return apply_filters( 'evf_style_free_token_keys', $keys );
171 }
172
173 /**
174 * Look up a single token by key.
175 *
176 * @param string $key Token key (e.g. `wrap.pad`).
177 * @return array|null
178 */
179 public static function get( $key ) {
180 if ( null === self::$index ) {
181 self::$index = array();
182 foreach ( self::tokens() as $token ) {
183 self::$index[ $token['key'] ] = $token;
184 }
185 }
186 return isset( self::$index[ $key ] ) ? self::$index[ $key ] : null;
187 }
188
189 /**
190 * key => default value, for building an unstyled (default) record and for
191 * `array_replace_recursive`-style merges in the compiler.
192 *
193 * @return array
194 */
195 public static function defaults() {
196 $out = array();
197 foreach ( self::tokens() as $token ) {
198 $out[ $token['key'] ] = $token['default'];
199 }
200 return $out;
201 }
202
203 /**
204 * key => CSS custom property, for the compiler (skips meta-only tokens with no var).
205 *
206 * @return array
207 */
208 public static function css_var_map() {
209 $out = array();
210 foreach ( self::tokens() as $token ) {
211 if ( ! empty( $token['var'] ) ) {
212 $out[ $token['key'] ] = $token['var'];
213 }
214 }
215 return $out;
216 }
217
218 /**
219 * All 11 named colour palettes (2 free + 9 Pro), each with the old customizer's 6 colours.
220 * The 9 Pro palettes' preview metadata ships here too (unlike other pro tokens) so the panel
221 * can render a real swatch + name for all 11 and lock the Pro ones instead of hiding them;
222 * applying one is still gated by {@see Sanitizer::sanitize_palette_id()}.
223 *
224 * @return array
225 */
226 public static function palettes() {
227 $palettes = array(
228 array( 'id' => 'classic', 'name' => __( 'Classic', 'everest-forms' ), 'is_pro' => false, 'colors' => array( 'form_background' => '#ffffff', 'field_background' => '#f6f6f6', 'field_label' => '#081d2b', 'field_sublabel' => '#0f3a57', 'button_text' => '#ffffff', 'button_background' => '#3951a5' ) ),
229 array( 'id' => 'mono', 'name' => __( 'Monochrome', 'everest-forms' ), 'is_pro' => false, 'colors' => array( 'form_background' => '#ffffff', 'field_background' => '#f7f7f7', 'field_label' => '#262626', 'field_sublabel' => '#666666', 'button_text' => '#ffffff', 'button_background' => '#1a1a1a' ) ),
230 array( 'id' => 'autumn', 'name' => __( 'Autumn Blaze', 'everest-forms' ), 'is_pro' => true, 'colors' => array( 'form_background' => '#fffafa', 'field_background' => '#fff5f5', 'field_label' => '#330300', 'field_sublabel' => '#4d0500', 'button_text' => '#ffffff', 'button_background' => '#ff5d52' ) ),
231 array( 'id' => 'sunset', 'name' => __( 'Sunset Glow', 'everest-forms' ), 'is_pro' => true, 'colors' => array( 'form_background' => '#fffdfa', 'field_background' => '#fff9f0', 'field_label' => '#664000', 'field_sublabel' => '#805100', 'button_text' => '#ffffff', 'button_background' => '#ffa305' ) ),
232 array( 'id' => 'majestic', 'name' => __( 'Majestic', 'everest-forms' ), 'is_pro' => true, 'colors' => array( 'form_background' => '#fcfbfe', 'field_background' => '#f7f4fb', 'field_label' => '#3a225d', 'field_sublabel' => '#5d3795', 'button_text' => '#ffffff', 'button_background' => '#7545bb' ) ),
233 array( 'id' => 'greenery', 'name' => __( 'Fresh Greenery', 'everest-forms' ), 'is_pro' => true, 'colors' => array( 'form_background' => '#f9fdf6', 'field_background' => '#e9f6ea', 'field_label' => '#334745', 'field_sublabel' => '#557773', 'button_text' => '#ffffff', 'button_background' => '#405956' ) ),
234 array( 'id' => 'cloudy', 'name' => __( 'Cloudy Sky', 'everest-forms' ), 'is_pro' => true, 'colors' => array( 'form_background' => '#f2f3f8', 'field_background' => '#e4e7f1', 'field_label' => '#252b41', 'field_sublabel' => '#2e3651', 'button_text' => '#ffffff', 'button_background' => '#445079' ) ),
235 array( 'id' => 'earthy', 'name' => __( 'Earthy Warm', 'everest-forms' ), 'is_pro' => true, 'colors' => array( 'form_background' => '#f7f6f0', 'field_background' => '#f1efe4', 'field_label' => '#474648', 'field_sublabel' => '#616062', 'button_text' => '#ffffff', 'button_background' => '#463700' ) ),
236 array( 'id' => 'blossom', 'name' => __( 'Blushing Blossom', 'everest-forms' ), 'is_pro' => true, 'colors' => array( 'form_background' => '#fdf7fa', 'field_background' => '#fbeff5', 'field_label' => '#532f42', 'field_sublabel' => '#824a68', 'button_text' => '#ffffff', 'button_background' => '#46102c' ) ),
237 array( 'id' => 'thunder', 'name' => __( 'Thunder', 'everest-forms' ), 'is_pro' => true, 'colors' => array( 'form_background' => '#ededed', 'field_background' => '#f7f7f7', 'field_label' => '#333333', 'field_sublabel' => '#595959', 'button_text' => '#ffffff', 'button_background' => '#1a1a1a' ) ),
238 array( 'id' => 'midnight', 'name' => __( 'Midnight Charm', 'everest-forms' ), 'is_pro' => true, 'colors' => array( 'form_background' => '#363636', 'field_background' => '#3d3d3d', 'field_label' => '#ffffff', 'field_sublabel' => '#f2f2f2', 'button_text' => '#1a1a1a', 'button_background' => '#ffffff' ) ),
239 );
240
241 /**
242 * Filter the available colour palettes.
243 *
244 * @param array $palettes Palette definitions.
245 */
246 return apply_filters( 'evf_style_palettes', $palettes );
247 }
248
249 /**
250 * Palette slot => token key(s) it drives. `button_background` also derives `btn.bgHover`.
251 *
252 * @return array
253 */
254 public static function palette_map() {
255 return array(
256 'form_background' => array( 'wrap.bg' ),
257 'field_background' => array( 'input.bg' ),
258 'field_label' => array( 'label.color', 'title.color' ),
259 'field_sublabel' => array( 'sub.color', 'desc.color' ),
260 'button_text' => array( 'btn.color' ),
261 'button_background' => array( 'btn.bg', 'input.focusBorder', 'choice.checked', 'file.icon' ),
262 );
263 }
264
265 /**
266 * The free-tier token VALUES a registered palette id implies (the six palette-driven slots,
267 * plus the derived hover shade) — the single source of truth {@see Sanitizer::sanitize_record()}
268 * uses to DERIVE (never trust raw from the client) a non-Pro site's palette-driven tokens.
269 *
270 * Without this, a non-Pro site's "free" token keys only exist so the 2-swatch palette picker
271 * has something to write to — the sanitizer only ever validated their VALUE TYPE (a well-formed
272 * colour), not that the colour actually came from picking one of the 2 free palettes. A crafted
273 * request (an AI response, a hand-built REST call) could set any hex it liked on these exact
274 * keys and reconstruct a full Pro-palette look with no Pro required (see EVF-2708).
275 *
276 * @param string $palette_id Palette id (see {@see self::palettes()}).
277 * @return array Token key => hex value (bare, not a device bag); empty if the id is unknown.
278 */
279 public static function palette_token_values( $palette_id ) {
280 foreach ( self::palettes() as $palette ) {
281 if ( $palette['id'] !== $palette_id ) {
282 continue;
283 }
284 $colors = isset( $palette['colors'] ) && is_array( $palette['colors'] ) ? $palette['colors'] : array();
285 $out = array();
286 foreach ( self::palette_map() as $slot => $keys ) {
287 if ( ! isset( $colors[ $slot ] ) ) {
288 continue;
289 }
290 foreach ( (array) $keys as $key ) {
291 $out[ $key ] = $colors[ $slot ];
292 }
293 }
294 if ( ! empty( $colors['button_background'] ) ) {
295 $out['btn.bgHover'] = self::mix_hex( $colors['button_background'], '#000000', 0.14 );
296 }
297 return $out;
298 }
299 return array();
300 }
301
302 /**
303 * Linear-interpolate two hex colours — PHP port of `mixHex()` in constants.ts (kept local
304 * rather than reusing {@see Migrator}'s copy, so this class has no cross-class coupling).
305 *
306 * @param string $a First hex colour (`#rgb` or `#rrggbb`).
307 * @param string $b Second hex colour.
308 * @param float $t Mix factor, 0 = $a, 1 = $b.
309 * @return string `#rrggbb`.
310 */
311 protected static function mix_hex( $a, $b, $t ) {
312 $parse = static function ( $hex ) {
313 $s = ltrim( (string) $hex, '#' );
314 if ( 3 === strlen( $s ) ) {
315 $s = $s[0] . $s[0] . $s[1] . $s[1] . $s[2] . $s[2];
316 }
317 return array( hexdec( substr( $s, 0, 2 ) ), hexdec( substr( $s, 2, 2 ) ), hexdec( substr( $s, 4, 2 ) ) );
318 };
319 $from = $parse( $a );
320 $to = $parse( $b );
321 $out = '#';
322 foreach ( array( 0, 1, 2 ) as $i ) {
323 $out .= str_pad( dechex( (int) round( $from[ $i ] + ( $to[ $i ] - $from[ $i ] ) * $t ) ), 2, '0', STR_PAD_LEFT );
324 }
325 return $out;
326 }
327
328 /* --------------------------------------------------------------------- *
329 * Token assembly
330 * --------------------------------------------------------------------- */
331
332 /**
333 * Raw (un-normalized) token list, in panel display order.
334 *
335 * @return array
336 */
337 protected static function raw_tokens() {
338 $border = self::border_options();
339
340 $form = array(
341 array( 'key' => 'fonts.theme', 'section' => 'form', 'group' => 'Typography', 'label' => __( 'Use theme fonts', 'everest-forms' ), 'type' => 'toggle', 'default' => false, 'hidden' => true, 'keywords' => array( 'inherit', 'typeface', 'theme font' ) ),
342 array( 'key' => 'fonts.family', 'section' => 'form', 'group' => 'Typography', 'label' => __( 'Font family', 'everest-forms' ), 'type' => 'select', 'var' => '--evf-font', 'default' => '', 'source' => 'google_fonts', 'options' => self::font_options(), 'keywords' => array( 'typeface', 'typography', 'google font' ) ),
343 // Palette-driven by default (form_background slot); editing it directly here unlinks
344 // this form from the palette for just this token — see store.ts's paletteMap unlink.
345 array( 'key' => 'wrap.bg', 'section' => 'form', 'group' => 'Background', 'label' => __( 'Background color', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-wrap-bg', 'default' => '#ffffff', 'gradientable' => true, 'keywords' => array( 'fill', 'color', 'gradient' ) ),
346 array( 'key' => 'wrap.bgImage', 'section' => 'form', 'group' => 'Background', 'label' => __( 'Background image', 'everest-forms' ), 'type' => 'media', 'var' => '--evf-wrap-image', 'default' => '', 'advanced' => true, 'keywords' => array( 'image', 'photo', 'picture' ) ),
347 array( 'key' => 'wrap.bgPreset', 'section' => 'form', 'group' => 'Background', 'label' => __( 'Background preset', 'everest-forms' ), 'type' => 'select', 'default' => 'custom', 'options' => self::opts( array( 'custom' => __( 'Custom', 'everest-forms' ), 'fill' => __( 'Fill', 'everest-forms' ), 'fit' => __( 'Fit', 'everest-forms' ), 'repeat' => __( 'Repeat', 'everest-forms' ), 'center' => __( 'Center', 'everest-forms' ) ) ), 'advanced' => true, 'show_when_image' => true, 'keywords' => array( 'image', 'background' ) ),
348 array( 'key' => 'wrap.bgSize', 'section' => 'form', 'group' => 'Background', 'label' => __( 'Image size', 'everest-forms' ), 'type' => 'select', 'var' => '--evf-wrap-bg-size', 'default' => 'cover', 'options' => self::opts( array( 'cover' => __( 'Cover', 'everest-forms' ), 'contain' => __( 'Contain', 'everest-forms' ), 'auto' => __( 'Auto', 'everest-forms' ) ) ), 'advanced' => true, 'show_when_image' => true, 'keywords' => array( 'image', 'background' ) ),
349 array( 'key' => 'wrap.bgPosition', 'section' => 'form', 'group' => 'Background', 'label' => __( 'Image position', 'everest-forms' ), 'type' => 'select', 'var' => '--evf-wrap-bg-position', 'default' => 'center', 'options' => self::opts( array( 'center' => __( 'Center', 'everest-forms' ), 'top' => __( 'Top', 'everest-forms' ), 'bottom' => __( 'Bottom', 'everest-forms' ), 'left' => __( 'Left', 'everest-forms' ), 'right' => __( 'Right', 'everest-forms' ) ) ), 'advanced' => true, 'show_when_image' => true, 'keywords' => array( 'image', 'background' ) ),
350 array( 'key' => 'wrap.bgRepeat', 'section' => 'form', 'group' => 'Background', 'label' => __( 'Repeat background image', 'everest-forms' ), 'type' => 'select', 'var' => '--evf-wrap-bg-repeat', 'default' => 'no-repeat', 'options' => self::opts( array( 'no-repeat' => __( 'No repeat', 'everest-forms' ), 'repeat' => __( 'Repeat', 'everest-forms' ), 'repeat-x' => __( 'Repeat X', 'everest-forms' ), 'repeat-y' => __( 'Repeat Y', 'everest-forms' ) ) ), 'advanced' => true, 'show_when_image' => true, 'keywords' => array( 'image', 'tile' ) ),
351 array( 'key' => 'wrap.bgAttachment', 'section' => 'form', 'group' => 'Background', 'label' => __( 'Scroll with page', 'everest-forms' ), 'type' => 'select', 'var' => '--evf-wrap-bg-attachment', 'default' => 'scroll', 'options' => self::opts( array( 'scroll' => __( 'Scroll', 'everest-forms' ), 'fixed' => __( 'Fixed', 'everest-forms' ) ) ), 'advanced' => true, 'show_when_image' => true, 'keywords' => array( 'parallax', 'image' ) ),
352 array( 'key' => 'wrap.bgOpacity', 'section' => 'form', 'group' => 'Background', 'label' => __( 'Image opacity', 'everest-forms' ), 'type' => 'slider', 'var' => '--evf-wrap-bg-opacity', 'default' => 1, 'min' => 0, 'max' => 1, 'step' => 0.1, 'unit' => '', 'advanced' => true, 'show_when_image' => true, 'keywords' => array( 'image', 'transparency' ) ),
353 array( 'key' => 'wrap.width', 'section' => 'form', 'group' => 'Layout', 'label' => __( 'Width', 'everest-forms' ), 'type' => 'slider', 'var' => '--evf-wrap-width', 'default' => 100, 'min' => 40, 'max' => 100, 'step' => 1, 'unit' => '%', 'advanced' => true, 'keywords' => array( 'size', 'narrow' ) ),
354 self::box( 'wrap.margin', __( 'Form margin', 'everest-forms' ), '--evf-wrap-margin', array( 0, 0, 0, 0 ), 'form', null, -100, 100 ),
355 self::box( 'wrap.pad', __( 'Form padding', 'everest-forms' ), '--evf-wrap-pad', array( 32, 32, 32, 32 ), 'form', null, 0, 100 ),
356 array( 'key' => 'wrap.borderStyle', 'section' => 'form', 'group' => 'Border', 'label' => __( 'Border type', 'everest-forms' ), 'type' => 'select', 'var' => '--evf-wrap-border-style', 'default' => 'solid', 'options' => $border, 'deps' => array( 'wrap.bw', 'wrap.borderC' ), 'advanced' => true, 'keywords' => array( 'outline' ) ),
357 self::bwidth( 'wrap.bw', '--evf-wrap-bw', 1, 'form', null, 'Border', true ),
358 array( 'key' => 'wrap.borderC', 'section' => 'form', 'group' => 'Border', 'label' => __( 'Border color', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-wrap-border-c', 'default' => '#ececf1', 'advanced' => true ),
359 self::radius( 'wrap.radius', '--evf-wrap-radius', 14, 'form', null, 'Border', false ),
360 );
361
362 $fields = array(
363 // Palette-driven by default (field_background slot); editing it directly here unlinks
364 // this form from the palette for just this token — see store.ts's paletteMap unlink.
365 array( 'key' => 'input.bg', 'section' => 'fields', 'group' => 'Surface', 'label' => __( 'Background', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-input-bg', 'default' => '#ffffff', 'gradientable' => true, 'keywords' => array( 'fill', 'color', 'gradient' ) ),
366 array( 'key' => 'input.borderC', 'section' => 'fields', 'group' => 'Surface', 'label' => __( 'Border color', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-input-border-c', 'default' => '#969696' ),
367 self::radius( 'input.radius', '--evf-input-radius', 3, 'fields', null, 'Surface', false ),
368 array( 'key' => 'input.size', 'section' => 'fields', 'group' => 'Typography', 'label' => __( 'Font size', 'everest-forms' ), 'type' => 'slider', 'var' => '--evf-input-size', 'default' => 14, 'min' => 11, 'max' => 22, 'step' => 1, 'unit' => 'px', 'advanced' => true, 'keywords' => array( 'text' ) ),
369 // Not gradientable: this rule's element already spends its one `background` on
370 // input.bg — background-clip:text for the text colour would collide with it.
371 array( 'key' => 'input.color', 'section' => 'fields', 'group' => 'Typography', 'label' => __( 'Text color', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-input-color', 'default' => '#575757', 'advanced' => true ),
372 array( 'key' => 'input.ph', 'section' => 'fields', 'group' => 'Typography', 'label' => __( 'Placeholder color', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-input-ph', 'default' => '#c6ccd7', 'advanced' => true, 'keywords' => array( 'hint' ) ),
373 self::fstyle( 'input.fstyle', 'input', 'fields', null, '400' ),
374 self::letter_spacing( 'input.ls', '--evf-input-ls', 'fields', null ),
375 self::talign( 'input.align', '--evf-input-align', 'fields', null ),
376 array( 'key' => 'input.borderStyle', 'section' => 'fields', 'group' => 'Border', 'label' => __( 'Border type', 'everest-forms' ), 'type' => 'select', 'var' => '--evf-input-border-style', 'default' => 'solid', 'options' => $border, 'deps' => array( 'input.bw', 'input.borderC' ), 'advanced' => true ),
377 self::bwidth( 'input.bw', '--evf-input-bw', 1, 'fields', null, 'Border', true ),
378 array( 'key' => 'input.pad', 'section' => 'fields', 'group' => 'Spacing', 'label' => __( 'Field padding', 'everest-forms' ), 'type' => 'box4', 'var' => '--evf-input-pad', 'default' => array( 6, 12, 6, 12 ), 'responsive' => true, 'advanced' => true, 'keywords' => array( 'inner' ) ),
379 array( 'key' => 'field.margin', 'section' => 'fields', 'group' => 'Spacing', 'label' => __( 'Field margin', 'everest-forms' ), 'type' => 'box4', 'var' => '--evf-field-margin', 'default' => array( 0, 0, 10, 0 ), 'responsive' => true, 'advanced' => true, 'keywords' => array( 'gap', 'between' ) ),
380 array( 'key' => 'file.size', 'section' => 'fields', 'group' => 'Upload area', 'label' => __( 'Font size', 'everest-forms' ), 'type' => 'slider', 'var' => '--evf-file-size', 'default' => 14, 'min' => 10, 'max' => 22, 'step' => 1, 'unit' => 'px', 'advanced' => true, 'keywords' => array( 'upload', 'file' ) ),
381 array( 'key' => 'file.color', 'section' => 'fields', 'group' => 'Upload area', 'label' => __( 'Font color', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-file-color', 'default' => '#494d50', 'advanced' => true, 'gradientable' => true, 'keywords' => array( 'upload', 'file', 'text', 'gradient' ) ),
382 array( 'key' => 'file.bg', 'section' => 'fields', 'group' => 'Upload area', 'label' => __( 'Background', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-file-bg', 'default' => 'rgba(255,255,255,0.99)', 'advanced' => true, 'gradientable' => true, 'keywords' => array( 'upload', 'dropzone', 'file', 'gradient' ) ),
383 array( 'key' => 'file.iconBg', 'section' => 'fields', 'group' => 'Upload area', 'label' => __( 'Icon background', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-file-iconbg', 'default' => 'rgba(255,255,255,0.99)', 'advanced' => true, 'gradientable' => true, 'keywords' => array( 'upload', 'file', 'gradient' ) ),
384 array( 'key' => 'file.icon', 'section' => 'fields', 'group' => 'Upload area', 'label' => __( 'Icon color', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-file-icon', 'default' => '#494d50', 'advanced' => true, 'keywords' => array( 'upload', 'file' ) ),
385 array( 'key' => 'file.borderStyle', 'section' => 'fields', 'group' => 'Upload area', 'label' => __( 'Border type', 'everest-forms' ), 'type' => 'select', 'var' => '--evf-file-border-style', 'default' => 'dashed', 'options' => $border, 'advanced' => true, 'keywords' => array( 'upload', 'file' ) ),
386 self::bwidth( 'file.bw', '--evf-file-bw', 1, 'fields', null, 'Upload area', true ),
387 array( 'key' => 'file.border', 'section' => 'fields', 'group' => 'Upload area', 'label' => __( 'Border color', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-file-border', 'default' => '#8e98a2', 'advanced' => true, 'keywords' => array( 'upload', 'file' ) ),
388 self::radius( 'file.radius', '--evf-file-radius', 0, 'fields', null, 'Upload area', true ),
389 array( 'key' => 'file.margin', 'section' => 'fields', 'group' => 'Upload area', 'label' => __( 'Margin', 'everest-forms' ), 'type' => 'box4', 'var' => '--evf-file-margin', 'default' => array( 0, 0, 10, 0 ), 'responsive' => true, 'advanced' => true, 'keywords' => array( 'upload', 'file', 'spacing' ) ),
390 array( 'key' => 'file.pad', 'section' => 'fields', 'group' => 'Upload area', 'label' => __( 'Padding', 'everest-forms' ), 'type' => 'box4', 'var' => '--evf-file-pad', 'default' => array( 6, 12, 6, 12 ), 'responsive' => true, 'advanced' => true, 'keywords' => array( 'upload', 'file', 'spacing', 'inner' ) ),
391 // Applies only when no palette is set — a palette still derives this per palette_map().
392 array( 'key' => 'input.focusBorder', 'section' => 'fields', 'group' => '', 'label' => __( 'Focus border color', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-input-focus-border', 'default' => '#7ca8eb', 'state' => 'focus', 'keywords' => array( 'outline', 'ring', 'active' ) ),
393 );
394
395 $choice = array(
396 array( 'key' => 'choice.checked', 'section' => 'choice', 'group' => '', 'label' => __( 'Selected color', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-choice-checked', 'default' => '#575757', 'keywords' => array( 'checked', 'radio', 'checkbox', 'rating', 'scale', 'likert', 'payment', 'yes no', 'image choice', 'star' ) ),
397 array( 'key' => 'choice.size', 'section' => 'choice', 'group' => '', 'label' => __( 'Mark size', 'everest-forms' ), 'type' => 'slider', 'var' => '--evf-choice-size', 'default' => 16, 'min' => 12, 'max' => 28, 'step' => 1, 'unit' => 'px', 'keywords' => array( 'radio', 'checkbox' ) ),
398 array( 'key' => 'choice.border', 'section' => 'choice', 'group' => '', 'label' => __( 'Unselected border', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-choice-border', 'default' => '#d8dae2', 'keywords' => array( 'radio', 'checkbox', 'unchecked' ) ),
399 array( 'key' => 'choice.variation', 'section' => 'choice', 'group' => '', 'label' => __( 'Style variation', 'everest-forms' ), 'type' => 'select', 'default' => 'default', 'options' => self::opts( array( 'default' => __( 'Default', 'everest-forms' ), 'filled' => __( 'Filled', 'everest-forms' ), 'outline' => __( 'Outline', 'everest-forms' ) ) ), 'advanced' => true, 'special' => 'cvar', 'keywords' => array( 'modern', 'inline' ) ),
400 array( 'key' => 'choice.fsize', 'section' => 'choice', 'group' => '', 'label' => __( 'Option font size', 'everest-forms' ), 'type' => 'slider', 'var' => '--evf-choice-fsize', 'default' => 14, 'min' => 11, 'max' => 22, 'step' => 1, 'unit' => 'px', 'advanced' => true, 'keywords' => array( 'text' ) ),
401 array( 'key' => 'choice.color', 'section' => 'choice', 'group' => '', 'label' => __( 'Option text color', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-choice-color', 'default' => '#575757', 'advanced' => true, 'gradientable' => true, 'keywords' => array( 'text', 'gradient' ) ),
402 self::fstyle( 'choice.fstyle', 'choice', 'choice', null, '400' ),
403 self::letter_spacing( 'choice.ls', '--evf-choice-ls', 'choice', null ),
404 self::talign( 'choice.align', '--evf-choice-align', 'choice', null ),
405 self::box( 'choice.margin', __( 'Margin', 'everest-forms' ), '--evf-choice-margin', array( 0, 0, 5, 0 ), 'choice', null ),
406 );
407
408 $button = array(
409 // Palette-driven by default (button_background/button_text slots); editing either
410 // directly here unlinks this form from the palette for just that one token.
411 array( 'key' => 'btn.bg', 'section' => 'button', 'group' => '', 'label' => __( 'Button color', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-btn-bg', 'default' => '#0073aa', 'gradientable' => true, 'keywords' => array( 'submit', 'background', 'color', 'gradient' ) ),
412 // Not gradientable: shares its rule's `background` with btn.bg (see input.color).
413 array( 'key' => 'btn.color', 'section' => 'button', 'group' => '', 'label' => __( 'Text color', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-btn-color', 'default' => '#ffffff', 'keywords' => array( 'submit', 'color' ) ),
414 self::radius( 'btn.radius', '--evf-btn-radius', 3, 'button', null, '', false ),
415 // No 'var': like choice.variation, this drives a wrapper class (`evf-btn-width-fill`)
416 // instead of a CSS variable — see FrontendEnqueue::container_class() (real frontend)
417 // and PreviewBridge.ts (live preview). New property, no v1 equivalent, so old records
418 // simply get the default ('fit') — today's exact behavior — no migration needed.
419 array( 'key' => 'btn.widthMode', 'section' => 'button', 'group' => '', 'label' => __( 'Button width', 'everest-forms' ), 'type' => 'select', 'default' => 'fit', 'options' => self::opts( array( 'fit' => __( 'Fit content', 'everest-forms' ), 'fill' => __( 'Fill width', 'everest-forms' ) ) ), 'keywords' => array( 'full width', 'block', 'stretch', 'size' ) ),
420 array( 'key' => 'btn.size', 'section' => 'button', 'group' => '', 'label' => __( 'Font size', 'everest-forms' ), 'type' => 'slider', 'var' => '--evf-btn-size', 'default' => 14, 'min' => 11, 'max' => 24, 'step' => 1, 'unit' => 'px', 'advanced' => true, 'keywords' => array( 'submit' ) ),
421 self::fstyle( 'btn.fstyle', 'btn', 'button', null, '400' ),
422 self::letter_spacing( 'btn.ls', '--evf-btn-ls', 'button', null ),
423 // 'left', not 'center': matches both v1's own implicit default and the built-in
424 // "Default" template's explicit value — a legacy form that never touched button
425 // alignment has no token for it at all (see Migrator), so this default IS its
426 // rendered alignment; 'center' silently flipped it on migration (EVF-2732).
427 self::talign( 'btn.align', '--evf-btn-align', 'button', null, 'left' ),
428 self::line( 'btn.line', '--evf-btn-lh', 1.5, 'button', null ),
429 self::box( 'btn.margin', __( 'Margin', 'everest-forms' ), '--evf-btn-margin', array( 0, 0, 0, 0 ), 'button', null ),
430 array( 'key' => 'btn.pad', 'section' => 'button', 'group' => '', 'label' => __( 'Padding', 'everest-forms' ), 'type' => 'box4', 'var' => '--evf-btn-pad', 'default' => array( 10, 15, 10, 15 ), 'responsive' => true, 'advanced' => true, 'keywords' => array( 'size' ) ),
431 array( 'key' => 'btn.borderStyle', 'section' => 'button', 'group' => '', 'label' => __( 'Border type', 'everest-forms' ), 'type' => 'select', 'var' => '--evf-btn-border-style', 'default' => 'solid', 'options' => $border, 'deps' => array( 'btn.bw', 'btn.borderC' ), 'advanced' => true, 'keywords' => array( 'outline' ) ),
432 self::bwidth( 'btn.bw', '--evf-btn-bw', 1, 'button', null, '', true ),
433 array( 'key' => 'btn.borderC', 'section' => 'button', 'group' => '', 'label' => __( 'Border color', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-btn-border-c', 'default' => '#cccccc', 'advanced' => true ),
434 // Palette-driven; the default only shows on a no-palette form.
435 array( 'key' => 'btn.bgHover', 'section' => 'button', 'group' => '', 'label' => __( 'Button color', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-btn-bg-hover', 'default' => '#eeeeee', 'state' => 'hover', 'gradientable' => true, 'keywords' => array( 'mouse over', 'gradient' ) ),
436 array( 'key' => 'btn.colorHover', 'section' => 'button', 'group' => '', 'label' => __( 'Text color', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-btn-color-hover', 'default' => '#23282d', 'state' => 'hover', 'keywords' => array( 'mouse over', 'hover font' ) ),
437 array( 'key' => 'btn.borderCHover', 'section' => 'button', 'group' => '', 'label' => __( 'Border color', 'everest-forms' ), 'type' => 'color', 'var' => '--evf-btn-border-c-hover', 'default' => '#cccccc', 'state' => 'hover', 'keywords' => array( 'mouse over', 'hover border' ) ),
438 );
439
440 // Messages (tier=pro) live in the Pro plugin, injected via `evf_style_schema` only when
441 // Pro is active — a free schema physically has no pro tokens to sanitize or compile.
442 return array_merge(
443 $form,
444 self::text_role_tokens(),
445 $fields,
446 $choice,
447 $button
448 );
449 }
450
451 /**
452 * The four Text roles (Label / Sublabel / Description / Section Title), 7 controls each.
453 * Label & sublabel colour are palette-driven (hidden). Margin & padding are responsive.
454 *
455 * @return array
456 */
457 protected static function text_role_tokens() {
458 $roles = array(
459 // state key => [ var-prefix, size, min, max, color, neutral-weight, margin, line, pad, color-hidden ].
460 // label/sublabel color: palette-driven by default (field_label/field_sublabel slots);
461 // editing directly here unlinks this form from the palette for just that one token —
462 // same as title/desc below, which have always worked this way.
463 'label' => array( 'sub' => 'label', 'size' => 14, 'min' => 10, 'max' => 24, 'color' => '#333333', 'nw' => '600', 'margin' => array( 0, 0, 10, 0 ), 'line' => 1.7, 'pad' => array( 0, 0, 0, 0 ), 'color_hidden' => false ),
464 'sublabel' => array( 'sub' => 'sub', 'size' => 12, 'min' => 9, 'max' => 20, 'color' => '#666666', 'nw' => '400', 'margin' => array( 0, 0, 10, 0 ), 'line' => 1.5, 'pad' => array( 0, 0, 0, 0 ), 'color_hidden' => false ),
465 'desc' => array( 'sub' => 'desc', 'size' => 14, 'min' => 9, 'max' => 20, 'color' => '#575757', 'nw' => '400', 'margin' => array( 0, 0, 10, 0 ), 'line' => 1.7, 'pad' => array( 0, 0, 0, 0 ), 'color_hidden' => false ),
466 'title' => array( 'sub' => 'title', 'size' => 16, 'min' => 12, 'max' => 34, 'color' => '#575757', 'nw' => '700', 'margin' => array( 25, 0, 25, 0 ), 'line' => 1.5, 'pad' => array( 0, 0, 0, 0 ), 'color_hidden' => false ),
467 );
468
469 $out = array();
470 foreach ( $roles as $state => $r ) {
471 $vp = $r['sub'];
472 $out[] = array( 'key' => "{$vp}.size", 'section' => 'text', 'state' => $state, 'group' => '', 'label' => __( 'Font size', 'everest-forms' ), 'type' => 'slider', 'var' => "--evf-{$vp}-size", 'default' => $r['size'], 'min' => $r['min'], 'max' => $r['max'], 'step' => 1, 'unit' => 'px', 'keywords' => array( 'text', $state ) );
473 $out[] = array( 'key' => "{$vp}.color", 'section' => 'text', 'state' => $state, 'group' => '', 'label' => __( 'Color', 'everest-forms' ), 'type' => 'color', 'var' => "--evf-{$vp}-color", 'default' => $r['color'], 'hidden' => $r['color_hidden'], 'gradientable' => true, 'keywords' => array( 'text', $state, 'gradient' ) );
474 $out[] = self::fstyle( "{$vp}.fstyle", $vp, 'text', $state, $r['nw'] );
475 $out[] = self::letter_spacing( "{$vp}.ls", "--evf-{$vp}-ls", 'text', $state );
476 $out[] = self::talign( "{$vp}.align", "--evf-{$vp}-align", 'text', $state );
477 $out[] = self::line( "{$vp}.line", "--evf-{$vp}-lh", $r['line'], 'text', $state );
478 $out[] = self::box( "{$vp}.margin", __( 'Margin', 'everest-forms' ), "--evf-{$vp}-margin", $r['margin'], 'text', $state );
479 $out[] = self::box( "{$vp}.pad", __( 'Padding', 'everest-forms' ), "--evf-{$vp}-pad", $r['pad'], 'text', $state );
480 }
481 return $out;
482 }
483
484 /* --------------------------------------------------------------------- *
485 * Token builders (mixins) — shared shape factory, also used by Pro/add-ons
486 * contributing tier=pro tokens through the `evf_style_schema` filter.
487 * --------------------------------------------------------------------- */
488
489 /**
490 * Font-style control (Bold / Italic / Underline / Uppercase, plus an optional Font weight).
491 *
492 * `weight` (an explicit numeric weight, e.g. '600') is an ADDITIVE sub-property alongside the
493 * original four flags — old stored values simply don't have this key, `!empty($v['weight'])`
494 * is false for them, and font-weight keeps deriving from `bold`/`neutral_weight` exactly as
495 * before (see Compiler::declarations() and cssVars.ts's tokenDeclarations()). No migration
496 * step needed: a missing key is indistinguishable from '' (the "Auto" default here), by design.
497 *
498 * @param string $key Token key.
499 * @param string $base CSS var base (e.g. `label`, `input`, `msg-success`).
500 * @param string $section Section key.
501 * @param string $state Variant/state (nullable).
502 * @param string $nw Neutral (non-bold) font-weight.
503 * @return array
504 */
505 public static function fstyle( $key, $base, $section, $state, $nw ) {
506 return array(
507 'key' => $key,
508 'section' => $section,
509 'state' => $state,
510 'group' => '',
511 'label' => __( 'Font style', 'everest-forms' ),
512 'type' => 'fontstyle',
513 'vars' => array(
514 'weight' => "--evf-{$base}-weight",
515 'style' => "--evf-{$base}-fs",
516 'decoration' => "--evf-{$base}-td",
517 'transform' => "--evf-{$base}-tt",
518 ),
519 'neutral_weight' => $nw,
520 'weight_options' => self::weight_options(),
521 'default' => array(
522 'bold' => false,
523 'italic' => false,
524 'underline' => false,
525 'uppercase' => false,
526 'weight' => '',
527 ),
528 'advanced' => true,
529 'keywords' => array( 'bold', 'italic', 'underline', 'uppercase', 'font style', 'font weight', 'weight' ),
530 );
531 }
532
533 /**
534 * Selectable font-weight values for the `fstyle` control's Font weight dropdown.
535 * '' ("Auto") keeps the original Bold-toggle-only behavior.
536 *
537 * @return array value => label.
538 */
539 protected static function weight_options() {
540 // EVF-2721: this used to offer all 9 standard CSS weight steps (100-900), but a form's
541 // font — a specific Google Font, or "Theme default" falling back to the visitor's system
542 // font stack — only actually HAS a handful of real weight faces. A numeric weight with no
543 // matching face just gets browser-synthesized (inconsistently, sometimes identically to a
544 // neighboring value), so half the dropdown silently did nothing. Trimmed to the 3 steps
545 // virtually every font (Google or system) truly ships, and {@see evfsc_enqueue_fonts}/
546 // PreviewBridge now request exactly these weights so a chosen Google Font's real files
547 // load instead of relying on synthesis.
548 //
549 // Built directly (not via self::opts(), which takes an associative array — PHP would
550 // coerce these numeric-string keys to ints) so every value is guaranteed a string.
551 return array(
552 array( 'value' => '', 'label' => __( 'Auto', 'everest-forms' ) ),
553 array( 'value' => '300', 'label' => __( 'Light', 'everest-forms' ) ),
554 array( 'value' => '400', 'label' => __( 'Regular', 'everest-forms' ) ),
555 array( 'value' => '700', 'label' => __( 'Bold', 'everest-forms' ) ),
556 );
557 }
558
559 /**
560 * Line-height slider (1–3, single value).
561 *
562 * @param string $key Token key.
563 * @param string $var CSS var.
564 * @param float $default Default line-height.
565 * @param string $section Section key.
566 * @param string $state Variant/state (nullable).
567 * @return array
568 */
569 protected static function line( $key, $var, $default, $section, $state ) {
570 return array(
571 'key' => $key,
572 'section' => $section,
573 'state' => $state,
574 'group' => '',
575 'label' => __( 'Line height', 'everest-forms' ),
576 'type' => 'slider',
577 'var' => $var,
578 'default' => $default,
579 'min' => 1,
580 'max' => 3,
581 'step' => 0.1,
582 'unit' => '',
583 'advanced' => true,
584 'keywords' => array( 'leading', 'line height' ),
585 );
586 }
587
588 /**
589 * Letter-spacing slider (-2px to 10px, single value). New, additive sibling of `line()` —
590 * every caller of `self::fstyle()` gets one alongside it, so it's added wherever font-weight
591 * was. No v1 equivalent ever existed, so a missing value here just means "not customized yet"
592 * — the schema default (0, i.e. `normal`) — same as any other brand-new token; no migration
593 * step needed.
594 *
595 * @param string $key Token key.
596 * @param string $var CSS var.
597 * @param string $section Section key.
598 * @param string $state Variant/state (nullable).
599 * @return array
600 */
601 protected static function letter_spacing( $key, $var, $section, $state ) {
602 return array(
603 'key' => $key,
604 'section' => $section,
605 'state' => $state,
606 'group' => '',
607 'label' => __( 'Letter spacing', 'everest-forms' ),
608 'type' => 'slider',
609 'var' => $var,
610 'default' => 0,
611 'min' => -2,
612 'max' => 10,
613 'step' => 0.1,
614 'unit' => 'px',
615 'advanced' => true,
616 'keywords' => array( 'tracking', 'letter spacing', 'kerning' ),
617 );
618 }
619
620 /**
621 * Alignment control (left/center/right). Single value — never responsive.
622 *
623 * @param string $key Token key.
624 * @param string $var CSS var.
625 * @param string $section Section key.
626 * @param string $state Variant/state (nullable).
627 * @param string $default Default alignment.
628 * @return array
629 */
630 public static function talign( $key, $var, $section, $state, $default = 'left' ) {
631 return array(
632 'key' => $key,
633 'section' => $section,
634 'state' => $state,
635 'group' => '',
636 'label' => __( 'Alignment', 'everest-forms' ),
637 'type' => 'align',
638 'var' => $var,
639 'default' => $default,
640 'advanced' => true,
641 'keywords' => array( 'left', 'center', 'right', 'alignment' ),
642 );
643 }
644
645 /**
646 * Margin / padding box (4-side). THE responsive control type — Desktop/Tablet/Mobile.
647 *
648 * @param string $key Token key.
649 * @param string $label Control label.
650 * @param string $var CSS var.
651 * @param array $default [top,right,bottom,left].
652 * @param string $section Section key.
653 * @param string $state Variant/state (nullable).
654 * @param int $min Optional per-side lower bound (null = type default).
655 * @param int $max Optional per-side upper bound (null = type default).
656 * @return array
657 */
658 protected static function box( $key, $label, $var, $default, $section, $state, $min = null, $max = null ) {
659 $out = array(
660 'key' => $key,
661 'section' => $section,
662 'state' => $state,
663 'group' => '',
664 'label' => $label,
665 'type' => 'box4',
666 'var' => $var,
667 'default' => $default,
668 'responsive' => true,
669 'advanced' => true,
670 'keywords' => array( 'spacing' ),
671 );
672 if ( null !== $min ) {
673 $out['min'] = $min;
674 }
675 if ( null !== $max ) {
676 $out['max'] = $max;
677 }
678 return $out;
679 }
680
681 /**
682 * Border-width box (4-side, single value — border width was never responsive).
683 *
684 * @param string $key Token key.
685 * @param string $var CSS var.
686 * @param int $d Default (applied to all four sides).
687 * @param string $section Section key.
688 * @param string $state Variant/state (nullable).
689 * @param string $group Group label.
690 * @param bool $advanced Whether behind the "advanced" reveal.
691 * @return array
692 */
693 public static function bwidth( $key, $var, $d, $section, $state, $group, $advanced = true ) {
694 return array(
695 'key' => $key,
696 'section' => $section,
697 'state' => $state,
698 'group' => $group,
699 'label' => __( 'Border width', 'everest-forms' ),
700 'type' => 'box4',
701 'var' => $var,
702 'default' => array( $d, $d, $d, $d ),
703 'max' => 12,
704 'advanced' => $advanced,
705 'keywords' => array( 'thickness', 'border' ),
706 );
707 }
708
709 /**
710 * Border-radius box (4-corner + px/% unit, single value).
711 *
712 * @param string $key Token key.
713 * @param string $var CSS var.
714 * @param int $d Default (applied to all four corners).
715 * @param string $section Section key.
716 * @param string $state Variant/state (nullable).
717 * @param string $group Group label.
718 * @param bool $advanced Whether behind the "advanced" reveal.
719 * @return array
720 */
721 public static function radius( $key, $var, $d, $section, $state, $group, $advanced ) {
722 return array(
723 'key' => $key,
724 'section' => $section,
725 'state' => $state,
726 'group' => $group,
727 'label' => __( 'Border radius', 'everest-forms' ),
728 'type' => 'box4',
729 'var' => $var,
730 'default' => array( $d, $d, $d, $d ),
731 'max' => 60,
732 'units' => array( 'px', '%' ),
733 'corners' => true,
734 'advanced' => $advanced,
735 'keywords' => array( 'corner', 'rounded' ),
736 );
737 }
738
739 /* --------------------------------------------------------------------- *
740 * Helpers
741 * --------------------------------------------------------------------- */
742
743 /**
744 * Shared border-type options — the full set the legacy customizer offered.
745 *
746 * @return array
747 */
748 public static function border_options() {
749 return self::opts(
750 array(
751 'none' => __( 'None', 'everest-forms' ),
752 'hidden' => __( 'Hidden', 'everest-forms' ),
753 'dotted' => __( 'Dotted', 'everest-forms' ),
754 'dashed' => __( 'Dashed', 'everest-forms' ),
755 'solid' => __( 'Solid', 'everest-forms' ),
756 'double' => __( 'Double', 'everest-forms' ),
757 'groove' => __( 'Groove', 'everest-forms' ),
758 'ridge' => __( 'Ridge', 'everest-forms' ),
759 'inset' => __( 'Inset', 'everest-forms' ),
760 'outset' => __( 'Outset', 'everest-forms' ),
761 'initial' => __( 'Initial', 'everest-forms' ),
762 'inherit' => __( 'Inherit', 'everest-forms' ),
763 )
764 );
765 }
766
767 /**
768 * Font-family options. Only "Theme default" is baked in; the full Google Fonts list is
769 * supplied to the panel at runtime via the REST payload's `google_fonts`.
770 *
771 * @return array
772 */
773 protected static function font_options() {
774 return self::opts(
775 array(
776 '' => __( 'Theme default', 'everest-forms' ),
777 )
778 );
779 }
780
781 /**
782 * Convert a value=>label map into the panel's [{value,label}] option shape.
783 *
784 * @param array $map value => label.
785 * @return array
786 */
787 protected static function opts( $map ) {
788 $out = array();
789 foreach ( $map as $value => $label ) {
790 $out[] = array(
791 'value' => $value,
792 'label' => $label,
793 );
794 }
795 return $out;
796 }
797
798 /**
799 * Fill every token with its default keys so consumers can rely on the full shape.
800 *
801 * @param array $token Raw token.
802 * @return array
803 */
804 protected static function normalize( $token ) {
805 $token = array_merge(
806 array(
807 'key' => '',
808 'section' => '',
809 'group' => '',
810 'label' => '',
811 'type' => '',
812 'var' => null,
813 'default' => null,
814 'state' => null,
815 'responsive' => false,
816 'advanced' => false,
817 'hidden' => false,
818 'tier' => 'free',
819 'keywords' => array(),
820 ),
821 $token
822 );
823
824 // Box defaults are authored compactly as [t,r,b,l]; store/emit them as the associative
825 // {top,right,bottom,left} shape the legacy engine uses, so migration is an identity copy.
826 if ( 'box4' === $token['type'] && is_array( $token['default'] ) && isset( $token['default'][0] ) ) {
827 $token['default'] = self::to_dim( $token['default'], ! empty( $token['units'] ) ? $token['units'][0] : null );
828 }
829
830 return $token;
831 }
832
833 /**
834 * [t,r,b,l] → { top, right, bottom, left [, unit ] }.
835 *
836 * @param array $box Indexed sides.
837 * @param string|null $unit Optional unit (radius).
838 * @return array
839 */
840 protected static function to_dim( $box, $unit = null ) {
841 $dim = array(
842 'top' => isset( $box[0] ) ? $box[0] : 0,
843 'right' => isset( $box[1] ) ? $box[1] : 0,
844 'bottom' => isset( $box[2] ) ? $box[2] : 0,
845 'left' => isset( $box[3] ) ? $box[3] : 0,
846 );
847 if ( null !== $unit ) {
848 $dim['unit'] = $unit;
849 }
850 return $dim;
851 }
852 }
853