fix: derive styling colors from brand color and add description font weight

- Adjust suggest colors mix ratios to match original behavior (inputs at
  92% white, input borders at 60% white, question color at 35% black)
- Derive button, progress, input, option, headline, and accent colors
  from the saved brand color on initial load via getBrandDerivedDefaults
- Update CSS defaults in globals.css to reference --fb-survey-brand-color
- Add elementDescriptionFontWeight field to styling type, form, and
  addCustomThemeToDom
- Add e2e test for initial load brand color derivation
This commit is contained in:
TheodorTomas
2026-02-06 17:43:57 +08:00
parent d31caf37ab
commit 8f02655925
8 changed files with 79 additions and 29 deletions

View File

@@ -1934,6 +1934,7 @@ checksums:
environments/workspace/look/advanced_styling_field_button_text: 3304e88bcc3869f3a306634b541e1e07
environments/workspace/look/advanced_styling_field_description_color: e2f4cbc96d3f0b75837a9edc95a5eeda
environments/workspace/look/advanced_styling_field_description_size: a0d51c3ab7dc56320ecedc2b27917842
environments/workspace/look/advanced_styling_field_description_weight: 514680cc7202ad29835c1cbcde3def1c
environments/workspace/look/advanced_styling_field_font_size: ca44d14429b2175a1b194793b4ab8f6b
environments/workspace/look/advanced_styling_field_font_weight: bfef83778146cf40550df9650d8a07da
environments/workspace/look/advanced_styling_field_headline_color: 4ccf3935ad90c88ad4add24f498673ce

View File

@@ -157,20 +157,22 @@ export const getSuggestedColors = (brandColor: string) => {
// Accent text: darken the brand if it's light, otherwise use as-is
const accentText = isBrandLight ? mixColor(brandColor, "#000000", 0.6) : brandColor;
// Dark text: mostly dark with a hint of brand
const darkText = mixColor(brandColor, "#0f172a", 0.9);
// Input / option background: almost white with a touch of brand
const inputBg = mixColor(brandColor, "#ffffff", 0.97);
// Question / dark text: brand darkened with black (visible brand tint)
const questionColor = mixColor(brandColor, "#000000", 0.35);
// Input / option background: white with noticeable brand tint
const inputBg = mixColor(brandColor, "#ffffff", 0.92);
// Input border: visible brand-tinted border
const inputBorder = mixColor(brandColor, "#ffffff", 0.6);
// Card tones
const cardBg = mixColor(brandColor, "#ffffff", 0.96);
const cardBorder = mixColor(brandColor, "#ffffff", 0.9);
const cardBg = mixColor(brandColor, "#ffffff", 0.97);
const cardBorder = mixColor(brandColor, "#ffffff", 0.8);
// Page background
const pageBg = mixColor(brandColor, "#ffffff", 0.95);
const pageBg = mixColor(brandColor, "#ffffff", 0.855);
return {
// General
"brandColor.light": brandColor,
"questionColor.light": darkText,
"questionColor.light": questionColor,
// Accent
"accentBgColor.light": brandColor,
@@ -187,19 +189,19 @@ export const getSuggestedColors = (brandColor: string) => {
// Inputs
"inputColor.light": inputBg,
"inputBorderColor.light": cardBorder,
"inputTextColor.light": darkText,
"inputBorderColor.light": inputBorder,
"inputTextColor.light": questionColor,
// Options (Radio / Checkbox)
"optionBgColor.light": inputBg,
"optionLabelColor.light": darkText,
"optionLabelColor.light": questionColor,
// Card
"cardBackgroundColor.light": cardBg,
"cardBorderColor.light": cardBorder,
// Highlight / Focus
"highlightBorderColor.light": brandColor,
"highlightBorderColor.light": mixColor(brandColor, "#ffffff", 0.25),
// Progress Bar
"progressIndicatorBgColor.light": brandColor,

View File

@@ -111,6 +111,11 @@ export const FormStylingSettings = ({
name="elementHeadlineFontWeight"
label={t("environments.workspace.look.advanced_styling_field_headline_weight")}
/>
<NumberField
form={form}
name="elementDescriptionFontWeight"
label={t("environments.workspace.look.advanced_styling_field_description_weight")}
/>
<ColorField
form={form}
name="elementUpperLabelColor.light"

View File

@@ -193,9 +193,7 @@ test.describe("Survey Styling", async () => {
await page.waitForTimeout(1000);
// Verify non-color CSS vars are set before suggesting
let cssBefore = await page.evaluate(
() => document.getElementById("formbricks__css__custom")?.innerHTML
);
let cssBefore = await page.evaluate(() => document.getElementById("formbricks__css__custom")?.innerHTML);
expect(cssBefore).toContain("--fb-input-border-radius: 12px");
expect(cssBefore).toContain("--fb-input-padding-y: 20px");
expect(cssBefore).toContain("--fb-option-border-radius: 10px");
@@ -214,9 +212,7 @@ test.describe("Survey Styling", async () => {
// Wait for the preview to update with derived colors
await page.waitForTimeout(1500);
const css = await page.evaluate(
() => document.getElementById("formbricks__css__custom")?.innerHTML
);
const css = await page.evaluate(() => document.getElementById("formbricks__css__custom")?.innerHTML);
expect(css).toBeDefined();
// --- Verify colors ARE derived from brand (not hardcoded) ---
@@ -255,6 +251,48 @@ test.describe("Survey Styling", async () => {
expect(css).toContain("--fb-option-padding-y: 14px");
});
test("Initial load derives button, progress, input, and option colors from brand color", async ({
page,
users,
}) => {
const user = await users.create();
await user.login();
// Navigate to Look & Feel settings
await page.getByRole("link", { name: "Configuration" }).click();
await page.getByRole("link", { name: "Look & Feel" }).click();
await page.waitForURL(/\/environments\/[^/]+\/workspace\/look/);
// Toggle "Enable custom styling"
const addCustomStyles = page.getByLabel("Enable custom styling");
if (!(await addCustomStyles.isChecked())) {
await addCustomStyles.click();
}
// Wait for the preview to render with default styling
await page.waitForTimeout(1500);
const css = await page.evaluate(() => document.getElementById("formbricks__css__custom")?.innerHTML);
expect(css).toBeDefined();
// On initial load (no saved styling), button and progress bar should derive from brand color (#64748b)
// NOT from the old hardcoded dark navy (#0f172a)
expect(css).not.toContain("--fb-button-bg-color: #0f172a");
expect(css).not.toContain("--fb-progress-indicator-bg-color: #0f172a");
// Input text and option label colors should be brand-derived, not hardcoded
expect(css).toContain("--fb-input-text-color:");
expect(css).not.toContain("--fb-input-text-color: #0f172a");
expect(css).toContain("--fb-option-label-color:");
expect(css).not.toContain("--fb-option-label-color: #0f172a");
// Option background should be brand-tinted, not plain white
expect(css).toContain("--fb-option-bg-color:");
// Headline color should be brand-derived
expect(css).toContain("--fb-element-headline-color:");
});
test("Survey Specific Styling (Survey Editor Override)", async ({ page, users }) => {
const user = await users.create();
await user.login();

View File

@@ -126,7 +126,7 @@ export const CustomStyles: Story = {
control: "color",
table: {
category: "Progress Styling",
defaultValue: { summary: "hsl(222.2 47.4% 11.2% / 0.2)" },
defaultValue: { summary: "brand color at 20% opacity" },
},
},
trackBorderRadius: {
@@ -140,7 +140,7 @@ export const CustomStyles: Story = {
control: "color",
table: {
category: "Progress Styling",
defaultValue: { summary: "hsl(222.2 47.4% 11.2%)" },
defaultValue: { summary: "brand color (#64748b)" },
},
},
indicatorBorderRadius: {

View File

@@ -59,8 +59,8 @@
--------------------------------------------------------------------------- */
--fb-survey-brand-color: #64748b;
--fb-accent-background-color: var(--slate-200);
--fb-accent-background-color-selected: var(--slate-100);
--fb-accent-background-color: var(--fb-survey-brand-color);
--fb-accent-background-color-selected: color-mix(in srgb, var(--fb-survey-brand-color) 90%, white);
/* ---------------------------------------------------------------------------
Element Headline Tokens
@@ -69,7 +69,7 @@
--fb-element-headline-font-family: inherit;
--fb-element-headline-font-weight: 600;
--fb-element-headline-font-size: 1rem;
--fb-element-headline-color: var(--input);
--fb-element-headline-color: var(--fb-survey-brand-color);
--fb-element-headline-opacity: 1;
/* ---------------------------------------------------------------------------
@@ -79,7 +79,7 @@
--fb-element-description-font-family: inherit;
--fb-element-description-font-weight: 400;
--fb-element-description-font-size: 0.875rem;
--fb-element-description-color: var(--input);
--fb-element-description-color: color-mix(in srgb, var(--fb-survey-brand-color) 70%, white);
/* ---------------------------------------------------------------------------
Element Upper Label Tokens
Used for "Required" label and other upper label text.
@@ -87,8 +87,8 @@
--fb-element-upper-label-font-family: inherit;
--fb-element-upper-label-font-weight: 400;
--fb-element-upper-label-font-size: 0.75rem;
--fb-element-upper-label-color: var(--muted-foreground);
--fb-element-upper-label-opacity: 0.6;
--fb-element-upper-label-color: color-mix(in srgb, var(--fb-survey-brand-color) 50%, white);
--fb-element-upper-label-opacity: 1;
/* ---------------------------------------------------------------------------
Label Tokens
@@ -125,7 +125,7 @@
--fb-input-font-family: inherit;
--fb-input-font-size: 0.875rem;
--fb-input-font-weight: 400;
--fb-input-color: var(--foreground);
--fb-input-color: color-mix(in srgb, var(--fb-survey-brand-color) 10%, #0f172a);
--fb-input-placeholder-color: var(--fb-input-color);
--fb-input-placeholder-opacity: 0.5;
--fb-input-width: 100%;
@@ -139,9 +139,9 @@
Used for the Progress component track and indicator.
--------------------------------------------------------------------------- */
--fb-progress-track-height: 0.5rem;
--fb-progress-track-bg-color: hsl(222.2 47.4% 11.2% / 0.2);
--fb-progress-track-bg-color: color-mix(in srgb, var(--fb-survey-brand-color) 20%, transparent);
--fb-progress-track-border-radius: var(--radius);
--fb-progress-indicator-bg-color: hsl(222.2 47.4% 11.2%);
--fb-progress-indicator-bg-color: var(--fb-survey-brand-color);
--fb-progress-indicator-border-radius: var(--radius);
/* ---------------------------------------------------------------------------

View File

@@ -248,6 +248,8 @@ export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TS
if (styling.elementDescriptionFontSize !== undefined)
appendCssVariable("element-description-font-size", formatDimension(styling.elementDescriptionFontSize));
if (styling.elementDescriptionFontWeight !== undefined && styling.elementDescriptionFontWeight !== null)
appendCssVariable("element-description-font-weight", `${styling.elementDescriptionFontWeight}`);
appendCssVariable(
"element-description-color",
styling.elementDescriptionColor?.light ?? styling.questionColor?.light
@@ -293,6 +295,7 @@ export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TS
#fbjs .label-description,
#fbjs .label-description * {
font-size: var(--fb-element-description-font-size) !important;
font-weight: var(--fb-element-description-font-weight) !important;
color: var(--fb-element-description-color) !important;
}

View File

@@ -81,6 +81,7 @@ export const ZBaseStyling = z.object({
elementHeadlineFontWeight: z.union([z.string(), z.number()]).nullish(),
elementHeadlineColor: ZStylingColor.nullish(),
elementDescriptionFontSize: z.union([z.number(), z.string()]).nullish(),
elementDescriptionFontWeight: z.union([z.string(), z.number()]).nullish(),
elementDescriptionColor: ZStylingColor.nullish(),
elementUpperLabelFontSize: z.union([z.number(), z.string()]).nullish(),
elementUpperLabelColor: ZStylingColor.nullish(),