fix: App theme being used in routing-form embed (#19489)

* fix wrong theme routing-form embed

* fix wrong theme routing-form embed
This commit is contained in:
Hariom Balhara
2025-02-28 23:50:46 +05:30
committed by GitHub
parent 6068829d70
commit 564c84dade
13 changed files with 433 additions and 157 deletions
@@ -7,7 +7,7 @@ export default async function BookingPageWrapperLayout({ children }: { children:
const nonce = h.get("x-nonce") ?? undefined;
return (
<PageWrapper isBookingPage={true} requiresLicense={false} nonce={nonce} themeBasis={null}>
<PageWrapper isBookingPage={true} requiresLicense={false} nonce={nonce}>
{children}
</PageWrapper>
);
+1 -1
View File
@@ -7,7 +7,7 @@ export default async function PageWrapperLayout({ children }: { children: React.
const nonce = h.get("x-nonce") ?? undefined;
return (
<PageWrapper requiresLicense={false} nonce={nonce} themeBasis={null}>
<PageWrapper requiresLicense={false} nonce={nonce}>
{children}
</PageWrapper>
);
-40
View File
@@ -78,46 +78,6 @@ export default async function RootLayout({ children }: { children: React.ReactNo
}}
/>
)}
<script
nonce={nonce}
id="headScript"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: `
window.calNewLocale = "${locale}";
(function applyTheme() {
try {
const appTheme = localStorage.getItem('app-theme');
if (!appTheme) return;
let bookingTheme, username;
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith('booking-theme:')) {
bookingTheme = localStorage.getItem(key);
username = key.split("booking-theme:")[1];
break;
}
}
const onReady = () => {
const isBookingPage = username && window.location.pathname.slice(1).startsWith(username);
if (document.body) {
document.body.classList.add(isBookingPage ? bookingTheme : appTheme);
} else {
requestAnimationFrame(onReady);
}
};
requestAnimationFrame(onReady);
} catch (e) {
console.error('Error applying theme:', e);
}
})();
`,
}}
/>
<style>{`
:root {
--font-inter: ${interFont.style.fontFamily.replace(/\'/g, "")};
+1 -1
View File
@@ -246,7 +246,7 @@ const ServerPage = async () => {
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
return (
<PageWrapper requiresLicense={false} nonce={nonce} themeBasis={null}>
<PageWrapper requiresLicense={false} nonce={nonce}>
<NotFound t={t} headers={h} />
</PageWrapper>
);
@@ -15,7 +15,6 @@ export type PageWrapperProps = Readonly<{
children: React.ReactNode;
requiresLicense: boolean;
nonce: string | undefined;
themeBasis: string | null;
isBookingPage?: boolean;
i18n?: SSRConfig;
}>;
@@ -0,0 +1,240 @@
import type { ReadonlyURLSearchParams } from "next/navigation";
import { describe, expect, it } from "vitest";
import { EmbedTheme } from "@calcom/features/embed/lib/constants";
import { getThemeProviderProps } from "../getThemeProviderProps";
import { getUniqueIdentifierForBookingPage } from "../getThemeProviderProps";
describe("getThemeProviderProps", () => {
const fnArg = {
props: {
isBookingPage: false,
isThemeSupported: true,
nonce: "test-nonce",
},
isEmbedMode: false,
embedNamespace: null,
searchParams: new URLSearchParams() as unknown as ReadonlyURLSearchParams,
};
describe("App Theme Support", () => {
const expectedStorageKey = "app-theme";
const appThemeExpectedProps = {
attribute: "class",
storageKey: expectedStorageKey,
key: expectedStorageKey,
nonce: "test-nonce",
enableColorScheme: false,
};
it("should return app theme configuration when not in booking page or embed mode", () => {
const result = getThemeProviderProps({
...fnArg,
pathname: "/test",
});
expect(result).toEqual({
forcedTheme: undefined,
enableSystem: true,
...appThemeExpectedProps,
});
});
it("should force light theme and no storageKey and system theme support is required when isThemeSupported is false", () => {
const result = getThemeProviderProps({
...fnArg,
pathname: "/test",
props: {
...fnArg.props,
isThemeSupported: false,
},
});
expect(result).toEqual({
...appThemeExpectedProps,
storageKey: undefined,
key: undefined,
forcedTheme: "light",
enableSystem: false,
});
});
it("should not force theme when isThemeSupported is not explicitly set", () => {
const result = getThemeProviderProps({
...fnArg,
pathname: "/test",
props: {
...fnArg.props,
isThemeSupported: undefined,
},
});
expect(result).toEqual({
...appThemeExpectedProps,
forcedTheme: undefined,
enableSystem: true,
});
});
});
describe("Booking Page Theme Support", () => {
const bookingPageExpectedProps = {
attribute: "class",
nonce: "test-nonce",
enableColorScheme: false,
};
it("should handle booking page theme", () => {
const expectedStorageKey = "booking-theme:free";
const result = getThemeProviderProps({
...fnArg,
pathname: "/free/30min",
props: {
...fnArg.props,
isBookingPage: true,
},
});
expect(result).toEqual({
...bookingPageExpectedProps,
storageKey: expectedStorageKey,
key: expectedStorageKey,
forcedTheme: undefined,
enableSystem: true,
});
});
it("should handle team booking page theme configuration", () => {
const result = getThemeProviderProps({
...fnArg,
props: {
...fnArg.props,
isBookingPage: true,
},
pathname: "/team/sales/30min",
});
expect(result.storageKey).toBe("booking-theme:sales");
});
});
describe("Embed Mode Theme Support", () => {
const embedProps = {
...fnArg,
isEmbedMode: true,
};
const embedPageExpectedProps = {
attribute: "class",
nonce: "test-nonce",
enableColorScheme: false,
enableSystem: true,
forcedTheme: undefined,
};
it("should handle embed mode with default theme", () => {
const result = getThemeProviderProps({
...embedProps,
embedNamespace: "ns",
pathname: "/free",
});
const storageKey = "embed-theme-ns:free";
expect(result).toEqual({
...embedPageExpectedProps,
storageKey,
key: storageKey,
});
});
it("should handle embed mode with explicit theme. Must have theme name in storageKey so that different themes for different booking pages under same namespace also work", () => {
const searchParams = new URLSearchParams();
searchParams.set("theme", EmbedTheme.dark);
const result = getThemeProviderProps({
...embedProps,
searchParams: searchParams as unknown as ReadonlyURLSearchParams,
pathname: "/free",
embedNamespace: "ns",
});
const expectedStorageKey = "embed-theme-ns:free:dark";
expect(result).toEqual({
...embedPageExpectedProps,
storageKey: expectedStorageKey,
key: expectedStorageKey,
});
});
it("should not append theme suffix for auto theme as that is kind of default", () => {
const searchParams = new URLSearchParams();
searchParams.set("theme", EmbedTheme.auto);
const result = getThemeProviderProps({
...embedProps,
searchParams: searchParams as unknown as ReadonlyURLSearchParams,
pathname: "/free",
embedNamespace: "ns",
});
expect(result).toEqual({
...embedPageExpectedProps,
storageKey: "embed-theme-ns:free",
key: "embed-theme-ns:free",
});
});
});
});
describe("getUniqueIdentifierForBookingPage", () => {
describe("User Pages", () => {
it.each([
{ path: "/test", expected: "test" },
{ path: "/test/anything", expected: "test" },
])("should return $expected for $path", ({ path, expected }) => {
const result = getUniqueIdentifierForBookingPage({ pathname: path });
expect(result).toBe(expected);
});
});
describe("Team Pages", () => {
it.each([
{ path: "/team/test", expected: "test" },
{ path: "/team/test/anything", expected: "test" },
])("should return $expected for $path", ({ path, expected }) => {
const result = getUniqueIdentifierForBookingPage({ pathname: path });
expect(result).toBe(expected);
});
});
describe("Routing Pages", () => {
it("should return formId for routing pages", () => {
const result = getUniqueIdentifierForBookingPage({
pathname: "/forms/test",
});
expect(result).toBe("test");
});
});
describe("Private Booking Pages", () => {
it.each([
{ path: "/d/private", expected: "private" },
{ path: "/d/private/anything", expected: "private" },
])("should return $expected for $path", ({ path, expected }) => {
const result = getUniqueIdentifierForBookingPage({ pathname: path });
expect(result).toBe(expected);
});
});
describe("Dynamic Booking Pages", () => {
it.each([
{ path: "/john+jane", expected: "john+jane" },
{ path: "/john+jane/", expected: "john+jane" },
{ path: "/john+jane/anything", expected: "john+jane" },
{ path: "/john%2bjane/anything", expected: "john%2bjane" },
])("should return $expected for $path", ({ path, expected }) => {
const result = getUniqueIdentifierForBookingPage({ pathname: path });
expect(result).toBe(expected);
});
});
});
+11 -98
View File
@@ -8,7 +8,7 @@ import { appWithTranslation } from "next-i18next";
import { ThemeProvider } from "next-themes";
import type { AppProps as NextAppProps, AppProps as NextJsAppProps } from "next/app";
import type { ReadonlyURLSearchParams } from "next/navigation";
import { useSearchParams } from "next/navigation";
import { usePathname, useSearchParams } from "next/navigation";
import type { ReactNode } from "react";
import { useEffect } from "react";
@@ -26,6 +26,8 @@ import type { WithNonceProps } from "@lib/withNonce";
import { useViewerI18n } from "@components/I18nLanguageHandler";
import type { PageWrapperProps } from "@components/PageWrapperAppDir";
import { getThemeProviderProps } from "./getThemeProviderProps";
const I18nextAdapter = appWithTranslation<
NextJsAppProps<SSRConfig> & {
children: React.ReactNode;
@@ -113,18 +115,8 @@ const CustomI18nextProvider = (props: { children: React.ReactElement; i18n?: SSR
);
};
const enum ThemeSupport {
// e.g. Login Page
None = "none",
// Entire App except Booking Pages
App = "appConfigured",
// Booking Pages(including Routing Forms)
Booking = "bookingConfigured",
}
type CalcomThemeProps = Readonly<{
isBookingPage: boolean;
themeBasis: string | null;
nonce: string | undefined;
children: React.ReactNode;
isThemeSupported: boolean;
@@ -137,8 +129,14 @@ const CalcomThemeProvider = (props: CalcomThemeProps) => {
const searchParams = useSearchParams();
const embedNamespace = searchParams ? getEmbedNamespace(searchParams) : null;
const isEmbedMode = typeof embedNamespace === "string";
const { key, ...themeProviderProps } = getThemeProviderProps({ props, isEmbedMode, embedNamespace });
const pathname = usePathname();
const { key, ...themeProviderProps } = getThemeProviderProps({
props,
isEmbedMode,
embedNamespace,
pathname,
searchParams,
});
return (
<ThemeProvider key={key} {...themeProviderProps}>
@@ -158,90 +156,6 @@ const CalcomThemeProvider = (props: CalcomThemeProps) => {
);
};
/**
* The most important job for this fn is to generate correct storageKey for theme persistenc.
* `storageKey` is important because that key is listened for changes(using [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event) and any pages opened will change it's theme based on that(as part of next-themes implementation).
* Choosing the right storageKey avoids theme flickering caused by another page using different theme
* So, we handle all the cases here namely,
* - Both Booking Pages, /free/30min and /pro/30min but configured with different themes but being operated together.
* - Embeds using different namespace. They can be completely themed different on the same page.
* - Embeds using the same namespace but showing different cal.com links with different themes
* - Embeds using the same namespace and showing same cal.com links with different themes(Different theme is possible for same cal.com link in case of embed because of theme config available in embed)
* - App has different theme then Booking Pages.
*
* All the above cases have one thing in common, which is the origin and thus localStorage is shared and thus `storageKey` is critical to avoid theme flickering.
*
* Some things to note:
* - There is a side effect of so many factors in `storageKey` that many localStorage keys will be created if a user goes through all these scenarios(e.g like booking a lot of different users)
* - Some might recommend disabling localStorage persistence but that doesn't give good UX as then we would default to light theme always for a few seconds before switching to dark theme(if that's the user's preference).
* - We can't disable [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event handling as well because changing theme in one tab won't change the theme without refresh in other tabs. That's again a bad UX
* - Theme flickering becomes infinitely ongoing in case of embeds because of the browser's delay in processing `storage` event within iframes. Consider two embeds simultaneously opened with pages A and B. Note the timeline and keep in mind that it happened
* because 'setItem(A)' and 'Receives storageEvent(A)' allowed executing setItem(B) in b/w because of the delay.
* - t1 -> setItem(A) & Fires storageEvent(A) - On Page A) - Current State(A)
* - t2 -> setItem(B) & Fires storageEvent(B) - On Page B) - Current State(B)
* - t3 -> Receives storageEvent(A) & thus setItem(A) & thus fires storageEvent(A) (On Page B) - Current State(A)
* - t4 -> Receives storageEvent(B) & thus setItem(B) & thus fires storageEvent(B) (On Page A) - Current State(B)
* - ... and so on ...
*/
function getThemeProviderProps({
props,
isEmbedMode,
embedNamespace,
}: {
props: Omit<CalcomThemeProps, "children">;
isEmbedMode: boolean;
embedNamespace: string | null;
}) {
const themeSupport = props.isBookingPage
? ThemeSupport.Booking
: props.isThemeSupported === false
? ThemeSupport.None
: ThemeSupport.App;
const isBookingPageThemeSupportRequired = themeSupport === ThemeSupport.Booking;
const themeBasis = props.themeBasis;
if (!process.env.NEXT_PUBLIC_IS_E2E && (isBookingPageThemeSupportRequired || isEmbedMode) && !themeBasis) {
console.warn(
"`themeBasis` is required for booking page theme support. Not providing it will cause theme flicker."
);
}
const appearanceIdSuffix = themeBasis ? `:${themeBasis}` : "";
const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
let embedExplicitlySetThemeSuffix = "";
if (typeof window !== "undefined") {
const embedTheme = window.getEmbedTheme();
if (embedTheme) {
embedExplicitlySetThemeSuffix = `:${embedTheme}`;
}
}
const storageKey = isEmbedMode
? // Same Namespace, Same Organizer but different themes would still work seamless and not cause theme flicker
// Even though it's recommended to use different namespaces when you want to theme differently on the same page but if the embeds are on different pages, the problem can still arise
`embed-theme-${embedNamespace}${appearanceIdSuffix}${embedExplicitlySetThemeSuffix}`
: themeSupport === ThemeSupport.App
? "app-theme"
: isBookingPageThemeSupportRequired
? `booking-theme${appearanceIdSuffix}`
: undefined;
return {
storageKey,
forcedTheme,
themeSupport,
nonce: props.nonce,
enableColorScheme: false,
enableSystem: themeSupport !== ThemeSupport.None,
// next-themes doesn't listen to changes on storageKey. So we need to force a re-render when storageKey changes
// This is how login to dashboard soft navigation changes theme from light to dark
key: storageKey,
attribute: "class",
};
}
function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
const flags = useFlags();
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
@@ -268,7 +182,6 @@ const AppProviders = (props: PageWrapperProps) => {
<TooltipProvider>
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
<CalcomThemeProvider
themeBasis={props.themeBasis}
nonce={props.nonce}
isThemeSupported={isThemeSupported}
isBookingPage={props.isBookingPage || isBookingPage}>
+137
View File
@@ -0,0 +1,137 @@
import type { ReadonlyURLSearchParams } from "next/navigation";
import { EmbedTheme } from "@calcom/features/embed/lib/constants";
const enum ThemeSupport {
// e.g. Login Page
None = "none",
// Entire App except Booking Pages
App = "appConfigured",
// Booking Pages(including Routing Forms)
Booking = "bookingConfigured",
}
/**
* Returns a unique indentifier that remains same as long as we land on the public page for same entity like event-type/routing-form/dynamic-group-booking etc
* The theme stays same for same identifier.
*/
export function getUniqueIdentifierForBookingPage({ pathname }: { pathname: string }) {
const pathTokens = pathname.split("/").slice(1);
// If it is a booking page then it could be one of the following:
// User booking page e.g. /free and /free/30mins
// Team booking page e.g. /team/sales and /team/sales/30mins
// Organization Team booking page e.g. /sales and /sales/30mins - Same pattern as User Booking Page
const startsWithTeam = pathTokens[0] === "team";
const isRoutingFormPage = pathTokens[0] === "forms";
const isPrivateBookingPage = pathTokens[0] === "d";
const isDynamicBookingPage = pathTokens[0].toLowerCase().split(/\+|%2B/).length > 1;
if (isRoutingFormPage) {
return pathTokens[1];
}
if (isPrivateBookingPage) {
return pathTokens[1];
}
if (isDynamicBookingPage) {
return pathTokens[0];
}
if (startsWithTeam) {
// Note: Different organizations can have same team slugs but those are on different subdomains and thus it isn't a problem to have same themeBasis because storage is different for different origins
return pathTokens[1];
}
return pathTokens[0];
}
/**
* The most important job for this fn is to generate correct storageKey for theme persistenc.
* `storageKey` is important because that key is listened for changes(using [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event) and any pages opened will change it's theme based on that(as part of next-themes implementation).
* Choosing the right storageKey avoids theme flickering caused by another page using different theme
* So, we handle all the cases here namely,
* - Both Booking Pages, /free/30min and /pro/30min but configured with different themes but being operated together.
* - Embeds using different namespace. They can be completely themed different on the same page.
* - Embeds using the same namespace but showing different cal.com links with different themes
* - Embeds using the same namespace and showing same cal.com links with different themes(Different theme is possible for same cal.com link in case of embed because of theme config available in embed)
* - App has different theme then Booking Pages.
*
* All the above cases have one thing in common, which is the origin and thus localStorage is shared and thus `storageKey` is critical to avoid theme flickering.
*
* Some things to note:
* - There is a side effect of so many factors in `storageKey` that many localStorage keys will be created if a user goes through all these scenarios(e.g like booking a lot of different users)
* - Some might recommend disabling localStorage persistence but that doesn't give good UX as then we would default to light theme always for a few seconds before switching to dark theme(if that's the user's preference).
* - We can't disable [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event handling as well because changing theme in one tab won't change the theme without refresh in other tabs. That's again a bad UX
* - Theme flickering becomes infinitely ongoing in case of embeds because of the browser's delay in processing `storage` event within iframes. Consider two embeds simultaneously opened with pages A and B. Note the timeline and keep in mind that it happened
* because 'setItem(A)' and 'Receives storageEvent(A)' allowed executing setItem(B) in b/w because of the delay.
* - t1 -> setItem(A) & Fires storageEvent(A) - On Page A) - Current State(A)
* - t2 -> setItem(B) & Fires storageEvent(B) - On Page B) - Current State(B)
* - t3 -> Receives storageEvent(A) & thus setItem(A) & thus fires storageEvent(A) (On Page B) - Current State(A)
* - t4 -> Receives storageEvent(B) & thus setItem(B) & thus fires storageEvent(B) (On Page A) - Current State(B)
* - ... and so on ...
*/
export function getThemeProviderProps({
props,
isEmbedMode,
embedNamespace,
pathname,
searchParams,
}: {
props: {
isBookingPage: boolean;
nonce: string | undefined;
isThemeSupported?: boolean | undefined;
};
isEmbedMode: boolean;
embedNamespace: string | null;
pathname: string | null;
searchParams: ReadonlyURLSearchParams | null;
}) {
const isBookingPage = props.isBookingPage;
const themeSupport = isBookingPage
? ThemeSupport.Booking
: props.isThemeSupported === false
? ThemeSupport.None
: ThemeSupport.App;
const isBookingPageThemeSupportRequired = themeSupport === ThemeSupport.Booking;
const themeBasis = pathname ? getUniqueIdentifierForBookingPage({ pathname }) : null;
const isThemeBasisRequired = isBookingPageThemeSupportRequired || isEmbedMode;
if (isThemeBasisRequired && !themeBasis) {
console.error("`themeBasis` is nullish. This should not happen.", {
pathname,
});
}
const appearanceIdSuffix = themeBasis ? `:${themeBasis}` : "";
const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
const themeQueryParam = searchParams?.get("theme");
const embedExplicitlySetThemeSuffix =
isEmbedMode && themeQueryParam && themeQueryParam !== EmbedTheme.auto ? `:${themeQueryParam}` : "";
const storageKey = isEmbedMode
? // Same Namespace, Same Organizer but different themes would still work seamless and not cause theme flicker
// Even though it's recommended to use different namespaces when you want to theme differently on the same page but if the embeds are on different pages, the problem can still arise
`embed-theme-${embedNamespace}${appearanceIdSuffix}${embedExplicitlySetThemeSuffix}`
: themeSupport === ThemeSupport.App
? "app-theme"
: isBookingPageThemeSupportRequired
? `booking-theme${appearanceIdSuffix}`
: undefined;
const themeProviderProps = {
storageKey,
// Can force a theme through this, no matter what system preference is or storage value is
forcedTheme,
nonce: props.nonce,
enableColorScheme: false,
// Enables theme switching based on system preference if true
enableSystem: themeSupport !== ThemeSupport.None,
// next-themes doesn't listen to changes on storageKey. So we need to force a re-render when storageKey changes
// This is how login to dashboard soft navigation changes theme from light to dark
key: storageKey,
attribute: "class",
};
return themeProviderProps;
}
+20
View File
@@ -0,0 +1,20 @@
- We use next-themes
- It provides useNextTheme hook which does the job of applying a theme for the app reliably. It persists the preference in localStorage and then ensures that the theme is reliably and instantly applied on next page load through its JS snippet.
- It also provides the support for system theme that allows theme to change automatically when user changes their system theme preference.
- Different Cal.com circumstances
- Standalone/Direct Booking Pages
- Here user have the option to force a certain theme(light/dark) or choose system theme.
- StorageKey: booking-theme${appearanceIdSuffix}
- appearanceIdSuffix -> :${themeBasis} // themeBasis is usually the username of the organizer.
- Embeds
- There could be many embeds on the same origin(https://acme.cal.com) using different themes. e.g. for demo page someone wants to force light theme(as that page doesn't have dark mode) and for landing page someone wants to adapt to system theme as that page supports both modes.
- Remember that embed theme if provided will take precedence over bookingConfiguredTheme(i.e. theme configured in appearance for booking pages)
- StorageKey: `embed-theme-${embedNamespace}${appearanceIdSuffix}${embedExplicitlySetThemeSuffix}`
- (Same Namespace, Same Organizer but different themes would still work seamless and not cause theme flicker)
- `embedExplicitlySetThemeSuffix` -> `:${embedTheme}` // `embedTheme` is the theme explicitly configured for the embed)
- Non Booking Pages(which would be all dashboard pages)
- Here also, user has the option to force a certain theme(light/dark) or choose system theme.
- StorageKey: app-theme
Also, see inline comments in getThemeProviderProps.ts for more details.
@@ -82,7 +82,6 @@ export const getServerSideProps = async function getServerSideProps(
return {
props: {
isEmbed,
themeBasis: form.user.username,
profile: {
theme: form.user.theme,
brandColor: form.user.brandColor,
+7 -2
View File
@@ -664,7 +664,13 @@ class CalApi {
config.prerender = "true";
}
const configWithGuestKeyAndColorScheme = withColorScheme(Cal.ensureGuestKey(config), containerEl);
const configWithGuestKeyAndColorScheme = withColorScheme(
Cal.ensureGuestKey({
...config,
embedType: "modal",
}),
containerEl
);
const existingModalEl = document.querySelector(`cal-modal-box[uid="${uid}"]`);
if (existingModalEl) {
@@ -695,7 +701,6 @@ class CalApi {
throw new Error("iframeAttrs should be an object");
}
config.embedType = "modal";
let iframe = null;
if (!iframe) {
+10 -12
View File
@@ -43,6 +43,7 @@ import {
import { useBookerTime } from "../bookings/Booker/components/hooks/useBookerTime";
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";
@@ -68,12 +69,6 @@ type GotoStateProps = {
dialog?: string;
};
const enum Theme {
auto = "auto",
light = "light",
dark = "dark",
}
const queryParamsForDialog = [
"embedType",
"embedTabName",
@@ -178,7 +173,10 @@ function useEmbedGoto(noQueryParamMode = false) {
return { gotoState, resetState, gotoEmbedTypeSelectionState };
}
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
const ThemeSelectControl = ({
children,
...props
}: ControlProps<{ value: EmbedTheme; label: string }, false>) => {
return (
<components.Control {...props}>
<Icon name="sun" className="text-subtle mr-2 h-4 w-4" />
@@ -764,7 +762,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
height: "100%",
config: defaultConfig,
} as PreviewState["inline"],
theme: Theme.auto,
theme: EmbedTheme.auto,
layout: defaultConfig.layout,
floatingPopup: {
config: defaultConfig,
@@ -882,9 +880,9 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
}
const ThemeOptions = [
{ value: Theme.auto, label: "Auto" },
{ value: Theme.dark, label: "Dark Theme" },
{ value: Theme.light, label: "Light Theme" },
{ value: EmbedTheme.auto, label: "Auto" },
{ value: EmbedTheme.dark, label: "Dark EmbedTheme" },
{ value: EmbedTheme.light, label: "Light EmbedTheme" },
];
const layoutOptions = [
@@ -1113,7 +1111,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<CollapsibleContent>
<div className="text-sm">
<Label className="mb-6">
<div className="mb-2">Theme</div>
<div className="mb-2">EmbedTheme</div>
<Select
className="w-full"
defaultValue={ThemeOptions[0]}
+5
View File
@@ -2,3 +2,8 @@ import { EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
export const embedLibUrl = EMBED_LIB_URL;
export const EMBED_PREVIEW_HTML_URL = `${WEBAPP_URL}/embed/preview.html`;
export const enum EmbedTheme {
auto = "auto",
light = "light",
dark = "dark",
}