| 1 | // Shared Material UI theme bridge for the React-mounted Shuffle embeds. |
| 2 | // |
| 3 | // Background: `@shuffleio/shuffle-mcps` is built with `@mui/material`. |
| 4 | // Without a `<ThemeProvider>` wrapping the React mount, MUI falls back |
| 5 | // to its built-in light-mode palette — which, against CoPilot's dark |
| 6 | // backdrop, looks washed-out and transparent (the AppDetailDrawer in |
| 7 | // particular ended up unreadable for users on the default theme). |
| 8 | // |
| 9 | // We expose: |
| 10 | // - `getMuiTheme(isDark)` — returns a memoised MUI Theme matching |
| 11 | // CoPilot's light/dark mode. We don't pipe every Naive UI token in |
| 12 | // (the surface area is huge and the MUI palette has its own |
| 13 | // semantics) — just the mode plus a darkened paper background so |
| 14 | // drawer/menu surfaces are opaque against the page underneath. |
| 15 | // - `MuiProvider` — small React component that wraps children in |
| 16 | // `<ThemeProvider value={mui-theme}>`. Each embed wrapper renders |
| 17 | // this around the package's exported component. |
| 18 | |
| 19 | import type { Theme } from "@mui/material/styles" |
| 20 | import type { ReactNode } from "react" |
| 21 | import { createTheme, ThemeProvider } from "@mui/material/styles" |
| 22 | import { createElement } from "react" |
| 23 | import { useThemeStore } from "@/stores/theme" |
| 24 | |
| 25 | let lightTheme: Theme | null = null |
| 26 | let darkTheme: Theme | null = null |
| 27 | |
| 28 | function buildTheme(isDark: boolean): Theme { |
| 29 | const themeStore = useThemeStore() |
| 30 | |
| 31 | return createTheme({ |
| 32 | palette: { |
| 33 | mode: isDark ? "dark" : "light", |
| 34 | ...(isDark |
| 35 | ? { |
| 36 | background: { |
| 37 | default: "var(--bg-body-color) !important", |
| 38 | paper: "var(--bg-default-color) !important" |
| 39 | }, |
| 40 | primary: { |
| 41 | main: themeStore.style["primary-color"] |
| 42 | } |
| 43 | } |
| 44 | : {}) |
| 45 | }, |
| 46 | components: { |
| 47 | MuiPaper: { |
| 48 | styleOverrides: { |
| 49 | root: { |
| 50 | backgroundImage: "none !important", |
| 51 | boxShadow: "none !important" |
| 52 | } |
| 53 | } |
| 54 | }, |
| 55 | MuiCard: { |
| 56 | styleOverrides: { |
| 57 | root: { |
| 58 | border: "1px solid var(--border-color) !important", |
| 59 | borderColor: "var(--border-color) !important", |
| 60 | backgroundImage: "none !important", |
| 61 | backgroundColor: "var(--bg-secondary-color) !important" |
| 62 | } |
| 63 | } |
| 64 | }, |
| 65 | MuiDrawer: { |
| 66 | styleOverrides: { |
| 67 | root: { |
| 68 | zIndex: "2104 !important" |
| 69 | }, |
| 70 | paper: { |
| 71 | borderLeft: "none !important", |
| 72 | borderTopLeftRadius: "var(--border-radius) !important", |
| 73 | borderBottomLeftRadius: "var(--border-radius) !important" |
| 74 | } |
| 75 | } |
| 76 | }, |
| 77 | MuiButton: { |
| 78 | styleOverrides: { |
| 79 | root: { |
| 80 | borderRadius: "var(--border-radius) !important", |
| 81 | boxShadow: "none !important" |
| 82 | } |
| 83 | } |
| 84 | } |
| 85 | }, |
| 86 | typography: { |
| 87 | fontFamily: "var(--font-family)" |
| 88 | } |
| 89 | }) |
| 90 | } |
| 91 | |
| 92 | export function getMuiTheme(isDark: boolean): Theme { |
| 93 | if (isDark) { |
| 94 | if (!darkTheme) darkTheme = buildTheme(true) |
| 95 | return darkTheme |
| 96 | } |
| 97 | if (!lightTheme) lightTheme = buildTheme(false) |
| 98 | return lightTheme |
| 99 | } |
| 100 | |
| 101 | export function MuiProvider(props: { isDark: boolean; children: ReactNode }) { |
| 102 | return createElement(ThemeProvider as never, { theme: getMuiTheme(props.isDark) }, props.children) |
| 103 | } |