feat(packages/surveys): ability to customize colors & other improvements (#916)

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
Co-authored-by: Neil Chauhan <neilchauhan2@gmail.com>
This commit is contained in:
Midka
2023-11-15 17:29:27 +02:00
committed by GitHub
parent 3eeea7d1b2
commit 9d4e21f8a7
37 changed files with 334 additions and 231 deletions

View File

@@ -1,3 +1,26 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Example on overriding packages/js colors */
.dark {
--fb-brand-color: red;
--fb-brand-text-color: white;
--fb-border-color: green;
--fb-border-color-highlight: var(--slate-500);
--fb-focus-color: red;
--fb-heading-color: yellow;
--fb-subheading-color: green;
--fb-info-text-color: orange;
--fb-signature-text-color: blue;
--fb-survey-background-color: black;
--fb-accent-background-color: rgb(13, 13, 12);
--fb-accent-background-color-selected: red;
--fb-placeholder-color: white;
--fb-shadow-color: yellow;
--fb-rating-fill: var(--yellow-300);
--fb-rating-hover: var(--yellow-500);
--fb-back-btn-border: currentColor;
--fb-submit-btn-border: transparent;
--fb-rating-selected: black;
}

View File

@@ -33,6 +33,7 @@
"tailwindcss": "^3.3.3",
"terser": "^5.22.0",
"vite": "^4.4.11",
"vite-plugin-dts": "^3.6.0"
"vite-plugin-dts": "^3.6.0",
"vite-tsconfig-paths": "^4.2.1"
}
}

View File

@@ -1,9 +0,0 @@
export default function Progress({ progress, brandColor }: { progress: number; brandColor: string }) {
return (
<div className="h-2 w-full rounded-full bg-slate-200">
<div
className="transition-width z-20 h-2 rounded-full duration-500"
style={{ backgroundColor: brandColor, width: `${Math.floor(progress * 100)}%` }}></div>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { cn } from "../../../lib/cn";
import { cn } from "@/lib/utils";
interface BackButtonProps {
onClick: () => void;
@@ -12,7 +12,7 @@ export function BackButton({ onClick, backButtonLabel, tabIndex = 2 }: BackButto
tabIndex={tabIndex}
type={"button"}
className={cn(
"flex items-center rounded-md border border-transparent px-3 py-3 text-base font-medium leading-4 shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
"border-back-button-border text-heading focus:ring-focus flex items-center rounded-md border px-3 py-3 text-base font-medium leading-4 shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2"
)}
onClick={onClick}>
{backButtonLabel || "Back"}

View File

@@ -1,11 +1,8 @@
import { useCallback } from "preact/hooks";
import { cn } from "../../../lib/cn";
import { isLight } from "../lib/utils";
interface SubmitButtonProps {
buttonLabel: string | undefined;
isLastQuestion: boolean;
brandColor: string;
onClick: () => void;
focus?: boolean;
tabIndex?: number;
@@ -15,7 +12,6 @@ interface SubmitButtonProps {
function SubmitButton({
buttonLabel,
isLastQuestion,
brandColor,
onClick,
tabIndex = 1,
focus = false,
@@ -38,11 +34,7 @@ function SubmitButton({
type={type}
tabIndex={tabIndex}
autoFocus={focus}
className={cn(
"flex items-center rounded-md border border-transparent px-3 py-3 text-base font-medium leading-4 shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2",
isLight(brandColor) ? "text-black" : "text-white"
)}
style={{ backgroundColor: brandColor }}
className="bg-brand border-submit-button-border text-on-brand focus:ring-focus flex items-center rounded-md border px-3 py-3 text-base font-medium leading-4 shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2"
onClick={onClick}>
{buttonLabel || (isLastQuestion ? "Finish" : "Next")}
</button>

View File

@@ -5,10 +5,10 @@ export default function FormbricksBranding() {
target="_blank"
tabIndex={-1}
className="mb-5 mt-2 flex justify-center">
<p className="text-xs text-slate-400">
<p className="text-signature text-xs">
Powered by{" "}
<b>
<span className="text-slate-500 hover:text-slate-700">Formbricks</span>
<span className="text-info-text hover:text-heading">Formbricks</span>
</b>
</p>
</a>

View File

@@ -7,11 +7,14 @@ interface HeadlineProps {
export default function Headline({ headline, questionId, style, required = true }: HeadlineProps) {
return (
<label htmlFor={questionId} className="mb-1.5 block text-base font-semibold leading-6 text-slate-900">
<div className={"flex justify-between gap-4"} style={style}>
<label
htmlFor={questionId}
className="text-heading mb-1.5 block text-base font-semibold leading-6"
style={style}>
<div className={"mr-[3ch] flex items-center justify-between"} style={style}>
{headline}
{!required && (
<span className="self-start text-sm font-normal leading-7 text-slate-400" tabIndex={-1}>
<span className="text-info-text self-start text-sm font-normal leading-7" tabIndex={-1}>
Optional
</span>
)}

View File

@@ -1,11 +1,11 @@
import { cleanHtml } from "../lib/cleanHtml";
import { cleanHtml } from "@/lib/cleanHtml";
export default function HtmlBody({ htmlString, questionId }: { htmlString?: string; questionId: string }) {
if (!htmlString) return null;
return (
<label
htmlFor={questionId}
className="block text-sm font-normal leading-6 text-slate-600"
className="fb-htmlbody" // styles are in global.css
dangerouslySetInnerHTML={{ __html: cleanHtml(htmlString) }}></label>
);
}

View File

@@ -0,0 +1,9 @@
export default function Progress({ progress }: { progress: number }) {
return (
<div className="bg-accent-bg h-2 w-full rounded-full">
<div
className="transition-width bg-brand z-20 h-2 rounded-full duration-500"
style={{ width: `${Math.floor(progress * 100)}%` }}></div>
</div>
);
}

View File

@@ -1,17 +1,16 @@
import { TSurveyWithTriggers } from "@formbricks/types/js";
import { useEffect, useState } from "preact/hooks";
import Progress from "./Progress";
import { calculateElementIdx } from "../lib/utils";
import { calculateElementIdx } from "@/lib/utils";
interface ProgressBarProps {
survey: TSurveyWithTriggers;
questionId: string;
brandColor: string;
}
const PROGRESS_INCREMENT = 0.1;
export default function ProgressBar({ survey, questionId, brandColor }: ProgressBarProps) {
export default function ProgressBar({ survey, questionId }: ProgressBarProps) {
const [progress, setProgress] = useState(0); // [0, 1]
const [prevQuestionIdx, setPrevQuestionIdx] = useState(0); // [0, survey.questions.length
const [prevQuestionId, setPrevQuestionId] = useState(""); // [0, survey.questions.length
@@ -48,5 +47,5 @@ export default function ProgressBar({ survey, questionId, brandColor }: Progress
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [questionId, survey, setPrevQuestionIdx]);
return <Progress progress={progress} brandColor={brandColor} />;
return <Progress progress={progress} />;
}

View File

@@ -1,14 +1,13 @@
import { TResponseData } from "@formbricks/types/responses";
import { TSurveyQuestion } from "@formbricks/types/surveys";
import { TSurveyQuestionType } from "@formbricks/types/surveys";
import CTAQuestion from "./CTAQuestion";
import ConsentQuestion from "./ConsentQuestion";
import MultipleChoiceMultiQuestion from "./MultipleChoiceMultiQuestion";
import MultipleChoiceSingleQuestion from "./MultipleChoiceSingleQuestion";
import NPSQuestion from "./NPSQuestion";
import OpenTextQuestion from "./OpenTextQuestion";
import RatingQuestion from "./RatingQuestion";
import PictureSelectionQuestion from "./PictureSelectionQuestion";
import { TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys";
import CTAQuestion from "@/components/questions/CTAQuestion";
import ConsentQuestion from "@/components/questions/ConsentQuestion";
import MultipleChoiceMultiQuestion from "@/components/questions/MultipleChoiceMultiQuestion";
import MultipleChoiceSingleQuestion from "@/components/questions/MultipleChoiceSingleQuestion";
import NPSQuestion from "@/components/questions/NPSQuestion";
import OpenTextQuestion from "@/components/questions/OpenTextQuestion";
import PictureSelectionQuestion from "@/components/questions/PictureSelectionQuestion";
import RatingQuestion from "@/components/questions/RatingQuestion";
interface QuestionConditionalProps {
question: TSurveyQuestion;
@@ -18,7 +17,6 @@ interface QuestionConditionalProps {
onBack: () => void;
isFirstQuestion: boolean;
isLastQuestion: boolean;
brandColor: string;
autoFocus?: boolean;
}
@@ -30,7 +28,6 @@ export default function QuestionConditional({
onBack,
isFirstQuestion,
isLastQuestion,
brandColor,
autoFocus = true,
}: QuestionConditionalProps) {
return question.type === TSurveyQuestionType.OpenText ? (
@@ -42,7 +39,6 @@ export default function QuestionConditional({
onBack={onBack}
isFirstQuestion={isFirstQuestion}
isLastQuestion={isLastQuestion}
brandColor={brandColor}
autoFocus={autoFocus}
/>
) : question.type === TSurveyQuestionType.MultipleChoiceSingle ? (
@@ -54,7 +50,6 @@ export default function QuestionConditional({
onBack={onBack}
isFirstQuestion={isFirstQuestion}
isLastQuestion={isLastQuestion}
brandColor={brandColor}
/>
) : question.type === TSurveyQuestionType.MultipleChoiceMulti ? (
<MultipleChoiceMultiQuestion
@@ -65,7 +60,6 @@ export default function QuestionConditional({
onBack={onBack}
isFirstQuestion={isFirstQuestion}
isLastQuestion={isLastQuestion}
brandColor={brandColor}
/>
) : question.type === TSurveyQuestionType.NPS ? (
<NPSQuestion
@@ -76,7 +70,6 @@ export default function QuestionConditional({
onBack={onBack}
isFirstQuestion={isFirstQuestion}
isLastQuestion={isLastQuestion}
brandColor={brandColor}
/>
) : question.type === TSurveyQuestionType.CTA ? (
<CTAQuestion
@@ -87,7 +80,6 @@ export default function QuestionConditional({
onBack={onBack}
isFirstQuestion={isFirstQuestion}
isLastQuestion={isLastQuestion}
brandColor={brandColor}
/>
) : question.type === TSurveyQuestionType.Rating ? (
<RatingQuestion
@@ -98,7 +90,6 @@ export default function QuestionConditional({
onBack={onBack}
isFirstQuestion={isFirstQuestion}
isLastQuestion={isLastQuestion}
brandColor={brandColor}
/>
) : question.type === TSurveyQuestionType.Consent ? (
<ConsentQuestion
@@ -109,7 +100,6 @@ export default function QuestionConditional({
onBack={onBack}
isFirstQuestion={isFirstQuestion}
isLastQuestion={isLastQuestion}
brandColor={brandColor}
/>
) : question.type === TSurveyQuestionType.PictureSelection ? (
<PictureSelectionQuestion
@@ -120,7 +110,6 @@ export default function QuestionConditional({
onBack={onBack}
isFirstQuestion={isFirstQuestion}
isLastQuestion={isLastQuestion}
brandColor={brandColor}
/>
) : null;
}

View File

@@ -35,7 +35,7 @@ export default function RedirectCountDown({ redirectUrl, isRedirectDisabled }: R
return (
<div>
<div className="mt-10 rounded-md bg-slate-100 p-2 text-sm">
<div className="bg-accent-bg text-subheading mt-10 rounded-md p-2 text-sm">
<span>You&apos;re redirected in </span>
<span>{timeRemaining}</span>
</div>

View File

@@ -1,6 +1,6 @@
export default function Subheader({ subheader, questionId }: { subheader?: string; questionId: string }) {
return (
<label htmlFor={questionId} className="block text-sm font-normal leading-6 text-slate-600">
<label htmlFor={questionId} className="text-subheading block text-sm font-normal leading-6">
{subheader}
</label>
);

View File

@@ -1,10 +1,10 @@
import FormbricksBranding from "@/components/general/FormbricksBranding";
import { AutoCloseWrapper } from "@/components/wrappers/AutoCloseWrapper";
import { evaluateCondition } from "@/lib/logicEvaluator";
import { cn } from "@/lib/utils";
import { SurveyBaseProps } from "@/types/props";
import type { TResponseData } from "@formbricks/types/responses";
import { useEffect, useRef, useState } from "preact/hooks";
import { evaluateCondition } from "../lib/logicEvaluator";
import { cn } from "../lib/utils";
import { SurveyBaseProps } from "../types/props";
import { AutoCloseWrapper } from "./AutoCloseWrapper";
import FormbricksBranding from "./FormbricksBranding";
import ProgressBar from "./ProgressBar";
import QuestionConditional from "./QuestionConditional";
import ThankYouCard from "./ThankYouCard";
@@ -12,7 +12,6 @@ import WelcomeCard from "./WelcomeCard";
export function Survey({
survey,
brandColor,
isBrandingEnabled,
activeQuestionId,
onDisplay = () => {},
@@ -129,7 +128,6 @@ export function Survey({
fileUrl={survey.welcomeCard.fileUrl}
buttonLabel={survey.welcomeCard.buttonLabel}
timeToFinish={survey.welcomeCard.timeToFinish}
brandColor={brandColor}
onSubmit={onSubmit}
survey={survey}
/>
@@ -139,7 +137,6 @@ export function Survey({
<ThankYouCard
headline={survey.thankYouCard.headline}
subheader={survey.thankYouCard.subheader}
brandColor={brandColor}
redirectUrl={survey.redirectUrl}
isRedirectDisabled={isRedirectDisabled}
/>
@@ -160,7 +157,6 @@ export function Survey({
: currQues.id === survey?.questions[0]?.id
}
isLastQuestion={currQues.id === survey.questions[survey.questions.length - 1].id}
brandColor={brandColor}
/>
)
);
@@ -169,8 +165,8 @@ export function Survey({
return (
<>
<AutoCloseWrapper survey={survey} brandColor={brandColor} onClose={onClose}>
<div className="flex h-full w-full flex-col justify-between bg-white px-6 pb-3 pt-6">
<AutoCloseWrapper survey={survey} onClose={onClose}>
<div className="flex h-full w-full flex-col justify-between bg-[--fb-survey-background-color] px-6 pb-3 pt-6">
<div ref={contentRef} className={cn(loadingElement ? "animate-pulse opacity-60" : "", "my-auto")}>
{survey.questions.length === 0 && !survey.welcomeCard.enabled && !survey.thankYouCard.enabled ? (
// Handle the case when there are no questions and both welcome and thank you cards are disabled
@@ -181,7 +177,7 @@ export function Survey({
</div>
<div className="mt-8">
{isBrandingEnabled && <FormbricksBranding />}
<ProgressBar survey={survey} questionId={questionId} brandColor={brandColor} />
<ProgressBar survey={survey} questionId={questionId} />
</div>
</div>
</AutoCloseWrapper>

View File

@@ -1,9 +1,8 @@
import { SurveyBaseProps } from "../types/props";
import { SurveyBaseProps } from "@/types/props";
import { Survey } from "./Survey";
export function SurveyInline({
survey,
brandColor,
isBrandingEnabled,
activeQuestionId,
onDisplay = () => {},
@@ -14,10 +13,9 @@ export function SurveyInline({
isRedirectDisabled = false,
}: SurveyBaseProps) {
return (
<div id="fbjs" className="h-full w-full">
<div id="fbjs" className="formbricks-form h-full w-full">
<Survey
survey={survey}
brandColor={brandColor}
isBrandingEnabled={isBrandingEnabled}
activeQuestionId={activeQuestionId}
onDisplay={onDisplay}

View File

@@ -1,11 +1,10 @@
import { useState } from "preact/hooks";
import { SurveyModalProps } from "../types/props";
import Modal from "./Modal";
import { SurveyModalProps } from "@/types/props";
import Modal from "@/components/wrappers/Modal";
import { Survey } from "./Survey";
export function SurveyModal({
survey,
brandColor,
isBrandingEnabled,
activeQuestionId,
placement,
@@ -29,7 +28,7 @@ export function SurveyModal({
};
return (
<div id="fbjs">
<div id="fbjs" className="formbricks-form">
<Modal
placement={placement}
clickOutside={clickOutside}
@@ -39,7 +38,6 @@ export function SurveyModal({
onClose={close}>
<Survey
survey={survey}
brandColor={brandColor}
isBrandingEnabled={isBrandingEnabled}
activeQuestionId={activeQuestionId}
onDisplay={onDisplay}

View File

@@ -1,11 +1,10 @@
import Headline from "./Headline";
import RedirectCountDown from "./RedirectCountdown";
import Subheader from "./Subheader";
import Headline from "@/components/general/Headline";
import RedirectCountDown from "@/components/general/RedirectCountdown";
import Subheader from "@/components/general/Subheader";
interface ThankYouCardProps {
headline?: string;
subheader?: string;
brandColor: string;
redirectUrl: string | null;
isRedirectDisabled: boolean;
}
@@ -13,13 +12,12 @@ interface ThankYouCardProps {
export default function ThankYouCard({
headline,
subheader,
brandColor,
redirectUrl,
isRedirectDisabled,
}: ThankYouCardProps) {
return (
<div className="text-center">
<div className="flex items-center justify-center" style={{ color: brandColor }}>
<div className="text-brand flex items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
@@ -35,7 +33,7 @@ export default function ThankYouCard({
</svg>
</div>
<span className="mb-[10px] inline-block h-1 w-16 rounded-[100%] bg-slate-300"></span>
<span className="bg-shadow mb-[10px] inline-block h-1 w-16 rounded-[100%]"></span>
<div>
<Headline headline={headline} questionId="thankYouCard" style={{ "justify-content": "center" }} />

View File

@@ -1,8 +1,8 @@
import SubmitButton from "@/components/buttons/SubmitButton";
import { calculateElementIdx } from "@/lib/utils";
import { TSurveyWithTriggers } from "@formbricks/types/js";
import Headline from "./Headline";
import HtmlBody from "./HtmlBody";
import SubmitButton from "./SubmitButton";
import { calculateElementIdx } from "../lib/utils";
import { TSurveyWithTriggers } from "@formbricks/types/js";
interface WelcomeCardProps {
headline?: string;
@@ -10,7 +10,6 @@ interface WelcomeCardProps {
fileUrl?: string;
buttonLabel?: string;
timeToFinish?: boolean;
brandColor: string;
onSubmit: (data: { [x: string]: any }) => void;
survey: TSurveyWithTriggers;
}
@@ -38,7 +37,6 @@ export default function WelcomeCard({
fileUrl,
buttonLabel,
timeToFinish,
brandColor,
onSubmit,
survey,
}: WelcomeCardProps) {
@@ -85,14 +83,13 @@ export default function WelcomeCard({
<SubmitButton
buttonLabel={buttonLabel}
isLastQuestion={false}
brandColor={brandColor}
focus={true}
onClick={() => {
onSubmit({ ["welcomeCard"]: "clicked" });
}}
type="button"
/>
<div className="flex items-center text-xs text-slate-600">Press Enter </div>
<div className="text-subheading flex items-center text-xs">Press Enter </div>
</div>
</div>
{timeToFinish && (

View File

@@ -1,9 +1,9 @@
import { BackButton } from "@/components/buttons/BackButton";
import SubmitButton from "@/components/buttons/SubmitButton";
import Headline from "@/components/general/Headline";
import HtmlBody from "@/components/general/HtmlBody";
import { TResponseData } from "@formbricks/types/responses";
import type { TSurveyCTAQuestion } from "@formbricks/types/surveys";
import { BackButton } from "./BackButton";
import Headline from "./Headline";
import HtmlBody from "./HtmlBody";
import SubmitButton from "./SubmitButton";
interface CTAQuestionProps {
question: TSurveyCTAQuestion;
@@ -13,7 +13,6 @@ interface CTAQuestionProps {
onBack: () => void;
isFirstQuestion: boolean;
isLastQuestion: boolean;
brandColor: string;
}
export default function CTAQuestion({
@@ -22,7 +21,6 @@ export default function CTAQuestion({
onBack,
isFirstQuestion,
isLastQuestion,
brandColor,
}: CTAQuestionProps) {
return (
<div>
@@ -47,14 +45,13 @@ export default function CTAQuestion({
onClick={() => {
onSubmit({ [question.id]: "dismissed" });
}}
className="mr-4 flex items-center rounded-md px-3 py-3 text-base font-medium leading-4 hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2 dark:text-slate-400">
className="text-heading focus:ring-focus mr-4 flex items-center rounded-md px-3 py-3 text-base font-medium leading-4 hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2">
{question.dismissButtonLabel || "Skip"}
</button>
)}
<SubmitButton
buttonLabel={question.buttonLabel}
isLastQuestion={isLastQuestion}
brandColor={brandColor}
focus={true}
onClick={() => {
if (question.buttonExternal && question.buttonUrl) {

View File

@@ -1,9 +1,9 @@
import { TResponseData } from "@formbricks/types/responses";
import type { TSurveyConsentQuestion } from "@formbricks/types/surveys";
import { BackButton } from "./BackButton";
import Headline from "./Headline";
import HtmlBody from "./HtmlBody";
import SubmitButton from "./SubmitButton";
import { BackButton } from "@/components/buttons/BackButton";
import SubmitButton from "@/components/buttons/SubmitButton";
import Headline from "@/components/general/Headline";
import HtmlBody from "@/components/general/HtmlBody";
interface ConsentQuestionProps {
question: TSurveyConsentQuestion;
@@ -13,7 +13,6 @@ interface ConsentQuestionProps {
onBack: () => void;
isFirstQuestion: boolean;
isLastQuestion: boolean;
brandColor: string;
}
export default function ConsentQuestion({
@@ -24,7 +23,6 @@ export default function ConsentQuestion({
onBack,
isFirstQuestion,
isLastQuestion,
brandColor,
}: ConsentQuestionProps) {
return (
<div>
@@ -49,7 +47,7 @@ export default function ConsentQuestion({
onChange({ [question.id]: "accepted" });
}
}}
className="relative z-10 mt-4 flex w-full cursor-pointer items-center rounded-md border border-gray-200 p-4 text-sm text-slate-800 hover:bg-slate-50 focus:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2">
className="border-border bg-survey-bg text-heading hover:bg-accent-bg focus:bg-accent-bg focus:ring-border-highlight relative z-10 mt-4 flex w-full cursor-pointer items-center rounded-md border p-4 text-sm focus:outline-none focus:ring-2 focus:ring-offset-2">
<input
type="checkbox"
id={question.id}
@@ -63,9 +61,8 @@ export default function ConsentQuestion({
}
}}
checked={value === "accepted"}
className="h-4 w-4 border border-slate-300 focus:ring-0 focus:ring-offset-0"
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus:ring-offset-0"
aria-labelledby={`${question.id}-label`}
style={{ borderColor: brandColor, color: brandColor }}
required={question.required}
/>
<span id={`${question.id}-label`} className="ml-3 font-medium">
@@ -80,7 +77,6 @@ export default function ConsentQuestion({
<div />
<SubmitButton
tabIndex={2}
brandColor={brandColor}
buttonLabel={question.buttonLabel}
isLastQuestion={isLastQuestion}
onClick={() => {}}

View File

@@ -1,11 +1,11 @@
import { BackButton } from "@/components/buttons/BackButton";
import SubmitButton from "@/components/buttons/SubmitButton";
import Headline from "@/components/general/Headline";
import Subheader from "@/components/general/Subheader";
import { cn, shuffleQuestions } from "@/lib/utils";
import { TResponseData } from "@formbricks/types/responses";
import type { TSurveyMultipleChoiceMultiQuestion } from "@formbricks/types/surveys";
import { useMemo, useRef, useState, useEffect, useCallback } from "preact/hooks";
import { cn, shuffleQuestions } from "../lib/utils";
import { BackButton } from "./BackButton";
import Headline from "./Headline";
import Subheader from "./Subheader";
import SubmitButton from "./SubmitButton";
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
interface MultipleChoiceMultiProps {
question: TSurveyMultipleChoiceMultiQuestion;
@@ -15,7 +15,6 @@ interface MultipleChoiceMultiProps {
onBack: () => void;
isFirstQuestion: boolean;
isLastQuestion: boolean;
brandColor: string;
}
export default function MultipleChoiceMultiQuestion({
@@ -26,7 +25,6 @@ export default function MultipleChoiceMultiQuestion({
onBack,
isFirstQuestion,
isLastQuestion,
brandColor,
}: MultipleChoiceMultiProps) {
const getChoicesWithoutOtherLabels = useCallback(
() => question.choices.filter((choice) => choice.id !== "other").map((item) => item.label),
@@ -104,7 +102,7 @@ export default function MultipleChoiceMultiQuestion({
<div className="mt-4">
<fieldset>
<legend className="sr-only">Options</legend>
<div className="relative max-h-[42vh] space-y-2 overflow-y-auto rounded-md bg-white py-0.5 pr-2">
<div className="bg-survey-bg relative max-h-[42vh] space-y-2 overflow-y-auto rounded-md py-0.5 pr-2">
{questionChoices.map((choice, idx) => (
<label
key={choice.id}
@@ -119,8 +117,10 @@ export default function MultipleChoiceMultiQuestion({
}
}}
className={cn(
value === choice.label ? "z-10 border-slate-400 bg-slate-50" : "border-gray-200",
"relative flex cursor-pointer flex-col rounded-md border p-4 text-slate-800 focus-within:border-slate-400 hover:bg-slate-50 focus:bg-slate-50 focus:outline-none "
value === choice.label
? "border-border-highlight bg-accent-selected-bg z-10"
: "border-border",
"text-heading focus-within:border-border-highlight hover:bg-accent-bg focus:bg-accent-bg relative flex cursor-pointer flex-col rounded-md border p-4 focus:outline-none"
)}>
<span className="flex items-center text-sm">
<input
@@ -129,7 +129,7 @@ export default function MultipleChoiceMultiQuestion({
name={question.id}
tabIndex={-1}
value={choice.label}
className="h-4 w-4 border border-slate-300 focus:ring-0 focus:ring-offset-0"
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus:ring-offset-0"
aria-labelledby={`${choice.id}-label`}
onChange={(e) => {
if ((e.target as HTMLInputElement)?.checked) {
@@ -139,7 +139,6 @@ export default function MultipleChoiceMultiQuestion({
}
}}
checked={Array.isArray(value) && value.includes(choice.label)}
style={{ borderColor: brandColor, color: brandColor }}
required={
question.required && Array.isArray(value) && value.length ? false : question.required
}
@@ -154,8 +153,10 @@ export default function MultipleChoiceMultiQuestion({
<label
tabIndex={questionChoices.length + 1}
className={cn(
value === otherOption.label ? "z-10 border-slate-400 bg-slate-50" : "border-gray-200",
"relative flex cursor-pointer flex-col rounded-md border p-4 text-slate-800 focus-within:border-slate-400 focus-within:bg-slate-50 hover:bg-slate-50 focus:outline-none"
value === otherOption.label
? "border-border-highlight bg-accent-selected-bg z-10"
: "border-border",
"text-heading focus-within:border-border-highlight focus-within:bg-accent-bg hover:bg-accent-bg relative flex cursor-pointer flex-col rounded-md border p-4 focus:outline-none"
)}
onKeyDown={(e) => {
if (e.key == "Enter") {
@@ -169,7 +170,7 @@ export default function MultipleChoiceMultiQuestion({
id={otherOption.id}
name={question.id}
value={otherOption.label}
className="h-4 w-4 border border-slate-300 focus:ring-0 focus:ring-offset-0"
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus:ring-offset-0"
aria-labelledby={`${otherOption.id}-label`}
onChange={(e) => {
setOtherSelected(!otherSelected);
@@ -181,7 +182,6 @@ export default function MultipleChoiceMultiQuestion({
}
}}
checked={otherSelected}
style={{ borderColor: brandColor, color: brandColor }}
/>
<span id={`${otherOption.id}-label`} className="ml-3 font-medium">
{otherOption.label}
@@ -206,7 +206,7 @@ export default function MultipleChoiceMultiQuestion({
}
}}
placeholder="Please specify"
className="mt-3 flex h-10 w-full rounded-md border border-slate-300 bg-transparent bg-white px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-500 dark:text-slate-300"
className="placeholder:text-placeholder border-border bg-survey-bg text-heading focus:ring-focus mt-3 flex h-10 w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
required={question.required}
aria-labelledby={`${otherOption.id}-label`}
/>
@@ -229,7 +229,6 @@ export default function MultipleChoiceMultiQuestion({
tabIndex={questionChoices.length + 2}
buttonLabel={question.buttonLabel}
isLastQuestion={isLastQuestion}
brandColor={brandColor}
onClick={() => {}}
/>
</div>

View File

@@ -1,11 +1,11 @@
import { BackButton } from "@/components/buttons/BackButton";
import SubmitButton from "@/components/buttons/SubmitButton";
import Headline from "@/components/general/Headline";
import Subheader from "@/components/general/Subheader";
import { cn, shuffleQuestions } from "@/lib/utils";
import { TResponseData } from "@formbricks/types/responses";
import type { TSurveyMultipleChoiceSingleQuestion } from "@formbricks/types/surveys";
import { useMemo, useRef, useState, useEffect } from "preact/hooks";
import { cn, shuffleQuestions } from "../lib/utils";
import { BackButton } from "./BackButton";
import Headline from "./Headline";
import Subheader from "./Subheader";
import SubmitButton from "./SubmitButton";
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
interface MultipleChoiceSingleProps {
question: TSurveyMultipleChoiceSingleQuestion;
@@ -15,7 +15,6 @@ interface MultipleChoiceSingleProps {
onBack: () => void;
isFirstQuestion: boolean;
isLastQuestion: boolean;
brandColor: string;
}
export default function MultipleChoiceSingleQuestion({
@@ -26,7 +25,6 @@ export default function MultipleChoiceSingleQuestion({
onBack,
isFirstQuestion,
isLastQuestion,
brandColor,
}: MultipleChoiceSingleProps) {
const [otherSelected, setOtherSelected] = useState(
!!value && !question.choices.find((c) => c.label === value)
@@ -73,8 +71,9 @@ export default function MultipleChoiceSingleQuestion({
<div className="mt-4">
<fieldset>
<legend className="sr-only">Options</legend>
<div
className="relative max-h-[42vh] space-y-2 overflow-y-auto rounded-md bg-white py-0.5 pr-2"
className="bg-survey-bg relative max-h-[42vh] space-y-2 overflow-y-auto rounded-md py-0.5 pr-2"
role="radiogroup">
{questionChoices.map((choice, idx) => (
<label
@@ -89,8 +88,10 @@ export default function MultipleChoiceSingleQuestion({
}
}}
className={cn(
value === choice.label ? "z-10 border-slate-400 bg-slate-50" : "border-gray-200",
"relative flex cursor-pointer flex-col rounded-md border p-4 text-slate-800 focus-within:border-slate-400 focus-within:bg-slate-50 hover:bg-slate-50 focus:outline-none "
value === choice.label
? "border-border-highlight bg-accent-selected-bg z-10"
: "border-border",
"text-heading focus-within:border-border-highlight focus-within:bg-accent-bg hover:bg-accent-bg relative flex cursor-pointer flex-col rounded-md border p-4 focus:outline-none"
)}>
<span className="flex items-center text-sm">
<input
@@ -99,14 +100,13 @@ export default function MultipleChoiceSingleQuestion({
id={choice.id}
name={question.id}
value={choice.label}
className="h-4 w-4 border border-slate-300 focus:ring-0 focus:ring-offset-0"
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus:ring-offset-0"
aria-labelledby={`${choice.id}-label`}
onChange={() => {
setOtherSelected(false);
onChange({ [question.id]: choice.label });
}}
checked={value === choice.label}
style={{ borderColor: brandColor, color: brandColor }}
required={question.required && idx === 0}
/>
<span id={`${choice.id}-label`} className="ml-3 font-medium">
@@ -119,8 +119,10 @@ export default function MultipleChoiceSingleQuestion({
<label
tabIndex={questionChoices.length + 1}
className={cn(
value === otherOption.label ? "z-10 border-slate-400 bg-slate-50" : "border-gray-200",
"relative flex cursor-pointer flex-col rounded-md border p-4 text-slate-800 focus-within:border-slate-400 focus-within:bg-slate-50 hover:bg-slate-50 focus:outline-none"
value === otherOption.label
? "border-border-highlight bg-accent-selected-bg z-10"
: "border-border",
"text-heading focus-within:border-border-highlight focus-within:bg-accent-bg hover:bg-accent-bg relative flex cursor-pointer flex-col rounded-md border p-4 focus:outline-none"
)}
onKeyDown={(e) => {
if (e.key == "Enter") {
@@ -135,14 +137,13 @@ export default function MultipleChoiceSingleQuestion({
tabIndex={-1}
name={question.id}
value={otherOption.label}
className="h-4 w-4 border border-slate-300 focus:ring-0 focus:ring-offset-0"
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus:ring-offset-0"
aria-labelledby={`${otherOption.id}-label`}
onChange={() => {
setOtherSelected(!otherSelected);
onChange({ [question.id]: "" });
}}
checked={otherSelected}
style={{ borderColor: brandColor, color: brandColor }}
/>
<span id={`${otherOption.id}-label`} className="ml-3 font-medium">
{otherOption.label}
@@ -166,7 +167,7 @@ export default function MultipleChoiceSingleQuestion({
}
}}
placeholder="Please specify"
className="mt-3 flex h-10 w-full rounded-md border border-slate-300 bg-transparent bg-white px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-500 dark:text-slate-300"
className="placeholder:text-placeholder border-border bg-survey-bg text-heading focus:ring-focus mt-3 flex h-10 w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
required={question.required}
aria-labelledby={`${otherOption.id}-label`}
/>
@@ -189,7 +190,6 @@ export default function MultipleChoiceSingleQuestion({
tabIndex={questionChoices.length + 2}
buttonLabel={question.buttonLabel}
isLastQuestion={isLastQuestion}
brandColor={brandColor}
onClick={() => {}}
/>
</div>

View File

@@ -1,10 +1,10 @@
import { BackButton } from "@/components/buttons/BackButton";
import SubmitButton from "@/components/buttons/SubmitButton";
import Headline from "@/components/general/Headline";
import Subheader from "@/components/general/Subheader";
import { cn } from "@/lib/utils";
import { TResponseData } from "@formbricks/types/responses";
import type { TSurveyNPSQuestion } from "@formbricks/types/surveys";
import { cn } from "../lib/utils";
import { BackButton } from "./BackButton";
import Headline from "./Headline";
import Subheader from "./Subheader";
import SubmitButton from "./SubmitButton";
interface NPSQuestionProps {
question: TSurveyNPSQuestion;
@@ -14,7 +14,6 @@ interface NPSQuestionProps {
onBack: () => void;
isFirstQuestion: boolean;
isLastQuestion: boolean;
brandColor: string;
}
export default function NPSQuestion({
@@ -25,7 +24,6 @@ export default function NPSQuestion({
onBack,
isFirstQuestion,
isLastQuestion,
brandColor,
}: NPSQuestionProps) {
return (
<form
@@ -55,8 +53,8 @@ export default function NPSQuestion({
}
}}
className={cn(
value === number ? "z-10 border-slate-400 bg-slate-50" : "",
"relative h-10 flex-1 cursor-pointer border bg-white text-center text-sm leading-10 text-slate-800 first:rounded-l-md last:rounded-r-md hover:bg-gray-100 focus:bg-gray-100 focus:outline-none"
value === number ? "border-border-highlight bg-accent-selected-bg z-10" : "border-border",
"bg-survey-bg text-heading hover:bg-accent-bg relative h-10 flex-1 cursor-pointer border text-center text-sm leading-10 first:rounded-l-md last:rounded-r-md focus:outline-none"
)}>
<input
type="radio"
@@ -78,7 +76,7 @@ export default function NPSQuestion({
</label>
))}
</div>
<div className="flex justify-between px-1.5 text-xs leading-6 text-slate-500">
<div className="text-info-text flex justify-between px-1.5 text-xs leading-6">
<p>{question.lowerLabel}</p>
<p>{question.upperLabel}</p>
</div>
@@ -101,7 +99,6 @@ export default function NPSQuestion({
tabIndex={12}
buttonLabel={question.buttonLabel}
isLastQuestion={isLastQuestion}
brandColor={brandColor}
onClick={() => {}}
/>
)}

View File

@@ -1,9 +1,9 @@
import { BackButton } from "@/components/buttons/BackButton";
import SubmitButton from "@/components/buttons/SubmitButton";
import Headline from "@/components/general/Headline";
import Subheader from "@/components/general/Subheader";
import { TResponseData } from "@formbricks/types/responses";
import type { TSurveyOpenTextQuestion } from "@formbricks/types/surveys";
import { BackButton } from "./BackButton";
import Headline from "./Headline";
import Subheader from "./Subheader";
import SubmitButton from "./SubmitButton";
import { useCallback } from "react";
interface OpenTextQuestionProps {
@@ -14,7 +14,6 @@ interface OpenTextQuestionProps {
onBack: () => void;
isFirstQuestion: boolean;
isLastQuestion: boolean;
brandColor: string;
autoFocus?: boolean;
}
@@ -26,7 +25,6 @@ export default function OpenTextQuestion({
onBack,
isFirstQuestion,
isLastQuestion,
brandColor,
autoFocus = true,
}: OpenTextQuestionProps) {
const handleInputChange = (inputValue: string) => {
@@ -76,6 +74,7 @@ export default function OpenTextQuestion({
type={question.inputType}
onInput={(e) => handleInputChange(e.currentTarget.value)}
autoFocus={autoFocus}
className="border-border bg-survey-bg focus:border-border-highlight block w-full rounded-md border p-2 shadow-sm focus:outline-none focus:ring-0 sm:text-sm"
onKeyDown={(e) => {
if (e.key === "Enter" && isInputEmpty(value as string)) {
e.preventDefault(); // Prevent form submission
@@ -85,9 +84,6 @@ export default function OpenTextQuestion({
}}
pattern={question.inputType === "phone" ? "[+][0-9 ]+" : ".*"}
title={question.inputType === "phone" ? "Enter a valid phone number" : undefined}
className={`block w-full rounded-md border
border-slate-100
bg-slate-50 p-2 shadow-sm focus:border-slate-500 focus:outline-none focus:ring-0 sm:text-sm`}
/>
) : (
<textarea
@@ -102,11 +98,10 @@ export default function OpenTextQuestion({
type={question.inputType}
onInput={(e) => handleInputChange(e.currentTarget.value)}
autoFocus={autoFocus}
className="border-border bg-survey-bg text-subheading focus:border-border-highlight block w-full rounded-md border p-2 shadow-sm focus:ring-0 sm:text-sm"
pattern={question.inputType === "phone" ? "[+][0-9 ]+" : ".*"}
title={question.inputType === "phone" ? "Please enter a valid phone number" : undefined}
className={`block w-full rounded-md border
border-slate-100
bg-slate-50 p-2 shadow-sm focus:border-slate-500 focus:outline-none focus:ring-0 sm:text-sm`}></textarea>
/>
)}
</div>
@@ -120,12 +115,7 @@ export default function OpenTextQuestion({
/>
)}
<div></div>
<SubmitButton
buttonLabel={question.buttonLabel}
isLastQuestion={isLastQuestion}
brandColor={brandColor}
onClick={() => {}}
/>
<SubmitButton buttonLabel={question.buttonLabel} isLastQuestion={isLastQuestion} onClick={() => {}} />
</div>
</form>
);

View File

@@ -1,11 +1,11 @@
import { BackButton } from "@/components/buttons/BackButton";
import SubmitButton from "@/components/buttons/SubmitButton";
import Headline from "@/components/general/Headline";
import Subheader from "@/components/general/Subheader";
import { cn } from "@/lib/utils";
import { TResponseData } from "@formbricks/types/responses";
import type { TSurveyPictureSelectionQuestion } from "@formbricks/types/surveys";
import { useEffect } from "preact/hooks";
import { cn } from "../lib/utils";
import { BackButton } from "./BackButton";
import Headline from "./Headline";
import Subheader from "./Subheader";
import SubmitButton from "./SubmitButton";
interface PictureSelectionProps {
question: TSurveyPictureSelectionQuestion;
@@ -15,7 +15,6 @@ interface PictureSelectionProps {
onBack: () => void;
isFirstQuestion: boolean;
isLastQuestion: boolean;
brandColor: string;
}
export default function PictureSelectionQuestion({
@@ -26,7 +25,6 @@ export default function PictureSelectionQuestion({
onBack,
isFirstQuestion,
isLastQuestion,
brandColor,
}: PictureSelectionProps) {
const addItem = (item: string) => {
let values: string[] = [];
@@ -95,7 +93,7 @@ export default function PictureSelectionQuestion({
<div className="mt-4">
<fieldset>
<legend className="sr-only">Options</legend>
<div className="relative grid max-h-[42vh] grid-cols-2 gap-x-5 gap-y-4 overflow-y-auto rounded-md bg-white pr-2.5">
<div className="rounded-m bg-survey-bg relative grid max-h-[42vh] grid-cols-2 gap-x-5 gap-y-4 overflow-y-auto pr-2.5">
{questionChoices.map((choice, idx) => (
<label
key={choice.id}
@@ -106,17 +104,12 @@ export default function PictureSelectionQuestion({
handleChange(choice.id);
}
}}
style={{
borderColor:
Array.isArray(value) && value.includes(choice.id) ? brandColor : "border-slate-400",
color: brandColor,
}}
onClick={() => handleChange(choice.id)}
className={cn(
Array.isArray(value) && value.includes(choice.id)
? `z-10 border-4 shadow-xl focus:border-4`
? `border-brand text-brand z-10 border-4 shadow-xl focus:border-4`
: "",
"relative box-border inline-block h-28 w-full overflow-hidden rounded-xl border border-slate-400 focus:border-slate-600 focus:bg-slate-50 focus:outline-none"
"border-border focus:border-border-highlight focus:bg-accent-selected-bg relative box-border inline-block h-28 w-full overflow-hidden rounded-xl border focus:outline-none"
)}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
@@ -132,8 +125,10 @@ export default function PictureSelectionQuestion({
type="checkbox"
tabindex={-1}
checked={Array.isArray(value) && value.includes(choice.id)}
style={{ borderColor: brandColor, color: brandColor }}
className="pointer-events-none absolute right-2 top-2 z-20 h-5 w-5 rounded border border-slate-400"
className={cn(
"border-border pointer-events-none absolute right-2 top-2 z-20 h-5 w-5 rounded border",
Array.isArray(value) && value.includes(choice.id) ? "border-brand text-brand" : ""
)}
required={
question.required && Array.isArray(value) && value.length ? false : question.required
}
@@ -145,8 +140,10 @@ export default function PictureSelectionQuestion({
type="radio"
tabindex={-1}
checked={Array.isArray(value) && value.includes(choice.id)}
style={{ borderColor: brandColor, color: brandColor }}
className="pointer-events-none absolute right-2 top-2 z-20 h-5 w-5 "
className={cn(
"border-border pointer-events-none absolute right-2 top-2 z-20 h-5 w-5 rounded-full border",
Array.isArray(value) && value.includes(choice.id) ? "border-brand text-brand" : ""
)}
required={
question.required && Array.isArray(value) && value.length ? false : question.required
}
@@ -170,7 +167,6 @@ export default function PictureSelectionQuestion({
tabIndex={questionChoices.length + 2}
buttonLabel={question.buttonLabel}
isLastQuestion={isLastQuestion}
brandColor={brandColor}
onClick={() => {}}
/>
</div>

View File

@@ -1,9 +1,10 @@
import { BackButton } from "@/components/buttons/BackButton";
import SubmitButton from "@/components/buttons/SubmitButton";
import Headline from "@/components/general/Headline";
import { cn } from "@/lib/utils";
import { TResponseData } from "@formbricks/types/responses";
import type { TSurveyRatingQuestion } from "@formbricks/types/surveys";
import { useState } from "preact/hooks";
import { cn } from "../lib/utils";
import { BackButton } from "./BackButton";
import Headline from "./Headline";
import {
ConfusedFace,
FrowningFace,
@@ -15,9 +16,8 @@ import {
SmilingFaceWithSmilingEyes,
TiredFace,
WearyFace,
} from "./Smileys";
import Subheader from "./Subheader";
import SubmitButton from "./SubmitButton";
} from "../general/Smileys";
import Subheader from "../general/Subheader";
interface RatingQuestionProps {
question: TSurveyRatingQuestion;
@@ -27,7 +27,6 @@ interface RatingQuestionProps {
onBack: () => void;
isFirstQuestion: boolean;
isLastQuestion: boolean;
brandColor: string;
}
export default function RatingQuestion({
@@ -38,7 +37,6 @@ export default function RatingQuestion({
onBack,
isFirstQuestion,
isLastQuestion,
brandColor,
}: RatingQuestionProps) {
const [hoveredNumber, setHoveredNumber] = useState(0);
@@ -87,7 +85,7 @@ export default function RatingQuestion({
key={number}
onMouseOver={() => setHoveredNumber(number)}
onMouseLeave={() => setHoveredNumber(0)}
className="max-w-10 relative max-h-10 flex-1 cursor-pointer bg-white text-center text-sm leading-10">
className="max-w-10 bg-survey-bg relative max-h-10 flex-1 cursor-pointer text-center text-sm leading-10">
{question.scale === "number" ? (
<label
tabIndex={i + 1}
@@ -97,10 +95,10 @@ export default function RatingQuestion({
}
}}
className={cn(
value === number ? "z-10 border-slate-400 bg-slate-50" : "",
value === number ? "bg-accent-selected-bg border-border-highlight z-10" : "",
a.length === number ? "rounded-r-md" : "",
number === 1 ? "rounded-l-md" : "",
"block h-full w-full border text-slate-800 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none"
"text-heading hover:bg-accent-bg focus:bg-accent-bg block h-full w-full border focus:outline-none"
)}>
<HiddenRadioInput number={number} />
{number}
@@ -114,14 +112,14 @@ export default function RatingQuestion({
}
}}
className={cn(
number <= hoveredNumber ? "text-yellow-500" : "",
"flex h-full w-full justify-center focus:text-yellow-500 focus:outline-none"
number <= hoveredNumber ? "text-rating-focus" : "text-heading",
"focus:text-rating-focus flex h-full w-full justify-center focus:outline-none"
)}
onFocus={() => setHoveredNumber(number)}
onBlur={() => setHoveredNumber(0)}>
<HiddenRadioInput number={number} />
{typeof value === "number" && value >= number ? (
<span className="text-yellow-300">
<span className="text-rating-fill">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
@@ -152,13 +150,18 @@ export default function RatingQuestion({
</label>
) : (
<label
className={cn(
"flex h-full w-full justify-center",
value === number || hoveredNumber === number
? "stroke-rating-selected text-rating-selected"
: "stroke-heading text-heading"
)}
tabIndex={i + 1}
onKeyDown={(e) => {
if (e.key == "Enter") {
handleSelect(number);
}
}}
className="flex h-full w-full justify-center text-slate-800 focus:outline-none"
onFocus={() => setHoveredNumber(number)}
onBlur={() => setHoveredNumber(0)}>
<HiddenRadioInput number={number} />
@@ -172,7 +175,7 @@ export default function RatingQuestion({
</span>
))}
</div>
<div className="flex justify-between px-1.5 text-xs leading-6 text-slate-500">
<div className="text-subheading flex justify-between px-1.5 text-xs leading-6">
<p className="w-1/2 text-left">{question.lowerLabel}</p>
<p className="w-1/2 text-right">{question.upperLabel}</p>
</div>
@@ -195,7 +198,6 @@ export default function RatingQuestion({
tabIndex={question.range + 1}
buttonLabel={question.buttonLabel}
isLastQuestion={isLastQuestion}
brandColor={brandColor}
onClick={() => {}}
/>
)}
@@ -211,7 +213,7 @@ interface RatingSmileyProps {
}
function RatingSmiley({ active, idx, range }: RatingSmileyProps): JSX.Element {
const activeColor = "fill-yellow-500";
const activeColor = "fill-rating-fill";
const inactiveColor = "fill-none";
let icons = [
<TiredFace className={active ? activeColor : inactiveColor} />,

View File

@@ -1,15 +1,14 @@
import { TSurveyWithTriggers } from "@formbricks/types/js";
import { useEffect, useRef, useState } from "preact/hooks";
import Progress from "./Progress";
import Progress from "../general/Progress";
interface AutoCloseProps {
survey: TSurveyWithTriggers;
brandColor: string;
onClose: () => void;
children: any;
}
export function AutoCloseWrapper({ survey, brandColor, onClose, children }: AutoCloseProps) {
export function AutoCloseWrapper({ survey, onClose, children }: AutoCloseProps) {
const [countdownProgress, setCountdownProgress] = useState(100);
const [countdownStop, setCountdownStop] = useState(false);
const startRef = useRef(performance.now());
@@ -49,9 +48,7 @@ export function AutoCloseWrapper({ survey, brandColor, onClose, children }: Auto
return (
<>
{!countdownStop && survey.autoClose && (
<Progress progress={countdownProgress} brandColor={brandColor} />
)}
{!countdownStop && survey.autoClose && <Progress progress={countdownProgress} />}
<div onClick={handleStopCountdown} onMouseOver={handleStopCountdown} className="h-full w-full">
{children}
</div>

View File

@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils";
import { TPlacement } from "@formbricks/types/common";
import { VNode } from "preact";
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { cn } from "../lib/utils";
interface ModalProps {
children: VNode;
@@ -108,7 +108,7 @@ export default function Modal({
<button
type="button"
onClick={onClose}
class="relative rounded-md text-slate-400 hover:text-slate-500 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2">
class="text-close-button hover:text-close-button-focus focus:ring-close-button-focus relative rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2">
<span class="sr-only">Close</span>
<svg
class="h-4 w-4"

View File

@@ -1,11 +1,13 @@
import { SurveyInline } from "@/components/general/SurveyInline";
import { SurveyModal } from "@/components/general/SurveyModal";
import { addCustomThemeToDom, addStylesToDom } from "@/lib/styles";
import { SurveyInlineProps, SurveyModalProps } from "@/types/props";
import { h, render } from "preact";
import { SurveyModal } from "./components/SurveyModal";
import { addStylesToDom } from "./lib/styles";
import { SurveyInlineProps, SurveyModalProps } from "./types/props";
import { SurveyInline } from "./components/SurveyInline";
export const renderSurveyInline = (props: SurveyInlineProps) => {
export const renderSurveyInline = (props: SurveyInlineProps & { brandColor: string }) => {
addStylesToDom();
addCustomThemeToDom({ brandColor: props.brandColor });
const { containerId, ...surveyProps } = props;
const element = document.getElementById(containerId);
if (!element) {
@@ -14,8 +16,10 @@ export const renderSurveyInline = (props: SurveyInlineProps) => {
render(h(SurveyInline, surveyProps), element);
};
export const renderSurveyModal = (props: SurveyModalProps) => {
export const renderSurveyModal = (props: SurveyModalProps & { brandColor: string }) => {
addStylesToDom();
addCustomThemeToDom({ brandColor: props.brandColor });
// add container element to DOM
const element = document.createElement("div");
element.id = "formbricks-modal-container";

View File

@@ -1,5 +1,5 @@
import global from "../styles/global.css?inline";
import preflight from "../styles/preflight.css?inline";
import global from "@/styles/global.css?inline";
import preflight from "@/styles/preflight.css?inline";
import editorCss from "../../../ui/Editor/stylesEditorFrontend.css?inline";
export const addStylesToDom = () => {
@@ -10,3 +10,16 @@ export const addStylesToDom = () => {
document.head.appendChild(styleElement);
}
};
export const addCustomThemeToDom = ({ brandColor }: { brandColor: string }) => {
if (document.getElementById("formbricks__css") === null) return;
const styleElement = document.createElement("style");
styleElement.id = "formbricks__css__custom";
styleElement.innerHTML = `
:root {
--fb-brand-color: ${brandColor};
}
`;
document.head.appendChild(styleElement);
};

View File

@@ -2,6 +2,8 @@
@tailwind components;
@tailwind utilities;
/* @import "./survey.css"; */
/* Firefox */
#fbjs * {
scrollbar-width: thin;
@@ -24,3 +26,55 @@
border: 3px solid #cbd5e1;
border-radius: 99px;
}
/* this is for styling the HtmlBody component */
.fb-htmlbody {
@apply block text-sm font-normal leading-6;
/* need to use !important because in packages/ui/components/editor/stylesEditorFrontend.css the color is defined for some classes */
color: var(--fb-subheading-color) !important;
}
/* without this, it wont override the color */
p.fb-editor-paragraph {
color: var(--fb-subheading-color) !important;
}
/* theming */
:root {
--slate-50: rgb(248 250 252);
--slate-100: rgb(241 245 249);
--slate-200: rgb(226 232 240);
--slate-300: rgb(203 213 225);
--slate-400: rgb(148 163 184);
--slate-500: rgb(100 116 139);
--slate-600: rgb(71 85 105);
--slate-700: rgb(51 65 85);
--slate-800: rgb(30 41 59);
--slate-900: rgb(15 23 42);
--gray-100: rgb(243 244 246);
--gray-200: rgb(229 231 235);
--yellow-300: rgb(253 224 71);
--yellow-500: rgb(234 179 8);
/* Default Light Theme, you can override everything by changing these values */
--fb-brand-color: rgb(255, 255, 255);
--fb-brand-text-color: black;
--fb-border-color: var(--slate-300);
--fb-border-color-highlight: var(--slate-500);
--fb-focus-color: var(--slate-500);
--fb-heading-color: var(--slate-900);
--fb-subheading-color: var(--slate-700);
--fb-info-text-color: var(--slate-500);
--fb-signature-text-color: var(--slate-400);
--fb-survey-background-color: white;
--fb-accent-background-color: var(--slate-200);
--fb-accent-background-color-selected: var(--slate-100);
--fb-placeholder-color: var(--slate-400);
--fb-shadow-color: var(--slate-300);
--fb-rating-fill: var(--yellow-300);
--fb-rating-hover: var(--yellow-500);
--fb-back-btn-border: transparent;
--fb-submit-btn-border: transparent;
--fb-rating-selected: black;
--fb-close-btn-color: var(--slate-500);
--fb-close-btn-color-hover: var(--slate-700);
}

View File

@@ -3,7 +3,6 @@ import { TResponseData, TResponseUpdate } from "@formbricks/types/responses";
export interface SurveyBaseProps {
survey: TSurveyWithTriggers;
brandColor: string;
isBrandingEnabled: boolean;
activeQuestionId?: string;
onDisplay?: () => void;

View File

@@ -7,6 +7,29 @@ module.exports = {
},
content: ["./src/**/*.{tsx,ts,jsx,js}"],
theme: {
colors: {
brand: "var(--fb-brand-color)",
"on-brand": "var(--fb-brand-text-color)",
border: "var(--fb-border-color)",
"border-highlight": "var(--fb-border-color-highlight)",
focus: "var(--fb-focus-color)",
heading: "var(--fb-heading-color)",
subheading: "var(--fb-subheading-color)",
"info-text": "var(--fb-info-text-color)",
signature: "var(--fb-signature-text-color)",
"survey-bg": "var(--fb-survey-background-color)",
"accent-bg": "var(--fb-accent-background-color)",
"accent-selected-bg": "var(--fb-accent-background-color-selected)",
placeholder: "var(--fb-placeholder-color)",
shadow: "var(--fb-shadow-color)",
"rating-fill": "var(--fb-rating-fill)",
"rating-focus": "var(--fb-rating-hover)",
"rating-selected": "var(--fb-rating-selected)",
"back-button-border": "var(--fb-back-btn-border)",
"submit-button-border": "var(--fb-submit-btn-border)",
"close-button": "var(--fb-close-btn-color)",
"close-button-focus": "var(--fb-close-btn-hover-color)",
},
extend: {
zIndex: {
999999: "999999",

View File

@@ -6,6 +6,10 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"jsxImportSource": "preact"
"jsxImportSource": "preact",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@@ -2,6 +2,7 @@ import { resolve } from "path";
import { defineConfig } from "vite";
import preact from "@preact/preset-vite";
import dts from "vite-plugin-dts";
import tsconfigPaths from "vite-tsconfig-paths";
// https://vitejs.dev/config/
export default defineConfig({
@@ -18,5 +19,5 @@ export default defineConfig({
fileName: "index",
},
},
plugins: [preact(), dts({ rollupTypes: true })],
plugins: [preact(), dts({ rollupTypes: true }), tsconfigPaths()],
});

37
pnpm-lock.yaml generated
View File

@@ -744,6 +744,9 @@ importers:
vite-plugin-dts:
specifier: ^3.6.0
version: 3.6.2(typescript@5.2.2)(vite@4.5.0)
vite-tsconfig-paths:
specifier: ^4.2.1
version: 4.2.1(typescript@5.2.2)(vite@4.5.0)
packages/tailwind-config:
devDependencies:
@@ -14296,6 +14299,10 @@ packages:
merge2: 1.4.1
slash: 3.0.0
/globrex@0.1.2:
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
dev: true
/glogg@1.0.2:
resolution: {integrity: sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==}
engines: {node: '>= 0.10'}
@@ -22123,6 +22130,19 @@ packages:
resolution: {integrity: sha512-pefrkcd4lmIVR0LA49Imjf9DYLK8vtWhqBPA3Ya1ir8xCW0O2yjL9dsCVvI7pCodLC5q7smNpEtDR2yVulQxOg==}
dev: true
/tsconfck@2.1.2(typescript@5.2.2):
resolution: {integrity: sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==}
engines: {node: ^14.13.1 || ^16 || >=18}
hasBin: true
peerDependencies:
typescript: ^4.3.5 || ^5.0.0
peerDependenciesMeta:
typescript:
optional: true
dependencies:
typescript: 5.2.2
dev: true
/tsconfig-paths@3.14.2:
resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==}
dependencies:
@@ -23063,6 +23083,23 @@ packages:
- supports-color
dev: true
/vite-tsconfig-paths@4.2.1(typescript@5.2.2)(vite@4.5.0):
resolution: {integrity: sha512-GNUI6ZgPqT3oervkvzU+qtys83+75N/OuDaQl7HmOqFTb0pjZsuARrRipsyJhJ3enqV8beI1xhGbToR4o78nSQ==}
peerDependencies:
vite: '*'
peerDependenciesMeta:
vite:
optional: true
dependencies:
debug: 4.3.4
globrex: 0.1.2
tsconfck: 2.1.2(typescript@5.2.2)
vite: 4.5.0(terser@5.22.0)
transitivePeerDependencies:
- supports-color
- typescript
dev: true
/vite@4.5.0(terser@5.22.0):
resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==}
engines: {node: ^14.18.0 || >=16.0.0}