mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-19 18:18:48 -06:00
Co-authored-by: Theodór Tómas <theodortomas@gmail.com>
This commit is contained in:
committed by
GitHub
parent
91486f3dc9
commit
e3f3516fa7
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -194,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)
|
||||
@@ -211,12 +216,10 @@ 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);
|
||||
if (styling.inputTextColor?.light) {
|
||||
appendCssVariable(
|
||||
"input-placeholder-color",
|
||||
mixColor(styling.inputTextColor.light, "#ffffff", 0.3)
|
||||
);
|
||||
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));
|
||||
@@ -233,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)
|
||||
@@ -285,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";
|
||||
@@ -312,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);
|
||||
|
||||
@@ -322,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);
|
||||
|
||||
@@ -332,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";
|
||||
}
|
||||
@@ -340,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";
|
||||
@@ -363,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";
|
||||
@@ -393,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);
|
||||
|
||||
Reference in New Issue
Block a user