This is an automated email from the ASF dual-hosted git repository.

elizabeth pushed a commit to branch elizabeth/fix-resize-bug
in repository https://gitbox.apache.org/repos/asf/superset.git

commit cfd6d8e31deb4aaedc79f9f01ced83970e3947d1
Author: Gabriel Torres Ruiz <[email protected]>
AuthorDate: Mon Jul 28 23:26:17 2025 -0300

    feat(theming): Align embedded sdk with theme configs (#34273)
---
 .../TelemetryPixel/TelemetryPixel.test.tsx         |   2 +-
 .../src/components/TelemetryPixel/index.tsx        |   3 +-
 .../src/components/ThemeSelect/index.tsx           | 116 --------
 .../components/ThemeSubMenu/ThemeSubMenu.test.tsx  | 273 ++++++++++++++++++
 .../src/components/ThemeSubMenu/index.tsx          | 170 ++++++++++++
 .../superset-ui-core/src/components/index.ts       |   2 +
 .../packages/superset-ui-core/src/theme/index.tsx  |  14 +-
 .../packages/superset-ui-core/src/theme/types.ts   |  13 +
 .../src/embedded/EmbeddedContextProviders.tsx      |  93 +++++++
 superset-frontend/src/embedded/index.tsx           |  40 ++-
 superset-frontend/src/features/home/RightMenu.tsx  |  36 +--
 superset-frontend/src/theme/ThemeController.ts     |  74 +++--
 superset-frontend/src/theme/ThemeProvider.tsx      |   8 +-
 .../src/theme/tests/ThemeController.test.ts        | 309 ++++++++++++++++++++-
 .../src/theme/tests/ThemeProvider.test.tsx         |   3 +-
 superset-frontend/src/types/bootstrapTypes.ts      |  18 +-
 16 files changed, 988 insertions(+), 186 deletions(-)

diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/TelemetryPixel/TelemetryPixel.test.tsx
 
b/superset-frontend/packages/superset-ui-core/src/components/TelemetryPixel/TelemetryPixel.test.tsx
index 7d12a612d7..7dc5d4e63d 100644
--- 
a/superset-frontend/packages/superset-ui-core/src/components/TelemetryPixel/TelemetryPixel.test.tsx
+++ 
b/superset-frontend/packages/superset-ui-core/src/components/TelemetryPixel/TelemetryPixel.test.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 import { render } from '@superset-ui/core/spec';
-import TelemetryPixel from '.';
+import { TelemetryPixel } from '.';
 
 const OLD_ENV = process.env;
 
diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/TelemetryPixel/index.tsx
 
b/superset-frontend/packages/superset-ui-core/src/components/TelemetryPixel/index.tsx
index 9e7818a1b7..d7c86a492e 100644
--- 
a/superset-frontend/packages/superset-ui-core/src/components/TelemetryPixel/index.tsx
+++ 
b/superset-frontend/packages/superset-ui-core/src/components/TelemetryPixel/index.tsx
@@ -39,7 +39,7 @@ interface TelemetryPixelProps {
 
 const PIXEL_ID = '0d3461e1-abb1-4691-a0aa-5ed50de66af0';
 
-const TelemetryPixel = ({
+export const TelemetryPixel = ({
   version = 'unknownVersion',
   sha = 'unknownSHA',
   build = 'unknownBuild',
@@ -56,4 +56,3 @@ const TelemetryPixel = ({
     />
   );
 };
-export default TelemetryPixel;
diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/ThemeSelect/index.tsx
 
b/superset-frontend/packages/superset-ui-core/src/components/ThemeSelect/index.tsx
deleted file mode 100644
index e46a31614c..0000000000
--- 
a/superset-frontend/packages/superset-ui-core/src/components/ThemeSelect/index.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { Dropdown, Icons } from '@superset-ui/core/components';
-import type { MenuItem } from '@superset-ui/core/components/Menu';
-import { t, useTheme } from '@superset-ui/core';
-import { ThemeAlgorithm, ThemeMode } from '../../theme/types';
-
-export interface ThemeSelectProps {
-  setThemeMode: (newMode: ThemeMode) => void;
-  tooltipTitle?: string;
-  themeMode: ThemeMode;
-  hasLocalOverride?: boolean;
-  onClearLocalSettings?: () => void;
-  allowOSPreference?: boolean;
-}
-
-const ThemeSelect: React.FC<ThemeSelectProps> = ({
-  setThemeMode,
-  tooltipTitle = 'Select theme',
-  themeMode,
-  hasLocalOverride = false,
-  onClearLocalSettings,
-  allowOSPreference = true,
-}) => {
-  const theme = useTheme();
-
-  const handleSelect = (mode: ThemeMode) => {
-    setThemeMode(mode);
-  };
-
-  const themeIconMap: Record<ThemeAlgorithm | ThemeMode, React.ReactNode> = {
-    [ThemeAlgorithm.DEFAULT]: <Icons.SunOutlined />,
-    [ThemeAlgorithm.DARK]: <Icons.MoonOutlined />,
-    [ThemeMode.SYSTEM]: <Icons.FormatPainterOutlined />,
-    [ThemeAlgorithm.COMPACT]: <Icons.CompressOutlined />,
-  };
-
-  // Use different icon when local theme is active
-  const triggerIcon = hasLocalOverride ? (
-    <Icons.FormatPainterOutlined style={{ color: theme.colorErrorText }} />
-  ) : (
-    themeIconMap[themeMode] || <Icons.FormatPainterOutlined />
-  );
-
-  const menuItems: MenuItem[] = [
-    {
-      type: 'group',
-      label: t('Theme'),
-    },
-    {
-      key: ThemeMode.DEFAULT,
-      label: t('Light'),
-      icon: <Icons.SunOutlined />,
-      onClick: () => handleSelect(ThemeMode.DEFAULT),
-    },
-    {
-      key: ThemeMode.DARK,
-      label: t('Dark'),
-      icon: <Icons.MoonOutlined />,
-      onClick: () => handleSelect(ThemeMode.DARK),
-    },
-    ...(allowOSPreference
-      ? [
-          {
-            key: ThemeMode.SYSTEM,
-            label: t('Match system'),
-            icon: <Icons.FormatPainterOutlined />,
-            onClick: () => handleSelect(ThemeMode.SYSTEM),
-          },
-        ]
-      : []),
-  ];
-
-  // Add clear settings option only when there's a local theme active
-  if (onClearLocalSettings && hasLocalOverride) {
-    menuItems.push(
-      { type: 'divider' } as MenuItem,
-      {
-        key: 'clear-local',
-        label: t('Clear local theme'),
-        icon: <Icons.ClearOutlined />,
-        onClick: onClearLocalSettings,
-      } as MenuItem,
-    );
-  }
-
-  return (
-    <Dropdown
-      menu={{
-        items: menuItems,
-        selectedKeys: [themeMode],
-      }}
-      trigger={['hover']}
-    >
-      {triggerIcon}
-    </Dropdown>
-  );
-};
-
-export default ThemeSelect;
diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/ThemeSubMenu/ThemeSubMenu.test.tsx
 
b/superset-frontend/packages/superset-ui-core/src/components/ThemeSubMenu/ThemeSubMenu.test.tsx
new file mode 100644
index 0000000000..b806049e4d
--- /dev/null
+++ 
b/superset-frontend/packages/superset-ui-core/src/components/ThemeSubMenu/ThemeSubMenu.test.tsx
@@ -0,0 +1,273 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {
+  render,
+  screen,
+  userEvent,
+  waitFor,
+  within,
+} from '@superset-ui/core/spec';
+import { ThemeMode } from '@superset-ui/core';
+import { Menu } from '@superset-ui/core/components';
+import { ThemeSubMenu } from '.';
+
+// Mock the translation function
+jest.mock('@superset-ui/core', () => ({
+  ...jest.requireActual('@superset-ui/core'),
+  t: (key: string) => key,
+}));
+
+describe('ThemeSubMenu', () => {
+  const defaultProps = {
+    allowOSPreference: true,
+    setThemeMode: jest.fn(),
+    themeMode: ThemeMode.DEFAULT,
+    hasLocalOverride: false,
+    onClearLocalSettings: jest.fn(),
+  };
+
+  const renderThemeSubMenu = (props = defaultProps) =>
+    render(
+      <Menu>
+        <ThemeSubMenu {...props} />
+      </Menu>,
+    );
+
+  const findMenuWithText = async (text: string) => {
+    await waitFor(() => {
+      const found = screen
+        .getAllByRole('menu')
+        .some(m => within(m).queryByText(text));
+
+      if (!found) throw new Error(`Menu with text "${text}" not yet rendered`);
+    });
+
+    return screen.getAllByRole('menu').find(m => within(m).queryByText(text))!;
+  };
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('renders Light and Dark theme options by default', async () => {
+    renderThemeSubMenu();
+
+    userEvent.hover(await screen.findByRole('menuitem'));
+    const menu = await findMenuWithText('Light');
+
+    expect(within(menu!).getByText('Light')).toBeInTheDocument();
+    expect(within(menu!).getByText('Dark')).toBeInTheDocument();
+  });
+
+  it('does not render Match system option when allowOSPreference is false', 
async () => {
+    renderThemeSubMenu({ ...defaultProps, allowOSPreference: false });
+    userEvent.hover(await screen.findByRole('menuitem'));
+
+    await waitFor(() => {
+      expect(screen.queryByText('Match system')).not.toBeInTheDocument();
+    });
+  });
+
+  it('renders with allowOSPreference as true by default', async () => {
+    renderThemeSubMenu();
+
+    userEvent.hover(await screen.findByRole('menuitem'));
+    const menu = await findMenuWithText('Match system');
+
+    expect(within(menu).getByText('Match system')).toBeInTheDocument();
+  });
+
+  it('renders clear option when both hasLocalOverride and onClearLocalSettings 
are provided', async () => {
+    const mockClear = jest.fn();
+    renderThemeSubMenu({
+      ...defaultProps,
+      hasLocalOverride: true,
+      onClearLocalSettings: mockClear,
+    });
+
+    userEvent.hover(await screen.findByRole('menuitem'));
+    const menu = await findMenuWithText('Clear local theme');
+
+    expect(within(menu).getByText('Clear local theme')).toBeInTheDocument();
+  });
+
+  it('does not render clear option when hasLocalOverride is false', async () 
=> {
+    const mockClear = jest.fn();
+    renderThemeSubMenu({
+      ...defaultProps,
+      hasLocalOverride: false,
+      onClearLocalSettings: mockClear,
+    });
+
+    userEvent.hover(await screen.findByRole('menuitem'));
+
+    await waitFor(() => {
+      expect(screen.queryByText('Clear local theme')).not.toBeInTheDocument();
+    });
+  });
+
+  it('calls setThemeMode with DEFAULT when Light is clicked', async () => {
+    const mockSet = jest.fn();
+    renderThemeSubMenu({ ...defaultProps, setThemeMode: mockSet });
+
+    userEvent.hover(await screen.findByRole('menuitem'));
+    const menu = await findMenuWithText('Light');
+    userEvent.click(within(menu).getByText('Light'));
+
+    expect(mockSet).toHaveBeenCalledWith(ThemeMode.DEFAULT);
+  });
+
+  it('calls setThemeMode with DARK when Dark is clicked', async () => {
+    const mockSet = jest.fn();
+    renderThemeSubMenu({ ...defaultProps, setThemeMode: mockSet });
+
+    userEvent.hover(await screen.findByRole('menuitem'));
+    const menu = await findMenuWithText('Dark');
+    userEvent.click(within(menu).getByText('Dark'));
+
+    expect(mockSet).toHaveBeenCalledWith(ThemeMode.DARK);
+  });
+
+  it('calls setThemeMode with SYSTEM when Match system is clicked', async () 
=> {
+    const mockSet = jest.fn();
+    renderThemeSubMenu({ ...defaultProps, setThemeMode: mockSet });
+
+    userEvent.hover(await screen.findByRole('menuitem'));
+    const menu = await findMenuWithText('Match system');
+    userEvent.click(within(menu).getByText('Match system'));
+
+    expect(mockSet).toHaveBeenCalledWith(ThemeMode.SYSTEM);
+  });
+
+  it('calls onClearLocalSettings when Clear local theme is clicked', async () 
=> {
+    const mockClear = jest.fn();
+    renderThemeSubMenu({
+      ...defaultProps,
+      hasLocalOverride: true,
+      onClearLocalSettings: mockClear,
+    });
+
+    userEvent.hover(await screen.findByRole('menuitem'));
+    const menu = await findMenuWithText('Clear local theme');
+    userEvent.click(within(menu).getByText('Clear local theme'));
+
+    expect(mockClear).toHaveBeenCalledTimes(1);
+  });
+
+  it('displays sun icon for DEFAULT theme', () => {
+    renderThemeSubMenu({ ...defaultProps, themeMode: ThemeMode.DEFAULT });
+    expect(screen.getByTestId('sun')).toBeInTheDocument();
+  });
+
+  it('displays moon icon for DARK theme', () => {
+    renderThemeSubMenu({ ...defaultProps, themeMode: ThemeMode.DARK });
+    expect(screen.getByTestId('moon')).toBeInTheDocument();
+  });
+
+  it('displays format-painter icon for SYSTEM theme', () => {
+    renderThemeSubMenu({ ...defaultProps, themeMode: ThemeMode.SYSTEM });
+    expect(screen.getByTestId('format-painter')).toBeInTheDocument();
+  });
+
+  it('displays override icon when hasLocalOverride is true', () => {
+    renderThemeSubMenu({ ...defaultProps, hasLocalOverride: true });
+    expect(screen.getByTestId('format-painter')).toBeInTheDocument();
+  });
+
+  it('renders Theme group header', async () => {
+    renderThemeSubMenu();
+
+    userEvent.hover(await screen.findByRole('menuitem'));
+    const menu = await findMenuWithText('Theme');
+
+    expect(within(menu).getByText('Theme')).toBeInTheDocument();
+  });
+
+  it('renders sun icon for Light theme option', async () => {
+    renderThemeSubMenu();
+
+    userEvent.hover(await screen.findByRole('menuitem'));
+    const menu = await findMenuWithText('Light');
+    const lightOption = within(menu).getByText('Light').closest('li');
+
+    expect(within(lightOption!).getByTestId('sun')).toBeInTheDocument();
+  });
+
+  it('renders moon icon for Dark theme option', async () => {
+    renderThemeSubMenu();
+
+    userEvent.hover(await screen.findByRole('menuitem'));
+    const menu = await findMenuWithText('Dark');
+    const darkOption = within(menu).getByText('Dark').closest('li');
+
+    expect(within(darkOption!).getByTestId('moon')).toBeInTheDocument();
+  });
+
+  it('renders format-painter icon for Match system option', async () => {
+    renderThemeSubMenu({ ...defaultProps, allowOSPreference: true });
+
+    userEvent.hover(await screen.findByRole('menuitem'));
+    const menu = await findMenuWithText('Match system');
+    const matchOption = within(menu).getByText('Match system').closest('li');
+
+    expect(
+      within(matchOption!).getByTestId('format-painter'),
+    ).toBeInTheDocument();
+  });
+
+  it('renders clear icon for Clear local theme option', async () => {
+    renderThemeSubMenu({
+      ...defaultProps,
+      hasLocalOverride: true,
+      onClearLocalSettings: jest.fn(),
+    });
+
+    userEvent.hover(await screen.findByRole('menuitem'));
+    const menu = await findMenuWithText('Clear local theme');
+    const clearOption = within(menu)
+      .getByText('Clear local theme')
+      .closest('li');
+
+    expect(within(clearOption!).getByTestId('clear')).toBeInTheDocument();
+  });
+
+  it('renders divider before clear option when clear option is present', async 
() => {
+    renderThemeSubMenu({
+      ...defaultProps,
+      hasLocalOverride: true,
+      onClearLocalSettings: jest.fn(),
+    });
+
+    userEvent.hover(await screen.findByRole('menuitem'));
+
+    const menu = await findMenuWithText('Clear local theme');
+    const divider = within(menu).queryByRole('separator');
+
+    expect(divider).toBeInTheDocument();
+  });
+
+  it('does not render divider when clear option is not present', async () => {
+    renderThemeSubMenu({ ...defaultProps });
+
+    userEvent.hover(await screen.findByRole('menuitem'));
+    const divider = document.querySelector('.ant-menu-item-divider');
+
+    expect(divider).toBeNull();
+  });
+});
diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/ThemeSubMenu/index.tsx
 
b/superset-frontend/packages/superset-ui-core/src/components/ThemeSubMenu/index.tsx
new file mode 100644
index 0000000000..2dbb36104b
--- /dev/null
+++ 
b/superset-frontend/packages/superset-ui-core/src/components/ThemeSubMenu/index.tsx
@@ -0,0 +1,170 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useMemo } from 'react';
+import { Icons, Menu } from '@superset-ui/core/components';
+import {
+  css,
+  styled,
+  t,
+  ThemeMode,
+  useTheme,
+  ThemeAlgorithm,
+} from '@superset-ui/core';
+
+const StyledThemeSubMenu = styled(Menu.SubMenu)`
+  ${({ theme }) => css`
+    [data-icon='caret-down'] {
+      color: ${theme.colorIcon};
+      font-size: ${theme.fontSizeXS}px;
+      margin-left: ${theme.sizeUnit}px;
+    }
+    &.ant-menu-submenu-active {
+      .ant-menu-title-content {
+        color: ${theme.colorPrimary};
+      }
+    }
+  `}
+`;
+
+const StyledThemeSubMenuItem = styled(Menu.Item)<{ selected: boolean }>`
+  ${({ theme, selected }) => css`
+    &:hover {
+      color: ${theme.colorPrimary} !important;
+      cursor: pointer !important;
+    }
+    ${selected &&
+    css`
+      background-color: ${theme.colors.primary.light4} !important;
+      color: ${theme.colors.primary.dark1} !important;
+    `}
+  `}
+`;
+
+export interface ThemeSubMenuOption {
+  key: ThemeMode;
+  label: string;
+  icon: React.ReactNode;
+  onClick: () => void;
+}
+
+export interface ThemeSubMenuProps {
+  setThemeMode: (newMode: ThemeMode) => void;
+  themeMode: ThemeMode;
+  hasLocalOverride?: boolean;
+  onClearLocalSettings?: () => void;
+  allowOSPreference?: boolean;
+}
+
+export const ThemeSubMenu: React.FC<ThemeSubMenuProps> = ({
+  setThemeMode,
+  themeMode,
+  hasLocalOverride = false,
+  onClearLocalSettings,
+  allowOSPreference = true,
+}: ThemeSubMenuProps) => {
+  const theme = useTheme();
+
+  const handleSelect = (mode: ThemeMode) => {
+    setThemeMode(mode);
+  };
+
+  const themeIconMap: Record<ThemeAlgorithm | ThemeMode, React.ReactNode> =
+    useMemo(
+      () => ({
+        [ThemeAlgorithm.DEFAULT]: <Icons.SunOutlined />,
+        [ThemeAlgorithm.DARK]: <Icons.MoonOutlined />,
+        [ThemeMode.SYSTEM]: <Icons.FormatPainterOutlined />,
+        [ThemeAlgorithm.COMPACT]: <Icons.CompressOutlined />,
+      }),
+      [],
+    );
+
+  const selectedThemeModeIcon = useMemo(
+    () =>
+      hasLocalOverride ? (
+        <Icons.FormatPainterOutlined
+          style={{ color: theme.colors.error.base }}
+        />
+      ) : (
+        themeIconMap[themeMode]
+      ),
+    [hasLocalOverride, theme.colors.error.base, themeIconMap, themeMode],
+  );
+
+  const themeOptions: ThemeSubMenuOption[] = [
+    {
+      key: ThemeMode.DEFAULT,
+      label: t('Light'),
+      icon: <Icons.SunOutlined />,
+      onClick: () => handleSelect(ThemeMode.DEFAULT),
+    },
+    {
+      key: ThemeMode.DARK,
+      label: t('Dark'),
+      icon: <Icons.MoonOutlined />,
+      onClick: () => handleSelect(ThemeMode.DARK),
+    },
+    ...(allowOSPreference
+      ? [
+          {
+            key: ThemeMode.SYSTEM,
+            label: t('Match system'),
+            icon: <Icons.FormatPainterOutlined />,
+            onClick: () => handleSelect(ThemeMode.SYSTEM),
+          },
+        ]
+      : []),
+  ];
+
+  // Add clear settings option only when there's a local theme active
+  const clearOption =
+    onClearLocalSettings && hasLocalOverride
+      ? {
+          key: 'clear-local',
+          label: t('Clear local theme'),
+          icon: <Icons.ClearOutlined />,
+          onClick: onClearLocalSettings,
+        }
+      : null;
+
+  return (
+    <StyledThemeSubMenu
+      key="theme-sub-menu"
+      title={selectedThemeModeIcon}
+      icon={<Icons.CaretDownOutlined iconSize="xs" />}
+    >
+      <Menu.ItemGroup title={t('Theme')} />
+      {themeOptions.map(option => (
+        <StyledThemeSubMenuItem
+          key={option.key}
+          onClick={option.onClick}
+          selected={option.key === themeMode}
+        >
+          {option.icon} {option.label}
+        </StyledThemeSubMenuItem>
+      ))}
+      {clearOption && [
+        <Menu.Divider key="theme-divider" />,
+        <Menu.Item key={clearOption.key} onClick={clearOption.onClick}>
+          {clearOption.icon} {clearOption.label}
+        </Menu.Item>,
+      ]}
+    </StyledThemeSubMenu>
+  );
+};
diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/index.ts 
b/superset-frontend/packages/superset-ui-core/src/components/index.ts
index e149906997..0053d50164 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/components/index.ts
@@ -164,6 +164,8 @@ export * from './Steps';
 export * from './Table';
 export * from './TableView';
 export * from './Tag';
+export * from './TelemetryPixel';
+export * from './ThemeSubMenu';
 export * from './UnsavedChangesModal';
 export * from './constants';
 export * from './Result';
diff --git a/superset-frontend/packages/superset-ui-core/src/theme/index.tsx 
b/superset-frontend/packages/superset-ui-core/src/theme/index.tsx
index 26c208ad12..8b788f2e9c 100644
--- a/superset-frontend/packages/superset-ui-core/src/theme/index.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/theme/index.tsx
@@ -26,7 +26,9 @@ import {
   type ThemeStorage,
   type ThemeControllerOptions,
   type ThemeContextType,
+  type SupersetThemeConfig,
   ThemeAlgorithm,
+  ThemeMode,
 } from './types';
 
 export {
@@ -66,7 +68,16 @@ const themeObject: Theme = Theme.fromConfig({
 const { theme } = themeObject;
 const supersetTheme = theme;
 
-export { Theme, themeObject, styled, theme, supersetTheme };
+export {
+  Theme,
+  ThemeAlgorithm,
+  ThemeMode,
+  themeObject,
+  styled,
+  theme,
+  supersetTheme,
+};
+
 export type {
   SupersetTheme,
   SerializableThemeConfig,
@@ -74,6 +85,7 @@ export type {
   ThemeStorage,
   ThemeControllerOptions,
   ThemeContextType,
+  SupersetThemeConfig,
 };
 
 // Export theme utility functions
diff --git a/superset-frontend/packages/superset-ui-core/src/theme/types.ts 
b/superset-frontend/packages/superset-ui-core/src/theme/types.ts
index 41011eb73f..b3cf0ae411 100644
--- a/superset-frontend/packages/superset-ui-core/src/theme/types.ts
+++ b/superset-frontend/packages/superset-ui-core/src/theme/types.ts
@@ -429,3 +429,16 @@ export interface ThemeContextType {
   canDetectOSPreference: () => boolean;
   createDashboardThemeProvider: (themeId: string) => Promise<Theme | null>;
 }
+
+/**
+ * Configuration object for complete theme setup including default, dark 
themes and settings
+ */
+export interface SupersetThemeConfig {
+  theme_default: AnyThemeConfig;
+  theme_dark?: AnyThemeConfig;
+  theme_settings?: {
+    enforced?: boolean;
+    allowSwitching?: boolean;
+    allowOSPreference?: boolean;
+  };
+}
diff --git a/superset-frontend/src/embedded/EmbeddedContextProviders.tsx 
b/superset-frontend/src/embedded/EmbeddedContextProviders.tsx
new file mode 100644
index 0000000000..207a1d320d
--- /dev/null
+++ b/superset-frontend/src/embedded/EmbeddedContextProviders.tsx
@@ -0,0 +1,93 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Route } from 'react-router-dom';
+import { getExtensionsRegistry } from '@superset-ui/core';
+import { Provider as ReduxProvider } from 'react-redux';
+import { QueryParamProvider } from 'use-query-params';
+import { DndProvider } from 'react-dnd';
+import { HTML5Backend } from 'react-dnd-html5-backend';
+import { FlashProvider, DynamicPluginProvider } from 'src/components';
+import { EmbeddedUiConfigProvider } from 'src/components/UiConfigContext';
+import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
+import { ThemeController } from 'src/theme/ThemeController';
+import type { ThemeStorage } from '@superset-ui/core';
+import { store } from 'src/views/store';
+import getBootstrapData from 'src/utils/getBootstrapData';
+
+/**
+ * In-memory implementation of ThemeStorage interface for embedded contexts.
+ * Persistent storage is not required for embedded dashboards.
+ */
+class ThemeMemoryStorageAdapter implements ThemeStorage {
+  private storage = new Map<string, string>();
+
+  getItem(key: string): string | null {
+    return this.storage.get(key) || null;
+  }
+
+  setItem(key: string, value: string): void {
+    this.storage.set(key, value);
+  }
+
+  removeItem(key: string): void {
+    this.storage.delete(key);
+  }
+}
+
+const themeController = new ThemeController({
+  storage: new ThemeMemoryStorageAdapter(),
+});
+
+export const getThemeController = (): ThemeController => themeController;
+
+const { common } = getBootstrapData();
+const extensionsRegistry = getExtensionsRegistry();
+
+export const EmbeddedContextProviders: React.FC = ({ children }) => {
+  const RootContextProviderExtension = extensionsRegistry.get(
+    'root.context.provider',
+  );
+
+  return (
+    <SupersetThemeProvider themeController={themeController}>
+      <ReduxProvider store={store}>
+        <DndProvider backend={HTML5Backend}>
+          <FlashProvider messages={common.flash_messages}>
+            <EmbeddedUiConfigProvider>
+              <DynamicPluginProvider>
+                <QueryParamProvider
+                  ReactRouterRoute={Route}
+                  stringifyOptions={{ encode: false }}
+                >
+                  {RootContextProviderExtension ? (
+                    <RootContextProviderExtension>
+                      {children}
+                    </RootContextProviderExtension>
+                  ) : (
+                    children
+                  )}
+                </QueryParamProvider>
+              </DynamicPluginProvider>
+            </EmbeddedUiConfigProvider>
+          </FlashProvider>
+        </DndProvider>
+      </ReduxProvider>
+    </SupersetThemeProvider>
+  );
+};
diff --git a/superset-frontend/src/embedded/index.tsx 
b/superset-frontend/src/embedded/index.tsx
index c577015748..c207524961 100644
--- a/superset-frontend/src/embedded/index.tsx
+++ b/superset-frontend/src/embedded/index.tsx
@@ -21,20 +21,27 @@ import 'src/public-path';
 import { lazy, Suspense } from 'react';
 import ReactDOM from 'react-dom';
 import { BrowserRouter as Router, Route } from 'react-router-dom';
-import { makeApi, t, logging, themeObject } from '@superset-ui/core';
+import {
+  type SupersetThemeConfig,
+  makeApi,
+  t,
+  logging,
+} from '@superset-ui/core';
 import Switchboard from '@superset-ui/switchboard';
 import getBootstrapData, { applicationRoot } from 'src/utils/getBootstrapData';
 import setupClient from 'src/setup/setupClient';
 import setupPlugins from 'src/setup/setupPlugins';
 import { useUiConfig } from 'src/components/UiConfigContext';
-import { RootContextProviders } from 'src/views/RootContextProviders';
 import { store, USER_LOADED } from 'src/views/store';
 import { Loading } from '@superset-ui/core/components';
 import { ErrorBoundary } from 'src/components';
 import { addDangerToast } from 'src/components/MessageToasts/actions';
 import ToastContainer from 'src/components/MessageToasts/ToastContainer';
 import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
-import { AnyThemeConfig } from 'packages/superset-ui-core/src/theme/types';
+import {
+  EmbeddedContextProviders,
+  getThemeController,
+} from './EmbeddedContextProviders';
 import { embeddedApi } from './api';
 import { getDataMaskChangeTrigger } from './utils';
 
@@ -44,9 +51,7 @@ const debugMode = process.env.WEBPACK_MODE === 'development';
 const bootstrapData = getBootstrapData();
 
 function log(...info: unknown[]) {
-  if (debugMode) {
-    logging.debug(`[superset]`, ...info);
-  }
+  if (debugMode) logging.debug(`[superset]`, ...info);
 }
 
 const LazyDashboardPage = lazy(
@@ -85,12 +90,12 @@ const EmbededLazyDashboardPage = () => {
 
 const EmbeddedRoute = () => (
   <Suspense fallback={<Loading />}>
-    <RootContextProviders>
+    <EmbeddedContextProviders>
       <ErrorBoundary>
         <EmbededLazyDashboardPage />
       </ErrorBoundary>
       <ToastContainer position="top" />
-    </RootContextProviders>
+    </EmbeddedContextProviders>
   </Suspense>
 );
 
@@ -245,12 +250,13 @@ window.addEventListener('message', function 
embeddedPageInitializer(event) {
     Switchboard.defineMethod('getDataMask', embeddedApi.getDataMask);
     Switchboard.defineMethod(
       'setThemeConfig',
-      (payload: { themeConfig: AnyThemeConfig }) => {
+      (payload: { themeConfig: SupersetThemeConfig }) => {
         const { themeConfig } = payload;
         log('Received setThemeConfig request:', themeConfig);
 
         try {
-          themeObject.setConfig(themeConfig);
+          const themeController = getThemeController();
+          themeController.setThemeConfig(themeConfig);
           return { success: true, message: 'Theme applied' };
         } catch (error) {
           logging.error('Failed to apply theme config:', error);
@@ -258,8 +264,22 @@ window.addEventListener('message', function 
embeddedPageInitializer(event) {
         }
       },
     );
+
     Switchboard.start();
   }
 });
 
+// Clean up theme controller on page unload
+window.addEventListener('beforeunload', () => {
+  try {
+    const controller = getThemeController();
+    if (controller) {
+      log('Destroying theme controller');
+      controller.destroy();
+    }
+  } catch (error) {
+    logging.warn('Failed to destroy theme controller:', error);
+  }
+});
+
 log('embed page is ready to receive messages');
diff --git a/superset-frontend/src/features/home/RightMenu.tsx 
b/superset-frontend/src/features/home/RightMenu.tsx
index 2106ab3f53..c9573ab81d 100644
--- a/superset-frontend/src/features/home/RightMenu.tsx
+++ b/superset-frontend/src/features/home/RightMenu.tsx
@@ -17,13 +17,11 @@
  * under the License.
  */
 import { Fragment, useState, useEffect, FC, PureComponent } from 'react';
-
 import rison from 'rison';
 import { useSelector } from 'react-redux';
 import { Link } from 'react-router-dom';
 import { useQueryParams, BooleanParam } from 'use-query-params';
 import { get, isEmpty } from 'lodash';
-
 import {
   t,
   styled,
@@ -33,10 +31,15 @@ import {
   getExtensionsRegistry,
   useTheme,
 } from '@superset-ui/core';
-import { Menu } from '@superset-ui/core/components/Menu';
-import { Label, Tooltip } from '@superset-ui/core/components';
-import { Icons } from '@superset-ui/core/components/Icons';
-import { Typography } from '@superset-ui/core/components/Typography';
+import {
+  Label,
+  Tooltip,
+  ThemeSubMenu,
+  Menu,
+  Icons,
+  Typography,
+  TelemetryPixel,
+} from '@superset-ui/core/components';
 import { ensureAppRoot } from 'src/utils/pathUtils';
 import { findPermission } from 'src/utils/findPermission';
 import { isUserAdmin } from 'src/dashboard/util/permissionUtils';
@@ -49,9 +52,7 @@ import { RootState } from 'src/dashboard/types';
 import DatabaseModal from 'src/features/databases/DatabaseModal';
 import UploadDataModal from 'src/features/databases/UploadDataModel';
 import { uploadUserPerms } from 'src/views/CRUD/utils';
-import TelemetryPixel from '@superset-ui/core/components/TelemetryPixel';
 import { useThemeContext } from 'src/theme/ThemeProvider';
-import ThemeSelect from '@superset-ui/core/components/ThemeSelect';
 import LanguagePicker from './LanguagePicker';
 import {
   ExtensionConfigs,
@@ -138,6 +139,7 @@ const RightMenu = ({
     datasetAdded?: boolean;
   }) => void;
 }) => {
+  const theme = useTheme();
   const user = useSelector<any, UserWithPermissionsAndRoles>(
     state => state.user,
   );
@@ -371,7 +373,6 @@ const RightMenu = ({
     localStorage.removeItem('redux');
   };
 
-  const theme = useTheme();
   return (
     <StyledDiv align={align}>
       {canDatabase && (
@@ -493,16 +494,15 @@ const RightMenu = ({
             })}
           </StyledSubMenu>
         )}
+
         {canSetMode() && (
-          <span>
-            <ThemeSelect
-              setThemeMode={setThemeMode}
-              themeMode={themeMode}
-              hasLocalOverride={hasDevOverride()}
-              onClearLocalSettings={clearLocalOverrides}
-              allowOSPreference={canDetectOSPreference()}
-            />
-          </span>
+          <ThemeSubMenu
+            setThemeMode={setThemeMode}
+            themeMode={themeMode}
+            hasLocalOverride={hasDevOverride()}
+            onClearLocalSettings={clearLocalOverrides}
+            allowOSPreference={canDetectOSPreference()}
+          />
         )}
 
         <StyledSubMenu
diff --git a/superset-frontend/src/theme/ThemeController.ts 
b/superset-frontend/src/theme/ThemeController.ts
index 3f33894088..5a1851e8f5 100644
--- a/superset-frontend/src/theme/ThemeController.ts
+++ b/superset-frontend/src/theme/ThemeController.ts
@@ -17,13 +17,15 @@
  * under the License.
  */
 import {
+  type AnyThemeConfig,
+  type SupersetTheme,
+  type SupersetThemeConfig,
+  type ThemeControllerOptions,
+  type ThemeStorage,
   Theme,
-  AnyThemeConfig,
-  ThemeStorage,
-  ThemeControllerOptions,
+  ThemeMode,
   themeObject as supersetThemeObject,
 } from '@superset-ui/core';
-import { SupersetTheme, ThemeMode } from '@superset-ui/core/theme/types';
 import {
   getAntdConfig,
   normalizeThemeConfig,
@@ -94,7 +96,7 @@ export class ThemeController {
 
   private currentMode: ThemeMode;
 
-  private readonly hasBootstrapThemes: boolean;
+  private hasCustomThemes: boolean;
 
   private onChangeCallbacks: Set<(theme: Theme) => void> = new Set();
 
@@ -109,15 +111,13 @@ export class ThemeController {
 
   private dashboardCrudTheme: AnyThemeConfig | null = null;
 
-  constructor(options: ThemeControllerOptions = {}) {
-    const {
-      storage = new LocalStorageAdapter(),
-      modeStorageKey = STORAGE_KEYS.THEME_MODE,
-      themeObject = supersetThemeObject,
-      defaultTheme = (supersetThemeObject.theme as AnyThemeConfig) ?? {},
-      onChange = null,
-    } = options;
-
+  constructor({
+    storage = new LocalStorageAdapter(),
+    modeStorageKey = STORAGE_KEYS.THEME_MODE,
+    themeObject = supersetThemeObject,
+    defaultTheme = (supersetThemeObject.theme as AnyThemeConfig) ?? {},
+    onChange = undefined,
+  }: ThemeControllerOptions = {}) {
     this.storage = storage;
     this.modeStorageKey = modeStorageKey;
 
@@ -129,14 +129,14 @@ export class ThemeController {
       bootstrapDefaultTheme,
       bootstrapDarkTheme,
       bootstrapThemeSettings,
-      hasBootstrapThemes,
+      hasCustomThemes,
     }: BootstrapThemeData = this.loadBootstrapData();
 
-    this.hasBootstrapThemes = hasBootstrapThemes;
+    this.hasCustomThemes = hasCustomThemes;
     this.themeSettings = bootstrapThemeSettings || {};
 
     // Set themes based on bootstrap data availability
-    if (this.hasBootstrapThemes) {
+    if (this.hasCustomThemes) {
       this.darkTheme = bootstrapDarkTheme || bootstrapDefaultTheme || null;
       this.defaultTheme =
         bootstrapDefaultTheme || bootstrapDarkTheme || defaultTheme;
@@ -424,6 +424,42 @@ export class ThemeController {
     return allowOSPreference === true;
   }
 
+  /**
+   * Sets an entire new theme configuration, replacing all existing theme data 
and settings.
+   * This method is designed for use cases like embedded dashboards where 
themes are provided
+   * dynamically from external sources.
+   * @param config - The complete theme configuration object
+   */
+  public setThemeConfig(config: SupersetThemeConfig): void {
+    this.defaultTheme = config.theme_default;
+    this.darkTheme = config.theme_dark || null;
+    this.hasCustomThemes = true;
+
+    this.themeSettings = {
+      enforced: config.theme_settings?.enforced ?? false,
+      allowSwitching: config.theme_settings?.allowSwitching ?? true,
+      allowOSPreference: config.theme_settings?.allowOSPreference ?? true,
+    };
+
+    let newMode: ThemeMode;
+    try {
+      this.validateModeUpdatePermission(this.currentMode);
+      const hasRequiredTheme = this.isValidThemeMode(this.currentMode);
+      newMode = hasRequiredTheme
+        ? this.currentMode
+        : this.determineInitialMode();
+    } catch {
+      newMode = this.determineInitialMode();
+    }
+
+    this.currentMode = newMode;
+
+    const themeToApply =
+      this.getThemeForMode(this.currentMode) || this.defaultTheme;
+
+    this.updateTheme(themeToApply);
+  }
+
   /**
    * Handles system theme changes with error recovery.
    */
@@ -547,7 +583,7 @@ export class ThemeController {
       bootstrapDefaultTheme: hasValidDefault ? defaultTheme : null,
       bootstrapDarkTheme: hasValidDark ? darkTheme : null,
       bootstrapThemeSettings: hasValidSettings ? themeSettings : null,
-      hasBootstrapThemes: hasValidDefault || hasValidDark,
+      hasCustomThemes: hasValidDefault || hasValidDark,
     };
   }
 
@@ -607,7 +643,7 @@ export class ThemeController {
       resolvedMode = ThemeController.getSystemPreferredMode();
     }
 
-    if (!this.hasBootstrapThemes) {
+    if (!this.hasCustomThemes) {
       const baseTheme = this.defaultTheme.token as Partial<SupersetTheme>;
       return getAntdConfig(baseTheme, resolvedMode === ThemeMode.DARK);
     }
diff --git a/superset-frontend/src/theme/ThemeProvider.tsx 
b/superset-frontend/src/theme/ThemeProvider.tsx
index 5b4b64c799..2ec902889b 100644
--- a/superset-frontend/src/theme/ThemeProvider.tsx
+++ b/superset-frontend/src/theme/ThemeProvider.tsx
@@ -24,8 +24,12 @@ import {
   useMemo,
   useState,
 } from 'react';
-import { Theme, AnyThemeConfig, ThemeContextType } from '@superset-ui/core';
-import { ThemeMode } from '@superset-ui/core/theme/types';
+import {
+  type AnyThemeConfig,
+  type ThemeContextType,
+  Theme,
+  ThemeMode,
+} from '@superset-ui/core';
 import { ThemeController } from './ThemeController';
 
 const ThemeContext = createContext<ThemeContextType | null>(null);
diff --git a/superset-frontend/src/theme/tests/ThemeController.test.ts 
b/superset-frontend/src/theme/tests/ThemeController.test.ts
index cc242e849d..1851dfabd9 100644
--- a/superset-frontend/src/theme/tests/ThemeController.test.ts
+++ b/superset-frontend/src/theme/tests/ThemeController.test.ts
@@ -17,12 +17,17 @@
  * under the License.
  */
 import { theme as antdThemeImport } from 'antd';
-import { Theme } from '@superset-ui/core';
+import {
+  type AnyThemeConfig,
+  type SupersetThemeConfig,
+  Theme,
+  ThemeAlgorithm,
+  ThemeMode,
+} from '@superset-ui/core';
 import type {
   BootstrapThemeDataConfig,
   CommonBootstrapData,
 } from 'src/types/bootstrapTypes';
-import { ThemeAlgorithm, ThemeMode } from '@superset-ui/core/theme/types';
 import getBootstrapData from 'src/utils/getBootstrapData';
 import { LocalStorageAdapter, ThemeController } from '../ThemeController';
 
@@ -43,7 +48,7 @@ const mockThemeFromConfig = jest.fn();
 const mockSetConfig = jest.fn();
 
 // Mock data constants
-const DEFAULT_THEME = {
+const DEFAULT_THEME: AnyThemeConfig = {
   token: {
     colorBgBase: '#ededed',
     colorTextBase: '#120f0f',
@@ -55,7 +60,7 @@ const DEFAULT_THEME = {
   },
 };
 
-const DARK_THEME = {
+const DARK_THEME: AnyThemeConfig = {
   token: {
     colorBgBase: '#141118',
     colorTextBase: '#fdc7c7',
@@ -65,7 +70,7 @@ const DARK_THEME = {
     colorSuccess: '#3c7c1b',
     colorWarning: '#dc9811',
   },
-  algorithm: ThemeMode.DARK,
+  algorithm: ThemeAlgorithm.DARK,
 };
 
 const THEME_SETTINGS = {
@@ -1049,4 +1054,298 @@ describe('ThemeController', () => {
       );
     });
   });
+
+  describe('setThemeConfig', () => {
+    beforeEach(() => {
+      mockGetBootstrapData.mockReturnValue(
+        createMockBootstrapData({
+          default: {},
+          dark: {},
+          settings: {},
+        }),
+      );
+
+      controller = new ThemeController({
+        themeObject: mockThemeObject,
+        defaultTheme: { token: {} },
+      });
+
+      jest.clearAllMocks();
+    });
+
+    it('should set complete theme configuration', () => {
+      const themeConfig = {
+        theme_default: DEFAULT_THEME,
+        theme_dark: DARK_THEME,
+        theme_settings: {
+          enforced: false,
+          allowSwitching: true,
+          allowOSPreference: true,
+        },
+      };
+
+      controller.setThemeConfig(themeConfig);
+
+      expect(mockSetConfig).toHaveBeenCalledTimes(1);
+      expect(mockSetConfig).toHaveBeenCalledWith(
+        expect.objectContaining({
+          token: expect.objectContaining(DEFAULT_THEME.token),
+          algorithm: antdThemeImport.defaultAlgorithm,
+        }),
+      );
+
+      expect(controller.getCurrentMode()).toBe(ThemeMode.SYSTEM);
+      expect(controller.canSetTheme()).toBe(true);
+      expect(controller.canSetMode()).toBe(true);
+    });
+
+    it('should handle theme_default only', () => {
+      const themeConfig = {
+        theme_default: DEFAULT_THEME,
+      };
+
+      controller.setThemeConfig(themeConfig);
+
+      expect(mockSetConfig).toHaveBeenCalledTimes(1);
+      expect(mockSetConfig).toHaveBeenCalledWith(
+        expect.objectContaining({
+          token: expect.objectContaining(DEFAULT_THEME.token),
+          algorithm: antdThemeImport.defaultAlgorithm,
+        }),
+      );
+
+      expect(controller.canSetTheme()).toBe(true);
+      expect(controller.canSetMode()).toBe(true);
+    });
+
+    it('should handle theme_default and theme_dark without settings', () => {
+      const themeConfig = {
+        theme_default: DEFAULT_THEME,
+        theme_dark: DARK_THEME,
+      };
+
+      controller.setThemeConfig(themeConfig);
+
+      expect(mockSetConfig).toHaveBeenCalledTimes(1);
+      expect(mockSetConfig).toHaveBeenCalledWith(
+        expect.objectContaining({
+          token: expect.objectContaining(DEFAULT_THEME.token),
+        }),
+      );
+
+      jest.clearAllMocks();
+      controller.setThemeMode(ThemeMode.DARK);
+
+      expect(mockSetConfig).toHaveBeenCalledTimes(1);
+      expect(mockSetConfig).toHaveBeenCalledWith(
+        expect.objectContaining({
+          token: expect.objectContaining(DARK_THEME.token),
+          algorithm: antdThemeImport.darkAlgorithm,
+        }),
+      );
+    });
+
+    it('should handle enforced theme settings', () => {
+      const themeConfig = {
+        theme_default: DEFAULT_THEME,
+        theme_dark: DARK_THEME,
+        theme_settings: {
+          enforced: true,
+          allowSwitching: false,
+          allowOSPreference: false,
+        },
+      };
+
+      controller.setThemeConfig(themeConfig);
+
+      expect(controller.canSetTheme()).toBe(false);
+      expect(controller.canSetMode()).toBe(false);
+      expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT);
+
+      expect(() => {
+        controller.setThemeMode(ThemeMode.DARK);
+      }).toThrow('User does not have permission to update the theme mode');
+    });
+
+    it('should handle allowOSPreference: false setting', () => {
+      const themeConfig = {
+        theme_default: DEFAULT_THEME,
+        theme_dark: DARK_THEME,
+        theme_settings: {
+          enforced: false,
+          allowSwitching: true,
+          allowOSPreference: false,
+        },
+      };
+
+      controller.setThemeConfig(themeConfig);
+
+      expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT);
+      expect(controller.canSetMode()).toBe(true);
+
+      expect(() => {
+        controller.setThemeMode(ThemeMode.SYSTEM);
+      }).toThrow('System theme mode is not allowed');
+    });
+
+    it('should re-determine initial mode based on new settings', () => {
+      mockMatchMedia.mockReturnValue({
+        matches: true,
+        addEventListener: jest.fn(),
+        removeEventListener: jest.fn(),
+      });
+
+      const themeConfig = {
+        theme_default: DEFAULT_THEME,
+        theme_dark: DARK_THEME,
+        theme_settings: {
+          enforced: false,
+          allowSwitching: false,
+          allowOSPreference: true,
+        },
+      };
+
+      controller.setThemeConfig(themeConfig);
+
+      expect(controller.getCurrentMode()).toBe(ThemeMode.SYSTEM);
+      expect(controller.canSetMode()).toBe(false);
+    });
+
+    it('should apply appropriate theme after configuration', () => {
+      controller.setThemeMode(ThemeMode.DARK);
+      jest.clearAllMocks();
+
+      const themeConfig = {
+        theme_default: {
+          token: {
+            colorPrimary: '#00ff00',
+          },
+        },
+        theme_dark: {
+          token: {
+            colorPrimary: '#ff0000',
+            colorBgBase: '#000000',
+          },
+          algorithm: 'dark',
+        },
+      };
+
+      controller.setThemeConfig(themeConfig as SupersetThemeConfig);
+
+      expect(mockSetConfig).toHaveBeenCalledTimes(1);
+      expect(mockSetConfig).toHaveBeenCalledWith(
+        expect.objectContaining({
+          token: expect.objectContaining({
+            colorPrimary: '#ff0000',
+            colorBgBase: '#000000',
+          }),
+          algorithm: antdThemeImport.darkAlgorithm,
+        }),
+      );
+    });
+
+    it('should handle missing theme_dark gracefully', () => {
+      const themeConfig = {
+        theme_default: DEFAULT_THEME,
+        theme_settings: {
+          allowSwitching: true,
+        },
+      };
+
+      controller.setThemeConfig(themeConfig);
+
+      jest.clearAllMocks();
+      controller.setThemeMode(ThemeMode.DARK);
+
+      expect(mockSetConfig).toHaveBeenCalledTimes(1);
+      expect(mockSetConfig).toHaveBeenCalledWith(
+        expect.objectContaining({
+          token: expect.objectContaining(DEFAULT_THEME.token),
+          algorithm: antdThemeImport.defaultAlgorithm,
+        }),
+      );
+    });
+
+    it('should preserve existing theme mode when possible', () => {
+      controller.setThemeMode(ThemeMode.DARK);
+      const initialMode = controller.getCurrentMode();
+
+      jest.clearAllMocks();
+
+      const themeConfig = {
+        theme_default: DEFAULT_THEME,
+        theme_dark: DARK_THEME,
+        theme_settings: {
+          allowSwitching: true,
+          allowOSPreference: false,
+        },
+      };
+
+      controller.setThemeConfig(themeConfig);
+
+      expect(controller.getCurrentMode()).toBe(initialMode);
+    });
+
+    it('should trigger onChange callbacks', () => {
+      const changeCallback = jest.fn();
+      controller.onChange(changeCallback);
+
+      const themeConfig = {
+        theme_default: DEFAULT_THEME,
+        theme_dark: DARK_THEME,
+      };
+
+      controller.setThemeConfig(themeConfig);
+
+      expect(changeCallback).toHaveBeenCalledTimes(1);
+      expect(changeCallback).toHaveBeenCalledWith(mockThemeObject);
+    });
+
+    it('should handle partial theme_settings', () => {
+      const themeConfig = {
+        theme_default: DEFAULT_THEME,
+        theme_settings: {
+          enforced: true,
+        },
+      };
+
+      controller.setThemeConfig(themeConfig);
+
+      expect(controller.canSetTheme()).toBe(false);
+      expect(controller.canSetMode()).toBe(false);
+    });
+
+    it('should handle error in theme application', () => {
+      mockSetConfig.mockImplementationOnce(() => {
+        throw new Error('Theme application error');
+      });
+
+      const themeConfig = {
+        theme_default: DEFAULT_THEME,
+      };
+
+      expect(() => {
+        controller.setThemeConfig(themeConfig);
+      }).not.toThrow();
+
+      expect(consoleErrorSpy).toHaveBeenCalledWith(
+        'Failed to apply theme:',
+        expect.any(Error),
+      );
+    });
+
+    it('should update stored theme mode', () => {
+      const themeConfig = {
+        theme_default: DEFAULT_THEME,
+        theme_dark: DARK_THEME,
+      };
+
+      controller.setThemeConfig(themeConfig);
+
+      expect(mockLocalStorage.setItem).toHaveBeenCalledWith(
+        'superset-theme-mode',
+        expect.any(String),
+      );
+    });
+  });
 });
diff --git a/superset-frontend/src/theme/tests/ThemeProvider.test.tsx 
b/superset-frontend/src/theme/tests/ThemeProvider.test.tsx
index 6d34bfb1d0..a869d70ec5 100644
--- a/superset-frontend/src/theme/tests/ThemeProvider.test.tsx
+++ b/superset-frontend/src/theme/tests/ThemeProvider.test.tsx
@@ -17,8 +17,7 @@
  * under the License.
  */
 import { ReactNode } from 'react';
-import { Theme } from '@superset-ui/core';
-import { ThemeContextType, ThemeMode } from '@superset-ui/core/theme/types';
+import { type ThemeContextType, Theme, ThemeMode } from '@superset-ui/core';
 import { act, render, screen } from '@superset-ui/core/spec';
 import { renderHook } from '@testing-library/react-hooks';
 import { SupersetThemeProvider, useThemeContext } from '../ThemeProvider';
diff --git a/superset-frontend/src/types/bootstrapTypes.ts 
b/superset-frontend/src/types/bootstrapTypes.ts
index 055625620d..e64921900d 100644
--- a/superset-frontend/src/types/bootstrapTypes.ts
+++ b/superset-frontend/src/types/bootstrapTypes.ts
@@ -16,14 +16,6 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import {
-  ColorSchemeConfig,
-  FeatureFlagMap,
-  JsonObject,
-  LanguagePack,
-  Locale,
-  SequentialSchemeConfig,
-} from '@superset-ui/core';
 import { FormatLocaleDefinition } from 'd3-format';
 import { TimeLocaleDefinition } from 'd3-time-format';
 import { isPlainObject } from 'lodash';
@@ -31,8 +23,14 @@ import { Languages } from 'src/features/home/LanguagePicker';
 import type { FlashMessage } from 'src/components';
 import type {
   AnyThemeConfig,
+  ColorSchemeConfig,
+  FeatureFlagMap,
+  JsonObject,
+  LanguagePack,
+  Locale,
+  SequentialSchemeConfig,
   SerializableThemeConfig,
-} from '@superset-ui/core/theme/types';
+} from '@superset-ui/core';
 
 export type User = {
   createdOn?: string;
@@ -189,7 +187,7 @@ export interface BootstrapThemeData {
   bootstrapDefaultTheme: AnyThemeConfig | null;
   bootstrapDarkTheme: AnyThemeConfig | null;
   bootstrapThemeSettings: SerializableThemeSettings | null;
-  hasBootstrapThemes: boolean;
+  hasCustomThemes: boolean;
 }
 
 export function isUser(user: any): user is User {

Reply via email to