(() => {
+ return {
+ pathname,
+ searchParams: new URLSearchParams(),
+ navigate: (path) => setPathname(String(path)),
+ };
+ }, [pathname]);
+
+ // Remove this const when copying and pasting into your project.
+ const demoWindow = window !== undefined ? window() : undefined;
+
+ return (
+ // preview-start
+
+
+
+
+
+ // preview-end
+ );
+}
diff --git a/docs/data/toolpad/core/components/app-provider/AppProviderTheme.tsx.preview b/docs/data/toolpad/core/components/app-provider/AppProviderTheme.tsx.preview
new file mode 100644
index 00000000000..2112ddfced7
--- /dev/null
+++ b/docs/data/toolpad/core/components/app-provider/AppProviderTheme.tsx.preview
@@ -0,0 +1,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/data/toolpad/core/components/app-provider/app-provider.md b/docs/data/toolpad/core/components/app-provider/app-provider.md
index a2747fdda35..e6b1afa8ba6 100644
--- a/docs/data/toolpad/core/components/app-provider/app-provider.md
+++ b/docs/data/toolpad/core/components/app-provider/app-provider.md
@@ -6,20 +6,82 @@ components: AppProvider
# App Provider
-The app provider component provides the necessary context for a Toolpad application, such as routing, navigation and theming.
+The app provider component provides the necessary context to easily set up a Toolpad application.
+
+By wrapping an application at the root level with an `AppProvider` component, many of Toolpad's features (such as routing, navigation and theming) can be automatically enabled to their fullest extent, abstracting away complexity and helping you focus on the details that matter.
+
+It is not mandatory that every application is wrapped in an `AppProvider`, but it is highly recommended for most apps that use Toolpad.
## Basic
-Wrap the whole application with the `AppProvider` component to enable many of Toolpad's features.
+Wrap an application page with the `AppProvider` component.
+
+Ideally, the `AppProvider` should wrap every page in the application, therefore in most projects it should be imported and placed in the file that defines a **shared layout** for all pages.
+
+In the following example, an `AppProvider` component wrapping the page provides it with a default theme, and a `DashboardLayout` placed inside it gets its navigation and routing features automatically set based on the props passed to the `AppProvider`.
{{"demo": "AppProviderBasic.js", "height": 500, "iframe": true}}
## Next.js
-The `AppProvider` for Next.js applications includes routing out-of-the-box.
+The `AppProvider` for Next.js applications includes some Next.js integrations out-of-the-box.
+
+By using the specific `AppProvider` for Next.js you do not have to manually configure the integration between some Toolpad features and the corresponding Next.js features (such as routing), making the integration automatic and seamless.
```tsx
import { AppProvider } from '@toolpad/core/nextjs/AppProvider';
// or
import { AppProvider } from '@toolpad/core/nextjs';
```
+
+### Next.js App Router
+
+When using the **Next.js App Router**, the most typical file where to import and use `AppProvider` will be at the top level `layout.tsx` file that defines the layout for all the application pages.
+
+```tsx
+// app/layout.tsx
+
+import { AppProvider } from '@toolpad/core/nextjs/AppProvider';
+
+export default function Layout(props) {
+ const { children } = props;
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+```
+
+### Next.js Pages Router
+
+When using the **Next.js Pages Router**, the most typical file where to import and use `AppProvider` in order to wrap every page in the application will be the `pages/_app.tsx` file.
+
+```tsx
+// pages/_app.tsx
+
+import { AppProvider } from '@toolpad/core/nextjs/AppProvider';
+
+export default function App(props) {
+ const { Component, pageProps } = props;
+
+ return (
+
+
+
+ );
+}
+```
+
+## Theming
+
+An `AppProvider` can set a visual theme for all elements inside it to adopt via the `theme` prop. This prop can be set in a few distinct ways with different advantages and disadvantages:
+
+1. [CSS variables theme](https://mui.com/material-ui/experimental-api/css-theme-variables/overview/): the default and recommended theming option for Toolpad applications, as it is the only option that prevents issues such as [dark-mode SSR flickering](https://github.com/mui/material-ui/issues/27651) and supports both light and dark mode with a single theme definition. The provided default theme in Toolpad is already in this format.
+2. [Standard Material UI theme](https://mui.com/material-ui/customization/theming/): a single standard Material UI theme can be provided as the only theme to be used.
+3. **Light and dark themes**: two separate Material UI themes can be provided for light and dark mode in an object with the format `{ light: Theme, dark: Theme }`
+
+{{"demo": "AppProviderTheme.js", "height": 500, "iframe": true}}
diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.js b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.js
index 25d3e62948b..6881d21974e 100644
--- a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.js
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.js
@@ -1,4 +1,5 @@
import * as React from 'react';
+import PropTypes from 'prop-types';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import DashboardIcon from '@mui/icons-material/Dashboard';
@@ -55,7 +56,29 @@ const NAVIGATION = [
},
];
-export default function DashboardLayoutBasic() {
+function DemoPageContent({ pathname }) {
+ return (
+
+ Dashboard content for {pathname}
+
+ );
+}
+
+DemoPageContent.propTypes = {
+ pathname: PropTypes.string.isRequired,
+};
+
+function DashboardLayoutBasic(props) {
+ const { window } = props;
+
const [pathname, setPathname] = React.useState('/page');
const router = React.useMemo(() => {
@@ -66,20 +89,26 @@ export default function DashboardLayoutBasic() {
};
}, [pathname]);
+ // Remove this const when copying and pasting into your project.
+ const demoWindow = window !== undefined ? window() : undefined;
+
return (
-
+ // preview-start
+
-
- Dashboard content for {pathname}
-
+
+ // preview-end
);
}
+
+DashboardLayoutBasic.propTypes = {
+ /**
+ * Injected by the documentation to work in an iframe.
+ * Remove this when copying and pasting into your project.
+ */
+ window: PropTypes.func,
+};
+
+export default DashboardLayoutBasic;
diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.tsx b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.tsx
index 73d1000def8..587d3da4410 100644
--- a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.tsx
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.tsx
@@ -56,7 +56,33 @@ const NAVIGATION: Navigation = [
},
];
-export default function DashboardLayoutBasic() {
+function DemoPageContent({ pathname }: { pathname: string }) {
+ return (
+
+ Dashboard content for {pathname}
+
+ );
+}
+
+interface DemoProps {
+ /**
+ * Injected by the documentation to work in an iframe.
+ * Remove this when copying and pasting into your project.
+ */
+ window?: () => Window;
+}
+
+export default function DashboardLayoutBasic(props: DemoProps) {
+ const { window } = props;
+
const [pathname, setPathname] = React.useState('/page');
const router = React.useMemo(() => {
@@ -67,20 +93,16 @@ export default function DashboardLayoutBasic() {
};
}, [pathname]);
+ // Remove this const when copying and pasting into your project.
+ const demoWindow = window !== undefined ? window() : undefined;
+
return (
-
+ // preview-start
+
-
- Dashboard content for {pathname}
-
+
+ // preview-end
);
}
diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.tsx.preview b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.tsx.preview
index 519caf4aa97..512dc89fa09 100644
--- a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.tsx.preview
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.tsx.preview
@@ -1,12 +1,5 @@
-
-
- Dashboard content for {pathname}
-
-
\ No newline at end of file
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.js b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.js
index 92a7020bb29..522fb7c5630 100644
--- a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.js
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.js
@@ -1,4 +1,5 @@
import * as React from 'react';
+import PropTypes from 'prop-types';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import DashboardIcon from '@mui/icons-material/Dashboard';
@@ -19,12 +20,29 @@ const NAVIGATION = [
},
];
-const BRANDING = {
- logo: ,
- title: 'MUI',
+function DemoPageContent({ pathname }) {
+ return (
+
+ Dashboard content for {pathname}
+
+ );
+}
+
+DemoPageContent.propTypes = {
+ pathname: PropTypes.string.isRequired,
};
-export default function DashboardLayoutBranding() {
+function DashboardLayoutBranding(props) {
+ const { window } = props;
+
const [pathname, setPathname] = React.useState('/page');
const router = React.useMemo(() => {
@@ -35,20 +53,34 @@ export default function DashboardLayoutBranding() {
};
}, [pathname]);
+ // Remove this const when copying and pasting into your project.
+ const demoWindow = window !== undefined ? window() : undefined;
+
return (
-
+ // preview-start
+ ,
+ title: 'MUI',
+ }}
+ router={router}
+ window={demoWindow}
+ >
-
- Dashboard content for {pathname}
-
+
+ // preview-end
);
}
+
+DashboardLayoutBranding.propTypes = {
+ /**
+ * Injected by the documentation to work in an iframe.
+ * Remove this when copying and pasting into your project.
+ */
+ window: PropTypes.func,
+};
+
+export default DashboardLayoutBranding;
diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.tsx b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.tsx
index b2561a7d9e2..0aa5b5bb398 100644
--- a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.tsx
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.tsx
@@ -5,7 +5,7 @@ import DashboardIcon from '@mui/icons-material/Dashboard';
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
import { AppProvider, Router } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import type { Navigation, Branding } from '@toolpad/core';
+import type { Navigation } from '@toolpad/core';
const NAVIGATION: Navigation = [
{
@@ -20,12 +20,33 @@ const NAVIGATION: Navigation = [
},
];
-const BRANDING: Branding = {
- logo: ,
- title: 'MUI',
-};
+function DemoPageContent({ pathname }: { pathname: string }) {
+ return (
+
+ Dashboard content for {pathname}
+
+ );
+}
+
+interface DemoProps {
+ /**
+ * Injected by the documentation to work in an iframe.
+ * Remove this when copying and pasting into your project.
+ */
+ window?: () => Window;
+}
+
+export default function DashboardLayoutBranding(props: DemoProps) {
+ const { window } = props;
-export default function DashboardLayoutBranding() {
const [pathname, setPathname] = React.useState('/page');
const router = React.useMemo(() => {
@@ -36,20 +57,24 @@ export default function DashboardLayoutBranding() {
};
}, [pathname]);
+ // Remove this const when copying and pasting into your project.
+ const demoWindow = window !== undefined ? window() : undefined;
+
return (
-
+ // preview-start
+ ,
+ title: 'MUI',
+ }}
+ router={router}
+ window={demoWindow}
+ >
-
- Dashboard content for {pathname}
-
+
+ // preview-end
);
}
diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.tsx.preview b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.tsx.preview
index 519caf4aa97..f33054ed6a1 100644
--- a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.tsx.preview
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.tsx.preview
@@ -1,12 +1,13 @@
-
-
- Dashboard content for {pathname}
-
-
\ No newline at end of file
+,
+ title: 'MUI',
+ }}
+ router={router}
+ window={demoWindow}
+>
+
+
+
+
\ No newline at end of file
diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.js b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.js
index e46490549d5..acf465c3987 100644
--- a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.js
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.js
@@ -1,4 +1,5 @@
import * as React from 'react';
+import PropTypes from 'prop-types';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import DescriptionIcon from '@mui/icons-material/Description';
@@ -6,148 +7,29 @@ import FolderIcon from '@mui/icons-material/Folder';
import { AppProvider } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-const NAVIGATION = [
- {
- slug: '/home',
- title: 'Home',
- icon: ,
- },
- {
- slug: '/about',
- title: 'About Us',
- icon: ,
- },
- {
- slug: '/movies',
- title: 'Movies',
- icon: ,
- children: [
- {
- slug: '/fantasy',
- title: 'Fantasy',
- icon: ,
- children: [
- {
- kind: 'header',
- title: 'Epic Fantasy',
- },
- {
- slug: '/lord-of-the-rings',
- title: 'Lord of the Rings',
- icon: ,
- },
- {
- slug: '/harry-potter',
- title: 'Harry Potter',
- icon: ,
- },
- { kind: 'divider' },
- {
- kind: 'header',
- title: 'Modern Fantasy',
- },
- {
- slug: '/chronicles-of-narnia',
- title: 'Chronicles of Narnia',
- icon: ,
- },
- ],
- },
- {
- slug: '/action',
- title: 'Action',
- icon: ,
- children: [
- {
- slug: '/mad-max',
- title: 'Mad Max',
- icon: ,
- },
- {
- slug: '/die-hard',
- title: 'Die Hard',
- icon: ,
- },
- ],
- },
- {
- slug: '/sci-fi',
- title: 'Sci-Fi',
- icon: ,
- children: [
- {
- slug: '/star-wars',
- title: 'Star Wars',
- icon: ,
- },
- {
- slug: '/matrix',
- title: 'The Matrix',
- icon: ,
- },
- ],
- },
- ],
- },
- { kind: 'divider' },
- {
- kind: 'header',
- title: 'Animals',
- },
- {
- slug: '/mammals',
- title: 'Mammals',
- icon: ,
- children: [
- {
- slug: '/lion',
- title: 'Lion',
- icon: ,
- },
- {
- slug: '/elephant',
- title: 'Elephant',
- icon: ,
- },
- ],
- },
- {
- slug: '/birds',
- title: 'Birds',
- icon: ,
- children: [
- {
- slug: '/eagle',
- title: 'Eagle',
- icon: ,
- },
- {
- slug: '/parrot',
- title: 'Parrot',
- icon: ,
- },
- ],
- },
- {
- slug: '/reptiles',
- title: 'Reptiles',
- icon: ,
- children: [
- {
- slug: '/crocodile',
- title: 'Crocodile',
- icon: ,
- },
- {
- slug: '/snake',
- title: 'Snake',
- icon: ,
- },
- ],
- },
-];
+function DemoPageContent({ pathname }) {
+ return (
+
+ Dashboard content for {pathname}
+
+ );
+}
+
+DemoPageContent.propTypes = {
+ pathname: PropTypes.string.isRequired,
+};
+
+function DashboardLayoutNavigation(props) {
+ const { window } = props;
-export default function DashboardLayoutNavigation() {
const [pathname, setPathname] = React.useState('/page');
const router = React.useMemo(() => {
@@ -158,20 +40,169 @@ export default function DashboardLayoutNavigation() {
};
}, [pathname]);
+ // Remove this const when copying and pasting into your project.
+ const demoWindow = window !== undefined ? window() : undefined;
+
return (
-
+ // preview-start
+ ,
+ },
+ {
+ slug: '/about',
+ title: 'About Us',
+ icon: ,
+ },
+ {
+ slug: '/movies',
+ title: 'Movies',
+ icon: ,
+ children: [
+ {
+ slug: '/fantasy',
+ title: 'Fantasy',
+ icon: ,
+ children: [
+ {
+ kind: 'header',
+ title: 'Epic Fantasy',
+ },
+ {
+ slug: '/lord-of-the-rings',
+ title: 'Lord of the Rings',
+ icon: ,
+ },
+ {
+ slug: '/harry-potter',
+ title: 'Harry Potter',
+ icon: ,
+ },
+ { kind: 'divider' },
+ {
+ kind: 'header',
+ title: 'Modern Fantasy',
+ },
+ {
+ slug: '/chronicles-of-narnia',
+ title: 'Chronicles of Narnia',
+ icon: ,
+ },
+ ],
+ },
+ {
+ slug: '/action',
+ title: 'Action',
+ icon: ,
+ children: [
+ {
+ slug: '/mad-max',
+ title: 'Mad Max',
+ icon: ,
+ },
+ {
+ slug: '/die-hard',
+ title: 'Die Hard',
+ icon: ,
+ },
+ ],
+ },
+ {
+ slug: '/sci-fi',
+ title: 'Sci-Fi',
+ icon: ,
+ children: [
+ {
+ slug: '/star-wars',
+ title: 'Star Wars',
+ icon: ,
+ },
+ {
+ slug: '/matrix',
+ title: 'The Matrix',
+ icon: ,
+ },
+ ],
+ },
+ ],
+ },
+ { kind: 'divider' },
+ {
+ kind: 'header',
+ title: 'Animals',
+ },
+ {
+ slug: '/mammals',
+ title: 'Mammals',
+ icon: ,
+ children: [
+ {
+ slug: '/lion',
+ title: 'Lion',
+ icon: ,
+ },
+ {
+ slug: '/elephant',
+ title: 'Elephant',
+ icon: ,
+ },
+ ],
+ },
+ {
+ slug: '/birds',
+ title: 'Birds',
+ icon: ,
+ children: [
+ {
+ slug: '/eagle',
+ title: 'Eagle',
+ icon: ,
+ },
+ {
+ slug: '/parrot',
+ title: 'Parrot',
+ icon: ,
+ },
+ ],
+ },
+ {
+ slug: '/reptiles',
+ title: 'Reptiles',
+ icon: ,
+ children: [
+ {
+ slug: '/crocodile',
+ title: 'Crocodile',
+ icon: ,
+ },
+ {
+ slug: '/snake',
+ title: 'Snake',
+ icon: ,
+ },
+ ],
+ },
+ ]}
+ router={router}
+ window={demoWindow}
+ >
-
- Dashboard content for {pathname}
-
+
+ // preview-end
);
}
+
+DashboardLayoutNavigation.propTypes = {
+ /**
+ * Injected by the documentation to work in an iframe.
+ * Remove this when copying and pasting into your project.
+ */
+ window: PropTypes.func,
+};
+
+export default DashboardLayoutNavigation;
diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.tsx b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.tsx
index b1d7dee9e19..829c9d59613 100644
--- a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.tsx
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.tsx
@@ -5,150 +5,34 @@ import DescriptionIcon from '@mui/icons-material/Description';
import FolderIcon from '@mui/icons-material/Folder';
import { AppProvider, Router } from '@toolpad/core/AppProvider';
import { DashboardLayout } from '@toolpad/core/DashboardLayout';
-import type { Navigation } from '@toolpad/core';
-const NAVIGATION: Navigation = [
- {
- slug: '/home',
- title: 'Home',
- icon: ,
- },
- {
- slug: '/about',
- title: 'About Us',
- icon: ,
- },
- {
- slug: '/movies',
- title: 'Movies',
- icon: ,
- children: [
- {
- slug: '/fantasy',
- title: 'Fantasy',
- icon: ,
- children: [
- {
- kind: 'header',
- title: 'Epic Fantasy',
- },
- {
- slug: '/lord-of-the-rings',
- title: 'Lord of the Rings',
- icon: ,
- },
- {
- slug: '/harry-potter',
- title: 'Harry Potter',
- icon: ,
- },
- { kind: 'divider' },
- {
- kind: 'header',
- title: 'Modern Fantasy',
- },
- {
- slug: '/chronicles-of-narnia',
- title: 'Chronicles of Narnia',
- icon: ,
- },
- ],
- },
- {
- slug: '/action',
- title: 'Action',
- icon: ,
- children: [
- {
- slug: '/mad-max',
- title: 'Mad Max',
- icon: ,
- },
- {
- slug: '/die-hard',
- title: 'Die Hard',
- icon: ,
- },
- ],
- },
- {
- slug: '/sci-fi',
- title: 'Sci-Fi',
- icon: ,
- children: [
- {
- slug: '/star-wars',
- title: 'Star Wars',
- icon: ,
- },
- {
- slug: '/matrix',
- title: 'The Matrix',
- icon: ,
- },
- ],
- },
- ],
- },
- { kind: 'divider' },
- {
- kind: 'header',
- title: 'Animals',
- },
- {
- slug: '/mammals',
- title: 'Mammals',
- icon: ,
- children: [
- {
- slug: '/lion',
- title: 'Lion',
- icon: ,
- },
- {
- slug: '/elephant',
- title: 'Elephant',
- icon: ,
- },
- ],
- },
- {
- slug: '/birds',
- title: 'Birds',
- icon: ,
- children: [
- {
- slug: '/eagle',
- title: 'Eagle',
- icon: ,
- },
- {
- slug: '/parrot',
- title: 'Parrot',
- icon: ,
- },
- ],
- },
- {
- slug: '/reptiles',
- title: 'Reptiles',
- icon: ,
- children: [
- {
- slug: '/crocodile',
- title: 'Crocodile',
- icon: ,
- },
- {
- slug: '/snake',
- title: 'Snake',
- icon: ,
- },
- ],
- },
-];
+function DemoPageContent({ pathname }: { pathname: string }) {
+ return (
+
+ Dashboard content for {pathname}
+
+ );
+}
+
+interface DemoProps {
+ /**
+ * Injected by the documentation to work in an iframe.
+ * Remove this when copying and pasting into your project.
+ */
+ window?: () => Window;
+}
+
+export default function DashboardLayoutNavigation(props: DemoProps) {
+ const { window } = props;
-export default function DashboardLayoutNavigation() {
const [pathname, setPathname] = React.useState('/page');
const router = React.useMemo(() => {
@@ -159,20 +43,159 @@ export default function DashboardLayoutNavigation() {
};
}, [pathname]);
+ // Remove this const when copying and pasting into your project.
+ const demoWindow = window !== undefined ? window() : undefined;
+
return (
-
+ // preview-start
+ ,
+ },
+ {
+ slug: '/about',
+ title: 'About Us',
+ icon: ,
+ },
+ {
+ slug: '/movies',
+ title: 'Movies',
+ icon: ,
+ children: [
+ {
+ slug: '/fantasy',
+ title: 'Fantasy',
+ icon: ,
+ children: [
+ {
+ kind: 'header',
+ title: 'Epic Fantasy',
+ },
+ {
+ slug: '/lord-of-the-rings',
+ title: 'Lord of the Rings',
+ icon: ,
+ },
+ {
+ slug: '/harry-potter',
+ title: 'Harry Potter',
+ icon: ,
+ },
+ { kind: 'divider' },
+ {
+ kind: 'header',
+ title: 'Modern Fantasy',
+ },
+ {
+ slug: '/chronicles-of-narnia',
+ title: 'Chronicles of Narnia',
+ icon: ,
+ },
+ ],
+ },
+ {
+ slug: '/action',
+ title: 'Action',
+ icon: ,
+ children: [
+ {
+ slug: '/mad-max',
+ title: 'Mad Max',
+ icon: ,
+ },
+ {
+ slug: '/die-hard',
+ title: 'Die Hard',
+ icon: ,
+ },
+ ],
+ },
+ {
+ slug: '/sci-fi',
+ title: 'Sci-Fi',
+ icon: ,
+ children: [
+ {
+ slug: '/star-wars',
+ title: 'Star Wars',
+ icon: ,
+ },
+ {
+ slug: '/matrix',
+ title: 'The Matrix',
+ icon: ,
+ },
+ ],
+ },
+ ],
+ },
+ { kind: 'divider' },
+ {
+ kind: 'header',
+ title: 'Animals',
+ },
+ {
+ slug: '/mammals',
+ title: 'Mammals',
+ icon: ,
+ children: [
+ {
+ slug: '/lion',
+ title: 'Lion',
+ icon: ,
+ },
+ {
+ slug: '/elephant',
+ title: 'Elephant',
+ icon: ,
+ },
+ ],
+ },
+ {
+ slug: '/birds',
+ title: 'Birds',
+ icon: ,
+ children: [
+ {
+ slug: '/eagle',
+ title: 'Eagle',
+ icon: ,
+ },
+ {
+ slug: '/parrot',
+ title: 'Parrot',
+ icon: ,
+ },
+ ],
+ },
+ {
+ slug: '/reptiles',
+ title: 'Reptiles',
+ icon: ,
+ children: [
+ {
+ slug: '/crocodile',
+ title: 'Crocodile',
+ icon: ,
+ },
+ {
+ slug: '/snake',
+ title: 'Snake',
+ icon: ,
+ },
+ ],
+ },
+ ]}
+ router={router}
+ window={demoWindow}
+ >
-
- Dashboard content for {pathname}
-
+
+ // preview-end
);
}
diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.tsx.preview b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.tsx.preview
index 519caf4aa97..b45308001d4 100644
--- a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.tsx.preview
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.tsx.preview
@@ -1,12 +1,148 @@
-
-
- Dashboard content for {pathname}
-
-
\ No newline at end of file
+,
+ },
+ {
+ slug: '/about',
+ title: 'About Us',
+ icon: ,
+ },
+ {
+ slug: '/movies',
+ title: 'Movies',
+ icon: ,
+ children: [
+ {
+ slug: '/fantasy',
+ title: 'Fantasy',
+ icon: ,
+ children: [
+ {
+ kind: 'header',
+ title: 'Epic Fantasy',
+ },
+ {
+ slug: '/lord-of-the-rings',
+ title: 'Lord of the Rings',
+ icon: ,
+ },
+ {
+ slug: '/harry-potter',
+ title: 'Harry Potter',
+ icon: ,
+ },
+ { kind: 'divider' },
+ {
+ kind: 'header',
+ title: 'Modern Fantasy',
+ },
+ {
+ slug: '/chronicles-of-narnia',
+ title: 'Chronicles of Narnia',
+ icon: ,
+ },
+ ],
+ },
+ {
+ slug: '/action',
+ title: 'Action',
+ icon: ,
+ children: [
+ {
+ slug: '/mad-max',
+ title: 'Mad Max',
+ icon: ,
+ },
+ {
+ slug: '/die-hard',
+ title: 'Die Hard',
+ icon: ,
+ },
+ ],
+ },
+ {
+ slug: '/sci-fi',
+ title: 'Sci-Fi',
+ icon: ,
+ children: [
+ {
+ slug: '/star-wars',
+ title: 'Star Wars',
+ icon: ,
+ },
+ {
+ slug: '/matrix',
+ title: 'The Matrix',
+ icon: ,
+ },
+ ],
+ },
+ ],
+ },
+ { kind: 'divider' },
+ {
+ kind: 'header',
+ title: 'Animals',
+ },
+ {
+ slug: '/mammals',
+ title: 'Mammals',
+ icon: ,
+ children: [
+ {
+ slug: '/lion',
+ title: 'Lion',
+ icon: ,
+ },
+ {
+ slug: '/elephant',
+ title: 'Elephant',
+ icon: ,
+ },
+ ],
+ },
+ {
+ slug: '/birds',
+ title: 'Birds',
+ icon: ,
+ children: [
+ {
+ slug: '/eagle',
+ title: 'Eagle',
+ icon: ,
+ },
+ {
+ slug: '/parrot',
+ title: 'Parrot',
+ icon: ,
+ },
+ ],
+ },
+ {
+ slug: '/reptiles',
+ title: 'Reptiles',
+ icon: ,
+ children: [
+ {
+ slug: '/crocodile',
+ title: 'Crocodile',
+ icon: ,
+ },
+ {
+ slug: '/snake',
+ title: 'Snake',
+ icon: ,
+ },
+ ],
+ },
+ ]}
+ router={router}
+ window={demoWindow}
+>
+
+
+
+
\ No newline at end of file
diff --git a/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md b/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md
index af87d97cac4..57481961674 100644
--- a/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md
+++ b/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md
@@ -8,22 +8,35 @@ components: AppProvider, DashboardLayout
The dashboard layout component provides a customizable out-of-the-box layout for a typical dashboard page.
+The `DashboardLayout` component is a quick, easy way to provide a standard full-screen layout with a header and sidebar to any dashboard page, as well as ready-to-use and easy to customize navigation and branding.
+
+Many features of this component are configurable through the [AppProvider](https://mui.com/toolpad/core/react-app-provider/) component that should wrap it.
+
## Demo
-A `DashboardLayout` has a configurable header and sidebar with navigation.
+A `DashboardLayout` brings in a header and sidebar with navigation, as well as a scrollable area for page content.
-{{"demo": "DashboardLayoutBasic.js", "height": 500, "iframe": true}}
+If application [themes](https://mui.com/toolpad/core/react-app-provider/#theming) are defined for both light and dark mode, a theme switcher in the header allows for easy switching between the two modes.
-Some features of this layout depend on the [AppProvider](https://mui.com/toolpad/core/react-app-provider/) component that must be present at the base application level.
+{{"demo": "DashboardLayoutBasic.js", "height": 500, "iframe": true}}
## Branding
-The `branding` prop in the [AppProvider](https://mui.com/toolpad/core/react-app-provider/) allows for setting a `logo` or `title` in the page header.
+Some elements of the `DashboardLayout` can be configured to match your personalized brand.
+
+This can be done via the `branding` prop in the [AppProvider](https://mui.com/toolpad/core/react-app-provider/), which allows for setting a custom `logo` image or `title` text in the page header.
{{"demo": "DashboardLayoutBranding.js", "height": 500, "iframe": true}}
## Navigation
-The `navigation` prop in the [AppProvider](https://mui.com/toolpad/core/react-app-provider/) allows for setting any type of navigation structure in the sidebar, such as links, headings, nested collapsible lists and dividers, in any order.
+The `navigation` prop in the [AppProvider](https://mui.com/toolpad/core/react-app-provider/) allows for setting any type of navigation structure in the `DashboardLayout` sidebar by including different navigation elements as building blocks in any order, such as:
+
+- links `{ slug: '/home', title: 'Home', icon: }`;
+- headings `{ kind: 'header', title: 'Epic Fantasy' }`;
+- dividers `{ kind: 'divider' }`;
+- collapsible nested navigation `{ title: 'Fantasy', icon: , children: [ ... ] }`.
+
+The flexibility in composing and ordering these different elements allows for a great variety of navigation structures to fit your use case.
{{"demo": "DashboardLayoutNavigation.js", "height": 640, "iframe": true}}
diff --git a/docs/data/toolpad/core/introduction/TutorialDefault.js b/docs/data/toolpad/core/introduction/TutorialDefault.js
index 5a90f2cb218..f825efce79f 100644
--- a/docs/data/toolpad/core/introduction/TutorialDefault.js
+++ b/docs/data/toolpad/core/introduction/TutorialDefault.js
@@ -1,4 +1,5 @@
import * as React from 'react';
+import PropTypes from 'prop-types';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import DashboardIcon from '@mui/icons-material/Dashboard';
@@ -12,21 +13,43 @@ const NAVIGATION = [
},
];
-export default function TutorialDefault() {
+function DemoPageContent() {
return (
-
+
+ Dashboard content
+
+ );
+}
+
+function TutorialDefault(props) {
+ const { window } = props;
+
+ // Remove this const when copying and pasting into your project.
+ const demoWindow = window !== undefined ? window() : undefined;
+
+ return (
+
-
- Dashboard content
-
+
);
}
+
+TutorialDefault.propTypes = {
+ /**
+ * Injected by the documentation to work in an iframe.
+ * Remove this when copying and pasting into your project.
+ */
+ window: PropTypes.func,
+};
+
+export default TutorialDefault;
diff --git a/docs/data/toolpad/core/introduction/TutorialDefault.tsx b/docs/data/toolpad/core/introduction/TutorialDefault.tsx
index ae7e266274b..e9eb39d63f1 100644
--- a/docs/data/toolpad/core/introduction/TutorialDefault.tsx
+++ b/docs/data/toolpad/core/introduction/TutorialDefault.tsx
@@ -12,20 +12,40 @@ const NAVIGATION: Navigation = [
},
];
-export default function TutorialDefault() {
+function DemoPageContent() {
return (
-
+
+ Dashboard content
+
+ );
+}
+
+interface DemoProps {
+ /**
+ * Injected by the documentation to work in an iframe.
+ * Remove this when copying and pasting into your project.
+ */
+ window?: () => Window;
+}
+
+export default function TutorialDefault(props: DemoProps) {
+ const { window } = props;
+
+ // Remove this const when copying and pasting into your project.
+ const demoWindow = window !== undefined ? window() : undefined;
+
+ return (
+
-
- Dashboard content
-
+
);
diff --git a/docs/data/toolpad/core/introduction/TutorialDefault.tsx.preview b/docs/data/toolpad/core/introduction/TutorialDefault.tsx.preview
index 73cc7e927f4..3e244843ec9 100644
--- a/docs/data/toolpad/core/introduction/TutorialDefault.tsx.preview
+++ b/docs/data/toolpad/core/introduction/TutorialDefault.tsx.preview
@@ -1,12 +1,3 @@
-
- Dashboard content
-
+
\ No newline at end of file
diff --git a/docs/data/toolpad/core/introduction/TutorialPages.js b/docs/data/toolpad/core/introduction/TutorialPages.js
index 94f6e7330d7..163b82a75f6 100644
--- a/docs/data/toolpad/core/introduction/TutorialPages.js
+++ b/docs/data/toolpad/core/introduction/TutorialPages.js
@@ -1,4 +1,5 @@
import * as React from 'react';
+import PropTypes from 'prop-types';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import DashboardIcon from '@mui/icons-material/Dashboard';
@@ -24,7 +25,32 @@ const NAVIGATION = [
},
];
-export default function TutorialPages() {
+function DemoPageContent({ pathname }) {
+ return (
+
+ Dashboard content for {pathname}
+
+ );
+}
+
+DemoPageContent.propTypes = {
+ pathname: PropTypes.string.isRequired,
+};
+
+function TutorialPages(props) {
+ const { window } = props;
+
+ // Remove this const when copying and pasting into your project.
+ const demoWindow = window !== undefined ? window() : undefined;
+
const [pathname, setPathname] = React.useState('/page');
const router = React.useMemo(() => {
@@ -36,19 +62,20 @@ export default function TutorialPages() {
}, [pathname]);
return (
-
+
-
- Dashboard content for {pathname}
-
+
);
}
+
+TutorialPages.propTypes = {
+ /**
+ * Injected by the documentation to work in an iframe.
+ * Remove this when copying and pasting into your project.
+ */
+ window: PropTypes.func,
+};
+
+export default TutorialPages;
diff --git a/docs/data/toolpad/core/introduction/TutorialPages.tsx b/docs/data/toolpad/core/introduction/TutorialPages.tsx
index 0c2edcec8ff..dff3830a32c 100644
--- a/docs/data/toolpad/core/introduction/TutorialPages.tsx
+++ b/docs/data/toolpad/core/introduction/TutorialPages.tsx
@@ -24,7 +24,36 @@ const NAVIGATION: Navigation = [
},
];
-export default function TutorialPages() {
+function DemoPageContent({ pathname }: { pathname: string }) {
+ return (
+
+ Dashboard content for {pathname}
+
+ );
+}
+
+interface DemoProps {
+ /**
+ * Injected by the documentation to work in an iframe.
+ * Remove this when copying and pasting into your project.
+ */
+ window?: () => Window;
+}
+
+export default function TutorialPages(props: DemoProps) {
+ const { window } = props;
+
+ // Remove this const when copying and pasting into your project.
+ const demoWindow = window !== undefined ? window() : undefined;
+
const [pathname, setPathname] = React.useState('/page');
const router = React.useMemo(() => {
@@ -36,18 +65,9 @@ export default function TutorialPages() {
}, [pathname]);
return (
-
+
-
- Dashboard content for {pathname}
-
+
);
diff --git a/docs/data/toolpad/core/introduction/TutorialPages.tsx.preview b/docs/data/toolpad/core/introduction/TutorialPages.tsx.preview
index 519caf4aa97..96d68868d23 100644
--- a/docs/data/toolpad/core/introduction/TutorialPages.tsx.preview
+++ b/docs/data/toolpad/core/introduction/TutorialPages.tsx.preview
@@ -1,12 +1,3 @@
-
- Dashboard content for {pathname}
-
+
\ No newline at end of file
diff --git a/docs/pages/toolpad/core/api/app-provider.json b/docs/pages/toolpad/core/api/app-provider.json
index d4ce89b715a..9ccfcd43894 100644
--- a/docs/pages/toolpad/core/api/app-provider.json
+++ b/docs/pages/toolpad/core/api/app-provider.json
@@ -8,7 +8,7 @@
"navigation": {
"type": {
"name": "arrayOf",
- "description": "Array<{ children?: Array<object
| { kind: 'header', title: string }
| { kind: 'divider' }>, icon?: node, kind?: 'page', slug?: string, title: string }
| { kind: 'header', title: string }
| { kind: 'divider' }>"
+ "description": "Array<{ children?: Array<object
| { kind: 'header', title: string }
| { kind: 'divider' }>, icon?: node, kind?: 'page', slug?: string, title?: string }
| { kind: 'header', title: string }
| { kind: 'divider' }>"
},
"default": "[]"
},
@@ -19,7 +19,8 @@
},
"default": "null"
},
- "theme": { "type": { "name": "object" }, "default": "baseTheme" }
+ "theme": { "type": { "name": "object" }, "default": "extendTheme()" },
+ "window": { "type": { "name": "object" }, "default": "window" }
},
"name": "AppProvider",
"imports": [
diff --git a/docs/translations/api-docs/app-provider/app-provider.json b/docs/translations/api-docs/app-provider/app-provider.json
index c9a8d257b26..b34f90d9ef4 100644
--- a/docs/translations/api-docs/app-provider/app-provider.json
+++ b/docs/translations/api-docs/app-provider/app-provider.json
@@ -6,7 +6,10 @@
"navigation": { "description": "Navigation definition for the app." },
"router": { "description": "Router implementation used inside Toolpad components." },
"theme": {
- "description": "Theme used by the app."
+ "description": "Theme or themes to be used by the app in light/dark mode. A CSS variables theme is recommended."
+ },
+ "window": {
+ "description": "The window where the application is rendered. This is needed when rendering the app inside an iframe, for example."
}
},
"classDescriptions": {}
diff --git a/examples/core-tutorial/package.json b/examples/core-tutorial/package.json
index 4bc46204142..41f9fca3715 100644
--- a/examples/core-tutorial/package.json
+++ b/examples/core-tutorial/package.json
@@ -12,8 +12,8 @@
"react-dom": "^18.3.1",
"next": "^14.2.4",
"@toolpad/core": "latest",
- "@mui/material": "^5.15.21",
- "@mui/icons-material": "^5.15.21",
+ "@mui/material": "^6.0.0-beta.2",
+ "@mui/icons-material": "^6.0.0-beta.2",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@emotion/cache": "^11.11.0"
diff --git a/examples/core-tutorial/theme.ts b/examples/core-tutorial/theme.ts
index 354fcbdd88f..f00bd8d5007 100644
--- a/examples/core-tutorial/theme.ts
+++ b/examples/core-tutorial/theme.ts
@@ -1,111 +1,26 @@
'use client';
-import { createTheme } from '@mui/material/styles';
+import { extendTheme } from '@mui/material/styles';
+import type {} from '@mui/material/themeCssVarsAugmentation';
-const defaultTheme = createTheme();
-
-const theme = createTheme(defaultTheme, {
- palette: {
- background: {
- default: defaultTheme.palette.grey['50'],
- },
- },
- typography: {
- h6: {
- fontWeight: '700',
- },
- },
- components: {
- MuiAppBar: {
- styleOverrides: {
- root: {
- borderWidth: 0,
- borderBottomWidth: 1,
- borderStyle: 'solid',
- borderColor: defaultTheme.palette.divider,
- boxShadow: 'none',
+const theme = extendTheme({
+ colorSchemes: {
+ light: {
+ palette: {
+ background: {
+ default: 'var(--mui-palette-grey-50)',
+ defaultChannel: 'var(--mui-palette-grey-50)',
},
},
},
- MuiList: {
- styleOverrides: {
- root: {
- padding: 0,
+ dark: {
+ palette: {
+ background: {
+ default: 'var(--mui-palette-grey-900)',
+ defaultChannel: 'var(--mui-palette-grey-900)',
},
- },
- },
- MuiIconButton: {
- styleOverrides: {
- root: {
- color: defaultTheme.palette.primary.dark,
- padding: 8,
- },
- },
- },
- MuiListSubheader: {
- styleOverrides: {
- root: {
- color: defaultTheme.palette.grey['600'],
- fontSize: 12,
- fontWeight: '700',
- height: 40,
- paddingLeft: 32,
- },
- },
- },
- MuiListItem: {
- styleOverrides: {
- root: {
- paddingTop: 0,
- paddingBottom: 0,
- },
- },
- },
- MuiListItemButton: {
- styleOverrides: {
- root: {
- borderRadius: 8,
- '&.Mui-selected': {
- '& .MuiListItemIcon-root': {
- color: defaultTheme.palette.primary.dark,
- },
- '& .MuiTypography-root': {
- color: defaultTheme.palette.primary.dark,
- },
- '& .MuiSvgIcon-root': {
- color: defaultTheme.palette.primary.dark,
- },
- '& .MuiTouchRipple-child': {
- backgroundColor: defaultTheme.palette.primary.dark,
- },
- },
- '& .MuiSvgIcon-root': {
- color: defaultTheme.palette.action.active,
- },
- },
- },
- },
- MuiListItemText: {
- styleOverrides: {
- root: {
- '& .MuiTypography-root': {
- fontWeight: '500',
- },
- },
- },
- },
- MuiListItemIcon: {
- styleOverrides: {
- root: {
- minWidth: 34,
- },
- },
- },
- MuiDivider: {
- styleOverrides: {
- root: {
- borderBottomWidth: 2,
- marginLeft: '16px',
- marginRight: '16px',
+ text: {
+ primary: 'var(--mui-palette-grey-200)',
+ primaryChannel: 'var(--mui-palette-grey-200)',
},
},
},
diff --git a/package.json b/package.json
index c3526d3ba39..89d90ef182b 100644
--- a/package.json
+++ b/package.json
@@ -59,6 +59,7 @@
"@mui/x-charts": "7.11.0",
"@next/eslint-plugin-next": "14.2.5",
"@playwright/test": "1.45.1",
+ "@testing-library/jest-dom": "^6.4.6",
"@testing-library/react": "16.0.0",
"@testing-library/user-event": "14.5.2",
"@types/archiver": "6.0.2",
diff --git a/packages/create-toolpad-app/src/generateProject.ts b/packages/create-toolpad-app/src/generateProject.ts
index a06fe9ac40d..ab77f3b9a65 100644
--- a/packages/create-toolpad-app/src/generateProject.ts
+++ b/packages/create-toolpad-app/src/generateProject.ts
@@ -29,7 +29,7 @@ export default function generateProject(
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
-
+
@@ -103,113 +103,28 @@ export default function generateProject(
const themeContent = `
"use client";
- import { createTheme } from "@mui/material/styles";
+ import { extendTheme } from '@mui/material/styles';
+ import type {} from '@mui/material/themeCssVarsAugmentation';
- const defaultTheme = createTheme();
-
- const theme = createTheme(defaultTheme, {
- palette: {
- background: {
- default: defaultTheme.palette.grey['50'],
- },
- },
- typography: {
- h6: {
- fontWeight: '700',
- },
- },
- components: {
- MuiAppBar: {
- styleOverrides: {
- root: {
- borderWidth: 0,
- borderBottomWidth: 1,
- borderStyle: 'solid',
- borderColor: defaultTheme.palette.divider,
- boxShadow: 'none',
+ const theme = extendTheme({
+ colorSchemes: {
+ light: {
+ palette: {
+ background: {
+ default: 'var(--mui-palette-grey-50)',
+ defaultChannel: 'var(--mui-palette-grey-50)',
},
},
},
- MuiList: {
- styleOverrides: {
- root: {
- padding: 0,
+ dark: {
+ palette: {
+ background: {
+ default: 'var(--mui-palette-grey-900)',
+ defaultChannel: 'var(--mui-palette-grey-900)',
},
- },
- },
- MuiIconButton: {
- styleOverrides: {
- root: {
- color: defaultTheme.palette.primary.dark,
- padding: 8,
- },
- },
- },
- MuiListSubheader: {
- styleOverrides: {
- root: {
- color: defaultTheme.palette.grey['600'],
- fontSize: 12,
- fontWeight: '700',
- height: 40,
- paddingLeft: 32,
- },
- },
- },
- MuiListItem: {
- styleOverrides: {
- root: {
- paddingTop: 0,
- paddingBottom: 0,
- },
- },
- },
- MuiListItemButton: {
- styleOverrides: {
- root: {
- borderRadius: 8,
- '&.Mui-selected': {
- '& .MuiListItemIcon-root': {
- color: defaultTheme.palette.primary.dark,
- },
- '& .MuiTypography-root': {
- color: defaultTheme.palette.primary.dark,
- },
- '& .MuiSvgIcon-root': {
- color: defaultTheme.palette.primary.dark,
- },
- '& .MuiTouchRipple-child': {
- backgroundColor: defaultTheme.palette.primary.dark,
- },
- },
- '& .MuiSvgIcon-root': {
- color: defaultTheme.palette.action.active,
- },
- },
- },
- },
- MuiListItemText: {
- styleOverrides: {
- root: {
- '& .MuiTypography-root': {
- fontWeight: '500',
- },
- },
- },
- },
- MuiListItemIcon: {
- styleOverrides: {
- root: {
- minWidth: 34,
- },
- },
- },
- MuiDivider: {
- styleOverrides: {
- root: {
- borderBottomWidth: 2,
- marginLeft: '16px',
- marginRight: '16px',
+ text: {
+ primary: 'var(--mui-palette-grey-200)',
+ primaryChannel: 'var(--mui-palette-grey-200)',
},
},
},
@@ -279,9 +194,9 @@ export default function generateProject(
'react-dom': '^18',
next: '^14',
'@toolpad/core': 'latest',
- '@mui/material': '^5',
+ '@mui/material': '^6',
'@mui/material-nextjs': '^5',
- '@mui/icons-material': '^5',
+ '@mui/icons-material': '^6',
'@emotion/react': '^11',
'@emotion/styled': '^11',
'@emotion/cache': '^11',
diff --git a/packages/toolpad-core/package.json b/packages/toolpad-core/package.json
index 5565248454b..ff8bf0e4588 100644
--- a/packages/toolpad-core/package.json
+++ b/packages/toolpad-core/package.json
@@ -60,8 +60,8 @@
"prop-types": "15.8.1"
},
"devDependencies": {
- "@mui/icons-material": "5.16.4",
- "@mui/material": "5.16.4",
+ "@mui/icons-material": "v6.0.0-beta.2",
+ "@mui/material": "v6.0.0-beta.2",
"@types/invariant": "2.2.37",
"@types/prop-types": "15.7.12",
"@types/react": "18.3.3",
@@ -75,8 +75,8 @@
"vitest": "beta"
},
"peerDependencies": {
- "@mui/icons-material": "^5",
- "@mui/material": "^5",
+ "@mui/icons-material": "^6",
+ "@mui/material": "^6",
"next": "^14",
"react": "^18"
},
diff --git a/packages/toolpad-core/src/AppProvider/AppProvider.tsx b/packages/toolpad-core/src/AppProvider/AppProvider.tsx
index 3a12193605d..4492e72850f 100644
--- a/packages/toolpad-core/src/AppProvider/AppProvider.tsx
+++ b/packages/toolpad-core/src/AppProvider/AppProvider.tsx
@@ -1,11 +1,11 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
-import { ThemeProvider, Theme } from '@mui/material/styles';
-import CssBaseline from '@mui/material/CssBaseline';
-import { baseTheme } from '../themes';
+import { extendTheme, CssVarsTheme, Theme } from '@mui/material/styles';
import { NotificationsProvider } from '../useNotifications';
import { DialogsProvider } from '../useDialogs';
+import { BrandingContext, NavigationContext, RouterContext } from '../shared/context';
+import { AppThemeProvider } from './AppThemeProvider';
export interface NavigateOptions {
history?: 'auto' | 'push' | 'replace';
@@ -31,7 +31,7 @@ export interface Branding {
export interface NavigationPageItem {
kind?: 'page';
- title: string;
+ title?: string;
slug?: string;
icon?: React.ReactNode;
children?: Navigation;
@@ -50,23 +50,16 @@ export type NavigationItem = NavigationPageItem | NavigationSubheaderItem | Navi
export type Navigation = NavigationItem[];
-// TODO: hide these contexts from public API
-export const BrandingContext = React.createContext(null);
-
-export const NavigationContext = React.createContext([]);
-
-export const RouterContext = React.createContext(null);
-
export interface AppProviderProps {
/**
* The content of the app provider.
*/
children: React.ReactNode;
/**
- * [Theme](https://mui.com/material-ui/customization/theming/) used by the app.
- * @default baseTheme
+ * [Theme or themes](https://mui.com/toolpad/core/react-app-provider/#theming) to be used by the app in light/dark mode. A [CSS variables theme](https://mui.com/material-ui/experimental-api/css-theme-variables/overview/) is recommended.
+ * @default extendTheme()
*/
- theme?: Theme;
+ theme?: Theme | { light: Theme; dark: Theme } | CssVarsTheme;
/**
* Branding options for the app.
* @default null
@@ -77,12 +70,17 @@ export interface AppProviderProps {
* @default []
*/
navigation?: Navigation;
-
/**
* Router implementation used inside Toolpad components.
* @default null
*/
router?: Router;
+ /**
+ * The window where the application is rendered.
+ * This is needed when rendering the app inside an iframe, for example.
+ * @default window
+ */
+ window?: Window;
}
/**
@@ -97,12 +95,18 @@ export interface AppProviderProps {
* - [AppProvider API](https://mui.com/toolpad/core/api/app-provider)
*/
function AppProvider(props: AppProviderProps) {
- const { children, theme = baseTheme, branding = null, navigation = [], router = null } = props;
+ const {
+ children,
+ theme = extendTheme(),
+ branding = null,
+ navigation = [],
+ router = null,
+ window,
+ } = props;
return (
-
-
+
@@ -110,7 +114,7 @@ function AppProvider(props: AppProviderProps) {
-
+
);
}
@@ -154,7 +158,7 @@ AppProvider.propTypes /* remove-proptypes */ = {
icon: PropTypes.node,
kind: PropTypes.oneOf(['page']),
slug: PropTypes.string,
- title: PropTypes.string.isRequired,
+ title: PropTypes.string,
}),
PropTypes.shape({
kind: PropTypes.oneOf(['header']).isRequired,
@@ -175,10 +179,16 @@ AppProvider.propTypes /* remove-proptypes */ = {
searchParams: PropTypes.instanceOf(URLSearchParams),
}),
/**
- * [Theme](https://mui.com/material-ui/customization/theming/) used by the app.
- * @default baseTheme
+ * [Theme or themes](https://mui.com/toolpad/core/react-app-provider/#theming) to be used by the app in light/dark mode. A [CSS variables theme](https://mui.com/material-ui/experimental-api/css-theme-variables/overview/) is recommended.
+ * @default extendTheme()
*/
theme: PropTypes.object,
+ /**
+ * The window where the application is rendered.
+ * This is needed when rendering the app inside an iframe, for example.
+ * @default window
+ */
+ window: PropTypes.object,
} as any;
export { AppProvider };
diff --git a/packages/toolpad-core/src/AppProvider/AppThemeProvider.tsx b/packages/toolpad-core/src/AppProvider/AppThemeProvider.tsx
new file mode 100644
index 00000000000..841ca8a428d
--- /dev/null
+++ b/packages/toolpad-core/src/AppProvider/AppThemeProvider.tsx
@@ -0,0 +1,161 @@
+import * as React from 'react';
+import { PaletteMode, Theme, useMediaQuery } from '@mui/material';
+import { CssVarsProvider, ThemeProvider, useColorScheme, CssVarsTheme } from '@mui/material/styles';
+import InitColorSchemeScript from '@mui/material/InitColorSchemeScript';
+import CssBaseline from '@mui/material/CssBaseline';
+import { useLocalStorageState } from '../useLocalStorageState';
+import { PaletteModeContext } from '../shared/context';
+import type { AppProviderProps } from './AppProvider';
+
+function usePreferredMode() {
+ const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
+ return prefersDarkMode ? 'dark' : 'light';
+}
+
+type ThemeMode = PaletteMode | 'system';
+
+function useThemeMode() {
+ const [themeMode, setThemeMode] = useLocalStorageState(
+ 'toolpad-palette-mode',
+ 'system',
+ );
+ return { themeMode, setThemeMode };
+}
+
+function useStandardPaletteMode() {
+ const preferredMode = usePreferredMode();
+ const { themeMode, setThemeMode } = useThemeMode();
+
+ return {
+ paletteMode: !themeMode || themeMode === 'system' ? preferredMode : themeMode,
+ setPaletteMode: setThemeMode,
+ };
+}
+
+interface StandardThemeProviderProps {
+ children: React.ReactNode;
+ theme: NonNullable;
+}
+
+/**
+ * @ignore - internal component.
+ */
+function StandardThemeProvider(props: StandardThemeProviderProps) {
+ const { children, theme } = props;
+
+ const { paletteMode, setPaletteMode } = useStandardPaletteMode();
+
+ const isDualTheme = 'light' in theme || 'dark' in theme;
+
+ const dualAwareTheme = React.useMemo(
+ () =>
+ isDualTheme
+ ? theme[paletteMode === 'dark' ? 'dark' : 'light'] ??
+ theme[paletteMode === 'dark' ? 'light' : 'dark']
+ : theme,
+ [isDualTheme, paletteMode, theme],
+ );
+
+ const paletteModeContextValue = React.useMemo(
+ () => ({ paletteMode, setPaletteMode, isDualTheme }),
+ [isDualTheme, paletteMode, setPaletteMode],
+ );
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+interface CSSVarsThemeConsumerProps {
+ children: React.ReactNode;
+ isDualTheme: boolean;
+}
+
+/**
+ * @ignore - internal component.
+ */
+function CSSVarsThemeConsumer(props: CSSVarsThemeConsumerProps) {
+ const { children, isDualTheme } = props;
+
+ const preferredMode = usePreferredMode();
+ const { mode, setMode } = useColorScheme();
+
+ const paletteModeContextValue = React.useMemo(() => {
+ return {
+ paletteMode: !mode || mode === 'system' ? preferredMode : mode,
+ setPaletteMode: setMode,
+ isDualTheme,
+ };
+ }, [isDualTheme, mode, preferredMode, setMode]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+interface CSSVarsThemeProviderProps {
+ children: React.ReactNode;
+ theme: NonNullable;
+ window?: AppProviderProps['window'];
+}
+
+/**
+ * @ignore - internal component.
+ */
+function CSSVarsThemeProvider(props: CSSVarsThemeProviderProps) {
+ const { children, theme, window } = props;
+
+ const isDualTheme = 'light' in theme.colorSchemes && 'dark' in theme.colorSchemes;
+
+ return (
+
+ {children}
+
+ );
+}
+
+interface AppThemeProviderProps {
+ children: React.ReactNode;
+ theme: NonNullable;
+ window?: AppProviderProps['window'];
+}
+
+/**
+ * @ignore - internal component.
+ */
+function AppThemeProvider(props: AppThemeProviderProps) {
+ const { children, theme, window } = props;
+
+ const isCSSVarsTheme = 'colorSchemes' in theme;
+
+ const themeChildren = (
+
+
+ {children}
+
+ );
+
+ return isCSSVarsTheme ? (
+
+
+
+ {themeChildren}
+
+
+ ) : (
+ {themeChildren}
+ );
+}
+
+export { AppThemeProvider };
diff --git a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx
index e0c73bae3db..e807b2474e4 100644
--- a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx
+++ b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx
@@ -11,8 +11,9 @@ import BarChartIcon from '@mui/icons-material/BarChart';
import DescriptionIcon from '@mui/icons-material/Description';
import LayersIcon from '@mui/icons-material/Layers';
import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom/vitest';
+import { AppProvider, Navigation } from '../AppProvider';
import { DashboardLayout } from './DashboardLayout';
-import { BrandingContext, Navigation, NavigationContext } from '../AppProvider';
describe('DashboardLayout', () => {
test('renders content correctly', async () => {
@@ -28,9 +29,9 @@ describe('DashboardLayout', () => {
};
render(
-
+
Hello world
- ,
+ ,
);
const header = screen.getByRole('banner');
@@ -39,6 +40,34 @@ describe('DashboardLayout', () => {
expect(within(header).getByAltText('Placeholder Logo')).toBeTruthy();
});
+ test('can switch theme', async () => {
+ const user = userEvent.setup();
+ render(
+
+ Hello world
+ ,
+ );
+
+ const getBackgroundColorCSSVariable = () =>
+ getComputedStyle(document.documentElement).getPropertyValue(
+ '--mui-palette-common-background',
+ );
+
+ const header = screen.getByRole('banner');
+
+ const themeSwitcherButton = within(header).getByLabelText('Switch to dark mode');
+
+ expect(getBackgroundColorCSSVariable()).toBe('#fff');
+
+ await user.click(themeSwitcherButton);
+
+ expect(getBackgroundColorCSSVariable()).toBe('#000');
+
+ await user.click(themeSwitcherButton);
+
+ expect(getBackgroundColorCSSVariable()).toBe('#fff');
+ });
+
test('navigation works correctly', async () => {
const NAVIGATION: Navigation = [
{
@@ -84,9 +113,9 @@ describe('DashboardLayout', () => {
const user = userEvent.setup();
render(
-
+
Hello world
- ,
+ ,
);
const navigation = screen.getByRole('navigation');
diff --git a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx
index d7b8a46f1e3..18e58e76c35 100644
--- a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx
+++ b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx
@@ -1,12 +1,14 @@
+'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
-import { styled } from '@mui/material';
-import AppBar from '@mui/material/AppBar';
+import { styled, useTheme } from '@mui/material';
+import MuiAppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Collapse from '@mui/material/Collapse';
import Container from '@mui/material/Container';
import Divider from '@mui/material/Divider';
import Drawer from '@mui/material/Drawer';
+import IconButton from '@mui/material/IconButton';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
@@ -15,20 +17,35 @@ import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import Stack from '@mui/material/Stack';
import Toolbar from '@mui/material/Toolbar';
+import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
+import type {} from '@mui/material/themeCssVarsAugmentation';
+import DarkModeIcon from '@mui/icons-material/DarkMode';
+import LightModeIcon from '@mui/icons-material/LightMode';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import useSsr from '@toolpad/utils/hooks/useSsr';
import {
BrandingContext,
- Navigation,
NavigationContext,
- NavigationPageItem,
+ PaletteModeContext,
RouterContext,
-} from '../AppProvider/AppProvider';
+} from '../shared/context';
+import type { Navigation, NavigationPageItem } from '../AppProvider';
import { ToolpadLogo } from './ToolpadLogo';
const DRAWER_WIDTH = 320;
+const AppBar = styled(MuiAppBar)(({ theme }) => ({
+ backgroundColor: (theme.vars ?? theme).palette.background.paper,
+ borderWidth: 0,
+ borderBottomWidth: 1,
+ borderStyle: 'solid',
+ borderColor: (theme.vars ?? theme).palette.divider,
+ boxShadow: 'none',
+ zIndex: theme.zIndex.drawer + 1,
+}));
+
const LogoContainer = styled('div')({
position: 'relative',
height: 40,
@@ -37,6 +54,85 @@ const LogoContainer = styled('div')({
},
});
+const NavigationListItemButton = styled(ListItemButton)(({ theme }) => ({
+ borderRadius: 8,
+ '&.Mui-selected': {
+ '& .MuiListItemIcon-root': {
+ color: (theme.vars ?? theme).palette.primary.dark,
+ },
+ '& .MuiTypography-root': {
+ color: (theme.vars ?? theme).palette.primary.dark,
+ },
+ '& .MuiSvgIcon-root': {
+ color: (theme.vars ?? theme).palette.primary.dark,
+ },
+ '& .MuiTouchRipple-child': {
+ backgroundColor: (theme.vars ?? theme).palette.primary.dark,
+ },
+ },
+ '& .MuiSvgIcon-root': {
+ color: (theme.vars ?? theme).palette.action.active,
+ },
+}));
+
+function ThemeSwitcher() {
+ const isSsr = useSsr();
+ const theme = useTheme();
+
+ const { paletteMode, setPaletteMode, isDualTheme } = React.useContext(PaletteModeContext);
+
+ const toggleMode = React.useCallback(() => {
+ setPaletteMode(paletteMode === 'dark' ? 'light' : 'dark');
+ }, [paletteMode, setPaletteMode]);
+
+ return isDualTheme ? (
+
+
+
+ {theme.getColorSchemeSelector ? (
+
+
+
+
+ ) : null}
+ {!theme.getColorSchemeSelector ? (
+
+ {isSsr || paletteMode !== 'dark' ? : }
+
+ ) : null}
+
+
+
+ ) : null;
+}
+
interface DashboardSidebarSubNavigationProps {
subNavigation: Navigation;
basePath?: string;
@@ -72,10 +168,7 @@ function DashboardSidebarSubNavigation({
);
}),
)
- .map(
- ({ navigationItem, originalIndex }) =>
- `${(navigationItem as NavigationPageItem).title}-${depth}-${originalIndex}`,
- ),
+ .map(({ originalIndex }) => `${depth}-${originalIndex}`),
[basePath, depth, pathname, subNavigation],
);
@@ -106,11 +199,20 @@ function DashboardSidebarSubNavigation({
}, [routerContext]);
return (
-
+
{subNavigation.map((navigationItem, navigationItemIndex) => {
if (navigationItem.kind === 'header') {
return (
-
+
{navigationItem.title}
);
@@ -122,14 +224,20 @@ function DashboardSidebarSubNavigation({
return (
);
}
const navigationItemFullPath = `${basePath}${navigationItem.slug ?? ''}`;
- const navigationItemId = `${navigationItem.title}-${depth}-${navigationItemIndex}`;
+ const navigationItemId = `${depth}-${navigationItemIndex}`;
const isNestedNavigationExpanded = expandedSidebarItemIds.includes(navigationItemId);
@@ -140,15 +248,28 @@ function DashboardSidebarSubNavigation({
);
const listItem = (
-
-
+
- {navigationItem.icon}
-
+
+ {navigationItem.icon}
+
+
{navigationItem.children ? nestedNavigationCollapseIcon : null}
-
+
);
@@ -207,25 +328,26 @@ function DashboardLayout(props: DashboardLayoutProps) {
return (
- theme.zIndex.drawer + 1,
- }}
- >
-
+
+
{branding?.logo ?? }
- theme.palette.primary.main }}>
+ (theme.vars ?? theme).palette.primary.main,
+ fontWeight: '700',
+ }}
+ >
{branding?.title ?? 'Toolpad'}
+
({
+ position: 'absolute',
+ inset: '0 0 0 0',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: theme.spacing(2),
+}));
+
+export interface ErrorOverlayProps {
+ error?: unknown;
+}
+
+export function ErrorOverlay({ error }: ErrorOverlayProps) {
+ return (
+
+
+ Error
+
+ {(error as any)?.message ?? 'Unknown error'}
+
+ );
+}
+
+export function LoadingOverlay() {
+ return (
+
+
+
+ );
+}
diff --git a/packages/toolpad-core/src/shared/context.ts b/packages/toolpad-core/src/shared/context.ts
new file mode 100644
index 00000000000..9a75b58dda3
--- /dev/null
+++ b/packages/toolpad-core/src/shared/context.ts
@@ -0,0 +1,19 @@
+import * as React from 'react';
+import type { PaletteMode } from '@mui/material';
+import type { Branding, Navigation, Router } from '../AppProvider';
+
+export const BrandingContext = React.createContext(null);
+
+export const NavigationContext = React.createContext([]);
+
+export const PaletteModeContext = React.createContext<{
+ paletteMode: PaletteMode;
+ setPaletteMode: (mode: PaletteMode) => void;
+ isDualTheme: boolean;
+}>({
+ paletteMode: 'light',
+ setPaletteMode: () => {},
+ isDualTheme: false,
+});
+
+export const RouterContext = React.createContext(null);
diff --git a/packages/toolpad-core/src/themes/baseTheme.ts b/packages/toolpad-core/src/themes/baseTheme.ts
deleted file mode 100644
index 8ef32b02c81..00000000000
--- a/packages/toolpad-core/src/themes/baseTheme.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-import { createTheme } from '@mui/material/styles';
-
-const defaultTheme = createTheme();
-
-export const baseTheme = createTheme(defaultTheme, {
- palette: {
- background: {
- default: defaultTheme.palette.grey['50'],
- },
- },
- typography: {
- h6: {
- fontWeight: '700',
- },
- },
- components: {
- MuiAppBar: {
- styleOverrides: {
- root: {
- borderWidth: 0,
- borderBottomWidth: 1,
- borderStyle: 'solid',
- borderColor: defaultTheme.palette.divider,
- boxShadow: 'none',
- },
- },
- },
- MuiList: {
- styleOverrides: {
- root: {
- padding: 0,
- },
- },
- },
- MuiIconButton: {
- styleOverrides: {
- root: {
- color: defaultTheme.palette.primary.dark,
- padding: 8,
- },
- },
- },
- MuiListSubheader: {
- styleOverrides: {
- root: {
- color: defaultTheme.palette.grey['600'],
- fontSize: 12,
- fontWeight: '700',
- height: 40,
- paddingLeft: 32,
- },
- },
- },
- MuiListItem: {
- styleOverrides: {
- root: {
- paddingTop: 0,
- paddingBottom: 0,
- },
- },
- },
- MuiListItemButton: {
- styleOverrides: {
- root: {
- borderRadius: 8,
- '&.Mui-selected': {
- '& .MuiListItemIcon-root': {
- color: defaultTheme.palette.primary.dark,
- },
- '& .MuiTypography-root': {
- color: defaultTheme.palette.primary.dark,
- },
- '& .MuiSvgIcon-root': {
- color: defaultTheme.palette.primary.dark,
- },
- '& .MuiTouchRipple-child': {
- backgroundColor: defaultTheme.palette.primary.dark,
- },
- },
- '& .MuiSvgIcon-root': {
- color: defaultTheme.palette.action.active,
- },
- },
- },
- },
- MuiListItemText: {
- styleOverrides: {
- root: {
- '& .MuiTypography-root': {
- fontWeight: '500',
- },
- },
- },
- },
- MuiListItemIcon: {
- styleOverrides: {
- root: {
- minWidth: 34,
- },
- },
- },
- MuiDivider: {
- styleOverrides: {
- root: {
- borderBottomWidth: 2,
- marginLeft: '16px',
- marginRight: '16px',
- },
- },
- },
- },
-});
diff --git a/packages/toolpad-core/src/themes/index.ts b/packages/toolpad-core/src/themes/index.ts
deleted file mode 100644
index 4ed30a92200..00000000000
--- a/packages/toolpad-core/src/themes/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-'use client';
-export * from './baseTheme';
diff --git a/playground/nextjs-pages/src/pages/_document.tsx b/playground/nextjs-pages/src/pages/_document.tsx
index be16e742177..2797ed105d9 100644
--- a/playground/nextjs-pages/src/pages/_document.tsx
+++ b/playground/nextjs-pages/src/pages/_document.tsx
@@ -8,7 +8,7 @@ import {
export default function Document(props: DocumentProps & DocumentHeadTagsProps) {
return (
-
+
diff --git a/playground/nextjs-pages/src/pages/index.tsx b/playground/nextjs-pages/src/pages/index.tsx
index 761eebce518..ff57b97d844 100644
--- a/playground/nextjs-pages/src/pages/index.tsx
+++ b/playground/nextjs-pages/src/pages/index.tsx
@@ -11,6 +11,7 @@ export default function HomePage() {
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
+ textAlign: 'center',
}}
>
diff --git a/playground/nextjs-pages/src/pages/orders/index.tsx b/playground/nextjs-pages/src/pages/orders/index.tsx
index 9cb3b092c9b..c63eb5844df 100644
--- a/playground/nextjs-pages/src/pages/orders/index.tsx
+++ b/playground/nextjs-pages/src/pages/orders/index.tsx
@@ -11,6 +11,7 @@ export default function OrdersPage() {
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
+ textAlign: 'center',
}}
>
diff --git a/playground/nextjs/src/app/(dashboard)/orders/page.tsx b/playground/nextjs/src/app/(dashboard)/orders/page.tsx
index 9cb3b092c9b..c63eb5844df 100644
--- a/playground/nextjs/src/app/(dashboard)/orders/page.tsx
+++ b/playground/nextjs/src/app/(dashboard)/orders/page.tsx
@@ -11,6 +11,7 @@ export default function OrdersPage() {
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
+ textAlign: 'center',
}}
>
diff --git a/playground/nextjs/src/app/(dashboard)/page.tsx b/playground/nextjs/src/app/(dashboard)/page.tsx
index 761eebce518..ff57b97d844 100644
--- a/playground/nextjs/src/app/(dashboard)/page.tsx
+++ b/playground/nextjs/src/app/(dashboard)/page.tsx
@@ -11,6 +11,7 @@ export default function HomePage() {
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
+ textAlign: 'center',
}}
>
diff --git a/playground/nextjs/src/app/layout.tsx b/playground/nextjs/src/app/layout.tsx
index 91843f848d7..8b1a671cfc3 100644
--- a/playground/nextjs/src/app/layout.tsx
+++ b/playground/nextjs/src/app/layout.tsx
@@ -24,7 +24,7 @@ const NAVIGATION: Navigation = [
export default function RootLayout(props: { children: React.ReactNode }) {
return (
-
+
{props.children}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0c2fd4c53bd..7bf81924b30 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -108,6 +108,9 @@ importers:
'@playwright/test':
specifier: 1.45.1
version: 1.45.1
+ '@testing-library/jest-dom':
+ specifier: ^6.4.6
+ version: 6.4.6(vitest@2.0.0-beta.13(@types/node@20.14.10)(@vitest/browser@2.0.0-beta.13)(jsdom@24.1.0)(terser@5.31.1))
'@testing-library/react':
specifier: 16.0.0
version: 16.0.0(@testing-library/dom@10.2.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -587,7 +590,7 @@ importers:
version: 5.0.0-beta.40(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@mui/lab':
specifier: 5.0.0-alpha.172
- version: 5.0.0-alpha.172(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/material@5.16.4(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ version: 5.0.0-alpha.172(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/material@6.0.0-beta.2(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@mui/utils':
specifier: 5.16.4
version: 5.16.4(@types/react@18.3.3)(react@18.3.1)
@@ -608,11 +611,11 @@ importers:
version: 18.3.1
devDependencies:
'@mui/icons-material':
- specifier: 5.16.4
- version: 5.16.4(@mui/material@5.16.4(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
+ specifier: v6.0.0-beta.2
+ version: 6.0.0-beta.2(@mui/material@6.0.0-beta.2(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
'@mui/material':
- specifier: 5.16.4
- version: 5.16.4(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ specifier: v6.0.0-beta.2
+ version: 6.0.0-beta.2(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@types/invariant':
specifier: 2.2.37
version: 2.2.37
@@ -1274,6 +1277,9 @@ importers:
packages:
+ '@adobe/css-tools@4.4.0':
+ resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==}
+
'@algolia/autocomplete-core@1.9.3':
resolution: {integrity: sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==}
@@ -3614,6 +3620,27 @@ packages:
resolution: {integrity: sha512-CytIvb6tVOADRngTHGWNxH8LPgO/3hi/BdCEHOf7Qd2GvZVClhVP0Wo/QHzWhpki49Bk0b4VT6xpt3fx8HTSIw==}
engines: {node: '>=18'}
+ '@testing-library/jest-dom@6.4.6':
+ resolution: {integrity: sha512-8qpnGVincVDLEcQXWaHOf6zmlbwTKc6Us6PPu4CRnPXCzo2OGBS5cwgMMOWdxDpEz1mkbvXHpEy99M5Yvt682w==}
+ engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
+ peerDependencies:
+ '@jest/globals': '>= 28'
+ '@types/bun': latest
+ '@types/jest': '>= 28'
+ jest: '>= 28'
+ vitest: '>= 0.32'
+ peerDependenciesMeta:
+ '@jest/globals':
+ optional: true
+ '@types/bun':
+ optional: true
+ '@types/jest':
+ optional: true
+ jest:
+ optional: true
+ vitest:
+ optional: true
+
'@testing-library/react@16.0.0':
resolution: {integrity: sha512-guuxUKRWQ+FgNX0h0NS0FIq3Q3uLtWVpBzcLOggmfMoUpgBnzBzvLLd4fbm6yS8ydJd94cIfY4yP9qUQjM2KwQ==}
engines: {node: '>=18'}
@@ -4666,6 +4693,10 @@ packages:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
+ chalk@3.0.0:
+ resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==}
+ engines: {node: '>=8'}
+
chalk@4.1.0:
resolution: {integrity: sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==}
engines: {node: '>=10'}
@@ -9844,6 +9875,8 @@ packages:
snapshots:
+ '@adobe/css-tools@4.4.0': {}
+
'@algolia/autocomplete-core@1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0)(search-insights@2.14.0)':
dependencies:
'@algolia/autocomplete-plugin-algolia-insights': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0)(search-insights@2.14.0)
@@ -11567,6 +11600,23 @@ snapshots:
'@emotion/styled': 11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
+ '@mui/lab@5.0.0-alpha.172(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/material@6.0.0-beta.2(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@babel/runtime': 7.24.8
+ '@mui/base': 5.0.0-beta.40(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@mui/material': 6.0.0-beta.2(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@mui/system': 5.16.4(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
+ '@mui/types': 7.2.15(@types/react@18.3.3)
+ '@mui/utils': 5.16.4(@types/react@18.3.3)(react@18.3.1)
+ clsx: 2.1.1
+ prop-types: 15.8.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@emotion/react': 11.11.4(@types/react@18.3.3)(react@18.3.1)
+ '@emotion/styled': 11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+
'@mui/lab@6.0.0-beta.2(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/material@6.0.0-beta.2(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.24.8
@@ -12656,6 +12706,19 @@ snapshots:
lz-string: 1.5.0
pretty-format: 27.5.1
+ '@testing-library/jest-dom@6.4.6(vitest@2.0.0-beta.13(@types/node@20.14.10)(@vitest/browser@2.0.0-beta.13)(jsdom@24.1.0)(terser@5.31.1))':
+ dependencies:
+ '@adobe/css-tools': 4.4.0
+ '@babel/runtime': 7.24.8
+ aria-query: 5.3.0
+ chalk: 3.0.0
+ css.escape: 1.5.1
+ dom-accessibility-api: 0.6.3
+ lodash: 4.17.21
+ redent: 3.0.0
+ optionalDependencies:
+ vitest: 2.0.0-beta.13(@types/node@20.14.10)(@vitest/browser@2.0.0-beta.13)(jsdom@24.1.0)(terser@5.31.1)
+
'@testing-library/react@16.0.0(@testing-library/dom@10.2.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.24.8
@@ -14004,6 +14067,11 @@ snapshots:
escape-string-regexp: 1.0.5
supports-color: 5.5.0
+ chalk@3.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
chalk@4.1.0:
dependencies:
ansi-styles: 4.3.0
diff --git a/test/setupVitest.ts b/test/setupVitest.ts
index 91112248b5a..b5cb10f1dbb 100644
--- a/test/setupVitest.ts
+++ b/test/setupVitest.ts
@@ -1,4 +1,4 @@
-import { afterEach } from 'vitest';
+import { afterEach, vi } from 'vitest';
import failOnConsole from 'vitest-fail-on-console';
import { cleanup } from '@testing-library/react';
@@ -13,3 +13,21 @@ failOnConsole({
});
afterEach(cleanup);
+
+// Mocks
+
+if (typeof window !== 'undefined' && !window.matchMedia) {
+ Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: vi.fn().mockImplementation((query) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(), // deprecated
+ removeListener: vi.fn(), // deprecated
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+ });
+}