Compare commits

...

7 Commits

Author SHA1 Message Date
Anshuman Pandey
817e108ff5 docs: adds migration docs (#7281)
Co-authored-by: Bhagya Amarasinghe <b.sithumini@yahoo.com>
2026-02-17 17:01:46 +01:00
Theodór Tómas
33542d0c54 fix: default preview colors (#7277)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2026-02-17 11:28:58 +00:00
Matti Nannt
f37d22f13d docs: align rate limiting docs with current code enforcement (#7267)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2026-02-17 07:42:53 +00:00
Anshuman Pandey
202ae903ac chore: makes rate limit config const (#7274) 2026-02-17 06:49:56 +00:00
Dhruwang Jariwala
6ab5cc367c fix: reduced default height of input (#7259) 2026-02-17 05:11:29 +00:00
Theodór Tómas
21559045ba fix: input placeholder color (#7265) 2026-02-17 05:11:01 +00:00
Theodór Tómas
d7c57a7a48 fix: disabling cache in dev (#7269) 2026-02-17 04:44:22 +00:00
26 changed files with 440 additions and 86 deletions

View File

@@ -2036,12 +2036,12 @@ checksums:
environments/workspace/look/advanced_styling_field_headline_size_description: 13debc3855e4edae992c7a1ebff599c3
environments/workspace/look/advanced_styling_field_headline_weight: 0c8b8262945c61f8e2978502362e0a42
environments/workspace/look/advanced_styling_field_headline_weight_description: 1a9c40bd76ff5098b1e48b1d3893171b
environments/workspace/look/advanced_styling_field_height: f4da6d7ecd26e3fa75cfea03abb60c00
environments/workspace/look/advanced_styling_field_height: 40ca2224bb2936ad1329091b35a9ffe2
environments/workspace/look/advanced_styling_field_indicator_bg: 00febda2901af0f1b0c17e44f9917c38
environments/workspace/look/advanced_styling_field_indicator_bg_description: 7eb3b54a8b331354ec95c0dc1545c620
environments/workspace/look/advanced_styling_field_input_border_radius_description: 0007f1bb572b35d9a3720daeb7a55617
environments/workspace/look/advanced_styling_field_input_font_size_description: 5311f95dcbd083623e35c98ea5374c3b
environments/workspace/look/advanced_styling_field_input_height_description: b704fc67e805223992c811d6f86a9c00
environments/workspace/look/advanced_styling_field_input_height_description: e19ec0dc432478def0fd1199ad765e38
environments/workspace/look/advanced_styling_field_input_padding_x_description: 10e14296468321c13fda77fd1ba58dfd
environments/workspace/look/advanced_styling_field_input_padding_y_description: 98b4aeff2940516d05ea61bdc1211d0d
environments/workspace/look/advanced_styling_field_input_placeholder_opacity_description: f55a6700884d24014404e58876121ddf

View File

@@ -118,10 +118,10 @@ export const STYLE_DEFAULTS: TProjectStyling = {
// Inputs
inputTextColor: { light: _colors["inputTextColor.light"] },
inputBorderRadius: 8,
inputHeight: 40,
inputHeight: 20,
inputFontSize: 14,
inputPaddingX: 16,
inputPaddingY: 16,
inputPaddingX: 8,
inputPaddingY: 8,
inputPlaceholderOpacity: 0.5,
inputShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
@@ -149,6 +149,42 @@ export const STYLE_DEFAULTS: TProjectStyling = {
progressIndicatorBgColor: { light: _colors["progressIndicatorBgColor.light"] },
};
/**
* Fills in new v4.7 color fields from legacy v4.6 fields when they are missing.
*
* v4.6 stored: brandColor, questionColor, inputColor, inputBorderColor.
* v4.7 adds: elementHeadlineColor, buttonBgColor, optionBgColor, etc.
*
* When loading v4.6 data the new fields are absent. Without this helper the
* form would fall back to STYLE_DEFAULTS (derived from the *default* brand
* colour), causing a visible mismatch. This function derives the new fields
* from the actually-saved legacy fields so the preview and form stay coherent.
*
* Only sets a field when the legacy source exists AND the new field is absent.
*/
export const deriveNewFieldsFromLegacy = (saved: Record<string, unknown>): Record<string, unknown> => {
const light = (key: string): string | undefined =>
(saved[key] as { light?: string } | null | undefined)?.light;
const q = light("questionColor");
const b = light("brandColor");
const i = light("inputColor");
return {
...(q && !saved.elementHeadlineColor && { elementHeadlineColor: { light: q } }),
...(q && !saved.elementDescriptionColor && { elementDescriptionColor: { light: q } }),
...(q && !saved.elementUpperLabelColor && { elementUpperLabelColor: { light: q } }),
...(q && !saved.inputTextColor && { inputTextColor: { light: q } }),
...(q && !saved.optionLabelColor && { optionLabelColor: { light: q } }),
...(b && !saved.buttonBgColor && { buttonBgColor: { light: b } }),
...(b && !saved.buttonTextColor && { buttonTextColor: { light: isLight(b) ? "#0f172a" : "#ffffff" } }),
...(i && !saved.optionBgColor && { optionBgColor: { light: i } }),
...(b && !saved.progressIndicatorBgColor && { progressIndicatorBgColor: { light: b } }),
...(b &&
!saved.progressTrackBgColor && { progressTrackBgColor: { light: mixColor(b, "#ffffff", 0.8) } }),
};
};
/**
* Builds a complete TProjectStyling object from a single brand color.
*

View File

@@ -2153,12 +2153,12 @@
"advanced_styling_field_headline_size_description": "Skaliert den Überschriftentext.",
"advanced_styling_field_headline_weight": "Schriftstärke der Überschrift",
"advanced_styling_field_headline_weight_description": "Macht den Überschriftentext heller oder fetter.",
"advanced_styling_field_height": "Höhe",
"advanced_styling_field_height": "Mindesthöhe",
"advanced_styling_field_indicator_bg": "Indikator-Hintergrund",
"advanced_styling_field_indicator_bg_description": "Färbt den gefüllten Teil des Balkens.",
"advanced_styling_field_input_border_radius_description": "Rundet die Eingabeecken ab.",
"advanced_styling_field_input_font_size_description": "Skaliert den eingegebenen Text in Eingabefeldern.",
"advanced_styling_field_input_height_description": "Steuert die Höhe des Eingabefelds.",
"advanced_styling_field_input_height_description": "Legt die Mindesthöhe des Eingabefelds fest.",
"advanced_styling_field_input_padding_x_description": "Fügt links und rechts Abstand hinzu.",
"advanced_styling_field_input_padding_y_description": "Fügt oben und unten Abstand hinzu.",
"advanced_styling_field_input_placeholder_opacity_description": "Blendet den Platzhaltertext aus.",

View File

@@ -2153,12 +2153,12 @@
"advanced_styling_field_headline_size_description": "Scales the headline text.",
"advanced_styling_field_headline_weight": "Headline Font Weight",
"advanced_styling_field_headline_weight_description": "Makes headline text lighter or bolder.",
"advanced_styling_field_height": "Height",
"advanced_styling_field_height": "Minimum Height",
"advanced_styling_field_indicator_bg": "Indicator Background",
"advanced_styling_field_indicator_bg_description": "Colors the filled portion of the bar.",
"advanced_styling_field_input_border_radius_description": "Rounds the input corners.",
"advanced_styling_field_input_font_size_description": "Scales the typed text in inputs.",
"advanced_styling_field_input_height_description": "Controls the input field height.",
"advanced_styling_field_input_height_description": "Controls the minimum height of the input field.",
"advanced_styling_field_input_padding_x_description": "Adds space on the left and right.",
"advanced_styling_field_input_padding_y_description": "Adds space on the top and bottom.",
"advanced_styling_field_input_placeholder_opacity_description": "Fades the placeholder hint text.",

View File

@@ -2153,12 +2153,12 @@
"advanced_styling_field_headline_size_description": "Escala el texto del titular.",
"advanced_styling_field_headline_weight": "Grosor de fuente del titular",
"advanced_styling_field_headline_weight_description": "Hace el texto del titular más ligero o más grueso.",
"advanced_styling_field_height": "Altura",
"advanced_styling_field_height": "Altura mínima",
"advanced_styling_field_indicator_bg": "Fondo del indicador",
"advanced_styling_field_indicator_bg_description": "Colorea la porción rellena de la barra.",
"advanced_styling_field_input_border_radius_description": "Redondea las esquinas del campo.",
"advanced_styling_field_input_font_size_description": "Escala el texto escrito en los campos.",
"advanced_styling_field_input_height_description": "Controla la altura del campo de entrada.",
"advanced_styling_field_input_height_description": "Controla la altura mínima del campo de entrada.",
"advanced_styling_field_input_padding_x_description": "Añade espacio a la izquierda y a la derecha.",
"advanced_styling_field_input_padding_y_description": "Añade espacio en la parte superior e inferior.",
"advanced_styling_field_input_placeholder_opacity_description": "Atenúa el texto de sugerencia del marcador de posición.",

View File

@@ -2153,12 +2153,12 @@
"advanced_styling_field_headline_size_description": "Ajuste la taille du texte du titre.",
"advanced_styling_field_headline_weight": "Graisse de police du titre",
"advanced_styling_field_headline_weight_description": "Rend le texte du titre plus léger ou plus gras.",
"advanced_styling_field_height": "Hauteur",
"advanced_styling_field_height": "Hauteur minimale",
"advanced_styling_field_indicator_bg": "Arrière-plan de l'indicateur",
"advanced_styling_field_indicator_bg_description": "Colore la partie remplie de la barre.",
"advanced_styling_field_input_border_radius_description": "Arrondit les coins du champ de saisie.",
"advanced_styling_field_input_font_size_description": "Ajuste la taille du texte saisi dans les champs.",
"advanced_styling_field_input_height_description": "Contrôle la hauteur du champ de saisie.",
"advanced_styling_field_input_height_description": "Contrôle la hauteur minimale du champ de saisie.",
"advanced_styling_field_input_padding_x_description": "Ajoute de l'espace à gauche et à droite.",
"advanced_styling_field_input_padding_y_description": "Ajoute de l'espace en haut et en bas.",
"advanced_styling_field_input_placeholder_opacity_description": "Atténue le texte d'indication du placeholder.",

View File

@@ -2153,12 +2153,12 @@
"advanced_styling_field_headline_size_description": "Átméretezi a címsor szövegét.",
"advanced_styling_field_headline_weight": "Címsor betűvastagsága",
"advanced_styling_field_headline_weight_description": "Vékonyabbá vagy vastagabbá teszi a címsor szövegét.",
"advanced_styling_field_height": "Magasság",
"advanced_styling_field_height": "Minimális magasság",
"advanced_styling_field_indicator_bg": "Jelző háttere",
"advanced_styling_field_indicator_bg_description": "Kiszínezi a sáv kitöltött részét.",
"advanced_styling_field_input_border_radius_description": "Lekerekíti a beviteli mező sarkait.",
"advanced_styling_field_input_font_size_description": "Átméretezi a beviteli mezőkbe beírt szöveget.",
"advanced_styling_field_input_height_description": "A beviteli mező magasságát vezérli.",
"advanced_styling_field_input_height_description": "A beviteli mező minimális magasságát szabályozza.",
"advanced_styling_field_input_padding_x_description": "Térközt ad hozzá balra és jobbra.",
"advanced_styling_field_input_padding_y_description": "Térközt ad hozzá fent és lent.",
"advanced_styling_field_input_placeholder_opacity_description": "Elhalványítja a helykitöltő súgószöveget.",

View File

@@ -2153,12 +2153,12 @@
"advanced_styling_field_headline_size_description": "見出しテキストのサイズを調整します。",
"advanced_styling_field_headline_weight": "見出しのフォントの太さ",
"advanced_styling_field_headline_weight_description": "見出しテキストを細くまたは太くします。",
"advanced_styling_field_height": "高さ",
"advanced_styling_field_height": "最小の高さ",
"advanced_styling_field_indicator_bg": "インジケーターの背景",
"advanced_styling_field_indicator_bg_description": "バーの塗りつぶし部分に色を付けます。",
"advanced_styling_field_input_border_radius_description": "入力フィールドの角を丸めます。",
"advanced_styling_field_input_font_size_description": "入力フィールド内の入力テキストのサイズを調整します。",
"advanced_styling_field_input_height_description": "入力フィールドの高さを調整します。",
"advanced_styling_field_input_height_description": "入力フィールドの最小の高さを制御します。",
"advanced_styling_field_input_padding_x_description": "左右にスペースを追加します。",
"advanced_styling_field_input_padding_y_description": "上下にスペースを追加します。",
"advanced_styling_field_input_placeholder_opacity_description": "プレースホルダーのヒントテキストを薄くします。",

View File

@@ -2153,12 +2153,12 @@
"advanced_styling_field_headline_size_description": "Schaalt de koptekst.",
"advanced_styling_field_headline_weight": "Letterdikte kop",
"advanced_styling_field_headline_weight_description": "Maakt koptekst lichter of vetter.",
"advanced_styling_field_height": "Hoogte",
"advanced_styling_field_height": "Minimale hoogte",
"advanced_styling_field_indicator_bg": "Indicatorachtergrond",
"advanced_styling_field_indicator_bg_description": "Kleurt het gevulde deel van de balk.",
"advanced_styling_field_input_border_radius_description": "Rondt de invoerhoeken af.",
"advanced_styling_field_input_font_size_description": "Schaalt de getypte tekst in invoervelden.",
"advanced_styling_field_input_height_description": "Bepaalt de hoogte van het invoerveld.",
"advanced_styling_field_input_height_description": "Bepaalt de minimale hoogte van het invoerveld.",
"advanced_styling_field_input_padding_x_description": "Voegt ruimte toe aan de linker- en rechterkant.",
"advanced_styling_field_input_padding_y_description": "Voegt ruimte toe aan de boven- en onderkant.",
"advanced_styling_field_input_placeholder_opacity_description": "Vervaagt de tijdelijke aanwijzingstekst.",

View File

@@ -2153,12 +2153,12 @@
"advanced_styling_field_headline_size_description": "Ajusta o tamanho do texto do título.",
"advanced_styling_field_headline_weight": "Peso da fonte do título",
"advanced_styling_field_headline_weight_description": "Torna o texto do título mais leve ou mais negrito.",
"advanced_styling_field_height": "Altura",
"advanced_styling_field_height": "Altura mínima",
"advanced_styling_field_indicator_bg": "Fundo do indicador",
"advanced_styling_field_indicator_bg_description": "Colore a porção preenchida da barra.",
"advanced_styling_field_input_border_radius_description": "Arredonda os cantos do campo.",
"advanced_styling_field_input_font_size_description": "Ajusta o tamanho do texto digitado nos campos.",
"advanced_styling_field_input_height_description": "Controla a altura do campo de entrada.",
"advanced_styling_field_input_height_description": "Controla a altura mínima do campo de entrada.",
"advanced_styling_field_input_padding_x_description": "Adiciona espaço à esquerda e à direita.",
"advanced_styling_field_input_padding_y_description": "Adiciona espaço na parte superior e inferior.",
"advanced_styling_field_input_placeholder_opacity_description": "Esmaece o texto de dica do placeholder.",

View File

@@ -2153,12 +2153,12 @@
"advanced_styling_field_headline_size_description": "Ajusta o tamanho do texto do título.",
"advanced_styling_field_headline_weight": "Peso da fonte do título",
"advanced_styling_field_headline_weight_description": "Torna o texto do título mais leve ou mais negrito.",
"advanced_styling_field_height": "Altura",
"advanced_styling_field_height": "Altura mínima",
"advanced_styling_field_indicator_bg": "Fundo do indicador",
"advanced_styling_field_indicator_bg_description": "Colore a porção preenchida da barra.",
"advanced_styling_field_input_border_radius_description": "Arredonda os cantos do campo.",
"advanced_styling_field_input_font_size_description": "Ajusta o tamanho do texto digitado nos campos.",
"advanced_styling_field_input_height_description": "Controla a altura do campo de entrada.",
"advanced_styling_field_input_height_description": "Controla a altura mínima do campo de entrada.",
"advanced_styling_field_input_padding_x_description": "Adiciona espaço à esquerda e à direita.",
"advanced_styling_field_input_padding_y_description": "Adiciona espaço no topo e na base.",
"advanced_styling_field_input_placeholder_opacity_description": "Atenua o texto de sugestão do placeholder.",

View File

@@ -2153,12 +2153,12 @@
"advanced_styling_field_headline_size_description": "Scalează textul titlului.",
"advanced_styling_field_headline_weight": "Grosime font titlu",
"advanced_styling_field_headline_weight_description": "Face textul titlului mai subțire sau mai îngroșat.",
"advanced_styling_field_height": "Înălțime",
"advanced_styling_field_height": "Înălțime minimă",
"advanced_styling_field_indicator_bg": "Fundal indicator",
"advanced_styling_field_indicator_bg_description": "Colorează partea umplută a barei.",
"advanced_styling_field_input_border_radius_description": "Rotunjește colțurile câmpurilor de introducere.",
"advanced_styling_field_input_font_size_description": "Scalează textul introdus în câmpuri.",
"advanced_styling_field_input_height_description": "Controlează înălțimea câmpului de introducere.",
"advanced_styling_field_input_height_description": "Controlează înălțimea minimă a câmpului de introducere.",
"advanced_styling_field_input_padding_x_description": "Adaugă spațiu la stânga și la dreapta.",
"advanced_styling_field_input_padding_y_description": "Adaugă spațiu deasupra și dedesubt.",
"advanced_styling_field_input_placeholder_opacity_description": "Estompează textul de sugestie din placeholder.",

View File

@@ -2153,12 +2153,12 @@
"advanced_styling_field_headline_size_description": "Масштабирует текст заголовка.",
"advanced_styling_field_headline_weight": "Толщина шрифта заголовка",
"advanced_styling_field_headline_weight_description": "Делает текст заголовка тоньше или жирнее.",
"advanced_styling_field_height": "Высота",
"advanced_styling_field_height": "Минимальная высота",
"advanced_styling_field_indicator_bg": "Фон индикатора",
"advanced_styling_field_indicator_bg_description": "Задаёт цвет заполненной части полосы.",
"advanced_styling_field_input_border_radius_description": "Скругляет углы полей ввода.",
"advanced_styling_field_input_font_size_description": "Масштабирует введённый текст в полях ввода.",
"advanced_styling_field_input_height_description": "Определяет высоту поля ввода.",
"advanced_styling_field_input_height_description": "Определяет минимальную высоту поля ввода.",
"advanced_styling_field_input_padding_x_description": "Добавляет отступы слева и справа.",
"advanced_styling_field_input_padding_y_description": "Добавляет пространство сверху и снизу.",
"advanced_styling_field_input_placeholder_opacity_description": "Делает текст подсказки менее заметным.",

View File

@@ -2153,12 +2153,12 @@
"advanced_styling_field_headline_size_description": "Ändrar storleken på rubriken.",
"advanced_styling_field_headline_weight": "Rubrikens teckentjocklek",
"advanced_styling_field_headline_weight_description": "Gör rubriktexten tunnare eller fetare.",
"advanced_styling_field_height": "Höjd",
"advanced_styling_field_height": "Minsta höjd",
"advanced_styling_field_indicator_bg": "Indikatorns bakgrund",
"advanced_styling_field_indicator_bg_description": "Färglägger den fyllda delen av stapeln.",
"advanced_styling_field_input_border_radius_description": "Rundar av hörnen på inmatningsfält.",
"advanced_styling_field_input_font_size_description": "Ändrar storleken på texten i inmatningsfält.",
"advanced_styling_field_input_height_description": "Styr höjden på inmatningsfältet.",
"advanced_styling_field_input_height_description": "Styr den minsta höjden på inmatningsfältet.",
"advanced_styling_field_input_padding_x_description": "Lägger till utrymme till vänster och höger.",
"advanced_styling_field_input_padding_y_description": "Lägger till utrymme upptill och nedtill.",
"advanced_styling_field_input_placeholder_opacity_description": "Tonar ut platshållartexten.",

View File

@@ -2153,12 +2153,12 @@
"advanced_styling_field_headline_size_description": "调整主标题文字大小。",
"advanced_styling_field_headline_weight": "标题字体粗细",
"advanced_styling_field_headline_weight_description": "设置主标题文字的粗细。",
"advanced_styling_field_height": "高度",
"advanced_styling_field_height": "最小高度",
"advanced_styling_field_indicator_bg": "指示器背景",
"advanced_styling_field_indicator_bg_description": "设置进度条已填充部分的颜色。",
"advanced_styling_field_input_border_radius_description": "设置输入框圆角。",
"advanced_styling_field_input_font_size_description": "调整输入框内文字大小。",
"advanced_styling_field_input_height_description": "控制输入框高度。",
"advanced_styling_field_input_height_description": "设置输入框的最小高度。",
"advanced_styling_field_input_padding_x_description": "增加输入框左右间距。",
"advanced_styling_field_input_padding_y_description": "为输入框上下添加间距。",
"advanced_styling_field_input_placeholder_opacity_description": "调整占位提示文字的透明度。",

View File

@@ -2153,12 +2153,12 @@
"advanced_styling_field_headline_size_description": "調整標題文字的大小。",
"advanced_styling_field_headline_weight": "標題字體粗細",
"advanced_styling_field_headline_weight_description": "讓標題文字變細或變粗。",
"advanced_styling_field_height": "高度",
"advanced_styling_field_height": "最小高度",
"advanced_styling_field_indicator_bg": "指示器背景",
"advanced_styling_field_indicator_bg_description": "設定進度條已填滿部分的顏色。",
"advanced_styling_field_input_border_radius_description": "調整輸入框的圓角。",
"advanced_styling_field_input_font_size_description": "調整輸入框內輸入文字的大小。",
"advanced_styling_field_input_height_description": "調整輸入欄位的高度。",
"advanced_styling_field_input_height_description": "設定輸入欄位的最小高度。",
"advanced_styling_field_input_padding_x_description": "在左右兩側增加間距。",
"advanced_styling_field_input_padding_y_description": "在上方和下方增加間距。",
"advanced_styling_field_input_placeholder_opacity_description": "讓提示文字變得更淡。",

View File

@@ -30,4 +30,4 @@ export const rateLimitConfigs = {
upload: { interval: 60, allowedPerInterval: 5, namespace: "storage:upload" }, // 5 per minute
delete: { interval: 60, allowedPerInterval: 5, namespace: "storage:delete" }, // 5 per minute
},
};
} as const;

View File

@@ -11,7 +11,12 @@ import { useTranslation } from "react-i18next";
import { TProjectStyling, ZProjectStyling } from "@formbricks/types/project";
import { TSurveyStyling, TSurveyType } from "@formbricks/types/surveys/types";
import { previewSurvey } from "@/app/lib/templates";
import { STYLE_DEFAULTS, getSuggestedColors } from "@/lib/styling/constants";
import {
COLOR_DEFAULTS,
STYLE_DEFAULTS,
deriveNewFieldsFromLegacy,
getSuggestedColors,
} from "@/lib/styling/constants";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { updateProjectAction } from "@/modules/projects/settings/actions";
import { FormStylingSettings } from "@/modules/survey/editor/components/form-styling-settings";
@@ -62,11 +67,23 @@ export const ThemeStyling = ({
? Object.fromEntries(Object.entries(savedStyling).filter(([, v]) => v != null))
: {};
const legacyFills = deriveNewFieldsFromLegacy(cleanSaved);
const form = useForm<TProjectStyling>({
defaultValues: { ...STYLE_DEFAULTS, ...cleanSaved },
defaultValues: { ...STYLE_DEFAULTS, ...legacyFills, ...cleanSaved },
resolver: zodResolver(ZProjectStyling),
});
// Brand color shown in the preview. Only updated when the user triggers
// "Suggest colors", "Save", or "Reset to default" — NOT on every keystroke
// in the brand-color picker. This prevents the loading-spinner / progress
// bar from updating while the user is still picking a colour.
const [previewBrandColor, setPreviewBrandColor] = useState<string>(
(cleanSaved as Partial<TProjectStyling>).brandColor?.light ??
STYLE_DEFAULTS.brandColor?.light ??
COLOR_DEFAULTS.brandColor
);
const [previewSurveyType, setPreviewSurveyType] = useState<TSurveyType>("link");
const [confirmResetStylingModalOpen, setConfirmResetStylingModalOpen] = useState(false);
const [confirmSuggestColorsOpen, setConfirmSuggestColorsOpen] = useState(false);
@@ -84,6 +101,7 @@ export const ThemeStyling = ({
if (updatedProjectResponse?.data) {
form.reset({ ...STYLE_DEFAULTS });
setPreviewBrandColor(STYLE_DEFAULTS.brandColor?.light ?? COLOR_DEFAULTS.brandColor);
toast.success(t("environments.workspace.look.styling_updated_successfully"));
router.refresh();
} else {
@@ -100,6 +118,9 @@ export const ThemeStyling = ({
form.setValue(key as keyof TProjectStyling, value, { shouldDirty: true });
}
// Commit brand color to the preview now that all derived colours are in sync.
setPreviewBrandColor(brandColor ?? STYLE_DEFAULTS.brandColor?.light ?? COLOR_DEFAULTS.brandColor);
toast.success(t("environments.workspace.look.suggested_colors_applied_please_save"));
setConfirmSuggestColorsOpen(false);
};
@@ -113,7 +134,11 @@ export const ThemeStyling = ({
});
if (updatedProjectResponse?.data) {
form.reset({ ...updatedProjectResponse.data.styling });
const saved = updatedProjectResponse.data.styling;
form.reset({ ...saved });
setPreviewBrandColor(
saved?.brandColor?.light ?? STYLE_DEFAULTS.brandColor?.light ?? COLOR_DEFAULTS.brandColor
);
toast.success(t("environments.workspace.look.styling_updated_successfully"));
} else {
const errorMessage = getFormattedErrorMessage(updatedProjectResponse);
@@ -249,7 +274,9 @@ export const ThemeStyling = ({
survey={previewSurvey(project.name, t)}
project={{
...project,
styling: form.watch("allowStyleOverwrite") ? form.watch() : STYLE_DEFAULTS,
styling: form.watch("allowStyleOverwrite")
? { ...form.watch(), brandColor: { light: previewBrandColor } }
: STYLE_DEFAULTS,
}}
previewType={previewSurveyType}
setPreviewType={setPreviewSurveyType}

View File

@@ -9,7 +9,7 @@ import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { TProjectStyling } from "@formbricks/types/project";
import { TSurvey, TSurveyStyling } from "@formbricks/types/surveys/types";
import { STYLE_DEFAULTS, getSuggestedColors } from "@/lib/styling/constants";
import { STYLE_DEFAULTS, deriveNewFieldsFromLegacy, getSuggestedColors } from "@/lib/styling/constants";
import { FormStylingSettings } from "@/modules/survey/editor/components/form-styling-settings";
import { LogoSettingsCard } from "@/modules/survey/editor/components/logo-settings-card";
import { AlertDialog } from "@/modules/ui/components/alert-dialog";
@@ -68,10 +68,15 @@ export const StylingView = ({
? Object.fromEntries(Object.entries(localSurvey.styling).filter(([, v]) => v != null))
: {};
const projectLegacyFills = deriveNewFieldsFromLegacy(cleanProject);
const surveyLegacyFills = deriveNewFieldsFromLegacy(cleanSurvey);
const form = useForm<TSurveyStyling>({
defaultValues: {
...STYLE_DEFAULTS,
...projectLegacyFills,
...cleanProject,
...surveyLegacyFills,
...cleanSurvey,
},
});

View File

@@ -40,7 +40,10 @@ export const SurveyInline = (props: Omit<SurveyContainerProps, "containerId">) =
isLoadingScript = true;
try {
const scriptUrl = props.appUrl ? `${props.appUrl}/js/surveys.umd.cjs` : "/js/surveys.umd.cjs";
const response = await fetch(scriptUrl);
const response = await fetch(
scriptUrl,
process.env.NODE_ENV === "development" ? { cache: "no-store" } : {}
);
if (!response.ok) {
throw new Error("Failed to load the surveys package");

View File

@@ -96,6 +96,7 @@ test.describe("Survey Styling", async () => {
expect(css).toContain("--fb-input-background-color: #eeeeee");
expect(css).toContain("--fb-input-border-color: #cccccc");
expect(css).toContain("--fb-input-text-color: #024eff");
expect(css).toContain("--fb-input-placeholder-color:");
expect(css).toContain("--fb-input-border-radius: 5px");
expect(css).toContain("--fb-input-height: 50px");
expect(css).toContain("--fb-input-font-size: 16px");

View File

@@ -4,12 +4,182 @@ description: "Formbricks Self-hosted version migration"
icon: "arrow-right"
---
## v4.7
Formbricks v4.7 introduces **typed contact attributes** with native `number` and `date` data types. This enables comparison-based segment filters (e.g. "signup date before 2025-01-01") that were previously not possible with string-only attribute values.
### What Happens Automatically
When Formbricks v4.7 starts for the first time, the data migration will:
1. Analyze all existing contact attribute keys and infer their data types (`text`, `number`, or `date`) based on the stored values
2. Update the `ContactAttributeKey` table with the detected `dataType` for each key
3. **If your instance has fewer than 1,000,000 contact attribute rows**: backfill the new `valueNumber` and `valueDate` columns inline. No manual action is needed.
4. **If your instance has 1,000,000 or more contact attribute rows**: the value backfill is skipped to avoid hitting the migration timeout. You will need to run a standalone backfill script after the upgrade.
<Info>
Most self-hosted instances have far fewer than 1,000,000 contact attribute rows (a typical setup with 100K
contacts and 5-10 attributes each lands around 500K-1M rows). If you are below the threshold, the migration
handles everything automatically and you can skip the manual backfill step below.
</Info>
### Steps to Migrate
**1. Backup your Database**
<Tabs>
<Tab title="Docker">
Before running these steps, navigate to the `formbricks` directory where your `docker-compose.yml` file is located.
```bash
docker exec formbricks-postgres-1 pg_dump -Fc -U postgres -d formbricks > formbricks_pre_v4.7_$(date +%Y%m%d_%H%M%S).dump
```
<Info>
If you run into "**No such container**", use `docker ps` to find your container name, e.g.
`formbricks_postgres_1`.
</Info>
</Tab>
<Tab title="Kubernetes">
If you are using the **in-cluster PostgreSQL** deployed by the Helm chart:
```bash
kubectl exec -n formbricks formbricks-postgresql-0 -- pg_dump -Fc -U formbricks -d formbricks > formbricks_pre_v4.7_$(date +%Y%m%d_%H%M%S).dump
```
<Info>
If your PostgreSQL pod has a different name, run `kubectl get pods -n formbricks` to find it.
</Info>
If you are using a **managed PostgreSQL** service (e.g. AWS RDS, Cloud SQL), use your provider's backup/snapshot feature or run `pg_dump` directly against the external host.
</Tab>
</Tabs>
**2. Upgrade to Formbricks v4.7**
<Tabs>
<Tab title="Docker">
```bash
# Pull the latest version
docker compose pull
# Stop the current instance
docker compose down
# Start with Formbricks v4.7
docker compose up -d
```
</Tab>
<Tab title="Kubernetes">
```bash
helm upgrade formbricks oci://ghcr.io/formbricks/helm-charts/formbricks \
-n formbricks \
--set deployment.image.tag=v4.7.0
```
<Info>
The Helm chart includes a migration Job that automatically runs Prisma schema migrations as a
PreSync hook before the new pods start. No manual migration step is needed.
</Info>
</Tab>
</Tabs>
**3. Check the Migration Logs**
After Formbricks starts, check the logs to see whether the value backfill was completed or skipped:
<Tabs>
<Tab title="Docker">
```bash
docker compose logs formbricks | grep -i "backfill"
```
</Tab>
<Tab title="Kubernetes">
```bash
# Check the application pod logs
kubectl logs -n formbricks -l app.kubernetes.io/name=formbricks --tail=200 | grep -i "backfill"
```
If the Helm migration Job ran, you can also inspect its logs:
```bash
kubectl logs -n formbricks job/formbricks-migration
```
</Tab>
</Tabs>
If you see a message like `Skipping value backfill (X rows >= 1000000 threshold)`, proceed to step 4. Otherwise, the migration is complete and no further action is needed.
**4. Run the Backfill Script (large datasets only)**
If the migration skipped the value backfill, run the standalone backfill script inside the running Formbricks container:
<Tabs>
<Tab title="Docker">
```bash
docker exec formbricks node packages/database/dist/scripts/backfill-attribute-values.js
```
<Info>Replace `formbricks` with your actual container name if it differs. Use `docker ps` to find it.</Info>
</Tab>
<Tab title="Kubernetes">
```bash
kubectl exec -n formbricks deploy/formbricks -- node packages/database/dist/scripts/backfill-attribute-values.js
```
<Info>
If your Formbricks deployment has a different name, run `kubectl get deploy -n formbricks` to find it.
</Info>
</Tab>
</Tabs>
The script will output progress as it runs:
```
========================================
Attribute Value Backfill Script
========================================
Fetching number-type attribute keys...
Found 12 number-type keys. Backfilling valueNumber...
Number backfill progress: 10/12 keys (48230 rows updated)
Number backfill progress: 12/12 keys (52104 rows updated)
Fetching date-type attribute keys...
Found 5 date-type keys. Backfilling valueDate...
Date backfill progress: 5/5 keys (31200 rows updated)
========================================
Backfill Complete!
========================================
valueNumber rows updated: 52104
valueDate rows updated: 31200
Duration: 42.3s
========================================
```
Key characteristics of the backfill script:
- **Safe to run while Formbricks is live** -- it does not lock the entire table or wrap work in a long transaction
- **Idempotent** -- it only updates rows where the typed columns are still `NULL`, so you can safely run it multiple times
- **Resumable** -- each batch commits independently, so if the process is interrupted you can re-run it and it picks up where it left off
- **No timeout risk** -- unlike the migration, this script runs outside the migration transaction and has no time limit
**5. Verify the Upgrade**
- Access your Formbricks instance at the same URL as before
- If you use contact segments with number or date filters, verify they return the expected results
- Check that existing surveys and response data are intact
---
## v4.0
<Warning>
**Important: Migration Required**
Formbricks 4 introduces additional requirements for self-hosting setups and makes a dedicated Redis cache as well as S3-compatible file storage mandatory.
Formbricks 4 introduces additional requirements for self-hosting setups and makes a dedicated Redis cache as well as S3-compatible file storage mandatory.
</Warning>
Formbricks 4.0 is a **major milestone** that sets up the technical foundation for future iterations and feature improvements. This release focuses on modernizing core infrastructure components to improve reliability, scalability, and enable advanced features going forward.
@@ -17,9 +187,11 @@ Formbricks 4.0 is a **major milestone** that sets up the technical foundation fo
### What's New in Formbricks 4.0
**🚀 New Enterprise Features:**
- **Quotas Management**: Advanced quota controls for enterprise users
**🏗️ Technical Foundation Improvements:**
- **Enhanced File Storage**: Improved file handling with better performance and reliability
- **Improved Caching**: New caching functionality improving speed, extensibility and reliability
- **Database Optimization**: Removal of unused database tables and fields for better performance
@@ -39,7 +211,8 @@ These services are already included in the updated one-click setup for self-host
We know this represents more moving parts in your infrastructure and might even introduce more complexity in hosting Formbricks, and we don't take this decision lightly. As Formbricks grows into a comprehensive Survey and Experience Management platform, we've reached a point where the simple, single-service approach was holding back our ability to deliver the reliable, feature-rich product our users demand and deserve.
By moving to dedicated, professional-grade services for these critical functions, we're building the foundation needed to deliver:
- **Enterprise-grade reliability** with proper redundancy and backup capabilities
- **Enterprise-grade reliability** with proper redundancy and backup capabilities
- **Advanced features** that require sophisticated caching and file processing
- **Better performance** through optimized, dedicated services
- **Future scalability** to support larger deployments and more complex use cases without the need to maintain two different approaches
@@ -52,7 +225,7 @@ Additional migration steps are needed if you are using a self-hosted Formbricks
### One-Click Setup
For users using our official one-click setup, we provide an automated migration using a migration script:
For users using our official one-click setup, we provide an automated migration using a migration script:
```bash
# Download the latest script
@@ -67,11 +240,11 @@ chmod +x migrate-to-v4.sh
```
This script guides you through the steps for the infrastructure migration and does the following:
- Adds a Redis service to your setup and configures it
- Adds a MinIO service (open source S3-alternative) to your setup, configures it and migrates local files to it
- Pulls the latest Formbricks image and updates your instance
### Manual Setup
If you use a different setup to host your Formbricks instance, you need to make sure to make the necessary adjustments to run Formbricks 4.0.
@@ -87,6 +260,7 @@ You need to configure the `REDIS_URL` environment variable and point it to your
To use file storage (e.g., file upload questions, image choice questions, custom survey backgrounds, etc.), you need to have S3-compatible file storage set up and connected to Formbricks.
Formbricks supports multiple storage providers (among many other S3-compatible storages):
- AWS S3
- Digital Ocean Spaces
- Hetzner Object Storage
@@ -101,6 +275,7 @@ Please make sure to set up a storage bucket with one of these solutions and then
S3_BUCKET_NAME: formbricks-uploads
S3_ENDPOINT_URL: http://minio:9000 # not needed for AWS S3
```
#### Upgrade Process
**1. Backup your Database**
@@ -112,8 +287,8 @@ docker exec formbricks-postgres-1 pg_dump -Fc -U postgres -d formbricks > formbr
```
<Info>
If you run into "**No such container**", use `docker ps` to find your container name,
e.g. `formbricks_postgres_1`.
If you run into "**No such container**", use `docker ps` to find your container name, e.g.
`formbricks_postgres_1`.
</Info>
**2. Upgrade to Formbricks 4.0**
@@ -134,6 +309,7 @@ docker compose up -d
**3. Automatic Database Migration**
When you start Formbricks 4.0 for the first time, it will **automatically**:
- Detect and apply required database schema updates
- Remove unused database tables and fields
- Optimize the database structure for better performance

View File

@@ -1,41 +1,94 @@
---
title: "Rate Limiting"
description: "Rate limiting for Formbricks"
description: "Current request rate limits in Formbricks"
icon: "timer"
---
To protect the platform from abuse and ensure fair usage, rate limiting is enforced by default on an IP-address basis. If a client exceeds the allowed number of requests within the specified time window, the API will return a `429 Too Many Requests` status code.
Formbricks applies request rate limits to protect against abuse and keep API usage fair.
## Default Rate Limits
Rate limits are scoped by identifier, depending on the endpoint:
The following rate limits apply to various endpoints:
- IP hash (for unauthenticated/client-side routes and public actions)
- API key ID (for authenticated API calls)
- User ID (for authenticated session-based calls and server actions)
- Organization ID (for follow-up email dispatch)
| **Endpoint** | **Rate Limit** | **Time Window** |
| ----------------------- | -------------- | --------------- |
| `POST /login` | 30 requests | 15 minutes |
| `POST /signup` | 30 requests | 60 minutes |
| `POST /verify-email` | 10 requests | 60 minutes |
| `POST /forgot-password` | 5 requests | 60 minutes |
| `GET /client-side-api` | 100 requests | 1 minute |
| `POST /share` | 100 requests | 60 minutes |
When a limit is exceeded, the API returns `429 Too Many Requests`.
If a request exceeds the defined rate limit, the server will respond with:
## Management API Rate Limits
These are the current limits for Management APIs:
| **Route Group** | **Limit** | **Window** | **Identifier** |
| --- | --- | --- | --- |
| `/api/v1/management/*` (except `/api/v1/management/storage`), `/api/v1/webhooks/*`, `/api/v1/integrations/*`, `/api/v1/management/me` | 100 requests | 1 minute | API key ID or session user ID |
| `/api/v2/management/*` (and other v2 authenticated routes that use `authenticatedApiClient`) | 100 requests | 1 minute | API key ID |
| `POST /api/v1/management/storage` | 5 requests | 1 minute | API key ID or session user ID |
## All Enforced Limits
| **Config** | **Limit** | **Window** | **Identifier** | **Used For** |
| --- | --- | --- | --- | --- |
| `auth.login` | 10 requests | 15 minutes | IP hash | Email/password login flow (`/api/auth/callback/credentials`) |
| `auth.signup` | 30 requests | 60 minutes | IP hash | Signup server action |
| `auth.forgotPassword` | 5 requests | 60 minutes | IP hash | Forgot password server action |
| `auth.verifyEmail` | 10 requests | 60 minutes | IP hash | Email verification callback + resend verification action |
| `api.v1` | 100 requests | 1 minute | API key ID or session user ID | v1 management, webhooks, integrations, and `/api/v1/management/me` |
| `api.v2` | 100 requests | 1 minute | API key ID | v2 authenticated API wrapper (`authenticatedApiClient`) |
| `api.client` | 100 requests | 1 minute | IP hash | v1 client API routes (except `/api/v1/client/og` and storage upload override), plus v2 routes that re-use those v1 handlers |
| `storage.upload` | 5 requests | 1 minute | IP hash or authenticated ID | Client storage upload and management storage upload |
| `storage.delete` | 5 requests | 1 minute | API key ID or session user ID | `DELETE /storage/[environmentId]/[accessType]/[fileName]` |
| `actions.emailUpdate` | 3 requests | 60 minutes | User ID | Profile email update action |
| `actions.surveyFollowUp` | 50 requests | 60 minutes | Organization ID | Survey follow-up email processing |
| `actions.sendLinkSurveyEmail` | 10 requests | 60 minutes | IP hash | Link survey email send action |
| `actions.licenseRecheck` | 5 requests | 1 minute | User ID | Enterprise license recheck action |
## Current Endpoint Exceptions
The following routes are currently not rate-limited by the server-side limiter:
- `GET /api/v1/client/og` (explicitly excluded)
- `POST /api/v2/client/[environmentId]/responses`
- `POST /api/v2/client/[environmentId]/displays`
- `GET /api/v2/health`
## 429 Response Shape
v1-style endpoints return:
```json
{
"code": 429,
"error": "Too many requests, Please try after a while!"
"code": "too_many_requests",
"message": "Maximum number of requests reached. Please try again later.",
"details": {}
}
```
v2-style endpoints return:
```json
{
"error": {
"code": 429,
"message": "Too Many Requests"
}
}
```
## Disabling Rate Limiting
For self-hosters, rate limiting can be disabled if necessary. However, we **strongly recommend keeping rate limiting enabled in production environments** to prevent abuse.
For self-hosters, rate limiting can be disabled if necessary. We strongly recommend keeping it enabled in production.
To disable rate limiting, set the following environment variable:
Set:
```bash
RATE_LIMITING_DISABLED=1
```
After making this change, restart your server to apply the new setting.
After changing this value, restart the server.
## Operational Notes
- Redis/Valkey is required for robust rate limiting (`REDIS_URL`).
- If Redis is unavailable at runtime, rate-limiter checks currently fail open (requests are allowed through without enforcement).
- Authentication failure audit logging uses a separate throttle (`shouldLogAuthFailure()`) and is intentionally **fail-closed**: when Redis is unavailable or errors occur, audit log entries are **skipped entirely** rather than written without throttle control. This prevents spam while preserving the hash-integrity chain required for compliance. In other words, if Redis is down, no authentication-failure audit logs will be recorded—requests themselves are still allowed (fail-open rate limiting above), but the audit trail for those failures will not be written.

View File

@@ -127,12 +127,12 @@
--fb-input-font-size: 14px;
--fb-input-font-weight: 400;
--fb-input-color: #414b5a;
--fb-input-placeholder-color: var(--fb-input-color);
--fb-input-placeholder-color: var(--fb-input-text-color, var(--fb-input-color));
--fb-input-placeholder-opacity: 0.5;
--fb-input-width: 100%;
--fb-input-height: 40px;
--fb-input-padding-x: 16px;
--fb-input-padding-y: 16px;
--fb-input-height: 20px;
--fb-input-padding-x: 8px;
--fb-input-padding-y: 8px;
--fb-input-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
/* ── Progress Bar ──────────────────────────────────────────────────── */
@@ -244,4 +244,4 @@
#fbjs textarea::-webkit-scrollbar-thumb:hover {
background-color: hsl(215.4 16.3% 46.9% / 0.5);
}
}

View File

@@ -443,6 +443,39 @@ describe("addCustomThemeToDom", () => {
expect(variables["--fb-button-font-size"]).toBe("1.5rem");
});
test("should derive input-placeholder-color from inputTextColor when set", () => {
const styling: TSurveyStyling = {
...getBaseProjectStyling(),
questionColor: { light: "#AABBCC" },
inputTextColor: { light: "#112233" },
};
addCustomThemeToDom({ styling });
const styleElement = document.getElementById("formbricks__css__custom") as HTMLStyleElement;
const variables = getCssVariables(styleElement);
// Placeholder should be derived from inputTextColor, not questionColor
expect(variables["--fb-input-placeholder-color"]).toBeDefined();
expect(variables["--fb-placeholder-color"]).toBeDefined();
// Both should be based on inputTextColor (#112233) mixed with white, not questionColor (#AABBCC)
// We can verify by checking the placeholder color doesn't contain the questionColor mix
expect(variables["--fb-input-placeholder-color"]).toBe(variables["--fb-placeholder-color"]);
});
test("should derive input-placeholder-color from questionColor when inputTextColor is not set", () => {
const styling: TSurveyStyling = {
...getBaseProjectStyling(),
questionColor: { light: "#AABBCC" },
};
addCustomThemeToDom({ styling });
const styleElement = document.getElementById("formbricks__css__custom") as HTMLStyleElement;
const variables = getCssVariables(styleElement);
// Placeholder should fall back to questionColor when inputTextColor is not set
expect(variables["--fb-input-placeholder-color"]).toBeDefined();
expect(variables["--fb-placeholder-color"]).toBeDefined();
expect(variables["--fb-input-placeholder-color"]).toBe(variables["--fb-placeholder-color"]);
});
test("should set signature and branding text colors for dark questionColor", () => {
const styling = getBaseProjectStyling({
questionColor: { light: "#202020" }, // A dark color

View File

@@ -111,8 +111,10 @@ export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TS
// Backwards-compat: legacy variables still used by some consumers/tests
appendCssVariable("subheading-color", styling.questionColor?.light);
if (styling.questionColor?.light) {
appendCssVariable("placeholder-color", mixColor(styling.questionColor.light, "#ffffff", 0.3));
const placeholderBaseColor = styling.inputTextColor?.light ?? styling.questionColor?.light;
if (placeholderBaseColor) {
appendCssVariable("placeholder-color", mixColor(placeholderBaseColor, "#ffffff", 0.3));
appendCssVariable("input-placeholder-color", mixColor(placeholderBaseColor, "#ffffff", 0.3));
}
appendCssVariable("border-color", styling.inputBorderColor?.light);
@@ -192,8 +194,13 @@ export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TS
}
// Buttons (Advanced)
appendCssVariable("button-bg-color", styling.buttonBgColor?.light);
appendCssVariable("button-text-color", styling.buttonTextColor?.light);
const buttonBg = styling.buttonBgColor?.light ?? styling.brandColor?.light;
let buttonText = styling.buttonTextColor?.light;
if (buttonText === undefined && buttonBg) {
buttonText = isLight(buttonBg) ? "#0f172a" : "#ffffff";
}
appendCssVariable("button-bg-color", buttonBg);
appendCssVariable("button-text-color", buttonText);
if (styling.buttonBorderRadius !== undefined)
appendCssVariable("button-border-radius", formatDimension(styling.buttonBorderRadius));
if (styling.buttonHeight !== undefined)
@@ -209,7 +216,11 @@ export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TS
// Inputs (Advanced)
appendCssVariable("input-background-color", styling.inputBgColor?.light ?? styling.inputColor?.light);
appendCssVariable("input-text-color", styling.inputTextColor?.light);
const inputTextColor = styling.inputTextColor?.light ?? styling.questionColor?.light;
appendCssVariable("input-text-color", inputTextColor);
if (inputTextColor) {
appendCssVariable("input-placeholder-color", mixColor(inputTextColor, "#ffffff", 0.3));
}
if (styling.inputBorderRadius !== undefined)
appendCssVariable("input-border-radius", formatDimension(styling.inputBorderRadius));
if (styling.inputHeight !== undefined)
@@ -225,8 +236,8 @@ export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TS
appendCssVariable("input-shadow", styling.inputShadow);
// Options (Advanced)
appendCssVariable("option-bg-color", styling.optionBgColor?.light);
appendCssVariable("option-label-color", styling.optionLabelColor?.light);
appendCssVariable("option-bg-color", styling.optionBgColor?.light ?? styling.inputColor?.light);
appendCssVariable("option-label-color", styling.optionLabelColor?.light ?? styling.questionColor?.light);
if (styling.optionBorderRadius !== undefined)
appendCssVariable("option-border-radius", formatDimension(styling.optionBorderRadius));
if (styling.optionPaddingX !== undefined)
@@ -277,8 +288,15 @@ export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TS
// Implicitly set the progress track border radius to the roundness of the card
appendCssVariable("progress-track-border-radius", formatDimension(roundness));
appendCssVariable("progress-track-bg-color", styling.progressTrackBgColor?.light);
appendCssVariable("progress-indicator-bg-color", styling.progressIndicatorBgColor?.light);
appendCssVariable(
"progress-track-bg-color",
styling.progressTrackBgColor?.light ??
(styling.brandColor?.light ? mixColor(styling.brandColor.light, "#ffffff", 0.8) : undefined)
);
appendCssVariable(
"progress-indicator-bg-color",
styling.progressIndicatorBgColor?.light ?? styling.brandColor?.light
);
// Close the #fbjs variable block
cssVariables += "}\n";
@@ -304,7 +322,7 @@ export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TS
headlineDecls += " font-size: var(--fb-element-headline-font-size) !important;\n";
if (styling.elementHeadlineFontWeight !== undefined && styling.elementHeadlineFontWeight !== null)
headlineDecls += " font-weight: var(--fb-element-headline-font-weight) !important;\n";
if (styling.elementHeadlineColor?.light)
if (styling.elementHeadlineColor?.light || styling.questionColor?.light)
headlineDecls += " color: var(--fb-element-headline-color) !important;\n";
addRule("#fbjs .label-headline,\n#fbjs .label-headline *", headlineDecls);
@@ -314,7 +332,7 @@ export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TS
descriptionDecls += " font-size: var(--fb-element-description-font-size) !important;\n";
if (styling.elementDescriptionFontWeight !== undefined && styling.elementDescriptionFontWeight !== null)
descriptionDecls += " font-weight: var(--fb-element-description-font-weight) !important;\n";
if (styling.elementDescriptionColor?.light)
if (styling.elementDescriptionColor?.light || styling.questionColor?.light)
descriptionDecls += " color: var(--fb-element-description-color) !important;\n";
addRule("#fbjs .label-description,\n#fbjs .label-description *", descriptionDecls);
@@ -324,7 +342,7 @@ export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TS
upperDecls += " font-size: var(--fb-element-upper-label-font-size) !important;\n";
if (styling.elementUpperLabelFontWeight !== undefined && styling.elementUpperLabelFontWeight !== null)
upperDecls += " font-weight: var(--fb-element-upper-label-font-weight) !important;\n";
if (styling.elementUpperLabelColor?.light) {
if (styling.elementUpperLabelColor?.light || styling.questionColor?.light) {
upperDecls += " color: var(--fb-element-upper-label-color) !important;\n";
upperDecls += " opacity: var(--fb-element-upper-label-opacity, 1) !important;\n";
}
@@ -332,9 +350,10 @@ export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TS
// --- Buttons ---
let buttonDecls = "";
if (styling.buttonBgColor?.light)
if (styling.buttonBgColor?.light || styling.brandColor?.light)
buttonDecls += " background-color: var(--fb-button-bg-color) !important;\n";
if (styling.buttonTextColor?.light) buttonDecls += " color: var(--fb-button-text-color) !important;\n";
if (styling.buttonTextColor?.light || styling.brandColor?.light)
buttonDecls += " color: var(--fb-button-text-color) !important;\n";
if (styling.buttonBorderRadius !== undefined)
buttonDecls += " border-radius: var(--fb-button-border-radius) !important;\n";
if (styling.buttonHeight !== undefined) buttonDecls += " height: var(--fb-button-height) !important;\n";
@@ -355,11 +374,11 @@ export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TS
// --- Options ---
if (styling.optionBorderRadius !== undefined)
addRule("#fbjs .rounded-option", " border-radius: var(--fb-option-border-radius) !important;\n");
if (styling.optionBgColor?.light)
if (styling.optionBgColor?.light || styling.inputColor?.light)
addRule("#fbjs .bg-option-bg", " background-color: var(--fb-option-bg-color) !important;\n");
let optionLabelDecls = "";
if (styling.optionLabelColor?.light)
if (styling.optionLabelColor?.light || styling.questionColor?.light)
optionLabelDecls += " color: var(--fb-option-label-color) !important;\n";
if (styling.optionFontSize !== undefined)
optionLabelDecls += " font-size: var(--fb-option-font-size) !important;\n";
@@ -385,7 +404,8 @@ export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TS
addRule("#fbjs .border-input-border", " border-color: var(--fb-input-border-color) !important;\n");
let inputTextDecls = "";
if (styling.inputTextColor?.light) inputTextDecls += " color: var(--fb-input-text-color) !important;\n";
if (styling.inputTextColor?.light || styling.questionColor?.light)
inputTextDecls += " color: var(--fb-input-text-color) !important;\n";
if (styling.inputFontSize !== undefined)
inputTextDecls += " font-size: var(--fb-input-font-size) !important;\n";
addRule("#fbjs .text-input-text", inputTextDecls);