Compare commits

...

15 Commits

Author SHA1 Message Date
review-agent-prime[bot]
a3c846a30c Edit packages/surveys/tailwind.config.js 2024-03-04 16:40:26 +00:00
pandeymangg
7a1af85141 fix: product settings 2024-03-04 22:07:13 +05:30
pandeymangg
2586a3ba3a feat: surveys package styling changes 2024-03-04 15:39:24 +05:30
pandeymangg
77408bf0b0 Merge branch 'main' of https://github.com/formbricks/formbricks into feat/form-styling 2024-03-04 13:19:00 +05:30
pandeymangg
d5b183155b feat: product settings page styling UI and service 2024-03-04 13:18:39 +05:30
pandeymangg
d7fc7995bc wip 2024-02-29 18:37:11 +05:30
pandeymangg
a9d8239a25 UI 2024-02-29 14:07:10 +05:30
pandeymangg
71bdb5095a fix: schema 2024-02-29 11:51:45 +05:30
pandeymangg
b84e322eee Merge branch 'main' into feat/form-styling 2024-02-29 11:49:41 +05:30
pandeymangg
08ccb954f3 fix: styling object 2024-02-28 14:43:10 +05:30
pandeymangg
38c6cb01df fix: styling object 2024-02-28 14:42:38 +05:30
pandeymangg
2c13121487 fix: data migration and product types 2024-02-28 12:04:06 +05:30
pandeymangg
73f1d09dc8 Merge branch 'main' into feat/form-styling 2024-02-28 11:24:42 +05:30
pandeymangg
1f884a408c feat: styling zod schema and data migration 2024-02-28 11:24:27 +05:30
pandeymangg
ed2253dcfc feat: styling zod schema and data migration 2024-02-28 11:23:58 +05:30
31 changed files with 1105 additions and 37 deletions

View File

@@ -43,7 +43,7 @@ export default async function SettingsLayout({ children, params }) {
membershipRole={currentUserMembership?.role}
/>
<div className="w-full md:ml-64">
<div className="max-w-4xl px-20 pb-6 pt-14 md:pt-6">
<div className="px-20 pb-6 pt-14 md:pt-6">
<div>{children}</div>
</div>
</div>

View File

@@ -0,0 +1,406 @@
"use client";
import UnifiedStylingPreviewSurvey from "@/app/(app)/environments/[environmentId]/settings/lookandfeel/components/UnifiedStylingPreviewSurvey";
import { RotateCcwIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { TProduct } from "@formbricks/types/product";
import { TSurvey } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
import { ColorPicker } from "@formbricks/ui/ColorPicker";
import { Slider } from "@formbricks/ui/Slider";
import CardArrangement from "@formbricks/ui/Styling/CardArrangement";
import ColorSelectorWithLabel from "@formbricks/ui/Styling/ColorSelectorWithLabel";
import DarkModeColors from "@formbricks/ui/Styling/DarkModeColors";
import { Switch } from "@formbricks/ui/Switch";
import { updateProductAction } from "../actions";
type UnifiedStylingProps = {
product: TProduct;
};
const colorDefaults = {
brandColor: "#64748b",
questionColor: "#2b2524",
inputColor: "#efefef",
inputBorderColor: "#c0c0c0",
cardBackgroundColor: "#c0c0c0",
highlighBorderColor: "#64748b",
};
const previewSurvey = {
id: "cltcppyqk00006uothzb3ybh0",
createdAt: new Date(),
updatedAt: new Date(),
name: "Product Market Fit (Superhuman)",
type: "link",
environmentId: "cltcf8i2n00099wlx7cu12zi6",
createdBy: "cltcf8i1c00009wlx3sk1ryss",
status: "draft",
welcomeCard: {
html: "Thanks for providing your feedback - let's go!",
enabled: false,
headline: "Welcome!",
timeToFinish: true,
showResponseCount: false,
},
questions: [
{
id: "uvnrhtngswxlibktglanh45f",
type: "openText",
headline: "This is a preview survey",
required: true,
inputType: "text",
subheader: "Click through it to check the look and feel of the surveying experience.",
longAnswer: true,
placeholder: "Type your answer here...",
},
{
id: "swfnndfht0ubsu9uh17tjcej",
type: "rating",
range: 5,
scale: "star",
headline: "How would you rate My Product",
required: true,
subheader: "Don't worry, be honest.",
lowerLabel: "Not good",
upperLabel: "Very good",
},
{
id: "je70a714xjdxc70jhxgv5web",
type: "multipleChoiceSingle",
choices: [
{
id: "vx9q4mlr6ffaw35m99bselwm",
label: "Eat the cake 🍰",
},
{
id: "ynj051qawxd4dszxkbvahoe5",
label: "Have the cake 🎂",
},
],
headline: "What do you do?",
required: true,
subheader: "Can't do both.",
shuffleOption: "none",
},
],
thankYouCard: {
enabled: true,
headline: "Thank you!",
subheader: "We appreciate your feedback.",
buttonLink: "https://formbricks.com/signup",
buttonLabel: "Create your own Survey",
},
hiddenFields: {
enabled: true,
fieldIds: [],
},
displayOption: "displayOnce",
recontactDays: null,
autoClose: null,
closeOnDate: null,
delay: 0,
displayPercentage: null,
autoComplete: null,
verifyEmail: null,
redirectUrl: null,
productOverwrites: null,
styling: null,
surveyClosedMessage: null,
singleUse: {
enabled: false,
isEncrypted: true,
},
pin: null,
resultShareKey: null,
triggers: [],
inlineTriggers: null,
segment: null,
};
const UnifiedStyling = ({ product }: UnifiedStylingProps) => {
const router = useRouter();
const [unifiedStyling, setUnifiedStyling] = useState(product.styling?.unifiedStyling ?? false);
const [allowStyleOverwrite, setAllowStyleOverwrite] = useState(
product.styling?.allowStyleOverwrite ?? false
);
const [brandColor, setBrandColor] = useState(
product.styling?.brandColor?.light ?? colorDefaults.brandColor
);
const [questionColor, setQuestionColor] = useState(
product.styling?.questionColor?.light ?? colorDefaults.questionColor
);
const [inputColor, setInputColor] = useState(
product.styling?.inputColor?.light ?? colorDefaults.inputColor
);
const [inputBorderColor, setInputBorderColor] = useState(
product.styling?.inputBorderColor?.light ?? colorDefaults.inputBorderColor
);
const [cardBackgroundColor, setCardBackgroundColor] = useState(
product.styling?.cardBackgroundColor?.light ?? colorDefaults.cardBackgroundColor
);
// highlight border
const [allowHighlightBorder, setAllowHighlightBorder] = useState(
!!product.styling?.highlightBorderColor?.light ?? false
);
const [highlightBorderColor, setHighlightBorderColor] = useState(
product.styling?.highlightBorderColor?.light ?? colorDefaults.highlighBorderColor
);
const [isDarkMode, setIsDarkMode] = useState(product.styling?.isDarkModeEnabled ?? false);
const [brandColorDark, setBrandColorDark] = useState(product.styling?.brandColor?.dark);
const [questionColorDark, setQuestionColorDark] = useState(product.styling?.questionColor?.dark);
const [inputColorDark, setInputColorDark] = useState(product.styling?.inputColor?.dark);
const [inputBorderColorDark, setInputBorderColorDark] = useState(product.styling?.inputBorderColor?.dark);
const [cardBackgroundColorDark, setCardBackgroundColorDark] = useState(
product.styling?.cardBackgroundColor?.dark
);
const [highlightBorderColorDark, setHighlightBorderColorDark] = useState(
product.styling?.highlightBorderColor?.dark
);
const [roundness, setRoundness] = useState(product.styling?.roundness ?? 8);
const [linkSurveysCardArrangement, setLinkSurveysCardArrangement] = useState(
product.styling?.cardArrangement?.linkSurveys ?? "casual"
);
const [inAppSurveysCardArrangement, setInAppSurveysCardArrangement] = useState(
product.styling?.cardArrangement?.inAppSurveys ?? "casual"
);
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
useEffect(() => {
setActiveQuestionId(previewSurvey.questions[0].id);
}, []);
useEffect(() => {
if (!unifiedStyling) {
setAllowStyleOverwrite(false);
}
}, [unifiedStyling]);
const onSave = async () => {
await updateProductAction(product.id, {
styling: {
unifiedStyling,
allowStyleOverwrite,
brandColor: {
light: brandColor,
dark: brandColorDark,
},
questionColor: {
light: questionColor,
dark: questionColorDark,
},
inputColor: {
light: inputColor,
dark: inputColorDark,
},
inputBorderColor: {
light: inputBorderColor,
dark: inputBorderColorDark,
},
cardBackgroundColor: {
light: cardBackgroundColor,
dark: cardBackgroundColorDark,
},
highlightBorderColor: allowHighlightBorder
? {
light: highlightBorderColor,
dark: highlightBorderColorDark,
}
: undefined,
isDarkModeEnabled: isDarkMode,
roundness,
cardArrangement: {
linkSurveys: linkSurveysCardArrangement,
inAppSurveys: inAppSurveysCardArrangement,
},
},
});
toast.success("Styling updated successfully.");
router.refresh();
};
return (
<div className="flex">
{/* Styling settings */}
<div className="w-1/2 pr-6">
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-4 rounded-lg bg-slate-50 p-4">
<div className="flex items-center gap-6">
<Switch
checked={unifiedStyling}
onCheckedChange={(value) => {
setUnifiedStyling(value);
}}
/>
<div className="flex flex-col">
<h3 className="text-base font-semibold">Enable unified styling</h3>
<p className="text-sm text-slate-800">Set base styles for all surveys below</p>
</div>
</div>
<div className="flex items-center gap-6">
<Switch
checked={allowStyleOverwrite}
onCheckedChange={(value) => {
setAllowStyleOverwrite(value);
}}
disabled={!unifiedStyling}
/>
<div className="flex flex-col">
<h3 className="text-base font-semibold">Allow overwriting styles</h3>
<p className="text-sm text-slate-800">
Activate if you want some surveys to be styled differently
</p>
</div>
</div>
</div>
<ColorSelectorWithLabel
label="Brand color"
color={brandColor}
setColor={setBrandColor}
description="Change the text color of the survey questions."
disabled
/>
<ColorSelectorWithLabel
label="Question color"
color={questionColor}
setColor={setQuestionColor}
description="Change the text color of the survey questions."
/>
<ColorSelectorWithLabel
label="Input color"
color={inputColor}
setColor={setInputColor}
description="Change the text color of the survey questions."
/>
<ColorSelectorWithLabel
label="Input border color"
color={inputBorderColor}
setColor={setInputBorderColor}
description="Change the text color of the survey questions."
/>
<ColorSelectorWithLabel
label="Card background color"
color={cardBackgroundColor}
setColor={setCardBackgroundColor}
description="Change the text color of the survey questions."
/>
<div className="flex flex-col gap-4">
<div className="flex items-center gap-6">
<Switch
checked={allowHighlightBorder}
onCheckedChange={(value) => {
setAllowHighlightBorder(value);
}}
disabled={!unifiedStyling}
/>
<div className="flex flex-col">
<h3 className="text-base font-semibold">Add highlight border</h3>
<p className="text-sm text-slate-800">Add on outer border to your survey card</p>
</div>
</div>
{allowHighlightBorder && (
<ColorPicker
color={highlightBorderColor}
onChange={setHighlightBorderColor}
containerClass="my-0"
/>
)}
</div>
<DarkModeColors
isDarkMode={isDarkMode}
setIsDarkMode={setIsDarkMode}
brandColor={brandColorDark}
cardBackgroundColor={cardBackgroundColorDark}
highlightBorderColor={highlightBorderColorDark}
inputBorderColor={inputBorderColorDark}
inputColor={inputColorDark}
questionColor={questionColorDark}
setBrandColor={setBrandColorDark}
setCardBackgroundColor={setCardBackgroundColorDark}
setHighlighBorderColor={setHighlightBorderColorDark}
setInputBorderColor={setInputBorderColorDark}
setInputColor={setInputColorDark}
setQuestionColor={setQuestionColorDark}
/>
<div className="flex flex-col gap-4">
<div className="flex flex-col">
<h3 className="text-base font-semibold text-slate-900">Roundness</h3>
<p className="text-sm text-slate-800">Change the border radius of the card and the inputs.</p>
</div>
<Slider
value={[roundness]}
max={16}
onValueChange={(value) => setRoundness(value[0])}
disabled={!unifiedStyling}
/>
</div>
<CardArrangement
activeCardArrangement={linkSurveysCardArrangement}
surveyType="link"
setActiveCardArrangement={setLinkSurveysCardArrangement}
/>
<CardArrangement
activeCardArrangement={inAppSurveysCardArrangement}
surveyType="web"
setActiveCardArrangement={setInAppSurveysCardArrangement}
/>
</div>
<div className="mt-8 flex items-center justify-end gap-2">
<Button variant="minimal" className="flex items-center gap-2">
Reset
<RotateCcwIcon className="h-4 w-4" />
</Button>
<Button variant="darkCTA" onClick={onSave}>
Save changes
</Button>
</div>
</div>
{/* Survey Preview */}
<div className="w-1/2 bg-slate-100 pt-4">
<div className="h-full max-h-[800px]">
<UnifiedStylingPreviewSurvey
activeQuestionId={activeQuestionId}
setActiveQuestionId={setActiveQuestionId}
survey={previewSurvey as TSurvey}
product={product}
/>
</div>
</div>
</div>
);
};
export default UnifiedStyling;

View File

@@ -0,0 +1,261 @@
"use client";
import Modal from "@/app/(app)/environments/[environmentId]/surveys/components/Modal";
import { MediaBackground } from "@/app/s/[surveyId]/components/MediaBackground";
import { ArrowPathRoundedSquareIcon } from "@heroicons/react/24/outline";
import { ArrowsPointingInIcon, ArrowsPointingOutIcon } from "@heroicons/react/24/solid";
import { Variants, motion } from "framer-motion";
import { useEffect, useMemo, useRef, useState } from "react";
import type { TProduct } from "@formbricks/types/product";
import { TStyling } from "@formbricks/types/styling";
import { TSurvey } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
import { SurveyInline } from "@formbricks/ui/Survey";
interface UnifiedStylingPreviewSurveyProps {
survey: TSurvey;
setActiveQuestionId: (id: string | null) => void;
activeQuestionId?: string | null;
product: TProduct;
}
let surveyNameTemp;
const previewParentContainerVariant: Variants = {
expanded: {
position: "fixed",
height: "100%",
width: "100%",
backgroundColor: "rgba(0, 0, 0, 0.4)",
backdropFilter: "blur(15px)",
left: 0,
top: 0,
zIndex: 1040,
transition: {
ease: "easeIn",
duration: 0.001,
},
},
shrink: {
display: "none",
position: "fixed",
backgroundColor: "rgba(0, 0, 0, 0.0)",
backdropFilter: "blur(0px)",
transition: {
duration: 0,
},
zIndex: -1,
},
};
export default function UnifiedStylingPreviewSurvey({
setActiveQuestionId,
activeQuestionId,
survey,
product,
}: UnifiedStylingPreviewSurveyProps) {
const [isModalOpen, setIsModalOpen] = useState(true);
const [isFullScreenPreview, setIsFullScreenPreview] = useState(false);
const [previewPosition, setPreviewPosition] = useState("relative");
const ContentRef = useRef<HTMLDivElement | null>(null);
const [shrink, setshrink] = useState(false);
const [previewType, setPreviewType] = useState<"link" | "web">("link");
const { productOverwrites } = survey || {};
const previewScreenVariants: Variants = {
expanded: {
right: "5%",
bottom: "10%",
top: "12%",
width: "40%",
position: "fixed",
height: "80%",
zIndex: 1050,
boxShadow: "0px 4px 5px 4px rgba(169, 169, 169, 0.25)",
transition: {
ease: "easeInOut",
duration: shrink ? 0.3 : 0,
},
},
expanded_with_fixed_positioning: {
zIndex: 1050,
position: "fixed",
top: "5%",
right: "5%",
bottom: "10%",
width: "90%",
height: "90%",
transition: {
ease: "easeOut",
duration: 0.4,
},
},
shrink: {
display: "relative",
width: ["83.33%"],
height: ["95%"],
},
};
const { placement: surveyPlacement } = productOverwrites || {};
const placement = surveyPlacement || product.placement;
const highlightBorderColor = product.styling?.highlightBorderColor?.light;
const styling: TStyling = useMemo(() => {
if (product.styling) {
return product.styling;
}
return {
unifiedStyling: true,
allowStyleOverwrite: true,
brandColor: {
light: product.brandColor || "#64748b",
},
};
}, [product.brandColor, product.styling]);
// this useEffect is fo refreshing the survey preview only if user is switching between templates on survey templates page and hence we are checking for survey.id === "someUniqeId1" which is a common Id for all templates
useEffect(() => {
if (survey.name !== surveyNameTemp && survey.id === "someUniqueId1") {
resetQuestionProgress();
surveyNameTemp = survey.name;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [survey]);
useEffect(() => {
if (previewType === "web") {
setIsModalOpen(true);
}
}, [previewType]);
function resetQuestionProgress() {
setActiveQuestionId(survey?.questions[0]?.id);
}
const onFileUpload = async (file: File) => file.name;
return (
<div className="flex h-full w-full flex-col items-center justify-items-center">
<motion.div
variants={previewParentContainerVariant}
className="fixed hidden h-[95%] w-5/6"
animate={isFullScreenPreview ? "expanded" : "shrink"}
/>
<motion.div
layout
variants={previewScreenVariants}
animate={
isFullScreenPreview
? previewPosition === "relative"
? "expanded"
: "expanded_with_fixed_positioning"
: "shrink"
}
className="relative flex h-[95] max-h-[95%] w-5/6 items-center justify-center rounded-lg border border-slate-300 bg-slate-200">
<div className="flex h-full w-5/6 flex-1 flex-col">
<div className="flex h-8 w-full items-center rounded-t-lg bg-slate-100">
<div className="ml-6 flex space-x-2">
<div className="h-3 w-3 rounded-full bg-red-500"></div>
<div className="h-3 w-3 rounded-full bg-amber-500"></div>
<div className="h-3 w-3 rounded-full bg-emerald-500"></div>
</div>
<div className="ml-4 flex w-full justify-between font-mono text-sm text-slate-400">
<p>{previewType === "web" ? "Your web app" : "Preview"}</p>
<div className="flex items-center">
{isFullScreenPreview ? (
<ArrowsPointingInIcon
className="mr-2 h-4 w-4 cursor-pointer"
onClick={() => {
setshrink(true);
setPreviewPosition("relative");
setTimeout(() => setIsFullScreenPreview(false), 300);
}}
/>
) : (
<ArrowsPointingOutIcon
className="mr-2 h-4 w-4 cursor-pointer"
onClick={() => {
setshrink(false);
setIsFullScreenPreview(true);
setTimeout(() => setPreviewPosition("fixed"), 300);
}}
/>
)}
<ResetProgressButton resetQuestionProgress={resetQuestionProgress} />
</div>
</div>
</div>
{previewType === "web" ? (
<Modal
isOpen
placement={placement}
highlightBorderColor={highlightBorderColor}
previewMode="desktop"
borderRadius={styling.roundness ?? 12}>
<SurveyInline
survey={survey}
activeQuestionId={activeQuestionId || undefined}
isBrandingEnabled={product.inAppSurveyBranding}
onActiveQuestionChange={setActiveQuestionId}
isRedirectDisabled={true}
onFileUpload={onFileUpload}
styling={styling}
/>
</Modal>
) : (
<MediaBackground survey={survey} ContentRef={ContentRef} isEditorView>
<div className="z-0 w-full max-w-md rounded-lg p-4">
<SurveyInline
survey={survey}
activeQuestionId={activeQuestionId || undefined}
isBrandingEnabled={product.linkSurveyBranding}
onActiveQuestionChange={setActiveQuestionId}
isRedirectDisabled={true}
onFileUpload={onFileUpload}
responseCount={42}
styling={styling}
/>
</div>
</MediaBackground>
)}
</div>
</motion.div>
{/* for toggling between mobile and desktop mode */}
<div className="mt-2 flex rounded-full border-2 border-slate-300 p-1">
<div
className={`${previewType === "link" ? "rounded-full bg-slate-200" : ""} cursor-pointer px-3 py-1`}
onClick={() => setPreviewType("link")}>
Link survey
</div>
<div
className={`${previewType === "web" ? "rounded-full bg-slate-200" : ""} cursor-pointer px-3 py-1`}
onClick={() => setPreviewType("web")}>
App survey
</div>
</div>
</div>
);
}
function ResetProgressButton({ resetQuestionProgress }) {
return (
<Button
variant="minimal"
className="py-0.2 mr-2 bg-white px-2 font-sans text-sm text-slate-500"
onClick={resetQuestionProgress}>
Restart
<ArrowPathRoundedSquareIcon className="ml-2 h-4 w-4" />
</Button>
);
}

View File

@@ -18,6 +18,7 @@ import { EditBrandColor } from "./components/EditBrandColor";
import { EditFormbricksBranding } from "./components/EditBranding";
import { EditHighlightBorder } from "./components/EditHighlightBorder";
import { EditPlacement } from "./components/EditPlacement";
import UnifiedStyling from "./components/UnifiedStyling";
export default async function ProfileSettingsPage({ params }: { params: { environmentId: string } }) {
const [session, team, product] = await Promise.all([
@@ -50,19 +51,24 @@ export default async function ProfileSettingsPage({ params }: { params: { enviro
return (
<div>
<SettingsTitle title="Look & Feel" />
<SettingsCard title="Brand Color" description="Match the surveys with your user interface.">
<SettingsCard
title="Unified Styling"
description="Set styling for ALL surveys in this project. You can still overwrite these styles in the survey editor.">
<UnifiedStyling product={product} />
</SettingsCard>
{/* <SettingsCard title="Brand Color" description="Match the surveys with your user interface.">
<EditBrandColor
product={product}
isBrandColorDisabled={isBrandColorEditDisabled}
environmentId={params.environmentId}
/>
</SettingsCard>
</SettingsCard> */}
<SettingsCard
title="In-app Survey Placement"
description="Change where surveys will be shown in your web app.">
<EditPlacement product={product} environmentId={params.environmentId} />
</SettingsCard>
<SettingsCard
{/* <SettingsCard
noPadding
title="Highlight Border"
description="Make sure your users notice the survey you display">
@@ -71,7 +77,7 @@ export default async function ProfileSettingsPage({ params }: { params: { enviro
defaultBrandColor={DEFAULT_BRAND_COLOR}
environmentId={params.environmentId}
/>
</SettingsCard>
</SettingsCard> */}
<SettingsCard
title="Formbricks Branding"
description="We love your support but understand if you toggle it off.">

View File

@@ -10,12 +10,14 @@ export default function Modal({
placement,
previewMode,
highlightBorderColor,
borderRadius,
}: {
children: ReactNode;
isOpen: boolean;
placement: TPlacement;
previewMode: string;
highlightBorderColor: string | null | undefined;
borderRadius?: number;
}) {
const [show, setShow] = useState(false);
const modalRef = useRef<HTMLDivElement | null>(null);
@@ -102,7 +104,14 @@ export default function Modal({
<div aria-live="assertive" className="relative h-full w-full overflow-hidden bg-slate-300">
<div
ref={modalRef}
style={{ ...highlightBorderColorStyle, ...scalingClasses }}
style={{
...highlightBorderColorStyle,
...scalingClasses,
...(borderRadius && {
borderRadius: `${borderRadius}px`,
}),
}}
className={cn(
"no-scrollbar pointer-events-auto absolute h-fit max-h-[90%] w-full max-w-sm overflow-y-auto rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out ",
previewMode === "desktop" ? getPlacementStyle(placement) : "max-w-full",

View File

@@ -11,11 +11,12 @@ import {
DevicePhoneMobileIcon,
} from "@heroicons/react/24/solid";
import { Variants, motion } from "framer-motion";
import { useEffect, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import type { TEnvironment } from "@formbricks/types/environment";
import type { TProduct } from "@formbricks/types/product";
import { TUploadFileConfig } from "@formbricks/types/storage";
import { TStyling } from "@formbricks/types/styling";
import { TSurvey } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
import { SurveyInline } from "@formbricks/ui/Survey";
@@ -120,10 +121,24 @@ export default function PreviewSurvey({
placement: surveyPlacement,
} = productOverwrites || {};
const brandColor = surveyBrandColor || product.brandColor;
// const brandColor = surveyBrandColor || product.brandColor;
const placement = surveyPlacement || product.placement;
const highlightBorderColor = surveyHighlightBorderColor || product.highlightBorderColor;
const styling: TStyling = useMemo(() => {
if (product.styling) {
return product.styling;
}
return {
unifiedStyling: true,
allowStyleOverwrite: true,
brandColor: {
light: product.brandColor || "#64748b",
},
};
}, [product.brandColor, product.styling]);
useEffect(() => {
// close modal if there are no questions left
if (survey.type === "web" && !survey.thankYouCard.enabled) {
@@ -207,12 +222,13 @@ export default function PreviewSurvey({
previewMode="mobile">
<SurveyInline
survey={survey}
brandColor={brandColor}
// brandColor={brandColor}
activeQuestionId={activeQuestionId || undefined}
isBrandingEnabled={product.inAppSurveyBranding}
onActiveQuestionChange={setActiveQuestionId}
isRedirectDisabled={true}
onFileUpload={onFileUpload}
styling={styling}
/>
</Modal>
) : (
@@ -220,12 +236,13 @@ export default function PreviewSurvey({
<div className="no-scrollbar z-10 w-full max-w-md overflow-y-auto rounded-lg border border-transparent">
<SurveyInline
survey={survey}
brandColor={brandColor}
// brandColor={brandColor}
activeQuestionId={activeQuestionId || undefined}
isBrandingEnabled={product.linkSurveyBranding}
onActiveQuestionChange={setActiveQuestionId}
onFileUpload={onFileUpload}
responseCount={42}
styling={styling}
/>
</div>
</div>
@@ -277,12 +294,13 @@ export default function PreviewSurvey({
previewMode="desktop">
<SurveyInline
survey={survey}
brandColor={brandColor}
// brandColor={brandColor}
activeQuestionId={activeQuestionId || undefined}
isBrandingEnabled={product.inAppSurveyBranding}
onActiveQuestionChange={setActiveQuestionId}
isRedirectDisabled={true}
onFileUpload={onFileUpload}
styling={styling}
/>
</Modal>
) : (
@@ -290,13 +308,14 @@ export default function PreviewSurvey({
<div className="z-0 w-full max-w-md rounded-lg p-4">
<SurveyInline
survey={survey}
brandColor={brandColor}
// brandColor={brandColor}
activeQuestionId={activeQuestionId || undefined}
isBrandingEnabled={product.linkSurveyBranding}
onActiveQuestionChange={setActiveQuestionId}
isRedirectDisabled={true}
onFileUpload={onFileUpload}
responseCount={42}
styling={styling}
/>
</div>
</MediaBackground>

View File

@@ -2,6 +2,7 @@ import { TActionClassNoCodeConfig } from "@formbricks/types/actionClasses";
import { TIntegrationConfig } from "@formbricks/types/integration";
import { TResponseData, TResponseMeta, TResponsePersonAttributes } from "@formbricks/types/responses";
import { TBaseFilters } from "@formbricks/types/segment";
import { TStyling } from "@formbricks/types/styling";
import {
TSurveyClosedMessage,
TSurveyHiddenFields,
@@ -38,5 +39,6 @@ declare global {
export type UserNotificationSettings = TUserNotificationSettings;
export type SegmentFilter = TBaseFilters;
export type SurveyInlineTriggers = TSurveyInlineTriggers;
export type Styling = TStyling;
}
}

View File

@@ -0,0 +1,57 @@
import { PrismaClient } from "@prisma/client";
import { TStyling } from "@formbricks/types/styling";
const prisma = new PrismaClient();
async function main() {
await prisma.$transaction(async (tx) => {
// product table with brand color and the highlight border color (if available)
// styling object needs to be created for each product
const products = await tx.product.findMany({});
if (!products) {
// something went wrong, could not find any products
return;
}
if (products.length) {
for (const product of products) {
if (product.styling !== null) {
// styling object already exists for this product
continue;
}
const styling: TStyling = {
unifiedStyling: true,
allowStyleOverwrite: true,
brandColor: {
light: product.brandColor,
},
...(product.highlightBorderColor && {
highlightBorderColor: {
light: product.highlightBorderColor,
dark: product.highlightBorderColor,
},
}),
};
await tx.product.update({
where: {
id: product.id,
},
data: {
styling,
},
});
}
}
});
}
main()
.catch(async (e) => {
console.error(e);
process.exit(1);
})
.finally(async () => await prisma.$disconnect());

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Product" ADD COLUMN "styling" JSONB DEFAULT '{"unifiedStyling":true,"allowStyleOverwrite":true,"brandColor":{"light":"#64748b"}}';

View File

@@ -23,7 +23,8 @@
"lint": "eslint ./src --fix",
"post-install": "pnpm generate",
"predev": "pnpm generate",
"data-migration:v1.6": "ts-node ./migrations/20240207041922_advanced_targeting/data-migration.ts"
"data-migration:v1.6": "ts-node ./migrations/20240207041922_advanced_targeting/data-migration.ts",
"data-migration:styling": "ts-node ./migrations/20240229062232_adds_styling_column_to_product_model/data-migration.ts"
},
"dependencies": {
"@prisma/client": "^5.10.2",

View File

@@ -419,6 +419,9 @@ model Product {
environments Environment[]
brandColor String @default("#64748b")
highlightBorderColor String?
/// @zod.custom(imports.ZStyling)
/// [Styling]
styling Json? @default("{\"unifiedStyling\":true,\"allowStyleOverwrite\":true,\"brandColor\":{\"light\":\"#64748b\"}}")
recontactDays Int @default(7)
linkSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in link surveys
inAppSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in in-app surveys

View File

@@ -1,5 +1,7 @@
import z from "zod";
export { ZStyling } from "@formbricks/types/styling";
export const ZActionProperties = z.record(z.string());
export { ZActionClassNoCodeConfig } from "@formbricks/types/actionClasses";
export { ZIntegrationConfig } from "@formbricks/types/integration";

View File

@@ -13,8 +13,11 @@ import { logoutPerson, resetPerson, setPersonAttribute, setPersonUserId } from "
declare global {
interface Window {
formbricksSurveys: {
renderSurveyInline: (props: SurveyInlineProps & { brandColor: string }) => void;
renderSurveyModal: (props: SurveyModalProps & { brandColor: string }) => void;
// renderSurveyInline: (props: SurveyInlineProps & { brandColor: string }) => void;
// renderSurveyModal: (props: SurveyModalProps & { brandColor: string }) => void;
renderSurveyInline: (props: SurveyInlineProps) => void;
renderSurveyModal: (props: SurveyModalProps) => void;
};
}
}

View File

@@ -34,6 +34,7 @@ const selectProduct = {
clickOutsideClose: true,
darkOverlay: true,
environments: true,
styling: true,
};
export const getProducts = async (teamId: string, page?: number): Promise<TProduct[]> => {

View File

@@ -32,6 +32,7 @@ export function Survey({
getSetIsResponseSendingFinished,
onFileUpload,
responseCount,
styling,
}: SurveyBaseProps) {
const [questionId, setQuestionId] = useState(
activeQuestionId || (survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id)
@@ -279,7 +280,7 @@ export function Survey({
return (
<>
<AutoCloseWrapper survey={survey} onClose={onClose}>
<div className="no-scrollbar flex h-full w-full flex-col justify-between rounded-lg bg-[--fb-survey-background-color] px-6 pb-3 pt-6">
<div className="no-scrollbar rounded-custom 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

View File

@@ -24,6 +24,7 @@ export function SurveyModal({
onRetry,
isRedirectDisabled = false,
responseCount,
styling,
}: SurveyModalProps) {
const [isOpen, setIsOpen] = useState(true);
@@ -67,6 +68,7 @@ export function SurveyModal({
onFileUpload={onFileUpload}
isRedirectDisabled={isRedirectDisabled}
responseCount={responseCount}
styling={styling}
/>
</Modal>
</div>

View File

@@ -85,7 +85,7 @@ export default function MultipleChoiceSingleQuestion({
<legend className="sr-only">Options</legend>
<div
className="bg-survey-bg relative max-h-[33vh] space-y-2 overflow-y-auto rounded-md py-0.5 pr-2"
className="bg-survey-bg rounded-custom relative max-h-[33vh] space-y-2 overflow-y-auto py-0.5 pr-2"
role="radiogroup">
{questionChoices.map((choice, idx) => (
<label

View File

@@ -41,8 +41,6 @@ export default function OpenTextQuestion({
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
const handleInputChange = (inputValue: string) => {
// const isValidInput = validateInput(inputValue, question.inputType, question.required);
// setIsValid(isValidInput);
onChange({ [question.id]: inputValue });
};
@@ -69,11 +67,9 @@ export default function OpenTextQuestion({
key={question.id}
onSubmit={(e) => {
e.preventDefault();
// if ( validateInput(value as string, question.inputType, question.required)) {
const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedttc);
onSubmit({ [question.id]: value, inputType: question.inputType }, updatedttc);
// }
}}
className="w-full">
{question.imageUrl && <QuestionImage imgUrl={question.imageUrl} />}
@@ -113,7 +109,7 @@ export default function OpenTextQuestion({
handleInputResize(e);
}}
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"
className="border-border bg-survey-bg text-subheading focus:border-border-highlight rounded-custom block w-full 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}
/>

View File

@@ -8,15 +8,16 @@ import { SurveyInlineProps, SurveyModalProps } from "@formbricks/types/formbrick
declare global {
interface Window {
formbricksSurveys: {
renderSurveyInline: (props: SurveyInlineProps & { brandColor: string }) => void;
renderSurveyModal: (props: SurveyModalProps & { brandColor: string }) => void;
renderSurveyInline: (props: SurveyInlineProps) => void;
renderSurveyModal: (props: SurveyModalProps) => void;
};
}
}
export const renderSurveyInline = (props: SurveyInlineProps & { brandColor: string }) => {
export const renderSurveyInline = (props: SurveyInlineProps) => {
addStylesToDom();
addCustomThemeToDom({ brandColor: props.brandColor });
// addCustomThemeToDom({ brandColor: props.styling?.brandColor?.light ?? "" });
addCustomThemeToDom({ styling: props.styling });
const element = document.getElementById(props.containerId);
if (!element) {
@@ -25,9 +26,10 @@ export const renderSurveyInline = (props: SurveyInlineProps & { brandColor: stri
render(h(SurveyInline, props), element);
};
export const renderSurveyModal = (props: SurveyModalProps & { brandColor: string }) => {
export const renderSurveyModal = (props: SurveyModalProps) => {
addStylesToDom();
addCustomThemeToDom({ brandColor: props.brandColor });
// addCustomThemeToDom({ brandColor: props.styling?.brandColor?.light ?? "" });
addCustomThemeToDom({ styling: props.styling });
// add container element to DOM
const element = document.createElement("div");

View File

@@ -2,6 +2,8 @@ import { isLight } from "@/lib/utils";
import global from "@/styles/global.css?inline";
import preflight from "@/styles/preflight.css?inline";
import { TStyling } from "@formbricks/types/styling";
import editorCss from "../../../ui/Editor/stylesEditorFrontend.css?inline";
export const addStylesToDom = () => {
@@ -13,15 +15,19 @@ export const addStylesToDom = () => {
}
};
export const addCustomThemeToDom = ({ brandColor }: { brandColor: string }) => {
export const addCustomThemeToDom = ({ styling }: { styling: TStyling }) => {
if (document.getElementById("formbricks__css") === null) return;
const styleElement = document.createElement("style");
styleElement.id = "formbricks__css__custom";
styleElement.innerHTML = `
:root {
--fb-brand-color: ${brandColor};
${isLight(brandColor) ? "--fb-brand-text-color: black;" : "--fb-brand-text-color: white;"}
--fb-brand-color: ${styling.brandColor?.light};
${isLight(styling.brandColor?.light ?? "") ? "--fb-brand-text-color: black;" : "--fb-brand-text-color: white;"}
--fb-heading-color: ${styling.questionColor?.light};
--fb-border-color: ${styling.inputBorderColor?.light};
--fb-survey-background-color: ${styling.cardBackgroundColor?.light};
--fb-border-radius: ${styling.roundness}px;
}
`;
document.head.appendChild(styleElement);

View File

@@ -81,4 +81,6 @@ p.fb-editor-paragraph {
--fb-rating-selected: black;
--fb-close-btn-color: var(--slate-500);
--fb-close-btn-color-hover: var(--slate-700);
--fb-border-radius: 8px;
}

View File

@@ -31,6 +31,9 @@ module.exports = {
"close-button": "var(--fb-close-btn-color)",
"close-button-focus": "var(--fb-close-btn-hover-color)",
},
borderRadius: {
fbBorderRadius: "var(--fb-border-radius)",
},
zIndex: {
999999: "999999",
},

View File

@@ -1,5 +1,6 @@
import { TResponseData, TResponseUpdate } from "./responses";
import { TUploadFileConfig } from "./storage";
import { TStyling } from "./styling";
import { TSurvey } from "./surveys";
export interface SurveyBaseProps {
@@ -19,6 +20,7 @@ export interface SurveyBaseProps {
prefillResponseData?: TResponseData;
onFileUpload: (file: File, config?: TUploadFileConfig) => Promise<string>;
responseCount?: number;
styling: TStyling;
}
export interface SurveyInlineProps extends SurveyBaseProps {
@@ -28,6 +30,6 @@ export interface SurveyInlineProps extends SurveyBaseProps {
export interface SurveyModalProps extends SurveyBaseProps {
clickOutside: boolean;
darkOverlay: boolean;
highlightBorderColor: string | null;
// highlightBorderColor: string | null;
placement: "bottomLeft" | "bottomRight" | "topLeft" | "topRight" | "center";
}

View File

@@ -2,6 +2,7 @@ import { z } from "zod";
import { ZColor, ZPlacement } from "./common";
import { ZEnvironment } from "./environment";
import { ZStyling } from "./styling";
export const ZProduct = z.object({
id: z.string().cuid2(),
@@ -11,6 +12,7 @@ export const ZProduct = z.object({
teamId: z.string(),
brandColor: ZColor,
highlightBorderColor: ZColor.nullable(),
styling: ZStyling.nullable(),
recontactDays: z.number().int(),
inAppSurveyBranding: z.boolean(),
linkSurveyBranding: z.boolean(),
@@ -34,6 +36,7 @@ export const ZProductUpdateInput = z.object({
clickOutsideClose: z.boolean().optional(),
darkOverlay: z.boolean().optional(),
environments: z.array(ZEnvironment).optional(),
styling: ZStyling.optional(),
});
export type TProductUpdateInput = z.infer<typeof ZProductUpdateInput>;

33
packages/types/styling.ts Normal file
View File

@@ -0,0 +1,33 @@
import { z } from "zod";
import { ZColor } from "./common";
export const ZStylingColor = z.object({
light: ZColor,
dark: ZColor.optional(),
});
export type TStylingColor = z.infer<typeof ZStylingColor>;
export const ZCardArrangementOptions = z.enum(["casual", "straight", "simple"]);
export type TCardArrangementOptions = z.infer<typeof ZCardArrangementOptions>;
export const ZCardArrangement = z.object({
linkSurveys: ZCardArrangementOptions,
inAppSurveys: ZCardArrangementOptions,
});
export const ZStyling = z.object({
unifiedStyling: z.boolean(),
allowStyleOverwrite: z.boolean(),
brandColor: ZStylingColor.optional(),
questionColor: ZStylingColor.optional(),
inputColor: ZStylingColor.optional(),
inputBorderColor: ZStylingColor.optional(),
cardBackgroundColor: ZStylingColor.optional(),
highlightBorderColor: ZStylingColor.optional(),
isDarkModeEnabled: z.boolean().optional(),
roundness: z.number().optional(),
cardArrangement: ZCardArrangement.optional(),
});
export type TStyling = z.infer<typeof ZStyling>;

View File

@@ -1,14 +1,24 @@
"use client";
/* import { persistForm, useForm } from "@/app/lib/forms"; */
import { useCallback, useRef, useState } from "react";
import { HexColorInput, HexColorPicker } from "react-colorful";
import { cn } from "@formbricks/lib/cn";
import useClickOutside from "@formbricks/lib/useClickOutside";
export const ColorPicker = ({ color, onChange }: { color: string; onChange: (v: string) => void }) => {
export const ColorPicker = ({
color,
onChange,
containerClass,
disabled,
}: {
color: string;
onChange: (v: string) => void;
containerClass?: string;
disabled?: boolean;
}) => {
return (
<div className="my-2">
<div className={cn("my-2", containerClass)}>
<div className="flex w-full items-center justify-between space-x-1 rounded-md border border-slate-300 bg-white px-2 text-sm text-slate-400">
<div className="flex w-full items-center">
#
@@ -18,6 +28,7 @@ export const ColorPicker = ({ color, onChange }: { color: string; onChange: (v:
onChange={onChange}
id="color"
aria-label="Primary color"
disabled={disabled}
/>
</div>
<PopoverPicker color={color} onChange={onChange} />

View File

@@ -0,0 +1,24 @@
"use client";
import * as SliderPrimitive from "@radix-ui/react-slider";
import * as React from "react";
import { cn } from "@formbricks/lib/cn";
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn("relative flex w-full touch-none select-none items-center", className)}
{...props}>
<SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full bg-gray-300">
<SliderPrimitive.Range className="absolute h-full bg-gray-300" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="border-primary ring-offset-background focus-visible:ring-ring block h-5 w-5 rounded-full border-2 bg-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider };

View File

@@ -0,0 +1,72 @@
import { useMemo } from "react";
import { cn } from "@formbricks/lib/cn";
import { TCardArrangementOptions } from "@formbricks/types/styling";
import { Button } from "../Button";
type CardArrangementProps = {
surveyType: "link" | "web";
activeCardArrangement: TCardArrangementOptions;
setActiveCardArrangement: (arrangement: TCardArrangementOptions) => void;
};
const CardArrangement = ({
activeCardArrangement,
surveyType,
setActiveCardArrangement,
}: CardArrangementProps) => {
const surveyTypeDerived = useMemo(() => {
return surveyType == "link" ? "Link" : "In App";
}, [surveyType]);
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col">
<h3 className="text-base font-semibold text-slate-900">
Card Arrangement for {surveyTypeDerived} Surveys
</h3>
<p className="text-sm text-slate-800">
How funky do you want your cards in {surveyTypeDerived} Surveys
</p>
</div>
<div className="flex gap-2 rounded-md border border-slate-300 bg-white p-1">
<Button
variant="minimal"
size="sm"
className={cn(
"flex flex-1 justify-center bg-white text-center",
activeCardArrangement === "casual" && "bg-slate-200"
)}
onClick={() => setActiveCardArrangement("casual")}>
Casual
</Button>
<Button
variant="minimal"
size="sm"
onClick={() => setActiveCardArrangement("straight")}
className={cn(
"flex flex-1 justify-center bg-white text-center",
activeCardArrangement === "straight" && "bg-slate-200"
)}>
Straight
</Button>
<Button
variant="minimal"
size="sm"
onClick={() => setActiveCardArrangement("simple")}
className={cn(
"flex flex-1 justify-center bg-white text-center",
activeCardArrangement === "simple" && "bg-slate-200"
)}>
Simple
</Button>
</div>
</div>
);
};
export default CardArrangement;

View File

@@ -0,0 +1,34 @@
import { cn } from "@formbricks/lib/cn";
import { ColorPicker } from "../ColorPicker";
type ColorSelectorWithLabelProps = {
label: string;
description?: string;
color: string;
setColor: React.Dispatch<React.SetStateAction<string>>;
className?: string;
disabled?: boolean;
};
const ColorSelectorWithLabel = ({
color,
description = "",
label,
setColor,
className = "",
disabled,
}: ColorSelectorWithLabelProps) => {
return (
<div className={cn("flex flex-col gap-4", className)}>
<div className="flex flex-col">
<h3 className="text-base font-semibold text-slate-900">{label}</h3>
{description && <p className="text-sm text-slate-800">{description}</p>}
</div>
<ColorPicker color={color} onChange={setColor} containerClass="my-0" disabled={disabled} />
</div>
);
};
export default ColorSelectorWithLabel;

View File

@@ -0,0 +1,105 @@
import { Switch } from "../Switch";
import ColorSelectorWithLabel from "./ColorSelectorWithLabel";
const colorDefaults = {
brandColor: "#64748b",
questionColor: "#2b2524",
inputColor: "#efefef",
inputBorderColor: "#c0c0c0",
cardBackgroundColor: "#c0c0c0",
highlightBorderColor: "#64748b",
};
type DarModeColorProps = {
isDarkMode: boolean;
setIsDarkMode: React.Dispatch<React.SetStateAction<boolean>>;
brandColor?: string;
setBrandColor: React.Dispatch<React.SetStateAction<string>>;
questionColor?: string;
setQuestionColor: React.Dispatch<React.SetStateAction<string>>;
inputColor?: string;
setInputColor: React.Dispatch<React.SetStateAction<string>>;
inputBorderColor?: string;
setInputBorderColor: React.Dispatch<React.SetStateAction<string>>;
cardBackgroundColor?: string;
setCardBackgroundColor: React.Dispatch<React.SetStateAction<string>>;
highlightBorderColor?: string;
setHighlighBorderColor: React.Dispatch<React.SetStateAction<string>>;
};
const DarkModeColors = ({
isDarkMode,
setIsDarkMode,
brandColor,
cardBackgroundColor,
highlightBorderColor,
inputBorderColor,
inputColor,
questionColor,
setBrandColor,
setCardBackgroundColor,
setHighlighBorderColor,
setInputBorderColor,
setInputColor,
setQuestionColor,
}: DarModeColorProps) => {
return (
<div className="flex flex-col gap-4 rounded-lg bg-slate-50 p-4">
<div className="flex items-center gap-4">
<Switch
checked={isDarkMode}
onCheckedChange={(value) => {
setIsDarkMode(value);
}}
/>
<div className="flex flex-col">
<h3 className="text-base font-semibold text-slate-900">Add &quot;Dark Mode&quot; Colors</h3>
<p className="text-sm text-slate-800">Your app has a dark mode? Set a different set of colors.</p>
</div>
</div>
{isDarkMode && (
<div className="grid grid-cols-2 gap-4">
<ColorSelectorWithLabel
label="Brand color"
color={brandColor ?? colorDefaults.brandColor}
setColor={setBrandColor}
className="gap-2"
/>
<ColorSelectorWithLabel
label="Question color"
color={questionColor ?? colorDefaults.questionColor}
setColor={setQuestionColor}
className="gap-2"
/>
<ColorSelectorWithLabel
label="Input color"
color={inputColor ?? colorDefaults.inputColor}
setColor={setInputColor}
className="gap-2"
/>
<ColorSelectorWithLabel
label="Input border color"
color={inputBorderColor ?? colorDefaults.inputBorderColor}
setColor={setInputBorderColor}
className="gap-2"
/>
<ColorSelectorWithLabel
label="Card background color"
color={cardBackgroundColor ?? colorDefaults.cardBackgroundColor}
setColor={setCardBackgroundColor}
className="gap-2"
/>
<ColorSelectorWithLabel
label="Highlight border color"
color={highlightBorderColor ?? colorDefaults.highlightBorderColor}
setColor={setHighlighBorderColor}
className="gap-2"
/>
</div>
)}
</div>
);
};
export default DarkModeColors;

View File

@@ -5,7 +5,7 @@ import { SurveyInlineProps, SurveyModalProps } from "@formbricks/types/formbrick
const createContainerId = () => `formbricks-survey-container`;
export const SurveyInline = (props: Omit<SurveyInlineProps & { brandColor: string }, "containerId">) => {
export const SurveyInline = (props: Omit<SurveyInlineProps, "containerId">) => {
const containerId = useMemo(() => createContainerId(), []);
useEffect(() => {
renderSurveyInline({
@@ -16,7 +16,7 @@ export const SurveyInline = (props: Omit<SurveyInlineProps & { brandColor: strin
return <div id={containerId} className="h-full w-full" />;
};
export const SurveyModal = (props: SurveyModalProps & { brandColor: string }) => {
export const SurveyModal = (props: SurveyModalProps) => {
useEffect(() => {
renderSurveyModal(props);
}, [props]);