PluginProbe
The Innovative Form Builder – IvyForms / 1.0
The Innovative Form Builder – IvyForms v1.0
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.0, at frontend/src/utils/formStyleGenerator.ts

962 lines 38.2 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 // Submit button (IvyButtonAction component)
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 if (buttonBackground) {
608 css.push(` background-color: ${buttonBackground} !important;`)
609 }
610 if (buttonTextColor) {
611 css.push(` color: ${buttonTextColor} !important;`)
612 }
613 if (buttonFontWeight) {
614 css.push(` font-weight: ${buttonFontWeight} !important;`)
615 }
616 if (buttonFontStyle) {
617 css.push(` font-style: ${buttonFontStyle} !important;`)
618 }
619 if (buttonFontSize) {
620 css.push(` font-size: ${buttonFontSize} !important;`)
621 }
622 css.push(` padding: ${buttonPaddingValue} !important;`)
623 if (buttonWidthValue) {
624 css.push(` width: ${buttonWidthValue} !important;`)
625 }
626 if (buttonHeightValue) {
627 css.push(` height: ${buttonHeightValue} !important;`)
628 }
629 if (buttonBorderWidth && buttonBorderWidth !== '0px') {
630 css.push(
631 ` border: ${buttonBorderWidth} solid ${buttonBorderColor || buttonBackground} !important;`,
632 )
633 } else {
634 css.push(` border: none !important;`)
635 }
636 if (buttonCornerRadius) {
637 css.push(` border-radius: ${buttonCornerRadius} !important;`)
638 }
639 css.push('}')
640
641 // Button hover state - simple opacity effect (submit only)
642 css.push(`${baseSelector} .ivyforms-form-submit .ivyforms-button-action:hover,`)
643 css.push(`${baseSelector} .ivyforms-form-submit button.ivyforms-button-action:hover {`)
644 css.push(' opacity: 0.9 !important;')
645 css.push(' transform: translateY(-1px) !important;')
646 css.push('}')
647
648 // Success/Confirmation message
649 const conf = styleSettings.advancedMode?.confirmationMessage || {}
650
651 const successBgColor = conf.backgroundColor || '#F3F4F5'
652 const confirmBorderColor =
653 conf.borderColor ||
654 styleSettings.advancedMode?.general?.formBorderColor ||
655 styleSettings.quickMode?.formBorderColor ||
656 '#8B929C'
657 const confirmBorderWidth =
658 conf.borderWidth ||
659 styleSettings.advancedMode?.general?.formBorderWidth ||
660 styleSettings.quickMode?.formBorderWidth ||
661 '1px'
662 const confirmBorderStyle = resolveBorderStyle(
663 conf.borderStyle || styleSettings.advancedMode?.general?.formBorderStyle,
664 )
665 const confirmCorner = resolveCornerRadiusValue(
666 conf.corner ||
667 styleSettings.advancedMode?.general?.formCorner ||
668 styleSettings.quickMode?.formCorner,
669 '12px',
670 )
671 const successColor = conf.textColor
672
673 css.push(`.ivyforms-form-wrapper-${formId} .ivyforms-success-message,`)
674 css.push(`.ivyforms-form-wrapper-${formId} .ivyforms-confirmation-message {`)
675 if (successBgColor) {
676 css.push(` background-color: ${successBgColor} !important;`)
677 }
678 if (successColor) {
679 css.push(` color: ${successColor} !important;`)
680 }
681 if (confirmBorderColor && confirmBorderWidth) {
682 css.push(
683 ` border: ${confirmBorderWidth} ${confirmBorderStyle} ${confirmBorderColor} !important;`,
684 )
685 } else if (confirmBorderColor || confirmBorderWidth) {
686 // Partial config
687 if (confirmBorderColor) {
688 css.push(` border-color: ${confirmBorderColor} !important;`)
689 }
690 if (confirmBorderWidth) {
691 css.push(` border-width: ${confirmBorderWidth} !important;`)
692 }
693 css.push(` border-style: ${confirmBorderStyle} !important;`)
694 } else {
695 // No border configured
696 css.push(' border: none !important;')
697 }
698 if (confirmCorner) {
699 css.push(` border-radius: ${confirmCorner} !important;`)
700 }
701 css.push(' padding: 12px 16px !important;')
702 css.push(' margin-bottom: 16px !important;')
703 css.push('}')
704
705 // Hide form wrapper border/background when confirmation is shown - but NOT for sample preview
706 if (formId !== 'sample-preview') {
707 css.push(`${baseSelector}:has(.ivyforms-success-message) {`)
708 css.push(' background: transparent !important;')
709 css.push(' border: none !important;')
710 css.push(' padding: 0 !important;')
711 css.push('}')
712 }
713
714 // Error message
715 const errorMessageColor = styleSettings.advancedMode?.fieldStates?.error?.textColor
716
717 if (errorMessageColor) {
718 css.push(`${baseSelector} .el-form-item__error {`)
719 css.push(` color: ${errorMessageColor} !important;`)
720 css.push('}')
721 }
722
723 // Rating
724 const ratingIconSize = styleSettings.advancedMode?.rating?.iconSize
725 const ratingIconColor = styleSettings.advancedMode?.rating?.iconColor
726
727 if (ratingIconSize) {
728 css.push(`${baseSelector} .ivyforms-field__rating .ivyforms-rating__star .ivyforms-icon {`)
729 css.push(` width: ${ratingIconSize} !important;`)
730 css.push(` height: ${ratingIconSize} !important;`)
731 css.push('}')
732 css.push(`${baseSelector} .ivyforms-field__rating .ivyforms-rating__star .ivyforms-icon svg {`)
733 css.push(` width: ${ratingIconSize} !important;`)
734 css.push(` height: ${ratingIconSize} !important;`)
735 css.push('}')
736 }
737
738 if (ratingIconColor) {
739 css.push(
740 `${baseSelector} .ivyforms-field__rating .ivyforms-rating__star.is-filled .ivyforms-icon,`,
741 )
742 css.push(
743 `${baseSelector} .ivyforms-field__rating .ivyforms-rating__star.is-hovered .ivyforms-icon {`,
744 )
745 css.push(` color: ${ratingIconColor} !important;`)
746 css.push('}')
747 css.push(
748 `${baseSelector} .ivyforms-field__rating .ivyforms-rating__star.is-filled .ivyforms-icon svg path,`,
749 )
750 css.push(
751 `${baseSelector} .ivyforms-field__rating .ivyforms-rating__star.is-hovered .ivyforms-icon svg path {`,
752 )
753 css.push(` fill: ${ratingIconColor} !important;`)
754 css.push(` stroke: ${ratingIconColor} !important;`)
755 css.push('}')
756 }
757
758 // Date & Time Picker
759 const dtCalendarBg = styleSettings.advancedMode?.dateTimePicker?.calendarBackground
760 const dtNumberTextColor = styleSettings.advancedMode?.dateTimePicker?.numberTextColor
761 const dtSelectionColor = styleSettings.advancedMode?.dateTimePicker?.selectionColor
762
763 if (dtCalendarBg) {
764 css.push(`.${formPopperScopeClass}.ivyforms-date-picker-popper.el-popper,`)
765 css.push(`.${formPopperScopeClass}.ivyforms-date-picker-popper .el-picker-panel,`)
766 css.push(`.${formPopperScopeClass}.ivyforms-time-popper.el-popper,`)
767 css.push(`.${formPopperScopeClass}.ivyforms-time-popper .el-time-panel {`)
768 css.push(` background-color: ${dtCalendarBg} !important;`)
769 css.push('}')
770 // Popper caret (Element Plus: .el-popper__arrow ::before draws the triangle)
771 css.push(
772 `.${formPopperScopeClass}.ivyforms-date-picker-popper.el-popper .el-popper__arrow::before,`,
773 )
774 css.push(
775 `.${formPopperScopeClass}.ivyforms-date-picker-popper.el-popper .el-popper__arrow:before,`,
776 )
777 css.push(`.${formPopperScopeClass}.ivyforms-time-popper.el-popper .el-popper__arrow::before,`)
778 css.push(`.${formPopperScopeClass}.ivyforms-time-popper.el-popper .el-popper__arrow:before {`)
779 css.push(` background: ${dtCalendarBg} !important;`)
780 css.push(` background-color: ${dtCalendarBg} !important;`)
781 css.push(` border-color: ${dtCalendarBg} !important;`)
782 css.push('}')
783 }
784 if (dtNumberTextColor) {
785 css.push(
786 `.${formPopperScopeClass}.ivyforms-date-picker-popper .el-date-table td .el-date-table-cell__text,`,
787 )
788 css.push(`.${formPopperScopeClass}.ivyforms-date-picker-popper .el-date-picker__header-label,`)
789 css.push(`.${formPopperScopeClass}.ivyforms-date-picker-popper .el-month-table td .cell,`)
790 css.push(`.${formPopperScopeClass}.ivyforms-date-picker-popper .el-year-table td .cell,`)
791 css.push(`.${formPopperScopeClass}.ivyforms-time-popper .el-time-spinner__item {`)
792 css.push(` color: ${dtNumberTextColor} !important;`)
793 css.push('}')
794 }
795 if (dtSelectionColor) {
796 css.push(
797 `.${formPopperScopeClass}.ivyforms-date-picker-popper .el-date-table td.current:not(.disabled) .el-date-table-cell__text,`,
798 )
799 css.push(
800 `.${formPopperScopeClass}.ivyforms-date-picker-popper .el-date-table td.available:hover .el-date-table-cell__text {`,
801 )
802 css.push(` background-color: ${dtSelectionColor} !important;`)
803 css.push(` border-color: ${dtSelectionColor} !important;`)
804 css.push('}')
805 css.push(`.${formPopperScopeClass}.ivyforms-time-popper .el-time-spinner__item.active,`)
806 css.push(`.${formPopperScopeClass}.ivyforms-time-popper .el-time-spinner__item:hover {`)
807 css.push(` color: ${dtSelectionColor} !important;`)
808 css.push('}')
809 css.push(
810 `.${formPopperScopeClass}.ivyforms-date-picker-popper .el-date-table td.start-date .el-date-table-cell__text,`,
811 )
812 css.push(
813 `.${formPopperScopeClass}.ivyforms-date-picker-popper .el-date-table td.end-date .el-date-table-cell__text {`,
814 )
815 css.push(` background-color: ${dtSelectionColor} !important;`)
816 css.push('}')
817 css.push(
818 `.${formPopperScopeClass}.ivyforms-date-picker-popper .el-date-table td.in-range .el-date-table-cell {`,
819 )
820 css.push(` background-color: ${applyAlpha(dtSelectionColor, '26')} !important;`)
821 css.push('}')
822 }
823
824 // Select (Dropdown)
825 const selectTagColor = styleSettings.advancedMode?.select?.tagColor
826 const selectTagTextColor = styleSettings.advancedMode?.select?.tagTextColor
827 const selectOptionHoverColor = styleSettings.advancedMode?.select?.optionHoverColor
828 const selectSelectionTextColor = styleSettings.advancedMode?.select?.selectionTextColor
829 const selectSelectionBackgroundColor =
830 styleSettings.advancedMode?.select?.selectionBackgroundColor
831
832 if (selectTagColor) {
833 css.push(`${baseSelector} .ivyforms-field__select .ivyforms-form-item-select {`)
834 css.push(` --ivyforms-select-tag-bg: ${selectTagColor} !important;`)
835 css.push('}')
836 css.push(`${baseSelector} .ivyforms-field__select .el-select .el-tag,`)
837 css.push(
838 `${baseSelector} .ivyforms-field__select .ivyforms-form-item-select .el-select__wrapper.is-focused .el-tag,`,
839 )
840 css.push(
841 `${baseSelector} .ivyforms-field__select .ivyforms-form-item-select .el-select__wrapper.is-active .el-tag,`,
842 )
843 css.push(
844 `${baseSelector} .ivyforms-field__select .ivyforms-form-item-select.is-secondary .el-select__wrapper.is-focused .el-tag,`,
845 )
846 css.push(
847 `${baseSelector} .ivyforms-field__select .ivyforms-form-item-select.is-secondary .el-select__wrapper.is-active .el-tag {`,
848 )
849 css.push(` background: ${selectTagColor} !important;`)
850 css.push(` background-color: ${selectTagColor} !important;`)
851 css.push(` border-color: ${selectTagColor} !important;`)
852 css.push('}')
853 }
854 if (selectTagTextColor) {
855 css.push(`${baseSelector} .ivyforms-field__select .ivyforms-form-item-select {`)
856 css.push(` --ivyforms-select-tag-text: ${selectTagTextColor} !important;`)
857 css.push('}')
858 css.push(`${baseSelector} .ivyforms-field__select .el-select .el-tag,`)
859 css.push(`${baseSelector} .ivyforms-field__select .el-select .el-tag .el-tag__content,`)
860 css.push(`${baseSelector} .ivyforms-field__select .el-select .el-select__tags-text,`)
861 css.push(`${baseSelector} .ivyforms-field__select .el-select .el-select__selected-item,`)
862 css.push(`${baseSelector} .ivyforms-field__select .el-select .el-tag .el-tag__close svg {`)
863 css.push(` color: ${selectTagTextColor} !important;`)
864 css.push('}')
865 }
866 if (selectOptionHoverColor) {
867 css.push(
868 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item:hover,`,
869 )
870 css.push(
871 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item.hover,`,
872 )
873 css.push(
874 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item.is-hovering {`,
875 )
876 css.push(` background-color: ${selectOptionHoverColor} !important;`)
877 css.push('}')
878 }
879 if (selectSelectionTextColor) {
880 css.push(`${baseSelector} .ivyforms-field__select .ivyforms-form-item-select {`)
881 css.push(` --ivyforms-select-option-selected-text: ${selectSelectionTextColor} !important;`)
882 css.push('}')
883 css.push(
884 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item.is-selected,`,
885 )
886 css.push(
887 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item.selected,`,
888 )
889 css.push(
890 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item.is-selected.is-hovering,`,
891 )
892 css.push(
893 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item.is-selected.is-secondary,`,
894 )
895 css.push(
896 `.${formPopperScopeClass}.ivyforms-select-popper.ivyforms-select-popper--secondary .ivyforms-select-option.el-select-dropdown__item.is-selected {`,
897 )
898 css.push(` color: ${selectSelectionTextColor} !important;`)
899 css.push('}')
900 }
901 if (selectSelectionBackgroundColor) {
902 css.push(`${baseSelector} .ivyforms-field__select .ivyforms-form-item-select {`)
903 css.push(
904 ` --ivyforms-select-option-selected-bg: ${selectSelectionBackgroundColor} !important;`,
905 )
906 css.push('}')
907 css.push(
908 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item.is-selected,`,
909 )
910 css.push(
911 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item.selected,`,
912 )
913 css.push(
914 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item.is-selected.is-hovering,`,
915 )
916 css.push(
917 `.${formPopperScopeClass}.ivyforms-select-popper .ivyforms-select-option.el-select-dropdown__item.is-selected.is-secondary,`,
918 )
919 css.push(
920 `.${formPopperScopeClass}.ivyforms-select-popper.ivyforms-select-popper--secondary .ivyforms-select-option.el-select-dropdown__item.is-selected {`,
921 )
922 css.push(` background-color: ${selectSelectionBackgroundColor} !important;`)
923 css.push('}')
924 }
925
926 return css.join('\n')
927 }
928
929 /**
930 * Inject CSS into the document
931 *
932 * @param formId - The form ID
933 * @param css - The CSS string
934 */
935 export function injectFormCSS(formId: string | number, css: string): void {
936 const styleId = `ivyforms-style-${formId}`
937
938 const existingStyle = document.getElementById(styleId)
939 if (existingStyle) {
940 existingStyle.remove()
941 }
942
943 const styleTag = document.createElement('style')
944 styleTag.id = styleId
945 styleTag.textContent = css
946 document.head.appendChild(styleTag)
947 }
948
949 /**
950 * Generate and inject form CSS
951 *
952 * @param formId - The form ID
953 * @param styleSettings - The style settings
954 */
955 export function applyFormStyles(
956 formId: string | number,
957 styleSettings: FormStyleSettings | null,
958 ): void {
959 const css = generateFormCSS(formId, styleSettings)
960 injectFormCSS(formId, css)
961 }
962