feat: adds stories for logo component (#6448)

This commit is contained in:
Piyush Gupta
2025-08-20 20:27:43 +05:30
committed by GitHub
parent 2e9ad3ce07
commit ec52bdf3fe
8 changed files with 338 additions and 227 deletions

View File

@@ -10,8 +10,12 @@ export const FormWrapper = ({ children }: FormWrapperProps) => {
<div className="mx-auto flex flex-1 flex-col justify-center px-4 py-12 sm:px-6 lg:flex-none lg:px-20 xl:px-24">
<div className="mx-auto w-full max-w-sm rounded-xl bg-white p-8 shadow-xl lg:w-96">
<div className="mb-8 text-center">
<Link target="_blank" href="https://formbricks.com?utm_source=ce" rel="noopener noreferrer">
<Logo className="mx-auto w-3/4" />
<Link
target="_blank"
href="https://formbricks.com?utm_source=ce"
rel="noopener noreferrer"
aria-label="Formbricks website">
<Logo className="mx-auto w-3/4" variant="wordmark" aria-hidden="true" />
</Link>
</div>
{children}

View File

@@ -1,4 +1,4 @@
import { FormbricksLogo } from "@/modules/ui/components/formbricks-logo";
import { Logo } from "@/modules/ui/components/logo";
import { Toaster } from "react-hot-toast";
export const SetupLayout = ({ children }: { children: React.ReactNode }) => {
@@ -10,7 +10,7 @@ export const SetupLayout = ({ children }: { children: React.ReactNode }) => {
style={{ scrollbarGutter: "stable both-edges" }}
className="flex max-h-[90vh] w-[40rem] flex-col items-center space-y-4 overflow-auto rounded-lg border bg-white p-12 text-center shadow-md">
<div className="h-20 w-20 rounded-lg bg-slate-900 p-2">
<FormbricksLogo className="h-full w-full" />
<Logo className="h-full w-full" variant="image" />
</div>
{children}
</div>

View File

@@ -41,8 +41,8 @@ vi.mock("next/link", () => ({
),
}));
vi.mock("@/modules/ui/components/formbricks-logo", () => ({
FormbricksLogo: () => <div data-testid="formbricks-logo">FormbricksLogo</div>,
vi.mock("@/modules/ui/components/logo", () => ({
Logo: () => <div data-testid="logo">Logo</div>,
}));
vi.mock("@/modules/ui/components/button", () => ({

View File

@@ -1,7 +1,7 @@
"use client";
import { Button } from "@/modules/ui/components/button";
import { FormbricksLogo } from "@/modules/ui/components/formbricks-logo";
import { Logo } from "@/modules/ui/components/logo";
import { useTranslate } from "@tolgee/react";
import Image, { StaticImageData } from "next/image";
import Link from "next/link";
@@ -51,7 +51,7 @@ export const ConnectIntegration = ({
<div className="flex w-1/2 flex-col items-center justify-center rounded-lg bg-white p-8 shadow">
<div className="flex w-1/2 justify-center -space-x-4">
<div className="flex h-32 w-32 items-center justify-center rounded-full bg-white p-6 shadow-md">
<FormbricksLogo />
<Logo variant="image" />
</div>
<div className="flex h-32 w-32 items-center justify-center rounded-full bg-white p-4 shadow-md">
<Image className="w-1/2" src={integrationLogoSrc} alt="logo" />

View File

@@ -1,197 +0,0 @@
interface FormbricksLogoProps {
className?: string;
}
export const FormbricksLogo = ({ className }: FormbricksLogoProps) => {
return (
<svg
width="220"
height="220"
viewBox="0 0 220 220"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
d="M39.1602 147.334H95.8321V175.67C95.8321 191.32 83.1457 204.006 67.4962 204.006C51.8466 204.006 39.1602 191.32 39.1602 175.67V147.334Z"
fill="url(#paint0_linear_415_2)"
/>
<path
d="M39.1602 81.8071H152.504C168.154 81.8071 180.84 94.4936 180.84 110.143C180.84 125.793 168.154 138.479 152.504 138.479H39.1602V81.8071Z"
fill="url(#paint1_linear_415_2)"
/>
<path
d="M39.1602 62.7322C39.1602 37.0773 59.9576 16.2798 85.6126 16.2798H152.504C168.154 16.2798 180.84 28.9662 180.84 44.6158C180.84 60.2653 168.154 72.9518 152.504 72.9518H39.1602V62.7322Z"
fill="url(#paint2_linear_415_2)"
/>
<mask
id="mask0_415_2"
style={{ maskType: "alpha" }}
maskUnits="userSpaceOnUse"
x="39"
y="16"
width="142"
height="189">
<path
d="M39.1602 147.335H95.8321V175.671C95.8321 191.32 83.1457 204.007 67.4962 204.007C51.8466 204.007 39.1602 191.32 39.1602 175.671V147.335Z"
fill="url(#paint3_linear_415_2)"
/>
<path
d="M39.1602 81.8081H152.504C168.154 81.8081 180.84 94.4946 180.84 110.144C180.84 125.794 168.154 138.48 152.504 138.48H39.1602V81.8081Z"
fill="url(#paint4_linear_415_2)"
/>
<path
d="M39.1602 62.7322C39.1602 37.0773 59.9576 16.2798 85.6126 16.2798H152.504C168.154 16.2798 180.84 28.9662 180.84 44.6158C180.84 60.2653 168.154 72.9518 152.504 72.9518H39.1602V62.7322Z"
fill="url(#paint5_linear_415_2)"
/>
</mask>
<g mask="url(#mask0_415_2)">
<g filter="url(#filter0_d_415_2)">
<mask
id="mask1_415_2"
style={{ maskType: "alpha" }}
maskUnits="userSpaceOnUse"
x="39"
y="16"
width="142"
height="189">
<path
d="M39.1602 147.335H95.8321V175.671C95.8321 191.32 83.1457 204.007 67.4962 204.007C51.8466 204.007 39.1602 191.32 39.1602 175.671V147.335Z"
fill="black"
fillOpacity="0.1"
/>
<path
d="M39.1602 62.7322C39.1602 37.0773 59.9576 16.2798 85.6126 16.2798H152.504C168.154 16.2798 180.84 28.9662 180.84 44.6158C180.84 60.2653 168.154 72.9518 152.504 72.9518H39.1602V62.7322Z"
fill="black"
fillOpacity="0.1"
/>
<path
d="M39.1602 81.8081H152.504C168.154 81.8081 180.84 94.4946 180.84 110.144C180.84 125.794 168.154 138.48 152.504 138.48H39.1602V81.8081Z"
fill="black"
fillOpacity="0.1"
/>
</mask>
<g mask="url(#mask1_415_2)">
<path
d="M42.1331 -32.5321C64.3329 -54.1986 120.626 -32.5321 120.626 -32.5321H42.1331C36.6806 -27.2105 33.2847 -19.2749 33.2847 -7.76218C33.2847 50.6243 96.5317 71.8561 96.5317 112.55C96.5317 152.386 35.9231 176.962 33.3678 231.092H120.626C120.626 231.092 33.2847 291.248 33.2847 234.631C33.2847 233.437 33.3128 232.258 33.3678 231.092H-5.11523L2.41417 -32.5321H42.1331Z"
fill="black"
fillOpacity="0.1"
/>
</g>
</g>
<g filter="url(#filter1_f_415_2)">
<circle cx="21.4498" cy="179.212" r="53.13" fill="#00C4B8" />
</g>
<g filter="url(#filter2_f_415_2)">
<circle cx="21.4498" cy="44.6163" r="53.13" fill="#00C4B8" />
</g>
</g>
<defs>
<filter
id="filter0_d_415_2"
x="34.5149"
y="-11.5917"
width="137.209"
height="243.47"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB">
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dx="23.2262" />
<feGaussianBlur stdDeviation="13.9357" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_415_2" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_415_2" result="shape" />
</filter>
<filter
id="filter1_f_415_2"
x="-78.1326"
y="79.6296"
width="199.165"
height="199.165"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB">
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="23.2262" result="effect1_foregroundBlur_415_2" />
</filter>
<filter
id="filter2_f_415_2"
x="-78.1326"
y="-54.9661"
width="199.165"
height="199.165"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB">
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="23.2262" result="effect1_foregroundBlur_415_2" />
</filter>
<linearGradient
id="paint0_linear_415_2"
x1="96.0786"
y1="174.643"
x2="39.1553"
y2="174.873"
gradientUnits="userSpaceOnUse">
<stop offset="1" stopColor="#00C4B8" />
</linearGradient>
<linearGradient
id="paint1_linear_415_2"
x1="181.456"
y1="109.116"
x2="39.1602"
y2="110.554"
gradientUnits="userSpaceOnUse">
<stop stopColor="#00DDD0" />
<stop offset="1" stopColor="#01E0C6" />
</linearGradient>
<linearGradient
id="paint2_linear_415_2"
x1="181.456"
y1="43.5891"
x2="39.1602"
y2="45.0264"
gradientUnits="userSpaceOnUse">
<stop stopColor="#00DDD0" />
<stop offset="1" stopColor="#01E0C6" />
</linearGradient>
<linearGradient
id="paint3_linear_415_2"
x1="96.0786"
y1="174.644"
x2="39.1553"
y2="174.874"
gradientUnits="userSpaceOnUse">
<stop stopColor="#00FFE1" />
<stop offset="1" stopColor="#01E0C6" />
</linearGradient>
<linearGradient
id="paint4_linear_415_2"
x1="181.456"
y1="109.117"
x2="39.1602"
y2="110.555"
gradientUnits="userSpaceOnUse">
<stop stopColor="#00FFE1" />
<stop offset="1" stopColor="#01E0C6" />
</linearGradient>
<linearGradient
id="paint5_linear_415_2"
x1="181.456"
y1="43.5891"
x2="39.1602"
y2="45.0264"
gradientUnits="userSpaceOnUse">
<stop stopColor="#00FFE1" />
<stop offset="1" stopColor="#01E0C6" />
</linearGradient>
</defs>
</svg>
);
};

View File

@@ -8,33 +8,59 @@ describe("Logo", () => {
cleanup();
});
test("renders correctly", () => {
const { container } = render(<Logo />);
const svg = container.querySelector("svg");
describe("default variant", () => {
test("renders default logo correctly", () => {
const { container } = render(<Logo />);
const svg = container.querySelector("svg");
expect(svg).toBeInTheDocument();
expect(svg).toHaveAttribute("viewBox", "0 0 697 150");
expect(svg).toHaveAttribute("fill", "none");
expect(svg).toHaveAttribute("xmlns", "http://www.w3.org/2000/svg");
expect(svg).toBeInTheDocument();
});
});
test("accepts and passes through props", () => {
const testClassName = "test-class";
const { container } = render(<Logo className={testClassName} />);
const svg = container.querySelector("svg");
describe("image variant", () => {
test("renders image logo correctly", () => {
const { container } = render(<Logo variant="image" />);
const svg = container.querySelector("svg");
expect(svg).toBeInTheDocument();
expect(svg).toHaveAttribute("class", testClassName);
expect(svg).toBeInTheDocument();
});
test("renders image logo with className correctly", () => {
const testClassName = "test-class";
const { container } = render(<Logo variant="image" className={testClassName} />);
const svg = container.querySelector("svg");
expect(svg).toBeInTheDocument();
expect(svg).toHaveAttribute("class", testClassName);
});
});
test("contains expected svg elements", () => {
const { container } = render(<Logo />);
const svg = container.querySelector("svg");
describe("wordmark variant", () => {
test("renders wordmark logo correctly", () => {
const { container } = render(<Logo variant="wordmark" />);
const svg = container.querySelector("svg");
expect(svg?.querySelectorAll("path").length).toBeGreaterThan(0);
expect(svg?.querySelector("line")).toBeInTheDocument();
expect(svg?.querySelectorAll("mask").length).toBe(2);
expect(svg?.querySelectorAll("filter").length).toBe(3);
expect(svg?.querySelectorAll("linearGradient").length).toBe(6);
expect(svg).toBeInTheDocument();
});
test("renders wordmark logo with className correctly", () => {
const testClassName = "test-class";
const { container } = render(<Logo variant="wordmark" className={testClassName} />);
const svg = container.querySelector("svg");
expect(svg).toBeInTheDocument();
expect(svg).toHaveAttribute("class", testClassName);
});
test("contains expected svg elements", () => {
const { container } = render(<Logo variant="wordmark" />);
const svg = container.querySelector("svg");
expect(svg?.querySelectorAll("path").length).toBeGreaterThan(0);
expect(svg?.querySelector("line")).toBeInTheDocument();
expect(svg?.querySelectorAll("mask").length).toBe(2);
expect(svg?.querySelectorAll("filter").length).toBe(3);
expect(svg?.querySelectorAll("linearGradient").length).toBe(6);
});
});
});

View File

@@ -1,4 +1,208 @@
export const Logo = (props: any) => {
interface LogoProps extends React.SVGProps<SVGSVGElement> {
variant?: "image" | "wordmark";
}
export const Logo = ({ variant = "wordmark", ...props }: LogoProps) => {
if (variant === "image") return <ImageLogo {...props} />;
return <WordmarkLogo {...props} />;
};
const ImageLogo = (props: React.SVGProps<SVGSVGElement>) => {
return (
<svg
width="220"
height="220"
viewBox="0 0 220 220"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}>
<path
d="M39.1602 147.334H95.8321V175.67C95.8321 191.32 83.1457 204.006 67.4962 204.006C51.8466 204.006 39.1602 191.32 39.1602 175.67V147.334Z"
fill="url(#paint0_linear_415_2)"
/>
<path
d="M39.1602 81.8071H152.504C168.154 81.8071 180.84 94.4936 180.84 110.143C180.84 125.793 168.154 138.479 152.504 138.479H39.1602V81.8071Z"
fill="url(#paint1_linear_415_2)"
/>
<path
d="M39.1602 62.7322C39.1602 37.0773 59.9576 16.2798 85.6126 16.2798H152.504C168.154 16.2798 180.84 28.9662 180.84 44.6158C180.84 60.2653 168.154 72.9518 152.504 72.9518H39.1602V62.7322Z"
fill="url(#paint2_linear_415_2)"
/>
<mask
id="mask0_415_2"
style={{ maskType: "alpha" }}
maskUnits="userSpaceOnUse"
x="39"
y="16"
width="142"
height="189">
<path
d="M39.1602 147.335H95.8321V175.671C95.8321 191.32 83.1457 204.007 67.4962 204.007C51.8466 204.007 39.1602 191.32 39.1602 175.671V147.335Z"
fill="url(#paint3_linear_415_2)"
/>
<path
d="M39.1602 81.8081H152.504C168.154 81.8081 180.84 94.4946 180.84 110.144C180.84 125.794 168.154 138.48 152.504 138.48H39.1602V81.8081Z"
fill="url(#paint4_linear_415_2)"
/>
<path
d="M39.1602 62.7322C39.1602 37.0773 59.9576 16.2798 85.6126 16.2798H152.504C168.154 16.2798 180.84 28.9662 180.84 44.6158C180.84 60.2653 168.154 72.9518 152.504 72.9518H39.1602V62.7322Z"
fill="url(#paint5_linear_415_2)"
/>
</mask>
<g mask="url(#mask0_415_2)">
<g filter="url(#filter0_d_415_2)">
<mask
id="mask1_415_2"
style={{ maskType: "alpha" }}
maskUnits="userSpaceOnUse"
x="39"
y="16"
width="142"
height="189">
<path
d="M39.1602 147.335H95.8321V175.671C95.8321 191.32 83.1457 204.007 67.4962 204.007C51.8466 204.007 39.1602 191.32 39.1602 175.671V147.335Z"
fill="black"
fillOpacity="0.1"
/>
<path
d="M39.1602 62.7322C39.1602 37.0773 59.9576 16.2798 85.6126 16.2798H152.504C168.154 16.2798 180.84 28.9662 180.84 44.6158C180.84 60.2653 168.154 72.9518 152.504 72.9518H39.1602V62.7322Z"
fill="black"
fillOpacity="0.1"
/>
<path
d="M39.1602 81.8081H152.504C168.154 81.8081 180.84 94.4946 180.84 110.144C180.84 125.794 168.154 138.48 152.504 138.48H39.1602V81.8081Z"
fill="black"
fillOpacity="0.1"
/>
</mask>
<g mask="url(#mask1_415_2)">
<path
d="M42.1331 -32.5321C64.3329 -54.1986 120.626 -32.5321 120.626 -32.5321H42.1331C36.6806 -27.2105 33.2847 -19.2749 33.2847 -7.76218C33.2847 50.6243 96.5317 71.8561 96.5317 112.55C96.5317 152.386 35.9231 176.962 33.3678 231.092H120.626C120.626 231.092 33.2847 291.248 33.2847 234.631C33.2847 233.437 33.3128 232.258 33.3678 231.092H-5.11523L2.41417 -32.5321H42.1331Z"
fill="black"
fillOpacity="0.1"
/>
</g>
</g>
<g filter="url(#filter1_f_415_2)">
<circle cx="21.4498" cy="179.212" r="53.13" fill="#00C4B8" />
</g>
<g filter="url(#filter2_f_415_2)">
<circle cx="21.4498" cy="44.6163" r="53.13" fill="#00C4B8" />
</g>
</g>
<defs>
<filter
id="filter0_d_415_2"
x="34.5149"
y="-11.5917"
width="137.209"
height="243.47"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB">
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dx="23.2262" />
<feGaussianBlur stdDeviation="13.9357" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_415_2" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_415_2" result="shape" />
</filter>
<filter
id="filter1_f_415_2"
x="-78.1326"
y="79.6296"
width="199.165"
height="199.165"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB">
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="23.2262" result="effect1_foregroundBlur_415_2" />
</filter>
<filter
id="filter2_f_415_2"
x="-78.1326"
y="-54.9661"
width="199.165"
height="199.165"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB">
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="23.2262" result="effect1_foregroundBlur_415_2" />
</filter>
<linearGradient
id="paint0_linear_415_2"
x1="96.0786"
y1="174.643"
x2="39.1553"
y2="174.873"
gradientUnits="userSpaceOnUse">
<stop offset="1" stopColor="#00C4B8" />
</linearGradient>
<linearGradient
id="paint1_linear_415_2"
x1="181.456"
y1="109.116"
x2="39.1602"
y2="110.554"
gradientUnits="userSpaceOnUse">
<stop stopColor="#00DDD0" />
<stop offset="1" stopColor="#01E0C6" />
</linearGradient>
<linearGradient
id="paint2_linear_415_2"
x1="181.456"
y1="43.5891"
x2="39.1602"
y2="45.0264"
gradientUnits="userSpaceOnUse">
<stop stopColor="#00DDD0" />
<stop offset="1" stopColor="#01E0C6" />
</linearGradient>
<linearGradient
id="paint3_linear_415_2"
x1="96.0786"
y1="174.644"
x2="39.1553"
y2="174.874"
gradientUnits="userSpaceOnUse">
<stop stopColor="#00FFE1" />
<stop offset="1" stopColor="#01E0C6" />
</linearGradient>
<linearGradient
id="paint4_linear_415_2"
x1="181.456"
y1="109.117"
x2="39.1602"
y2="110.555"
gradientUnits="userSpaceOnUse">
<stop stopColor="#00FFE1" />
<stop offset="1" stopColor="#01E0C6" />
</linearGradient>
<linearGradient
id="paint5_linear_415_2"
x1="181.456"
y1="43.5891"
x2="39.1602"
y2="45.0264"
gradientUnits="userSpaceOnUse">
<stop stopColor="#00FFE1" />
<stop offset="1" stopColor="#01E0C6" />
</linearGradient>
</defs>
</svg>
);
};
const WordmarkLogo = (props: React.SVGProps<SVGSVGElement>) => {
return (
<svg viewBox="0 0 697 150" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path

View File

@@ -0,0 +1,74 @@
import { Meta, StoryObj } from "@storybook/react-vite";
import { Logo } from "./index";
type StoryProps = React.ComponentProps<typeof Logo>;
const meta: Meta<StoryProps> = {
title: "UI/Logo",
component: Logo,
tags: ["autodocs"],
parameters: {
layout: "centered",
controls: { sort: "alpha", exclude: [] },
docs: {
description: {
component:
"** Logo ** renders the Formbricks brand as scalable SVG.It supports two variants('image' and 'wordmark') and is suitable for headers, navigation, and other branding areas.",
},
},
},
argTypes: {
variant: {
control: "select",
options: ["image", "wordmark"],
description: "The variant of the logo to display",
table: {
category: "Appearance",
type: { summary: "string" },
defaultValue: { summary: "wordmark" },
},
order: 1,
},
className: {
control: "text",
description: "Additional CSS classes for styling",
table: {
category: "Appearance",
type: { summary: "string" },
},
order: 1,
},
},
};
export default meta;
type Story = StoryObj<StoryProps>;
const renderLogoWithOptions = (args: StoryProps) => {
const { ...logoProps } = args;
return <Logo {...logoProps} />;
};
export const Default: Story = {
render: renderLogoWithOptions,
args: {
className: "h-20",
},
};
export const Image: Story = {
render: renderLogoWithOptions,
args: {
className: "h-20",
variant: "image",
},
};
export const Wordmark: Story = {
render: renderLogoWithOptions,
args: {
className: "h-20",
variant: "wordmark",
},
};