diff --git a/enterprise/frontend/src/embedding-sdk/components/private/AppInitializeController.tsx b/enterprise/frontend/src/embedding-sdk/components/private/AppInitializeController.tsx index 548f5aab76955d04782ba1d4a5d01f8ad938e7eb..ccd1249a0fa8cda978aecae8d4dfc35786bcc26a 100644 --- a/enterprise/frontend/src/embedding-sdk/components/private/AppInitializeController.tsx +++ b/enterprise/frontend/src/embedding-sdk/components/private/AppInitializeController.tsx @@ -7,7 +7,7 @@ import { useSdkSelector } from "embedding-sdk/store"; import { getIsInitialized } from "embedding-sdk/store/selectors"; import type { SDKConfig } from "embedding-sdk/types"; -import { SdkContentWrapper } from "./SdkContentWrapper"; +import { SdkGlobalStylesWrapper } from "./SdkGlobalStylesWrapper"; interface AppInitializeControllerProps { children: ReactNode; @@ -25,11 +25,11 @@ export const AppInitializeController = ({ const isInitialized = useSdkSelector(getIsInitialized); return ( - <SdkContentWrapper + <SdkGlobalStylesWrapper baseUrl={config.metabaseInstanceUrl} id={EMBEDDING_SDK_ROOT_ELEMENT_ID} > {!isInitialized ? <div>{t`Loading…`}</div> : children} - </SdkContentWrapper> + </SdkGlobalStylesWrapper> ); }; diff --git a/enterprise/frontend/src/embedding-sdk/components/private/PublicComponentStylesWrapper.tsx b/enterprise/frontend/src/embedding-sdk/components/private/PublicComponentStylesWrapper.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d85d79d3c45f247df40dc4cbd61c8a624b2bbae2 --- /dev/null +++ b/enterprise/frontend/src/embedding-sdk/components/private/PublicComponentStylesWrapper.tsx @@ -0,0 +1,26 @@ +import styled from "@emotion/styled"; + +import { aceEditorStyles } from "metabase/query_builder/components/NativeQueryEditor/NativeQueryEditor.styled"; +import { getMetabaseCssVariables } from "metabase/styled-components/theme/css-variables"; +import { saveDomImageStyles } from "metabase/visualizations/lib/save-chart-image"; + +/** + * Injects CSS variables and styles to the SDK components underneath them. + * This is to ensure that the SDK components are styled correctly, + * even when rendered under a React portal. + */ +export const PublicComponentStylesWrapper = styled.div` + width: 100%; + font-weight: 400; + color: var(--mb-color-text-dark); + font-family: var(--mb-default-font-family), sans-serif; + + ${({ theme }) => getMetabaseCssVariables(theme)} + + ${aceEditorStyles} + ${saveDomImageStyles} + + :where(svg) { + display: inline; + } +`; diff --git a/enterprise/frontend/src/embedding-sdk/components/private/PublicComponentWrapper/PublicComponentWrapper.tsx b/enterprise/frontend/src/embedding-sdk/components/private/PublicComponentWrapper/PublicComponentWrapper.tsx index 7b49dab351e0179c58b86bcacbcdeb928871a8ea..935ae760a48f9a7602dc594f6182ecfb38436311 100644 --- a/enterprise/frontend/src/embedding-sdk/components/private/PublicComponentWrapper/PublicComponentWrapper.tsx +++ b/enterprise/frontend/src/embedding-sdk/components/private/PublicComponentWrapper/PublicComponentWrapper.tsx @@ -1,6 +1,7 @@ import type { JSX } from "react"; import { t } from "ttag"; +import { PublicComponentStylesWrapper } from "embedding-sdk/components/private/PublicComponentStylesWrapper"; import { SdkError } from "embedding-sdk/components/private/PublicComponentWrapper/SdkError"; import { SdkLoader } from "embedding-sdk/components/private/PublicComponentWrapper/SdkLoader"; import { useSdkSelector } from "embedding-sdk/store"; @@ -29,5 +30,7 @@ export const PublicComponentWrapper = ({ return <SdkError message={loginStatus.error.message} />; } - return children; + return ( + <PublicComponentStylesWrapper>{children}</PublicComponentStylesWrapper> + ); }; diff --git a/enterprise/frontend/src/embedding-sdk/components/private/SdkContentWrapper.tsx b/enterprise/frontend/src/embedding-sdk/components/private/SdkContentWrapper.tsx deleted file mode 100644 index e14f76a6754ab4d954753d6d977816ef9f14622c..0000000000000000000000000000000000000000 --- a/enterprise/frontend/src/embedding-sdk/components/private/SdkContentWrapper.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { css } from "@emotion/react"; -import styled from "@emotion/styled"; -import type { HTMLAttributes } from "react"; - -import { rootStyle } from "metabase/css/core/base.styled"; -import { defaultFontFiles } from "metabase/css/core/fonts.styled"; -import { alpha, color, lighten } from "metabase/lib/colors"; -import { useSelector } from "metabase/lib/redux"; -import { aceEditorStyles } from "metabase/query_builder/components/NativeQueryEditor/NativeQueryEditor.styled"; -import { getFontFiles } from "metabase/styled-components/selectors"; -import { useThemeSpecificSelectors } from "metabase/styled-components/theme/theme"; -import { saveDomImageStyles } from "metabase/visualizations/lib/save-chart-image"; -import type { FontFile } from "metabase-types/api"; - -interface SdkContentWrapperProps { - baseUrl?: string; -} - -export function SdkContentWrapper({ - baseUrl, - ...divProps -}: SdkContentWrapperProps & HTMLAttributes<HTMLDivElement>) { - const fontFiles = useSelector(getFontFiles); - const themeSpecificSelectors = useThemeSpecificSelectors(); - return ( - <SdkContentWrapperInner - baseUrl={baseUrl} - fontFiles={fontFiles} - themeSpecificSelectors={themeSpecificSelectors} - {...divProps} - /> - ); -} - -const SdkContentWrapperInner = styled.div< - SdkContentWrapperProps & { - fontFiles: FontFile[] | null; - themeSpecificSelectors: string; - } ->` - --mb-default-font-family: "${({ theme }) => theme.fontFamily}"; - --mb-color-bg-light: ${({ theme }) => theme.fn.themeColor("bg-light")}; - --mb-color-bg-dark: ${({ theme }) => theme.fn.themeColor("bg-dark")}; - --mb-color-brand: ${({ theme }) => theme.fn.themeColor("brand")}; - --mb-color-brand-light: ${({ theme }) => - lighten(theme.fn.themeColor("brand"), 0.532)}; - --mb-color-brand-lighter: ${({ theme }) => - lighten(theme.fn.themeColor("brand"), 0.598)}; - --mb-color-brand-alpha-04: ${({ theme }) => - alpha(theme.fn.themeColor("brand"), 0.04)}; - --mb-color-brand-alpha-88: ${({ theme }) => - alpha(theme.fn.themeColor("brand"), 0.88)}; - --mb-color-focus: ${({ theme }) => theme.fn.themeColor("focus")}; - --mb-color-bg-white: ${({ theme }) => theme.fn.themeColor("bg-white")}; - --mb-color-bg-black: ${({ theme }) => theme.fn.themeColor("bg-black")}; - --mb-color-shadow: ${({ theme }) => theme.fn.themeColor("shadow")}; - --mb-color-border: ${({ theme }) => theme.fn.themeColor("border")}; - --mb-color-text-dark: ${({ theme }) => theme.fn.themeColor("text-dark")}; - --mb-color-text-medium: ${({ theme }) => theme.fn.themeColor("text-medium")}; - --mb-color-text-light: ${({ theme }) => theme.fn.themeColor("text-light")}; - --mb-color-danger: ${({ theme }) => theme.fn.themeColor("danger")}; - --mb-color-error: ${({ theme }) => theme.fn.themeColor("error")}; - --mb-color-filter: ${({ theme }) => theme.fn.themeColor("filter")}; - --mb-color-bg-error: ${() => color("bg-error")}; - --mb-color-bg-medium: ${({ theme }) => theme.fn.themeColor("bg-medium")}; - --mb-color-bg-night: ${() => color("bg-night")}; - --mb-color-text-white: ${({ theme }) => theme.fn.themeColor("text-white")}; - --mb-color-success: ${({ theme }) => theme.fn.themeColor("success")}; - --mb-color-summarize: ${({ theme }) => theme.fn.themeColor("summarize")}; - --mb-color-warning: ${({ theme }) => theme.fn.themeColor("warning")}; - - /** - Theming-specific CSS variables. - Keep in sync with [GlobalStyles.tsx] and [.storybook/preview-head.html]. - - Refer to DEFAULT_METABASE_COMPONENT_THEME for their defaults. - - These CSS variables are not part of the core design system colors. - Do NOT add them to [palette.ts] and [colors.ts]. - */ - ${({ themeSpecificSelectors }) => themeSpecificSelectors} - - font-size: ${({ theme }) => theme.other.fontSize}; - - ${aceEditorStyles} - ${saveDomImageStyles} - ${rootStyle} - - ${({ baseUrl }) => defaultFontFiles({ baseUrl })} - ${({ fontFiles }) => - fontFiles?.map( - file => css` - @font-face { - font-family: "Custom"; - src: url(${encodeURI(file.src)}) format("${file.fontFormat}"); - font-weight: ${file.fontWeight}; - font-style: normal; - font-display: swap; - } - `, - )} - - :where(svg) { - display: inline; - } -`; diff --git a/enterprise/frontend/src/embedding-sdk/components/private/SdkGlobalStylesWrapper.tsx b/enterprise/frontend/src/embedding-sdk/components/private/SdkGlobalStylesWrapper.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9ff400fe74583fd095cf6d76fc7b50cba550ada0 --- /dev/null +++ b/enterprise/frontend/src/embedding-sdk/components/private/SdkGlobalStylesWrapper.tsx @@ -0,0 +1,56 @@ +import { css } from "@emotion/react"; +import styled from "@emotion/styled"; +import type { HTMLAttributes } from "react"; + +import { rootStyle } from "metabase/css/core/base.styled"; +import { defaultFontFiles } from "metabase/css/core/fonts.styled"; +import { useSelector } from "metabase/lib/redux"; +import { getFontFiles } from "metabase/styled-components/selectors"; +import { getMetabaseCssVariables } from "metabase/styled-components/theme/css-variables"; +import type { FontFile } from "metabase-types/api"; + +interface SdkContentWrapperProps { + baseUrl?: string; +} + +export function SdkGlobalStylesWrapper({ + baseUrl, + ...divProps +}: SdkContentWrapperProps & HTMLAttributes<HTMLDivElement>) { + const fontFiles = useSelector(getFontFiles); + + return ( + <SdkGlobalStylesInner + baseUrl={baseUrl} + fontFiles={fontFiles} + {...divProps} + /> + ); +} + +const SdkGlobalStylesInner = styled.div< + SdkContentWrapperProps & { + fontFiles: FontFile[] | null; + } +>` + font-size: ${({ theme }) => theme.other.fontSize}; + + ${({ theme }) => getMetabaseCssVariables(theme)} + + ${rootStyle} + + ${({ baseUrl }) => defaultFontFiles({ baseUrl })} + + ${({ fontFiles }) => + fontFiles?.map( + file => css` + @font-face { + font-family: "Custom"; + src: url(${encodeURI(file.src)}) format("${file.fontFormat}"); + font-weight: ${file.fontWeight}; + font-style: normal; + font-display: swap; + } + `, + )} +`; diff --git a/enterprise/frontend/src/embedding-sdk/components/private/SdkContentWrapper.unit.spec.tsx b/enterprise/frontend/src/embedding-sdk/components/private/SdkGlobalStylesWrapper.unit.spec.tsx similarity index 78% rename from enterprise/frontend/src/embedding-sdk/components/private/SdkContentWrapper.unit.spec.tsx rename to enterprise/frontend/src/embedding-sdk/components/private/SdkGlobalStylesWrapper.unit.spec.tsx index adf89e78a07f73398f005028d3cff3f270cfd16f..a72c2b946861d2cf9289d16936fb0387679264e7 100644 --- a/enterprise/frontend/src/embedding-sdk/components/private/SdkContentWrapper.unit.spec.tsx +++ b/enterprise/frontend/src/embedding-sdk/components/private/SdkGlobalStylesWrapper.unit.spec.tsx @@ -1,11 +1,11 @@ import { renderWithProviders } from "__support__/ui"; -import { SdkContentWrapper } from "embedding-sdk/components/private/SdkContentWrapper"; +import { SdkGlobalStylesWrapper } from "embedding-sdk/components/private/SdkGlobalStylesWrapper"; import { createMockSettingsState, createMockState, } from "metabase-types/store/mocks"; -describe("SdkContentWrapper", () => { +describe("SdkGlobalStylesWrapper", () => { it("injects the font-face declaration when available", () => { const state = createMockState({ settings: createMockSettingsState({ @@ -19,7 +19,9 @@ describe("SdkContentWrapper", () => { }), }); - renderWithProviders(<SdkContentWrapper />, { storeInitialState: state }); + renderWithProviders(<SdkGlobalStylesWrapper />, { + storeInitialState: state, + }); const rules = Array.from(document.styleSheets).flatMap(sheet => Array.from(sheet.cssRules || []), diff --git a/frontend/src/metabase/css/core/colors.module.css b/frontend/src/metabase/css/core/colors.module.css index 7198220d9ad0ccb57a60a181a125f73866fa28a9..5fb95a735d762ecc6eb0c0a888ba3fbdcdc5d9a7 100644 --- a/frontend/src/metabase/css/core/colors.module.css +++ b/frontend/src/metabase/css/core/colors.module.css @@ -2,7 +2,7 @@ * NOTE: KEEP SYNCRONIZED WITH: * frontend/src/metabase/ui/utils/colors.ts * frontend/src/metabase/styled-components/containers/GlobalStyles/GlobalStyles.tsx - * enterprise/frontend/src/embedding-sdk/components/private/SdkContentWrapper.tsx + * frontend/src/metabase/styled-components/theme/css-variables.ts * .storybook/preview-head.html */ :root { diff --git a/frontend/src/metabase/lib/colors/palette.ts b/frontend/src/metabase/lib/colors/palette.ts index fa6e09ae4a91b82e7b7bd4f4a37d054f06845cd5..4b5a250f8caa7904805fb82799d6297dc7b1c90c 100644 --- a/frontend/src/metabase/lib/colors/palette.ts +++ b/frontend/src/metabase/lib/colors/palette.ts @@ -10,7 +10,7 @@ export const ACCENT_COUNT = 8; // NOTE: KEEP SYNCRONIZED WITH: // frontend/src/metabase/css/core/colors.module.css // frontend/src/metabase/styled-components/containers/GlobalStyles/GlobalStyles.tsx -// enterprise/frontend/src/embedding-sdk/components/private/SdkContentWrapper.tsx +// frontend/src/metabase/styled-components/theme/css-variables.ts // .storybook/preview-head.html export const colors = { brand: "#509EE3", diff --git a/frontend/src/metabase/styled-components/containers/GlobalStyles/GlobalStyles.tsx b/frontend/src/metabase/styled-components/containers/GlobalStyles/GlobalStyles.tsx index 99f3afb81a65aadf74ee976f3e0125ee0b259e94..ff6a7bf9efa0474617be72beeb2d21a68d287651 100644 --- a/frontend/src/metabase/styled-components/containers/GlobalStyles/GlobalStyles.tsx +++ b/frontend/src/metabase/styled-components/containers/GlobalStyles/GlobalStyles.tsx @@ -6,7 +6,8 @@ import { alpha, color, lighten } from "metabase/lib/colors"; import { getSitePath } from "metabase/lib/dom"; import { useSelector } from "metabase/lib/redux"; import { aceEditorStyles } from "metabase/query_builder/components/NativeQueryEditor/NativeQueryEditor.styled"; -import { useThemeSpecificSelectors } from "metabase/styled-components/theme/theme"; +import { getThemeSpecificCssVariables } from "metabase/styled-components/theme/css-variables"; +import { useMantineTheme } from "metabase/ui"; import { saveDomImageStyles } from "metabase/visualizations/lib/save-chart-image"; import { getFont, getFontFiles } from "../../selectors"; @@ -15,9 +16,8 @@ export const GlobalStyles = (): JSX.Element => { const font = useSelector(getFont); const fontFiles = useSelector(getFontFiles); - const themeSpecificSelectors = useThemeSpecificSelectors(); - const sitePath = getSitePath(); + const theme = useMantineTheme(); const styles = css` :root { @@ -66,7 +66,7 @@ export const GlobalStyles = (): JSX.Element => { transparent ); - ${themeSpecificSelectors} + ${getThemeSpecificCssVariables(theme)} } ${defaultFontFiles({ baseUrl: sitePath })} diff --git a/frontend/src/metabase/styled-components/theme/css-variables.ts b/frontend/src/metabase/styled-components/theme/css-variables.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a9d37b8f938599e0cdce7e7e685f18f64808977 --- /dev/null +++ b/frontend/src/metabase/styled-components/theme/css-variables.ts @@ -0,0 +1,118 @@ +import { css } from "@emotion/react"; +import { get } from "lodash"; + +import type { MetabaseComponentTheme } from "embedding-sdk"; +import { color } from "metabase/lib/colors"; +import type { MantineTheme } from "metabase/ui"; + +// https://www.raygesualdo.com/posts/flattening-object-keys-with-typescript-types/ +type FlattenObjectKeys< + T extends Record<string, unknown>, + Key = keyof T, +> = Key extends string + ? T[Key] extends Record<string, unknown> + ? `${Key}.${FlattenObjectKeys<T[Key]>}` + : `${Key}` + : never; + +type MetabaseComponentThemeKey = FlattenObjectKeys<MetabaseComponentTheme>; + +/** + * Defines the CSS variables used across Metabase. + */ +export function getMetabaseCssVariables(theme: MantineTheme) { + return css` + ${getDesignSystemCssVariables(theme)} + ${getThemeSpecificCssVariables(theme)} + `; +} + +/** + * Design System CSS variables. + * These CSS variables are part of the core design system colors. + + * DO NOT ADD COLORS WITHOUT EXTREMELY GOOD REASON AND DESIGN REVIEW. + * KEEP SYNCHRONIZED WITH: + * frontend/src/metabase/ui/utils/colors.ts + * frontend/src/metabase/css/core/colors.module.css + * frontend/src/metabase/styled-components/containers/GlobalStyles/GlobalStyles.tsx + * .storybook/preview-head.html +**/ +function getDesignSystemCssVariables(theme: MantineTheme) { + return css` + --mb-default-font-family: "${theme.fontFamily}"; + + --mb-color-bg-light: ${theme.fn.themeColor("bg-light")}; + --mb-color-bg-dark: ${theme.fn.themeColor("bg-dark")}; + --mb-color-brand: ${theme.fn.themeColor("brand")}; + + --mb-color-brand-light: color-mix(in srgb, var(--mb-color-brand) 53%, #fff); + --mb-color-brand-lighter: color-mix( + in srgb, + var(--mb-color-brand) 60%, + #fff + ); + --mb-color-brand-alpha-04: color-mix( + in srgb, + var(--mb-color-brand) 4%, + transparent + ); + --mb-color-brand-alpha-88: color-mix( + in srgb, + var(--mb-color-brand) 88%, + transparent + ); + + --mb-color-focus: ${theme.fn.themeColor("focus")}; + --mb-color-bg-white: ${theme.fn.themeColor("bg-white")}; + --mb-color-bg-black: ${theme.fn.themeColor("bg-black")}; + --mb-color-shadow: ${theme.fn.themeColor("shadow")}; + --mb-color-border: ${theme.fn.themeColor("border")}; + --mb-color-text-dark: ${theme.fn.themeColor("text-dark")}; + --mb-color-text-medium: ${theme.fn.themeColor("text-medium")}; + --mb-color-text-light: ${theme.fn.themeColor("text-light")}; + --mb-color-danger: ${theme.fn.themeColor("danger")}; + --mb-color-error: ${theme.fn.themeColor("error")}; + --mb-color-filter: ${theme.fn.themeColor("filter")}; + --mb-color-bg-error: ${color("bg-error")}; + --mb-color-bg-medium: ${theme.fn.themeColor("bg-medium")}; + --mb-color-bg-night: ${color("bg-night")}; + --mb-color-text-white: ${theme.fn.themeColor("text-white")}; + --mb-color-success: ${theme.fn.themeColor("success")}; + --mb-color-summarize: ${theme.fn.themeColor("summarize")}; + --mb-color-warning: ${theme.fn.themeColor("warning")}; + `; +} + +/** + * Theming-specific CSS variables. + * + * These CSS variables are NOT part of the core design system colors. + * Do NOT add them to [palette.ts] and [colors.ts]. + * + * Keep in sync with [GlobalStyles.tsx] and [.storybook/preview-head.html]. + * Refer to DEFAULT_METABASE_COMPONENT_THEME for their defaults. + **/ +export function getThemeSpecificCssVariables(theme: MantineTheme) { + // Get value from theme.other, which is typed as MetabaseComponentTheme + const getValue = (key: MetabaseComponentThemeKey) => get(theme.other, key); + + return css` + --mb-color-bg-dashboard: ${getValue("dashboard.backgroundColor")}; + --mb-color-bg-dashboard-card: ${getValue("dashboard.card.backgroundColor")}; + --mb-color-bg-question: ${getValue("question.backgroundColor")}; + + --mb-color-text-collection-browser-expand-button: ${getValue( + "collectionBrowser.breadcrumbs.expandButton.textColor", + )}; + --mb-color-bg-collection-browser-expand-button: ${getValue( + "collectionBrowser.breadcrumbs.expandButton.backgroundColor", + )}; + --mb-color-text-collection-browser-expand-button-hover: ${getValue( + "collectionBrowser.breadcrumbs.expandButton.hoverTextColor", + )}; + --mb-color-bg-collection-browser-expand-button-hover: ${getValue( + "collectionBrowser.breadcrumbs.expandButton.hoverBackgroundColor", + )}; + `; +} diff --git a/frontend/src/metabase/styled-components/theme/theme.ts b/frontend/src/metabase/styled-components/theme/theme.ts deleted file mode 100644 index c4c83393ba82e96c0e17501ca57caf8c38927080..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/styled-components/theme/theme.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { get } from "lodash"; - -import type { MetabaseComponentTheme } from "embedding-sdk"; -import { useMantineTheme } from "metabase/ui"; - -// https://www.raygesualdo.com/posts/flattening-object-keys-with-typescript-types/ -type FlattenObjectKeys< - T extends Record<string, unknown>, - Key = keyof T, -> = Key extends string - ? T[Key] extends Record<string, unknown> - ? `${Key}.${FlattenObjectKeys<T[Key]>}` - : `${Key}` - : never; - -type MetabaseComponentThemeKey = FlattenObjectKeys<MetabaseComponentTheme>; - -/* - Theming-specific CSS variables. - These CSS variables are not part of the core design system colors. -**/ -export const useThemeSpecificSelectors = () => { - const theme = useMantineTheme(); - - // get value from theme.other, which is typed as MetabaseComponentTheme - const getValue = (key: MetabaseComponentThemeKey) => get(theme.other, key); - - return ` - --mb-color-bg-dashboard: ${getValue("dashboard.backgroundColor")}; - --mb-color-bg-dashboard-card: ${getValue("dashboard.card.backgroundColor")}; - --mb-color-bg-question: ${getValue("question.backgroundColor")}; - - --mb-color-text-collection-browser-expand-button: ${getValue( - "collectionBrowser.breadcrumbs.expandButton.textColor", - )}; - --mb-color-bg-collection-browser-expand-button: ${getValue( - "collectionBrowser.breadcrumbs.expandButton.backgroundColor", - )}; - --mb-color-text-collection-browser-expand-button-hover: ${getValue( - "collectionBrowser.breadcrumbs.expandButton.hoverTextColor", - )}; - --mb-color-bg-collection-browser-expand-button-hover: ${getValue( - "collectionBrowser.breadcrumbs.expandButton.hoverBackgroundColor", - )}; - - `; -};