PluginProbe
The Innovative Form Builder – IvyForms / 1.1.1
The Innovative Form Builder – IvyForms v1.1.1
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / utils / formStyleGenerator.ts

formStyleGenerator.ts in The Innovative Form Builder – IvyForms 1.1.1, at frontend/src/utils/formStyleGenerator.ts

966 lines 38.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import type { FormStyleSettings } from '@/types/form-style'
2
3 import { formatDimensionToCss, resolveBorderStyle } from '@/utils/styleUtils'
4 import { applyAlpha } from '@/utils/colorUtils'
5 import { parseCssClassList } from '@/utils/form-styles/classParser'
6 import { buildFormPopperScopeClass, buildFormStyleSelectors } from '@/utils/form-styles/selectors'
7 import { resolveActiveColors, resolveCornerRadiusValue } from '@/utils/form-styles/styleSettings'
8 import { applyFormContainerStyles } from '@/utils/form-styles/sections/formContainer'
9 import { applyFormHeaderStyles } from '@/utils/form-styles/sections/formHeader'
10 import { applyFieldLabelStyles } from '@/utils/form-styles/sections/fieldLabels'
11 import { applyFieldStateVariantStyles } from '@/utils/form-styles/sections/fieldStateVariants'
12
13 export { parseCssClassList }
14
15 export function generateFormCSS(
16 formId: string | number,
17 styleSettings: FormStyleSettings | null,
18 ): string {
19 if (!styleSettings || styleSettings.stylesEnabled === false) {
20 return ''
21 }
22
23 const { formSelector, baseSelector } = buildFormStyleSelectors(
24 formId,
25 styleSettings.customCssClass,
26 )
27 const formPopperScopeClass = buildFormPopperScopeClass(formId)
28
29 const css: string[] = []
30 const activeColors = resolveActiveColors(styleSettings)
31
32 const formBackground =
33 styleSettings.advancedMode?.general?.formBackground ||
34 styleSettings.quickMode?.formBackground ||
35 'transparent'
36 const formBorderColor =
37 styleSettings.advancedMode?.general?.formBorderColor || styleSettings.quickMode?.formBorderColor
38 const formBorderWidth =
39 styleSettings.advancedMode?.general?.formBorderWidth || styleSettings.quickMode?.formBorderWidth
40 const formBorderStyle = resolveBorderStyle(styleSettings.advancedMode?.general?.formBorderStyle)
41 const formPaddingLeftRight = styleSettings.advancedMode?.general?.formPaddingLeftRight || '24px'
42 const formPaddingTopBottom = styleSettings.advancedMode?.general?.formPaddingTopBottom || '24px'
43 const verticalSpacing =
44 styleSettings.advancedMode?.general?.verticalSpacing ||
45 styleSettings.quickMode?.verticalSpacing ||
46 '16px'
47 const horizontalSpacing =
48 styleSettings.advancedMode?.general?.horizontalSpacing ||
49 styleSettings.quickMode?.horizontalSpacing ||
50 '12px'
51 const formAlignment = styleSettings.advancedMode?.general?.alignment || 'left'
52 const formCornerRadius = resolveCornerRadiusValue(
53 styleSettings.advancedMode?.general?.formCorner || styleSettings.quickMode?.formCorner,
54 '8px',
55 )
56
57 const fieldBackground =
58 styleSettings.advancedMode?.fieldStates?.default?.background ||
59 styleSettings.quickMode?.fieldBackground ||
60 'transparent'
61 const fieldBorderColor =
62 styleSettings.advancedMode?.fieldStates?.default?.borderColor ||
63 styleSettings.quickMode?.fieldBorderColor ||
64 '#6B737D'
65 const fieldTextColor =
66 styleSettings.advancedMode?.fieldStates?.default?.textColor || activeColors.fieldText
67 const fieldFontWeight = styleSettings.advancedMode?.fieldStates?.default?.fontWeight
68 const fieldFontStyle = styleSettings.advancedMode?.fieldStates?.default?.fontStyle
69 const fieldFontSize = formatDimensionToCss(
70 styleSettings.advancedMode?.fieldStates?.default?.fontSize,
71 )
72 const fieldCornerRadius = resolveCornerRadiusValue(
73 styleSettings.advancedMode?.fieldStates?.default?.corner ||
74 styleSettings.quickMode?.fieldCorner,
75 '6px',
76 )
77 const fieldPaddingX =
78 styleSettings.advancedMode?.fieldStates?.default?.paddingLeftRight ||
79 styleSettings.quickMode?.fieldPaddingLeftRight ||
80 '8px'
81 const fieldPaddingY =
82 styleSettings.advancedMode?.fieldStates?.default?.paddingTopBottom ||
83 styleSettings.quickMode?.fieldPaddingTopBottom ||
84 '0px'
85 const fieldBorderWidth =
86 styleSettings.advancedMode?.fieldStates?.default?.borderWidth ||
87 styleSettings.quickMode?.fieldBorderWidth ||
88 '1px'
89 const fieldBorderStyle = resolveBorderStyle(
90 styleSettings.advancedMode?.fieldStates?.default?.borderStyle,
91 )
92 const fieldDescriptionColor =
93 styleSettings.advancedMode?.fieldStates?.default?.descriptionColor || '#212832'
94
95 const buttonCornerRadius = resolveCornerRadiusValue(
96 styleSettings.advancedMode?.buttonStates?.normal?.corner ||
97 styleSettings.corners?.buttonCornerRadius ||
98 styleSettings.button?.borderRadius,
99 )
100 const buttonBackground =
101 styleSettings.advancedMode?.buttonStates?.normal?.background ||
102 styleSettings.quickMode?.buttonBackground ||
103 '#FFFFFF'
104 const buttonBorderColor =
105 styleSettings.advancedMode?.buttonStates?.normal?.borderColor ||
106 styleSettings.quickMode?.buttonBorderColor ||
107 styleSettings.quickMode?.buttonBackground ||
108 '#707883'
109 const buttonBorderWidth =
110 styleSettings.advancedMode?.buttonStates?.normal?.borderWidth ||
111 styleSettings.quickMode?.buttonBorderWidth ||
112 '1px'
113 const buttonTextColor =
114 styleSettings.advancedMode?.buttonStates?.normal?.textColor || activeColors.buttonText
115
116 applyFormContainerStyles(css, {
117 formId,
118 baseSelector,
119 formSelector,
120 formBackground,
121 formBorderColor,
122 formBorderWidth,
123 formBorderStyle,
124 formCornerRadius,
125 formPaddingTopBottom,
126 formPaddingLeftRight,
127 formAlignment,
128 verticalSpacing,
129 horizontalSpacing,
130 })
131
132 applyFormHeaderStyles(css, formId, styleSettings, activeColors)
133 applyFieldLabelStyles(css, baseSelector, styleSettings, activeColors)
134
135 // Text inputs and textareas (Element Plus wrappers) + phone/select/date/time variants - high specificity to override component styles
136 const textAndPhoneWrappers = [
137 `${baseSelector} .ivyforms-input.el-input .el-input__wrapper`,
138 `${baseSelector} .ivyforms-field .el-input__wrapper`,
139 `${baseSelector} .ivyforms-phone-input .el-input__wrapper`,
140 `${baseSelector} .ivyforms-field__phone .el-input__wrapper`,
141 `${baseSelector} .ivyforms-field .vue-tel-input`,
142 `${baseSelector} .ivyforms-field__select .el-input__wrapper`,
143 `${baseSelector} .ivyforms-field__select .el-select__wrapper`,
144 `${baseSelector} .el-select .el-select__wrapper`,
145 `${baseSelector} .ivyforms-field__address .el-input__wrapper`,
146 `${baseSelector} .ivyforms-field__date .el-input__wrapper`,
147 `${baseSelector} .ivyforms-field__time .el-input__wrapper`,
148 ]
149
150 css.push(textAndPhoneWrappers.join(',') + ' {')
151 if (fieldBackground) {
152 css.push(` background-color: ${fieldBackground} !important;`)
153 }
154 if (fieldBorderColor) {
155 css.push(` border: ${fieldBorderWidth} ${fieldBorderStyle} ${fieldBorderColor} !important;`)
156 css.push(` box-shadow: none !important;`)
157 }
158 if (fieldCornerRadius) {
159 css.push(` border-radius: ${fieldCornerRadius} !important;`)
160 }
161 css.push(` padding: ${fieldPaddingY} ${fieldPaddingX} !important;`)
162 css.push(' box-sizing: border-box !important;')
163 css.push('}')
164
165 if (fieldBackground || fieldBorderColor || fieldCornerRadius) {
166 css.push(`${baseSelector} .ivyforms-field__textarea .el-textarea__inner {`)
167 if (fieldBackground) {
168 css.push(` background-color: ${fieldBackground} !important;`)
169 }
170 if (fieldBorderColor) {
171 css.push(` border: ${fieldBorderWidth} ${fieldBorderStyle} ${fieldBorderColor} !important;`)
172 css.push(` box-shadow: none !important;`)
173 }
174 if (fieldCornerRadius) {
175 css.push(` border-radius: ${fieldCornerRadius} !important;`)
176 }
177 css.push(` padding: ${fieldPaddingY} ${fieldPaddingX} !important;`)
178 css.push(' box-sizing: border-box !important;')
179 css.push('}')
180 }
181 // Ensure custom number-field wrappers match the same border-radius and clip inner backgrounds
182 css.push(
183 `${baseSelector} .ivyforms-text-number, ${baseSelector} .ivyforms-text-number__input, ${baseSelector} .el-input-number {`,
184 )
185 if (fieldCornerRadius) {
186 css.push(` border-radius: ${fieldCornerRadius} !important;`)
187 }
188 css.push(' overflow: hidden !important;')
189 css.push('}')
190
191 // Override fixed height on .ivyforms-input.el-input to allow padding to control size
192 css.push(`${baseSelector} .ivyforms-input.el-input,`)
193 css.push(`${baseSelector} .ivyforms-phone-input,`)
194 css.push(`${baseSelector} .ivyforms-field__phone,`)
195 css.push(`${baseSelector} .ivyforms-field__select,`)
196 css.push(`${baseSelector} .ivyforms-field__address,`)
197 css.push(`${baseSelector} .ivyforms-field__date,`)
198 css.push(`${baseSelector} .ivyforms-field__time,`)
199 css.push(`${baseSelector} .ivyforms-form-item-select .el-select__wrapper,`)
200 css.push(`${baseSelector} .ivyforms-phone-input .vue-tel-input {`)
201 css.push(' height: auto !important;')
202 css.push(' min-height: 40px !important;')
203 css.push('}')
204
205 // Also override height on nested select/date picker elements
206 css.push(`${baseSelector} .el-select,`)
207 css.push(`${baseSelector} .el-date-picker,`)
208 css.push(`${baseSelector} .el-time-picker {`)
209 css.push(' height: auto !important;')
210 css.push('}')
211
212 // Override el-input height constraints
213 css.push(`${baseSelector} .el-input {`)
214 css.push(' height: auto !important;')
215 css.push('}')
216
217 // Ensure select and date picker inputs have consistent sizing
218 css.push(`${baseSelector} .el-select .el-input,`)
219 css.push(`${baseSelector} .ivyforms-field__select .el-input,`)
220 css.push(`${baseSelector} .ivyforms-field__date .el-input,`)
221 css.push(`${baseSelector} .ivyforms-field__time .el-input {`)
222 css.push(' height: auto !important;')
223 css.push('}')
224
225 // Override date picker specific height constraint
226 css.push(`${baseSelector} .ivyforms-date-picker .ivyforms-input .el-input__wrapper,`)
227 css.push(`${baseSelector} .ivyforms-date-picker .ivyforms-input.el-input__wrapper {`)
228 css.push(' height: auto !important;')
229 css.push(' min-height: 40px !important;')
230 css.push(` padding: ${fieldPaddingY} ${fieldPaddingX} !important;`)
231 css.push('}')
232
233 // Phone inner input (vue-tel-input) - remove inner borders to avoid double outline
234 css.push(`${baseSelector} .vti__input {`)
235 if (fieldTextColor) {
236 css.push(` color: ${fieldTextColor} !important;`)
237 }
238 css.push(' border: none !important;')
239 css.push(' box-shadow: none !important;')
240 css.push(' background: transparent !important;')
241 css.push(' padding: 0 !important;')
242 css.push('}')
243
244 // Ensure dropdowns display content vertically centered with flex
245 css.push(`${baseSelector} .el-select__wrapper,`)
246 css.push(`${baseSelector} .ivyforms-form-item-select .el-select__wrapper {`)
247 css.push(' display: flex !important;')
248 css.push(' align-items: center !important;')
249 css.push('}')
250
251 // Set select selection min-height to match text input inner height (30px)
252 css.push(`${baseSelector} .el-select__selection {`)
253 css.push(' min-height: 30px !important;')
254 css.push('}')
255
256 const selectWrappers = [
257 `${baseSelector} .el-select .el-input__wrapper`,
258 `${baseSelector} .el-select__wrapper`,
259 `${baseSelector} .ivyforms-field__select-input .el-select__wrapper`,
260 ]
261
262 css.push(selectWrappers.join(',') + ' {')
263 if (fieldBackground) {
264 css.push(` background-color: ${fieldBackground} !important;`)
265 }
266 if (fieldBorderColor) {
267 css.push(` border: ${fieldBorderWidth} ${fieldBorderStyle} ${fieldBorderColor} !important;`)
268 css.push(` box-shadow: none !important;`)
269 }
270 if (fieldCornerRadius) {
271 css.push(` border-radius: ${fieldCornerRadius} !important;`)
272 }
273 css.push('}')
274
275 // Select inner input - make transparent to avoid double borders
276 css.push(`${baseSelector} .el-select .el-input__inner,`)
277 css.push(`${baseSelector} .ivyforms-field__select .el-input__inner,`)
278 css.push(`${baseSelector} .el-select__selected-item {`)
279 if (fieldTextColor) {
280 css.push(` color: ${fieldTextColor} !important;`)
281 }
282 css.push(' border: none !important;')
283 css.push(' background: transparent !important;')
284 css.push('}')
285
286 // Reset select inner input padding to avoid covering borders
287 css.push(`${baseSelector} .el-select__input {`)
288 css.push(' padding: 0 !important;')
289 css.push(' background: transparent !important;')
290 css.push(' border: none !important;')
291 css.push('}')
292
293 // Country field in address - top/bottom borders only to match address row styling
294 if (fieldBorderColor) {
295 // Target using field class applied to country-address wrapper
296 css.push(`${baseSelector} .country-address .el-select__wrapper {`)
297 css.push(
298 ` border-top: ${fieldBorderWidth} ${fieldBorderStyle} ${fieldBorderColor} !important;`,
299 )
300 css.push(
301 ` border-bottom: ${fieldBorderWidth} ${fieldBorderStyle} ${fieldBorderColor} !important;`,
302 )
303 css.push(` border-left: none !important;`)
304 css.push(` border-right: none !important;`)
305 css.push('}')
306 }
307
308 if (fieldDescriptionColor) {
309 css.push(`${baseSelector} .ivyforms-form-item__info-text {`)
310 css.push(` color: ${fieldDescriptionColor} !important;`)
311 css.push(` --ivyforms-info-color: ${fieldDescriptionColor} !important;`)
312 css.push('}')
313 }
314 // Input text color
315 if (fieldTextColor || fieldFontWeight || fieldFontStyle || fieldFontSize) {
316 css.push(`${baseSelector} .el-input__inner,`)
317 css.push(`${baseSelector} .el-textarea__inner,`)
318 css.push(`${baseSelector} .ivyforms-field__phone .vti__input,`)
319 css.push(`${baseSelector} .ivyforms-field__phone .vue-tel-input input,`)
320 css.push(`${baseSelector} .ivyforms-field__select .el-select__selected-item,`)
321 css.push(`${baseSelector} .el-select .el-select__selected-item {`)
322 if (fieldTextColor) {
323 css.push(` color: ${fieldTextColor} !important;`)
324 }
325 if (fieldFontWeight) {
326 css.push(` font-weight: ${fieldFontWeight} !important;`)
327 }
328 if (fieldFontStyle) {
329 css.push(` font-style: ${fieldFontStyle} !important;`)
330 }
331 if (fieldFontSize) {
332 css.push(` font-size: ${fieldFontSize} !important;`)
333 }
334 css.push('}')
335 }
336
337 // Phone input (intl-tel-input) fallback
338 css.push(`${baseSelector} .iti input,`)
339 css.push(`${baseSelector} .iti__selected-flag,`)
340 css.push(`${baseSelector} .iti__country-list,`)
341 css.push(`${baseSelector} .vue-tel-input,`)
342 css.push(`${baseSelector} .vti__input {`)
343 if (fieldBackground) {
344 css.push(` background-color: ${fieldBackground} !important;`)
345 }
346 if (fieldBorderColor) {
347 css.push(` border-color: ${fieldBorderColor} !important;`)
348 }
349 if (fieldCornerRadius) {
350 css.push(` border-radius: ${fieldCornerRadius} !important;`)
351 }
352 css.push('}')
353
354 // Phone field text color (needs to be separate to avoid being overridden)
355 if (fieldTextColor || fieldFontWeight || fieldFontStyle || fieldFontSize) {
356 css.push(`${baseSelector} .vti__input {`)
357 if (fieldTextColor) {
358 css.push(` color: ${fieldTextColor} !important;`)
359 }
360 if (fieldFontWeight) {
361 css.push(` font-weight: ${fieldFontWeight} !important;`)
362 }
363 if (fieldFontStyle) {
364 css.push(` font-style: ${fieldFontStyle} !important;`)
365 }
366 if (fieldFontSize) {
367 css.push(` font-size: ${fieldFontSize} !important;`)
368 }
369 css.push('}')
370 }
371
372 css.push(`${baseSelector} .choices__inner {`)
373 if (fieldBackground) {
374 css.push(` background-color: ${fieldBackground};`)
375 }
376 if (fieldBorderColor) {
377 css.push(` border: ${fieldBorderWidth} ${fieldBorderStyle} ${fieldBorderColor};`)
378 }
379 if (fieldCornerRadius) {
380 css.push(` border-radius: ${fieldCornerRadius};`)
381 }
382 css.push(` padding: ${fieldPaddingY} ${fieldPaddingX};`)
383 css.push(' min-height: auto;')
384 css.push('}')
385
386 // Field placeholder
387 const fieldPlaceholderPaddingLR = formatDimensionToCss(
388 styleSettings.advancedMode?.fieldPlaceholder?.paddingLeftRight,
389 )
390 const fieldPlaceholderAlignment =
391 styleSettings.advancedMode?.fieldPlaceholder?.alignment || 'left'
392 const fieldPlaceholderFontWeight = styleSettings.advancedMode?.fieldPlaceholder?.fontWeight
393 const fieldPlaceholderFontStyle = styleSettings.advancedMode?.fieldPlaceholder?.fontStyle
394 const fieldPlaceholderFontSize = formatDimensionToCss(
395 styleSettings.advancedMode?.fieldPlaceholder?.fontSize,
396 )
397
398 if (
399 activeColors.fieldPlaceholder ||
400 fieldPlaceholderAlignment !== 'left' ||
401 fieldPlaceholderFontWeight ||
402 fieldPlaceholderFontStyle ||
403 fieldPlaceholderFontSize
404 ) {
405 const placeholderColor = activeColors.fieldPlaceholder
406
407 css.push(`${baseSelector} .el-input__inner::placeholder,`)
408 css.push(`${baseSelector} .el-textarea__inner::placeholder,`)
409 css.push(`${baseSelector} .el-select__placeholder,`)
410 css.push(`${baseSelector} input::placeholder,`)
411 css.push(`${baseSelector} textarea::placeholder {`)
412
413 if (placeholderColor) {
414 css.push(` color: ${placeholderColor} !important;`)
415 }
416 if (fieldPlaceholderPaddingLR) {
417 css.push(` padding-left: ${fieldPlaceholderPaddingLR} !important;`)
418 css.push(` padding-right: ${fieldPlaceholderPaddingLR} !important;`)
419 }
420 if (fieldPlaceholderAlignment !== 'left') {
421 css.push(` text-align: ${fieldPlaceholderAlignment} !important;`)
422 }
423 if (fieldPlaceholderFontWeight) {
424 css.push(` font-weight: ${fieldPlaceholderFontWeight} !important;`)
425 }
426 if (fieldPlaceholderFontStyle) {
427 css.push(` font-style: ${fieldPlaceholderFontStyle} !important;`)
428 }
429 if (fieldPlaceholderFontSize) {
430 css.push(` font-size: ${fieldPlaceholderFontSize} !important;`)
431 }
432 css.push('}')
433
434 if (placeholderColor) {
435 css.push(`${baseSelector} .el-select__placeholder {`)
436 css.push(` color: ${placeholderColor} !important;`)
437 css.push('}')
438 }
439 }
440
441 if (fieldTextColor) {
442 css.push(`${baseSelector} .ivyforms-field__textarea .el-textarea,`)
443 css.push(`${baseSelector} .ivyforms-field__textarea .ivyforms-textarea {`)
444 css.push(` --el-input-text-color: ${fieldTextColor};`)
445 css.push('}')
446 css.push(`${baseSelector} .ivyforms-field__textarea .el-textarea__inner,`)
447 css.push(`${baseSelector} .ivyforms-field__phone .vti__input,`)
448 css.push(`${baseSelector} .ivyforms-field__phone .vue-tel-input input {`)
449 css.push(` color: ${fieldTextColor} !important;`)
450 css.push('}')
451 }
452
453 applyFieldStateVariantStyles(css, {
454 baseSelector,
455 styleSettings,
456 fieldBorderWidth,
457 fieldTextColor,
458 fieldFontSize,
459 fieldFontWeight,
460 fieldFontStyle,
461 })
462
463 // Checkbox and Radio Selection (Element Plus components)
464 const selectionColor =
465 styleSettings.advancedMode?.checkboxRadio?.selectionColor ||
466 activeColors.checkboxRadioSelection ||
467 activeColors.checkboxRadioBorder
468
469 // Border color for unchecked state - use activeColors which includes both quick and advanced
470 const checkboxRadioBorderColor = activeColors.checkboxRadioBorder || selectionColor
471
472 // Label color for unchecked state
473 const checkboxRadioLabelColor = activeColors.checkboxRadioLabel
474 const checkboxRadioFontWeight = styleSettings.advancedMode?.checkboxRadio?.fontWeight
475 const checkboxRadioFontStyle = styleSettings.advancedMode?.checkboxRadio?.fontStyle
476 const checkboxRadioFontSize = formatDimensionToCss(
477 styleSettings.advancedMode?.checkboxRadio?.fontSize,
478 )
479
480 if (selectionColor) {
481 // Base border and radius for unchecked
482 css.push(`${baseSelector} .ivyforms-checkbox .el-checkbox__input .el-checkbox__inner,`)
483 css.push(`${baseSelector} .el-checkbox__input .el-checkbox__inner {`)
484 css.push(` border-color: ${checkboxRadioBorderColor} !important;`)
485 css.push(' border-width: 2px !important;')
486 css.push(' border-radius: 4px !important;')
487 css.push(' background-color: transparent !important;')
488 css.push(' box-shadow: none !important;')
489 css.push('}')
490
491 css.push(`${baseSelector} .ivyforms-radio .el-radio__input .el-radio__inner,`)
492 css.push(`${baseSelector} .el-radio__input .el-radio__inner {`)
493 css.push(` border-color: ${checkboxRadioBorderColor} !important;`)
494 css.push(' border-width: 2px !important;')
495 css.push(' border-radius: 50% !important;')
496 css.push(' background-color: transparent !important;')
497 css.push(' box-shadow: none !important;')
498 css.push('}')
499
500 // Checkbox checked state - use selectionColor for FILL
501 css.push(
502 `${baseSelector} .ivyforms-checkbox.priority-primary .el-checkbox__input.is-checked .el-checkbox__inner,`,
503 )
504 css.push(
505 `${baseSelector} .ivyforms-checkbox .el-checkbox__input.is-checked .el-checkbox__inner,`,
506 )
507 css.push(`${baseSelector} .el-checkbox__input.is-checked .el-checkbox__inner {`)
508 css.push(` background-color: ${selectionColor} !important;`)
509 css.push(` border-color: ${selectionColor} !important;`)
510 css.push(' box-shadow: none !important;')
511 css.push('}')
512
513 // Checkbox checked inner marker - override priority colors
514 css.push(
515 `${baseSelector} .ivyforms-checkbox.priority-primary .el-checkbox__input.is-checked .el-checkbox__inner::after,`,
516 )
517 css.push(
518 `${baseSelector} .ivyforms-checkbox .el-checkbox__input.is-checked .el-checkbox__inner::after {`,
519 )
520 css.push(` background-color: ${selectionColor} !important;`)
521 css.push(` border-color: ${selectionColor} !important;`)
522 css.push('}')
523
524 // Radio checked state - use selectionColor for outer ring and inner dot
525 css.push(`${baseSelector} .ivyforms-radio .el-radio__input.is-checked .el-radio__inner,`)
526 css.push(`${baseSelector} .el-radio__input.is-checked .el-radio__inner {`)
527 css.push(` border-color: ${selectionColor} !important;`)
528 css.push(' background-color: transparent !important;')
529 css.push(' box-shadow: none !important;')
530 css.push('}')
531 css.push(`${baseSelector} .ivyforms-radio .el-radio__input.is-checked .el-radio__inner::after,`)
532 css.push(`${baseSelector} .el-radio__input.is-checked .el-radio__inner::after {`)
533 css.push(` background-color: ${selectionColor} !important;`)
534 css.push(' width: 8px !important;')
535 css.push(' height: 8px !important;')
536 css.push('}')
537 }
538
539 // Apply checkbox and radio label color
540 if (
541 checkboxRadioLabelColor ||
542 checkboxRadioFontWeight ||
543 checkboxRadioFontStyle ||
544 checkboxRadioFontSize
545 ) {
546 css.push(`${baseSelector} .ivyforms-checkbox .el-checkbox__label,`)
547 css.push(`${baseSelector} .el-checkbox__label {`)
548 if (checkboxRadioLabelColor) {
549 css.push(` color: ${checkboxRadioLabelColor} !important;`)
550 }
551 if (checkboxRadioFontWeight) {
552 css.push(` font-weight: ${checkboxRadioFontWeight} !important;`)
553 }
554 if (checkboxRadioFontStyle) {
555 css.push(` font-style: ${checkboxRadioFontStyle} !important;`)
556 }
557 if (checkboxRadioFontSize) {
558 css.push(` font-size: ${checkboxRadioFontSize} !important;`)
559 }
560 css.push('}')
561
562 css.push(`${baseSelector} .ivyforms-radio .el-radio__label,`)
563 css.push(`${baseSelector} .el-radio__label {`)
564 if (checkboxRadioLabelColor) {
565 css.push(` color: ${checkboxRadioLabelColor} !important;`)
566 }
567 if (checkboxRadioFontWeight) {
568 css.push(` font-weight: ${checkboxRadioFontWeight} !important;`)
569 }
570 if (checkboxRadioFontStyle) {
571 css.push(` font-style: ${checkboxRadioFontStyle} !important;`)
572 }
573 if (checkboxRadioFontSize) {
574 css.push(` font-size: ${checkboxRadioFontSize} !important;`)
575 }
576 css.push('}')
577 }
578
579 // Form action buttons: standalone submit + multipage navigation (Prev/Next/Submit in page nav)
580 const buttonPaddingLR = formatDimensionToCss(
581 styleSettings.advancedMode?.buttonStates?.normal?.paddingLeftRight,
582 )
583 const buttonPaddingTB = formatDimensionToCss(
584 styleSettings.advancedMode?.buttonStates?.normal?.paddingTopBottom,
585 )
586 const hasButtonPaddingLR = buttonPaddingLR !== undefined && buttonPaddingLR !== ''
587 const hasButtonPaddingTB = buttonPaddingTB !== undefined && buttonPaddingTB !== ''
588 const buttonPaddingValue =
589 (hasButtonPaddingTB ? buttonPaddingTB : '8px') +
590 ' ' +
591 (hasButtonPaddingLR ? buttonPaddingLR : '16px')
592
593 const buttonWidthValue = formatDimensionToCss(
594 styleSettings.advancedMode?.buttonStates?.normal?.width,
595 )
596 const buttonHeightValue = formatDimensionToCss(
597 styleSettings.advancedMode?.buttonStates?.normal?.height,
598 )
599 const buttonFontWeight = styleSettings.advancedMode?.buttonStates?.normal?.fontWeight
600 const buttonFontStyle = styleSettings.advancedMode?.buttonStates?.normal?.fontStyle
601 const buttonFontSize = formatDimensionToCss(
602 styleSettings.advancedMode?.buttonStates?.normal?.fontSize,
603 )
604
605 css.push(`${baseSelector} .ivyforms-form-submit .ivyforms-button-action,`)
606 css.push(`${baseSelector} .ivyforms-form-submit button.ivyforms-button-action,`)
607 css.push(`${baseSelector} .ivyforms-form-page-navigation .ivyforms-button-action,`)
608 css.push(`${baseSelector} .ivyforms-form-page-navigation button.ivyforms-button-action {`)
609 if (buttonBackground) {
610 css.push(` background-color: ${buttonBackground} !important;`)
611 }
612 if (buttonTextColor) {
613 css.push(` color: ${buttonTextColor} !important;`)
614 }
615 if (buttonFontWeight) {
616 css.push(` font-weight: ${buttonFontWeight} !important;`)
617 }
618 if (buttonFontStyle) {
619 css.push(` font-style: ${buttonFontStyle} !important;`)
620 }
621 if (buttonFontSize) {
622 css.push(` font-size: ${buttonFontSize} !important;`)
623 }
624 css.push(` padding: ${buttonPaddingValue} !important;`)
625 if (buttonWidthValue) {
626 css.push(` width: ${buttonWidthValue} !important;`)
627 }
628 if (buttonHeightValue) {
629 css.push(` height: ${buttonHeightValue} !important;`)
630 }
631 if (buttonBorderWidth && buttonBorderWidth !== '0px') {
632 css.push(
633 ` border: ${buttonBorderWidth} solid ${buttonBorderColor || buttonBackground} !important;`,
634 )
635 } else {
636 css.push(` border: none !important;`)
637 }
638 if (buttonCornerRadius) {
639 css.push(` border-radius: ${buttonCornerRadius} !important;`)
640 }
641 css.push('}')
642
643 // Button hover state - simple opacity effect
644 css.push(`${baseSelector} .ivyforms-form-submit .ivyforms-button-action:hover,`)
645 css.push(`${baseSelector} .ivyforms-form-submit button.ivyforms-button-action:hover,`)
646 css.push(`${baseSelector} .ivyforms-form-page-navigation .ivyforms-button-action:hover,`)
647 css.push(`${baseSelector} .ivyforms-form-page-navigation button.ivyforms-button-action:hover {`)
648 css.push(' opacity: 0.9 !important;')
649 css.push(' transform: translateY(-1px) !important;')
650 css.push('}')
651
652 // Success/Confirmation message
653 const conf = styleSettings.advancedMode?.confirmationMessage || {}
654
655 const successBgColor = conf.backgroundColor || '#F3F4F5'
656 const confirmBorderColor =
657 conf.borderColor ||
658 styleSettings.advancedMode?.general?.formBorderColor ||
659 styleSettings.quickMode?.formBorderColor ||
660 '#8B929C'
661 const confirmBorderWidth =
662 conf.borderWidth ||
663 styleSettings.advancedMode?.general?.formBorderWidth ||
664 styleSettings.quickMode?.formBorderWidth ||
665 '1px'
666 const confirmBorderStyle = resolveBorderStyle(
667 conf.borderStyle || styleSettings.advancedMode?.general?.formBorderStyle,
668 )
669 const confirmCorner = resolveCornerRadiusValue(
670 conf.corner ||
671 styleSettings.advancedMode?.general?.formCorner ||
672 styleSettings.quickMode?.formCorner,
673 '12px',
674 )
675 const successColor = conf.textColor
676
677 css.push(`.ivyforms-form-wrapper-${formId} .ivyforms-success-message,`)
678 css.push(`.ivyforms-form-wrapper-${formId} .ivyforms-confirmation-message {`)
679 if (successBgColor) {
680 css.push(` background-color: ${successBgColor} !important;`)
681 }
682 if (successColor) {
683 css.push(` color: ${successColor} !important;`)
684 }
685 if (confirmBorderColor && confirmBorderWidth) {
686 css.push(
687 ` border: ${confirmBorderWidth} ${confirmBorderStyle} ${confirmBorderColor} !important;`,
688 )
689 } else if (confirmBorderColor || confirmBorderWidth) {
690 // Partial config
691 if (confirmBorderColor) {
692 css.push(` border-color: ${confirmBorderColor} !important;`)
693 }
694 if (confirmBorderWidth) {
695 css.push(` border-width: ${confirmBorderWidth} !important;`)
696 }
697 css.push(` border-style: ${confirmBorderStyle} !important;`)
698 } else {
699 // No border configured
700 css.push(' border: none !important;')
701 }
702 if (confirmCorner) {
703 css.push(` border-radius: ${confirmCorner} !important;`)
704 }
705 css.push(' padding: 12px 16px !important;')
706 css.push(' margin-bottom: 16px !important;')
707 css.push('}')
708
709 // Hide form wrapper border/background when confirmation is shown - but NOT for sample preview
710 if (formId !== 'sample-preview') {
711 css.push(`${baseSelector}:has(.ivyforms-success-message) {`)
712 css.push(' background: transparent !important;')
713 css.push(' border: none !important;')
714 css.push(' padding: 0 !important;')
715 css.push('}')
716 }
717
718 // Error message
719 const errorMessageColor = styleSettings.advancedMode?.fieldStates?.error?.textColor
720
721 if (errorMessageColor) {
722 css.push(`${baseSelector} .el-form-item__error {`)
723 css.push(` color: ${errorMessageColor} !important;`)
724 css.push('}')
725 }
726
727 // Rating
728 const ratingIconSize = styleSettings.advancedMode?.rating?.iconSize
729 const ratingIconColor = styleSettings.advancedMode?.rating?.iconColor
730
731 if (ratingIconSize) {
732 css.push(`${baseSelector} .ivyforms-field__rating .ivyforms-rating__star .ivyforms-icon {`)
733 css.push(` width: ${ratingIconSize} !important;`)
734 css.push(` height: ${ratingIconSize} !important;`)
735 css.push('}')
736 css.push(`${baseSelector} .ivyforms-field__rating .ivyforms-rating__star .ivyforms-icon svg {`)
737 css.push(` width: ${ratingIconSize} !important;`)
738 css.push(` height: ${ratingIconSize} !important;`)
739 css.push('}')
740 }
741
742 if (ratingIconColor) {
743 css.push(
744 `${baseSelector} .ivyforms-field__rating .ivyforms-rating__star.is-filled .ivyforms-icon,`,
745 )
746 css.push(
747 `${baseSelector} .ivyforms-field__rating .ivyforms-rating__star.is-hovered .ivyforms-icon {`,
748 )
749 css.push(` color: ${ratingIconColor} !important;`)
750 css.push('}')
751 css.push(
752 `${baseSelector} .ivyforms-field__rating .ivyforms-rating__star.is-filled .ivyforms-icon svg path,`,
753 )
754 css.push(
755 `${baseSelector} .ivyforms-field__rating .ivyforms-rating__star.is-hovered .ivyforms-icon svg path {`,
756 )
757 css.push(` fill: ${ratingIconColor} !important;`)
758 css.push(` stroke: ${ratingIconColor} !important;`)
759 css.push('}')
760 }
761
762 // Date & Time Picker
763 const dtCalendarBg = styleSettings.advancedMode?.dateTimePicker?.calendarBackground
764 const dtNumberTextColor = styleSettings.advancedMode?.dateTimePicker?.numberTextColor
765 const dtSelectionColor = styleSettings.advancedMode?.dateTimePicker?.selectionColor
766
767 if (dtCalendarBg) {
768 css.push(`.${formPopperScopeClass}.ivyforms-date-picker-popper.el-popper,`)
769 css.push(`.${formPopperScopeClass}.ivyforms-date-picker-popper .el-picker-panel,`)
770 css.push(`.${formPopperScopeClass}.ivyforms-time-popper.el-popper,`)
771 css.push(`.${formPopperScopeClass}.ivyforms-time-popper .el-time-panel {`)
772 css.push(` background-color: ${dtCalendarBg} !important;`)
773 css.push('}')
774 // Popper caret (Element Plus: .el-popper__arrow ::before draws the triangle)
775 css.push(
776 `.${formPopperScopeClass}.ivyforms-date-picker-popper.el-popper .el-popper__arrow::before,`,
777 )
778 css.push(
779 `.${formPopperScopeClass}.ivyforms-date-picker-popper.el-popper .el-popper__arrow:before,`,
780 )
781 css.push(`.${formPopperScopeClass}.ivyforms-time-popper.el-popper .el-popper__arrow::before,`)
782 css.push(`.${formPopperScopeClass}.ivyforms-time-popper.el-popper .el-popper__arrow:before {`)
783 css.push(` background: ${dtCalendarBg} !important;`)
784 css.push(` background-color: ${dtCalendarBg} !important;`)
785 css.push(` border-color: ${dtCalendarBg} !important;`)
786 css.push('}')
787 }
788 if (dtNumberTextColor) {
789 css.push(
790 `.${formPopperScopeClass}.ivyforms-date-picker-popper .el-date-table td .el-date-table-cell__text,`,
791 )
792 css.push(`.${formPopperScopeClass}.ivyforms-date-picker-popper .el-date-picker__header-label,`)
793 css.push(`.${formPopperScopeClass}.ivyforms-date-picker-popper .el-month-table td .cell,`)
794 css.push(`.${formPopperScopeClass}.ivyforms-date-picker-popper .el-year-table td .cell,`)
795 css.push(`.${formPopperScopeClass}.ivyforms-time-popper .el-time-spinner__item {`)
796 css.push(` color: ${dtNumberTextColor} !important;`)
797 css.push('}')
798 }
799 if (dtSelectionColor) {
800 css.push(
801 `.${formPopperScopeClass}.ivyforms-date-picker-popper .el-date-table td.current:not(.disabled) .el-date-table-cell__text,`,
802 )
803 css.push(
804 `.${formPopperScopeClass}.ivyforms-date-picker-popper .el-date-table td.available:hover .el-date-table-cell__text {`,
805 )
806 css.push(` background-color: ${dtSelectionColor} !important;`)
807 css.push(` border-color: ${dtSelectionColor} !important;`)
808 css.push('}')
809 css.push(`.${formPopperScopeClass}.ivyforms-time-popper .el-time-spinner__item.active,`)
810 css.push(`.${formPopperScopeClass}.ivyforms-time-popper .el-time-spinner__item:hover {`)
811 css.push(` color: ${dtSelectionColor} !important;`)
812 css.push('}')
813 css.push(
814 `.${formPopperScopeClass}.ivyforms-date-picker-popper .el-date-table td.start-date .el-date-table-cell__text,`,
815 )
816 css.push(
817 `.${formPopperScopeClass}.ivyforms-date-picker-popper .el-date-table td.end-date .el-date-table-cell__text {`,
818 )
819 css.push(` background-color: ${dtSelectionColor} !important;`)
820 css.push('}')
821 css.push(
822 `.${formPopperScopeClass}.ivyforms-date-picker-popper .el-date-table td.in-range .el-date-table-cell {`,
823 )
824 css.push(` background-color: ${applyAlpha(dtSelectionColor, '26')} !important;`)
825 css.push('}')
826 }
827
828 // Select (Dropdown)
829 const selectTagColor = styleSettings.advancedMode?.select?.tagColor
830 const selectTagTextColor = styleSettings.advancedMode?.select?.tagTextColor
831 const selectOptionHoverColor = styleSettings.advancedMode?.select?.optionHoverColor
832 const selectSelectionTextColor = styleSettings.advancedMode?.select?.selectionTextColor
833 const selectSelectionBackgroundColor =
834 styleSettings.advancedMode?.select?.selectionBackgroundColor
835
836 if (selectTagColor) {
837 css.push(`${baseSelector} .ivyforms-field__select .ivyforms-form-item-select {`)
838 css.push(` --ivyforms-select-tag-bg: ${selectTagColor} !important;`)
839 css.push('}')
840 css.push(`${baseSelector} .ivyforms-field__select .el-select .el-tag,`)
841 css.push(
842 `${baseSelector} .ivyforms-field__select .ivyforms-form-item-select .el-select__wrapper.is-focused .el-tag,`,
843 )
844 css.push(
845 `${baseSelector} .ivyforms-field__select .ivyforms-form-item-select .el-select__wrapper.is-active .el-tag,`,
846 )
847 css.push(
848 `${baseSelector} .ivyforms-field__select .ivyforms-form-item-select.is-secondary .el-select__wrapper.is-focused .el-tag,`,
849 )
850 css.push(
851 `${baseSelector} .ivyforms-field__select .ivyforms-form-item-select.is-secondary .el-select__wrapper.is-active .el-tag {`,
852 )
853 css.push(` background: ${selectTagColor} !important;`)
854 css.push(` background-color: ${selectTagColor} !important;`)
855 css.push(` border-color: ${selectTagColor} !important;`)
856 css.push('}')
857 }
858 if (selectTagTextColor) {
859 css.push(`${baseSelector} .ivyforms-field__select .ivyforms-form-item-select {`)
860 css.push(` --ivyforms-select-tag-text: ${selectTagTextColor} !important;`)
861 css.push('}')
862 css.push(`${baseSelector} .ivyforms-field__select .el-select .el-tag,`)
863 css.push(`${baseSelector} .ivyforms-field__select .el-select .el-tag .el-tag__content,`)
864 css.push(`${baseSelector} .ivyforms-field__select .el-select .el-select__tags-text,`)
865 css.push(`${baseSelector} .ivyforms-field__select .el-select .el-select__selected-item,`)
866 css.push(`${baseSelector} .ivyforms-field__select .el-select .el-tag .el-tag__close svg {`)
867 css.push(` color: ${selectTagTextColor} !important;`)
868 css.push('}')
869 }
870 if (selectOptionHoverColor) {
871 css.push(
872 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item:hover,`,
873 )
874 css.push(
875 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item.hover,`,
876 )
877 css.push(
878 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item.is-hovering {`,
879 )
880 css.push(` background-color: ${selectOptionHoverColor} !important;`)
881 css.push('}')
882 }
883 if (selectSelectionTextColor) {
884 css.push(`${baseSelector} .ivyforms-field__select .ivyforms-form-item-select {`)
885 css.push(` --ivyforms-select-option-selected-text: ${selectSelectionTextColor} !important;`)
886 css.push('}')
887 css.push(
888 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item.is-selected,`,
889 )
890 css.push(
891 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item.selected,`,
892 )
893 css.push(
894 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item.is-selected.is-hovering,`,
895 )
896 css.push(
897 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item.is-selected.is-secondary,`,
898 )
899 css.push(
900 `.${formPopperScopeClass}.ivyforms-select-popper.ivyforms-select-popper--secondary .ivyforms-select-option.el-select-dropdown__item.is-selected {`,
901 )
902 css.push(` color: ${selectSelectionTextColor} !important;`)
903 css.push('}')
904 }
905 if (selectSelectionBackgroundColor) {
906 css.push(`${baseSelector} .ivyforms-field__select .ivyforms-form-item-select {`)
907 css.push(
908 ` --ivyforms-select-option-selected-bg: ${selectSelectionBackgroundColor} !important;`,
909 )
910 css.push('}')
911 css.push(
912 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item.is-selected,`,
913 )
914 css.push(
915 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item.selected,`,
916 )
917 css.push(
918 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item.is-selected.is-hovering,`,
919 )
920 css.push(
921 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item.is-selected.is-secondary,`,
922 )
923 css.push(
924 `.${formPopperScopeClass}.ivyforms-select-popper.ivyforms-select-popper--secondary .ivyforms-select-option.el-select-dropdown__item.is-selected {`,
925 )
926 css.push(` background-color: ${selectSelectionBackgroundColor} !important;`)
927 css.push('}')
928 }
929
930 return css.join('\n')
931 }
932
933 /**
934 * Inject CSS into the document
935 *
936 * @param formId - The form ID
937 * @param css - The CSS string
938 */
939 export function injectFormCSS(formId: string | number, css: string): void {
940 const styleId = `ivyforms-style-${formId}`
941
942 const existingStyle = document.getElementById(styleId)
943 if (existingStyle) {
944 existingStyle.remove()
945 }
946
947 const styleTag = document.createElement('style')
948 styleTag.id = styleId
949 styleTag.textContent = css
950 document.head.appendChild(styleTag)
951 }
952
953 /**
954 * Generate and inject form CSS
955 *
956 * @param formId - The form ID
957 * @param styleSettings - The style settings
958 */
959 export function applyFormStyles(
960 formId: string | number,
961 styleSettings: FormStyleSettings | null,
962 ): void {
963 const css = generateFormCSS(formId, styleSettings)
964 injectFormCSS(formId, css)
965 }
966