feat: add "React (Atom)" to embed generator and rename rest (#20618)

* fix: trigger lingo.dev by removing duplicate value

* feat: add support for React Atom embed options

* fix: update locator for React code tab in embed code generator E2E tests

* fix: update embed tab names to clarify iframe usage

* fix failing test

* fix: indentation and installation commands for atoms

* fix: add note about iframe usage in Preview tab

* refactor: replace Cal component with BookerEmbed in EmbedCodes

* fix: Add EmbedConfig type import in Embed.tsx

- Added missing EmbedConfig type to the existing types import
- Resolves TypeScript error "Cannot find name 'EmbedConfig'"

* fix: update BookerEmbed usage in EmbedCodes component

* fix: enhance BookerEmbed implementation and update installation instructions in EmbedTabs

* fix: improve comments in EmbedCodes for clarity on layout options and styling references

* fix: streamline BookerEmbed usage in EmbedCodes with improved class handling and layout options

* fix: embed type selection

* fix: update layout selection in EmbedCodes to use dynamic configuration

* fix: update embed configuration to use theme instead of layout

* fix: conditionally render embed options based on if we are selecting atom

* fix: use simple props instead of embed specific logic to fix the active embed bug

* Fix double tabs activeness

* Revert isCurrent change

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
This commit is contained in:
amritandHariom Balhara authored and GitHub committed 2025-04-22 04:51:10 +00:00
1 parent f163e4638b
commit acf3a7c54e
4 files changed
+349 -156

No files matched your search

+122 -104
View File
@@ -37,12 +37,13 @@ import { HorizontalTabs } from "@calcom/ui/components/navigation";
import { showToast } from "@calcom/ui/components/toast";
import { useBookerTime } from "../bookings/Booker/components/hooks/useBookerTime";
import { EmbedTabName } from "./lib/EmbedTabs";
import { buildCssVarsPerTheme } from "./lib/buildCssVarsPerTheme";
import { EmbedTheme } from "./lib/constants";
import { getDimension } from "./lib/getDimension";
import { useEmbedDialogCtx } from "./lib/hooks/useEmbedDialogCtx";
import { useEmbedParams } from "./lib/hooks/useEmbedParams";
import type { EmbedTabs, EmbedType, EmbedTypes, PreviewState } from "./types";
import type { EmbedTabs, EmbedType, EmbedTypes, PreviewState, EmbedConfig } from "./types";
type EventType = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"] | undefined;
type EmbedDialogProps = {
@@ -932,55 +933,58 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
open={isEmbedCustomizationOpen}
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
<CollapsibleContent className="text-sm">
<div className={classNames(embedType === "inline" ? "block" : "hidden")}>
{/*TODO: Add Auto/Fixed toggle from Figma */}
<div className="text-default mb-[9px] text-sm">Window sizing</div>
<div className="justify-left mb-6 flex items-center !font-normal ">
<div className="mr-[9px]">
{/* Conditionally render Window Sizing only if inline embed AND NOT React Atom */}
{embedType === "inline" && embedParams.embedTabName !== EmbedTabName.ATOM_REACT && (
<div>
{/*TODO: Add Auto/Fixed toggle from Figma */}
<div className="text-default mb-[9px] text-sm">Window sizing</div>
<div className="justify-left mb-6 flex items-center !font-normal ">
<div className="mr-[9px]">
<TextField
labelProps={{ className: "hidden" }}
className="focus:ring-offset-0"
required
value={previewState.inline.width}
onChange={(e) => {
setPreviewState((previewState) => {
const width = e.target.value || "100%";
return {
...previewState,
inline: {
...previewState.inline,
width,
},
};
});
}}
addOnLeading={<>W</>}
/>
</div>
<TextField
labelProps={{ className: "hidden" }}
className="focus:ring-offset-0"
value={previewState.inline.height}
required
value={previewState.inline.width}
onChange={(e) => {
setPreviewState((previewState) => {
const width = e.target.value || "100%";
const height = e.target.value || "100%";
setPreviewState((previewState) => {
return {
...previewState,
inline: {
...previewState.inline,
width,
height,
},
};
});
}}
addOnLeading={<>W</>}
addOnLeading={<>H</>}
/>
</div>
<TextField
labelProps={{ className: "hidden" }}
className="focus:ring-offset-0"
value={previewState.inline.height}
required
onChange={(e) => {
const height = e.target.value || "100%";
setPreviewState((previewState) => {
return {
...previewState,
inline: {
...previewState.inline,
height,
},
};
});
}}
addOnLeading={<>H</>}
/>
</div>
</div>
)}
<div
className={classNames(
"items-center justify-between",
@@ -1105,51 +1109,53 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
<CollapsibleContent>
<div className="text-sm">
<Label className="mb-6">
<div className="mb-2">Theme</div>
<Select
className="w-full"
defaultValue={ThemeOptions[0]}
components={{
Control: ThemeSelectControl,
IndicatorSeparator: () => null,
}}
onChange={(option) => {
if (!option) {
return;
}
setPreviewState((previewState) => {
return {
...previewState,
inline: {
...previewState.inline,
config: {
...(previewState.inline.config ?? {}),
theme: option.value,
{/* Conditionally render EmbedTheme only if NOT React Atom */}
{embedParams.embedTabName !== EmbedTabName.ATOM_REACT && (
<Label className="mb-6">
<div className="mb-2">EmbedTheme</div>
<Select
className="w-full"
defaultValue={ThemeOptions[0]}
components={{
Control: ThemeSelectControl,
IndicatorSeparator: () => null,
}}
onChange={(option) => {
if (!option) {
return;
}
setPreviewState((previewState) => {
// Ensure theme is updated in config for all embed types
const newConfig = (currentConfig?: EmbedConfig) => ({
...(currentConfig ?? {}),
theme: option.value,
});
return {
...previewState,
inline: {
...previewState.inline,
config: newConfig(previewState.inline.config),
},
},
floatingPopup: {
...previewState.floatingPopup,
config: {
...(previewState.floatingPopup.config ?? {}),
theme: option.value,
floatingPopup: {
...previewState.floatingPopup,
config: newConfig(previewState.floatingPopup.config),
},
},
elementClick: {
...previewState.elementClick,
config: {
...(previewState.elementClick.config ?? {}),
theme: option.value,
elementClick: {
...previewState.elementClick,
config: newConfig(previewState.elementClick.config),
},
},
theme: option.value,
};
});
}}
options={ThemeOptions}
/>
</Label>
{!eventTypeHideOptionDisabled ? (
// Keep updating top-level theme for preview iframe
theme: option.value,
};
});
}}
options={ThemeOptions}
/>
</Label>
)}
{/* Conditionally render Hide Details Switch only if NOT Atom embed AND not disabled by prop */}
{!eventTypeHideOptionDisabled &&
embedParams.embedTabName !== EmbedTabName.ATOM_REACT ? (
<div className="mb-6 flex items-center justify-start space-x-2 rtl:space-x-reverse">
<Switch
checked={previewState.hideEventTypeDetails}
@@ -1165,31 +1171,33 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<div className="text-default text-sm">{t("hide_eventtype_details")}</div>
</div>
) : null}
{[
{ name: "brandColor", title: "light_brand_color" },
{ name: "darkBrandColor", title: "dark_brand_color" },
// { name: "lightColor", title: "Light Color" },
// { name: "lighterColor", title: "Lighter Color" },
// { name: "lightestColor", title: "Lightest Color" },
// { name: "highlightColor", title: "Highlight Color" },
// { name: "medianColor", title: "Median Color" },
].map((palette) => (
<Label key={palette.name} className="mb-6">
<div className="mb-2">{t(palette.title)}</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue={paletteDefaultValue(palette.name)}
onChange={(color) => {
addToPalette({
[palette.name as keyof (typeof previewState)["palette"]]: color,
});
}}
/>
</div>
</Label>
))}
{/* Conditionally render Brand Colors only if NOT React Atom */}
{embedParams.embedTabName !== EmbedTabName.ATOM_REACT &&
[
{ name: "brandColor", title: "light_brand_color" },
{ name: "darkBrandColor", title: "dark_brand_color" },
// { name: "lightColor", title: "Light Color" },
// { name: "lighterColor", title: "Lighter Color" },
// { name: "lightestColor", title: "Lightest Color" },
// { name: "highlightColor", title: "Highlight Color" },
// { name: "medianColor", title: "Median Color" },
].map((palette) => (
<Label key={palette.name} className="mb-6">
<div className="mb-2">{t(palette.title)}</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue={paletteDefaultValue(palette.name)}
onChange={(color) => {
addToPalette({
[palette.name as keyof (typeof previewState)["palette"]]: color,
});
}}
/>
</div>
</Label>
))}
<Label className="mb-6">
<div className="mb-2">{t("layout")}</div>
<Select
@@ -1200,16 +1208,26 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
return;
}
setPreviewState((previewState) => {
const config = {
...(previewState.floatingPopup.config ?? {}),
// Ensure layout is updated in config for all embed types
const newConfig = (currentConfig?: EmbedConfig) => ({
...(currentConfig ?? {}),
layout: option.value,
};
});
return {
...previewState,
inline: {
...previewState.inline,
config: newConfig(previewState.inline.config),
},
floatingPopup: {
...previewState.floatingPopup,
config,
config: newConfig(previewState.floatingPopup.config),
},
elementClick: {
...previewState.elementClick,
config: newConfig(previewState.elementClick.config),
},
// Keep updating top-level layout for preview iframe
layout: option.value,
};
});
+161 -45
View File
@@ -32,23 +32,23 @@ export const Codes = {
const namespaceProp = `${namespace ? `namespace="${namespace}"` : ""}`;
const argumentForGetCalApi = getArgumentForGetCalApi(namespace);
return code`
import Cal, { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
export default function MyApp() {
useEffect(()=>{
(async function () {
const cal = await getCalApi(${argumentForGetCalApi ? JSON.stringify(argumentForGetCalApi) : ""});
${uiInstructionCode}
})();
}, [])
return <Cal ${namespaceProp}
calLink="${calLink}"
style={{width:"${width}",height:"${height}",overflow:"scroll"}}
config={${JSON.stringify(previewState.config)}}
${doWeNeedCalOriginProp(embedCalOrigin) ? ` calOrigin="${embedCalOrigin}"` : ""}
${IS_SELF_HOSTED ? `embedJsUrl="${embedLibUrl}"` : ""}
/>;
};`;
import Cal, { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
export default function MyApp() {
useEffect(() => {
(async function () {
const cal = await getCalApi(${argumentForGetCalApi ? JSON.stringify(argumentForGetCalApi) : ""});
${uiInstructionCode}
})();
}, [])
return <Cal ${namespaceProp}
calLink="${calLink}"
style={{width:"${width}",height:"${height}",overflow:"scroll"}}
config={${JSON.stringify(previewState.config)}}
${doWeNeedCalOriginProp(embedCalOrigin) ? `calOrigin="${embedCalOrigin}"` : ""}
${IS_SELF_HOSTED ? `embedJsUrl="${embedLibUrl}"` : ""}
/>;
};`;
},
"floating-popup": ({
calLink,
@@ -70,17 +70,17 @@ export const Codes = {
...previewState,
});
return code`
import { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
export default function MyApp() {
useEffect(()=>{
(async function () {
const cal = await getCalApi(${argumentForGetCalApi ? JSON.stringify(argumentForGetCalApi) : ""});
${getApiNameForReactSnippet({ mainApiName: "cal" })}("floatingButton", ${floatingButtonArg});
${uiInstructionCode}
})();
}, [])
};`;
import { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
export default function MyApp() {
useEffect(() => {
(async function () {
const cal = await getCalApi(${argumentForGetCalApi ? JSON.stringify(argumentForGetCalApi) : ""});
${getApiNameForReactSnippet({ mainApiName: "cal" })}("floatingButton", ${floatingButtonArg});
${uiInstructionCode}
})();
}, [])
};`;
},
"element-click": ({
calLink,
@@ -97,21 +97,138 @@ export const Codes = {
}) => {
const argumentForGetCalApi = getArgumentForGetCalApi(namespace);
return code`
import { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
export default function MyApp() {
useEffect(()=>{
(async function () {
const cal = await getCalApi(${argumentForGetCalApi ? JSON.stringify(argumentForGetCalApi) : ""});
${uiInstructionCode}
})();
}, [])
return <button data-cal-namespace="${namespace}"
data-cal-link="${calLink}"
${doWeNeedCalOriginProp(embedCalOrigin) ? ` data-cal-origin="${embedCalOrigin}"` : ""}
${`data-cal-config='${JSON.stringify(previewState.config)}'`}
>Click me</button>;
};`;
import { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
export default function MyApp() {
useEffect(() => {
(async function () {
const cal = await getCalApi(${argumentForGetCalApi ? JSON.stringify(argumentForGetCalApi) : ""});
${uiInstructionCode}
})();
}, [])
return <button data-cal-namespace="${namespace}"
data-cal-link="${calLink}"
${doWeNeedCalOriginProp(embedCalOrigin) ? `data-cal-origin="${embedCalOrigin}"` : ""}
${`data-cal-config='${JSON.stringify(previewState.config)}'`}
>Click me</button>;
};`;
},
},
"react-atom": {
inline: ({
calLink,
uiInstructionCode,
previewState,
embedCalOrigin,
namespace,
}: {
calLink: string;
uiInstructionCode: string;
previewState: PreviewState["inline"];
embedCalOrigin: string;
namespace: string;
}) => {
return code`
import { BookerEmbed } from "@calcom/atoms";
// You might need to define or import BookerProps depending on your setup
// For example: type BookerProps = { eventTypeSlug: string; calUsername: string; /* other props */ };
export default function Booker( props : BookerProps ) {
return (
<>
<BookerEmbed
// Use the parsed username and event slug from calLink
eventSlug={eventSlug}
// layout can be of three types: COLUMN_VIEW, MONTH_VIEW or WEEK_VIEW,
// you can choose whichever you prefer
view="${previewState.config?.layout || "MONTH_VIEW"}"
username={calUsername}
customClassNames={{
bookerContainer: "border-subtle border",
}}
onCreateBookingSuccess={() => {
console.log("booking created successfully");
}}
/>
</>
);
};`;
},
"floating-popup": ({
calLink,
uiInstructionCode,
previewState,
embedCalOrigin,
namespace,
}: {
calLink: string;
embedCalOrigin: string;
uiInstructionCode: string;
namespace: string;
previewState: PreviewState["floatingPopup"];
}) => {
return code`
import { BookerEmbed } from "@calcom/atoms";
// You might need to define or import BookerProps depending on your setup
export default function Booker( props : BookerProps ) {
return (
<>
<BookerEmbed
// Use the parsed username and event slug from calLink
eventSlug={eventSlug}
// layout can be of three types: COLUMN_VIEW, MONTH_VIEW or WEEK_VIEW,
// you can choose whichever you prefer
view="${previewState.config?.layout || "MONTH_VIEW"}"
username={calUsername}
customClassNames={{
bookerContainer: "border-subtle border",
}}
onCreateBookingSuccess={() => {
console.log("booking created successfully");
}}
/>
</>
);
};`;
},
"element-click": ({
calLink,
uiInstructionCode,
previewState,
embedCalOrigin,
namespace,
}: {
calLink: string;
uiInstructionCode: string;
previewState: PreviewState["elementClick"];
embedCalOrigin: string;
namespace: string;
}) => {
return code`
import { BookerEmbed } from "@calcom/atoms";
// You might need to define or import BookerProps depending on your setup
export default function Booker( props : BookerProps ) {
return (
<>
<BookerEmbed
// Use the parsed username and event slug from calLink
eventSlug={eventSlug}
// layout can be of three types: COLUMN_VIEW, MONTH_VIEW or WEEK_VIEW,
// you can choose whichever you prefer
view="${previewState.config?.layout || "MONTH_VIEW"}"
username={calUsername}
customClassNames={{
bookerContainer: "border-subtle border",
}}
onCreateBookingSuccess={() => {
console.log("booking created successfully");
}}
/>
</>
);
};`;
},
},
HTML: {
@@ -176,8 +293,7 @@ export const Codes = {
${uiInstructionCode}`;
},
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} satisfies Record<string, Record<string, (...args: any[]) => string>>;
};
/**
* It allows us to show code with certain reusable blocks indented according to the block variable placement
+65 -6
View File
@@ -15,9 +15,15 @@ import { getApiNameForReactSnippet, getApiNameForVanillaJsSnippet } from "./getA
import { getDimension } from "./getDimension";
import { useEmbedCalOrigin } from "./hooks";
export const enum EmbedTabName {
HTML = "embed-code",
IFRAME_REACT = "embed-react",
ATOM_REACT = "embed-atom-react",
}
export const tabs = [
{
name: "HTML",
name: "HTML (iframe)",
href: "embedTabName=embed-code",
icon: "code" as const,
type: "code",
@@ -74,7 +80,7 @@ export const tabs = [
}),
},
{
name: "React",
name: "React (iframe)",
href: "embedTabName=embed-react",
"data-testid": "react",
icon: "code" as const,
@@ -104,11 +110,11 @@ export const tabs = [
style={{ resize: "none", overflow: "auto" }}
value={`/* First make sure that you have installed the package */
/* If you are using yarn */
// yarn add @calcom/embed-react
/* If you are using yarn */
// yarn add @calcom/embed-react
/* If you are using npm */
// npm install @calcom/embed-react
/* If you are using npm */
// npm install @calcom/embed-react
${getEmbedTypeSpecificString({
embedFramework: "react",
embedType,
@@ -123,6 +129,59 @@ export const tabs = [
);
}),
},
{
name: "React (Atom)",
href: `embedTabName=${EmbedTabName.ATOM_REACT}`,
"data-testid": "react-atom",
icon: "code" as const,
type: "code",
Component: forwardRef<
HTMLTextAreaElement | HTMLIFrameElement | null,
{ embedType: EmbedType; calLink: string; previewState: PreviewState; namespace: string }
>(function EmbedReactAtom({ embedType, calLink, previewState, namespace }, ref) {
const { t } = useLocale();
const embedCalOrigin = useEmbedCalOrigin();
if (ref instanceof Function || !ref) {
return null;
}
if (ref.current && !(ref.current instanceof HTMLTextAreaElement)) {
return null;
}
return (
<>
<small className="text-subtle flex py-2">{t("create_update_react_component")}</small>
<div className="text-default bg-attention mb-3 rounded-md p-3 text-sm">
<strong>Note:</strong> The preview shown in the Preview tab is generated using the iframe
approach.
</div>
<TextArea
data-testid={`${EmbedTabName.ATOM_REACT}`}
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
name={`${EmbedTabName.ATOM_REACT}`}
className="text-default bg-default h-[calc(100%-50px)] font-mono"
readOnly
style={{ resize: "none", overflow: "auto" }}
value={`/* First make sure that you have installed the package */
/* If you are using yarn */
// yarn add @calcom/atoms
/* If you are using npm */
// npm install @calcom/atoms
${getEmbedTypeSpecificString({
embedFramework: "react-atom" as EmbedFramework,
embedType,
calLink,
previewState,
embedCalOrigin,
namespace,
})}`}
/>
</>
);
}),
},
{
name: "Preview",
href: "embedTabName=embed-preview",
+1 -1
View File
@@ -53,6 +53,6 @@ export type PreviewState = {
layout: BookerLayouts;
};
export type EmbedFramework = "react" | "HTML";
export type EmbedFramework = "react" | "react-atom" | "HTML";
export type EmbedTabs = typeof tabs;
export type EmbedTypes = ReturnType<typeof useEmbedTypes>;