diff --git a/docs/data/toolpad/core/components/app-provider/AppProviderBasic.js b/docs/data/toolpad/core/components/app-provider/AppProviderBasic.js index bf62d0e84d2..7facac0f5e0 100644 --- a/docs/data/toolpad/core/components/app-provider/AppProviderBasic.js +++ b/docs/data/toolpad/core/components/app-provider/AppProviderBasic.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'; @@ -16,7 +17,6 @@ const NAVIGATION = [ title: 'Page', icon: , }, - // Add the following new item: { slug: '/page-2', title: 'Page 2', @@ -24,7 +24,29 @@ const NAVIGATION = [ }, ]; -export default function AppProviderBasic() { +function DemoPageContent({ pathname }) { + return ( + + Dashboard content for {pathname} + + ); +} + +DemoPageContent.propTypes = { + pathname: PropTypes.string.isRequired, +}; + +function AppProviderBasic(props) { + const { window } = props; + const [pathname, setPathname] = React.useState('/page'); const router = React.useMemo(() => { @@ -35,22 +57,26 @@ export default function AppProviderBasic() { }; }, [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 ); } + +AppProviderBasic.propTypes = { + /** + * Injected by the documentation to work in an iframe. + * Remove this when copying and pasting into your project. + */ + window: PropTypes.func, +}; + +export default AppProviderBasic; diff --git a/docs/data/toolpad/core/components/app-provider/AppProviderBasic.tsx b/docs/data/toolpad/core/components/app-provider/AppProviderBasic.tsx index 81344a07f7e..07db300713b 100644 --- a/docs/data/toolpad/core/components/app-provider/AppProviderBasic.tsx +++ b/docs/data/toolpad/core/components/app-provider/AppProviderBasic.tsx @@ -3,9 +3,9 @@ import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import DashboardIcon from '@mui/icons-material/Dashboard'; import TimelineIcon from '@mui/icons-material/Timeline'; -import { AppProvider, Router } from '@toolpad/core/AppProvider'; +import { AppProvider } from '@toolpad/core/AppProvider'; import { DashboardLayout } from '@toolpad/core/DashboardLayout'; -import type { Navigation } from '@toolpad/core'; +import type { Navigation, Router } from '@toolpad/core'; const NAVIGATION: Navigation = [ { @@ -17,7 +17,6 @@ const NAVIGATION: Navigation = [ title: 'Page', icon: , }, - // Add the following new item: { slug: '/page-2', title: 'Page 2', @@ -25,7 +24,33 @@ const NAVIGATION: Navigation = [ }, ]; -export default function AppProviderBasic() { +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 AppProviderBasic(props: DemoProps) { + const { window } = props; + const [pathname, setPathname] = React.useState('/page'); const router = React.useMemo(() => { @@ -36,20 +61,14 @@ export default function AppProviderBasic() { }; }, [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/app-provider/AppProviderBasic.tsx.preview b/docs/data/toolpad/core/components/app-provider/AppProviderBasic.tsx.preview index 00133b63b69..512dc89fa09 100644 --- a/docs/data/toolpad/core/components/app-provider/AppProviderBasic.tsx.preview +++ b/docs/data/toolpad/core/components/app-provider/AppProviderBasic.tsx.preview @@ -1,14 +1,5 @@ - + - - Dashboard content for {pathname} - + \ No newline at end of file diff --git a/docs/data/toolpad/core/components/app-provider/AppProviderTheme.js b/docs/data/toolpad/core/components/app-provider/AppProviderTheme.js new file mode 100644 index 00000000000..6eea396cf0d --- /dev/null +++ b/docs/data/toolpad/core/components/app-provider/AppProviderTheme.js @@ -0,0 +1,109 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import { extendTheme } from '@mui/material/styles'; +import DashboardIcon from '@mui/icons-material/Dashboard'; +import TimelineIcon from '@mui/icons-material/Timeline'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { DashboardLayout } from '@toolpad/core/DashboardLayout'; + +const NAVIGATION = [ + { + kind: 'header', + title: 'Main items', + }, + { + slug: '/page', + title: 'Page', + icon: , + }, + { + slug: '/page-2', + title: 'Page 2', + icon: , + }, +]; + +const customTheme = extendTheme({ + colorSchemes: { + light: { + palette: { + background: { + default: '#E2FAFF', + paper: '#D9FAFF', + }, + }, + }, + dark: { + palette: { + background: { + default: '#2A4364', + paper: '#112E4D', + }, + }, + }, + }, +}); + +function DemoPageContent({ pathname }) { + return ( + + Dashboard content for {pathname} + + ); +} + +DemoPageContent.propTypes = { + pathname: PropTypes.string.isRequired, +}; + +function AppProviderTheme(props) { + const { window } = props; + + const [pathname, setPathname] = React.useState('/page'); + + const router = React.useMemo(() => { + 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 + ); +} + +AppProviderTheme.propTypes = { + /** + * Injected by the documentation to work in an iframe. + * Remove this when copying and pasting into your project. + */ + window: PropTypes.func, +}; + +export default AppProviderTheme; diff --git a/docs/data/toolpad/core/components/app-provider/AppProviderTheme.tsx b/docs/data/toolpad/core/components/app-provider/AppProviderTheme.tsx new file mode 100644 index 00000000000..ec6ac7500c6 --- /dev/null +++ b/docs/data/toolpad/core/components/app-provider/AppProviderTheme.tsx @@ -0,0 +1,103 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import { extendTheme } from '@mui/material/styles'; +import DashboardIcon from '@mui/icons-material/Dashboard'; +import TimelineIcon from '@mui/icons-material/Timeline'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { DashboardLayout } from '@toolpad/core/DashboardLayout'; +import type { Navigation, Router } from '@toolpad/core'; + +const NAVIGATION: Navigation = [ + { + kind: 'header', + title: 'Main items', + }, + { + slug: '/page', + title: 'Page', + icon: , + }, + { + slug: '/page-2', + title: 'Page 2', + icon: , + }, +]; + +const customTheme = extendTheme({ + colorSchemes: { + light: { + palette: { + background: { + default: '#E2FAFF', + paper: '#D9FAFF', + }, + }, + }, + dark: { + palette: { + background: { + default: '#2A4364', + paper: '#112E4D', + }, + }, + }, + }, +}); + +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 AppProviderTheme(props: DemoProps) { + const { window } = props; + + const [pathname, setPathname] = React.useState('/page'); + + const router = React.useMemo(() => { + 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: MUI 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: MUI 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(), + })), + }); +}