diff --git a/docs/data/toolpad/core/api/components/dashboard-layout.md b/docs/data/toolpad/core/api/components/dashboard-layout.md
new file mode 100644
index 00000000000..e099cc8857f
--- /dev/null
+++ b/docs/data/toolpad/core/api/components/dashboard-layout.md
@@ -0,0 +1,3 @@
+# Dashboard Layout API
+
+
DashboardLayout API reference
diff --git a/docs/data/toolpad/core/api/components/dashboard.md b/docs/data/toolpad/core/api/components/dashboard.md
deleted file mode 100644
index 4fbe818460c..00000000000
--- a/docs/data/toolpad/core/api/components/dashboard.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Dashboard API
-
-Dashboard API reference
diff --git a/docs/data/toolpad/core/api/index.md b/docs/data/toolpad/core/api/index.md
index 2ee020a6458..b8fa7bc5387 100644
--- a/docs/data/toolpad/core/api/index.md
+++ b/docs/data/toolpad/core/api/index.md
@@ -4,7 +4,7 @@
## Components
-- [Dashboard](/toolpad/core/api/dashboard/)
+- [Dashboard Layout](/toolpad/core/api/dashboard-layout/)
- [Data Grid](/toolpad/core/api/data-grid/)
- [Line Chart](/toolpad/core/api/line-chart/)
- [Select Filter](/toolpad/core/api/select-filter/)
diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.js b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.js
new file mode 100644
index 00000000000..e14eb8afff2
--- /dev/null
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.js
@@ -0,0 +1,69 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import DashboardIcon from '@mui/icons-material/Dashboard';
+import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
+import BarChartIcon from '@mui/icons-material/BarChart';
+import DescriptionIcon from '@mui/icons-material/Description';
+import LayersIcon from '@mui/icons-material/Layers';
+import { AppProvider } from '@toolpad/core/AppProvider';
+import { DashboardLayout } from '@toolpad/core/DashboardLayout';
+
+const NAVIGATION = [
+ {
+ kind: 'header',
+ title: 'Main items',
+ },
+ {
+ title: 'Dashboard',
+ icon: ,
+ },
+ {
+ title: 'Orders',
+ icon: ,
+ },
+ {
+ kind: 'divider',
+ },
+ {
+ kind: 'header',
+ title: 'Analytics',
+ },
+ {
+ title: 'Reports',
+ icon: ,
+ children: [
+ {
+ title: 'Sales',
+ icon: ,
+ },
+ {
+ title: 'Traffic',
+ icon: ,
+ },
+ ],
+ },
+ {
+ title: 'Integrations',
+ icon: ,
+ },
+];
+
+export default function DashboardLayoutBasic() {
+ return (
+
+
+
+ Dashboard content goes here.
+
+
+
+ );
+}
diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.tsx b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.tsx
new file mode 100644
index 00000000000..3b1163d5b7c
--- /dev/null
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBasic.tsx
@@ -0,0 +1,70 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import DashboardIcon from '@mui/icons-material/Dashboard';
+import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
+import BarChartIcon from '@mui/icons-material/BarChart';
+import DescriptionIcon from '@mui/icons-material/Description';
+import LayersIcon from '@mui/icons-material/Layers';
+import { AppProvider } from '@toolpad/core/AppProvider';
+import { DashboardLayout } from '@toolpad/core/DashboardLayout';
+import type { Navigation } from '@toolpad/core';
+
+const NAVIGATION: Navigation = [
+ {
+ kind: 'header',
+ title: 'Main items',
+ },
+ {
+ title: 'Dashboard',
+ icon: ,
+ },
+ {
+ title: 'Orders',
+ icon: ,
+ },
+ {
+ kind: 'divider',
+ },
+ {
+ kind: 'header',
+ title: 'Analytics',
+ },
+ {
+ title: 'Reports',
+ icon: ,
+ children: [
+ {
+ title: 'Sales',
+ icon: ,
+ },
+ {
+ title: 'Traffic',
+ icon: ,
+ },
+ ],
+ },
+ {
+ title: 'Integrations',
+ icon: ,
+ },
+];
+
+export default function DashboardLayoutBasic() {
+ return (
+
+
+
+ Dashboard content goes here.
+
+
+
+ );
+}
diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.js b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.js
new file mode 100644
index 00000000000..3047c2500ae
--- /dev/null
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.js
@@ -0,0 +1,42 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import DashboardIcon from '@mui/icons-material/Dashboard';
+import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
+import { AppProvider } from '@toolpad/core/AppProvider';
+import { DashboardLayout } from '@toolpad/core/DashboardLayout';
+
+const NAVIGATION = [
+ {
+ title: 'Dashboard',
+ icon: ,
+ },
+ {
+ title: 'Orders',
+ icon: ,
+ },
+];
+
+const BRANDING = {
+ logo: ,
+ title: 'MUI',
+};
+
+export default function DashboardLayoutBranding() {
+ return (
+
+
+
+ Dashboard content goes here.
+
+
+
+ );
+}
diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.tsx b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.tsx
new file mode 100644
index 00000000000..b0a1b6b17d6
--- /dev/null
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutBranding.tsx
@@ -0,0 +1,43 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import DashboardIcon from '@mui/icons-material/Dashboard';
+import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
+import { AppProvider } from '@toolpad/core/AppProvider';
+import { DashboardLayout } from '@toolpad/core/DashboardLayout';
+import type { Navigation, Branding } from '@toolpad/core';
+
+const NAVIGATION: Navigation = [
+ {
+ title: 'Dashboard',
+ icon: ,
+ },
+ {
+ title: 'Orders',
+ icon: ,
+ },
+];
+
+const BRANDING: Branding = {
+ logo: ,
+ title: 'MUI',
+};
+
+export default function DashboardLayoutBranding() {
+ return (
+
+
+
+ Dashboard content goes here.
+
+
+
+ );
+}
diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.js b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.js
new file mode 100644
index 00000000000..c83c0c0593c
--- /dev/null
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.js
@@ -0,0 +1,109 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import DescriptionIcon from '@mui/icons-material/Description';
+import FolderIcon from '@mui/icons-material/Folder';
+import { AppProvider } from '@toolpad/core/AppProvider';
+import { DashboardLayout } from '@toolpad/core/DashboardLayout';
+
+const NAVIGATION = [
+ {
+ title: 'Item 1',
+ icon: ,
+ },
+ {
+ title: 'Item 2',
+ icon: ,
+ },
+ {
+ title: 'Folder 1',
+ icon: ,
+ children: [
+ {
+ title: 'Item A1',
+ icon: ,
+ },
+ {
+ title: 'Item A2',
+ icon: ,
+ },
+ {
+ title: 'Folder A1',
+ icon: ,
+ children: [
+ {
+ title: 'Item B1',
+ icon: ,
+ },
+ {
+ title: 'Item B2',
+ icon: ,
+ },
+ ],
+ },
+ ],
+ },
+ { kind: 'divider' },
+ {
+ kind: 'header',
+ title: 'Header 1',
+ },
+ {
+ title: 'Item A',
+ icon: ,
+ },
+ {
+ kind: 'header',
+ title: 'Header 2',
+ },
+ {
+ title: 'Item B',
+ icon: ,
+ },
+ {
+ title: 'Folder 2',
+ icon: ,
+ children: [
+ {
+ kind: 'header',
+ title: 'Header A1',
+ },
+ {
+ title: 'Item C1',
+ icon: ,
+ },
+ {
+ title: 'Item C2',
+ icon: ,
+ },
+ { kind: 'divider' },
+ {
+ kind: 'header',
+ title: 'Header A2',
+ },
+ {
+ title: 'Item C3',
+ icon: ,
+ },
+ ],
+ },
+];
+
+export default function DashboardLayoutNavigation() {
+ return (
+
+
+
+ Dashboard content goes here.
+
+
+
+ );
+}
diff --git a/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.tsx b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.tsx
new file mode 100644
index 00000000000..07e65802f2c
--- /dev/null
+++ b/docs/data/toolpad/core/components/dashboard-layout/DashboardLayoutNavigation.tsx
@@ -0,0 +1,110 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import DescriptionIcon from '@mui/icons-material/Description';
+import FolderIcon from '@mui/icons-material/Folder';
+import { AppProvider } from '@toolpad/core/AppProvider';
+import { DashboardLayout } from '@toolpad/core/DashboardLayout';
+import type { Navigation } from '@toolpad/core';
+
+const NAVIGATION: Navigation = [
+ {
+ title: 'Item 1',
+ icon: ,
+ },
+ {
+ title: 'Item 2',
+ icon: ,
+ },
+ {
+ title: 'Folder 1',
+ icon: ,
+ children: [
+ {
+ title: 'Item A1',
+ icon: ,
+ },
+ {
+ title: 'Item A2',
+ icon: ,
+ },
+ {
+ title: 'Folder A1',
+ icon: ,
+ children: [
+ {
+ title: 'Item B1',
+ icon: ,
+ },
+ {
+ title: 'Item B2',
+ icon: ,
+ },
+ ],
+ },
+ ],
+ },
+ { kind: 'divider' },
+ {
+ kind: 'header',
+ title: 'Header 1',
+ },
+ {
+ title: 'Item A',
+ icon: ,
+ },
+ {
+ kind: 'header',
+ title: 'Header 2',
+ },
+ {
+ title: 'Item B',
+ icon: ,
+ },
+ {
+ title: 'Folder 2',
+ icon: ,
+ children: [
+ {
+ kind: 'header',
+ title: 'Header A1',
+ },
+ {
+ title: 'Item C1',
+ icon: ,
+ },
+ {
+ title: 'Item C2',
+ icon: ,
+ },
+ { kind: 'divider' },
+ {
+ kind: 'header',
+ title: 'Header A2',
+ },
+ {
+ title: 'Item C3',
+ icon: ,
+ },
+ ],
+ },
+];
+
+export default function DashboardLayoutNavigation() {
+ return (
+
+
+
+ Dashboard content goes here.
+
+
+
+ );
+}
diff --git a/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md b/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md
new file mode 100644
index 00000000000..86f2aeccf8b
--- /dev/null
+++ b/docs/data/toolpad/core/components/dashboard-layout/dashboard-layout.md
@@ -0,0 +1,23 @@
+# Dashboard Layout
+
+The dashboard layout component provides a customizable out-of-the-box layout for a typical dashboard page.
+
+## Demo
+
+A Dashboard Layout has a configurable header and sidebar with navigation.
+
+{{"demo": "DashboardLayoutBasic.js", "height": 500, "iframe": true}}
+
+Some features of this layout depend on the `AppProvider` component that must be present at the base application level.
+
+## Branding
+
+The `branding` prop in the `AppProvider` allows for setting a `logo` or `title` in the page header.
+
+{{"demo": "DashboardLayoutBranding.js", "height": 500, "iframe": true}}
+
+## Navigation
+
+The `navigation` prop in the `AppProvider` allows for setting any type of navigation structure in the sidebar, such as links, headings, nested collapsible lists and dividers, in any order.
+
+{{"demo": "DashboardLayoutNavigation.js", "height": 640, "iframe": true}}
diff --git a/docs/data/toolpad/core/components/dashboard.md b/docs/data/toolpad/core/components/dashboard.md
deleted file mode 100644
index 98975441fdf..00000000000
--- a/docs/data/toolpad/core/components/dashboard.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Dashboard
-
-Dashboard component
diff --git a/docs/data/toolpad/core/components/index.md b/docs/data/toolpad/core/components/index.md
index 61b8c61eb76..d08d3d85418 100644
--- a/docs/data/toolpad/core/components/index.md
+++ b/docs/data/toolpad/core/components/index.md
@@ -2,7 +2,7 @@
This page contains an index to the component pages that come with Toolpad Core.
-- [Dashboard](/toolpad/core/components/dashboard/)
+- [Dashboard Layout](/toolpad/core/components/dashboard-layout/)
- [Data Grid](/toolpad/core/components/data-grid/)
- [Line Chart](/toolpad/core/components/line-chart/)
- [Select Filter](/toolpad/core/components/select-filter/)
diff --git a/docs/data/toolpad/core/pages.ts b/docs/data/toolpad/core/pages.ts
index f54c7095622..fc88395282b 100644
--- a/docs/data/toolpad/core/pages.ts
+++ b/docs/data/toolpad/core/pages.ts
@@ -66,8 +66,8 @@ const pages: MuiPage[] = [
subheader: 'Layout',
children: [
{
- pathname: '/toolpad/core/components/dashboard',
- title: 'Dashboard',
+ pathname: '/toolpad/core/components/dashboard-layout',
+ title: 'Dashboard Layout',
},
],
},
@@ -110,8 +110,8 @@ const pages: MuiPage[] = [
subheader: 'Components',
children: [
{
- pathname: '/toolpad/core/api/dashboard',
- title: 'Dashboard',
+ pathname: '/toolpad/core/api/dashboard-layout',
+ title: 'DashboardLayout',
},
{
pathname: '/toolpad/core/api/data-grid',
diff --git a/docs/package.json b/docs/package.json
index 651bb1a8a33..7acb798771e 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -36,6 +36,7 @@
"@mui/styles": "5.15.18",
"@mui/utils": "5.15.14",
"@mui/x-license": "7.2.0",
+ "@toolpad/core": "workspace:*",
"@toolpad/studio": "workspace:*",
"@trendmicro/react-interpolate": "0.5.5",
"@types/lodash": "4.17.1",
diff --git a/docs/pages/toolpad/core/api/dashboard.js b/docs/pages/toolpad/core/api/dashboard-layout.js
similarity index 85%
rename from docs/pages/toolpad/core/api/dashboard.js
rename to docs/pages/toolpad/core/api/dashboard-layout.js
index ccd1ebe5a92..6eb70763b78 100644
--- a/docs/pages/toolpad/core/api/dashboard.js
+++ b/docs/pages/toolpad/core/api/dashboard-layout.js
@@ -1,6 +1,6 @@
import * as React from 'react';
import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs';
-import * as pageProps from '../../../../data/toolpad/core/api/components/dashboard.md?muiMarkdown';
+import * as pageProps from '../../../../data/toolpad/core/api/components/dashboard-layout.md?muiMarkdown';
export default function Page() {
return ;
diff --git a/docs/pages/toolpad/core/components/dashboard.js b/docs/pages/toolpad/core/components/dashboard-layout.js
similarity index 81%
rename from docs/pages/toolpad/core/components/dashboard.js
rename to docs/pages/toolpad/core/components/dashboard-layout.js
index dbf7a2d6ae4..928b679bea5 100644
--- a/docs/pages/toolpad/core/components/dashboard.js
+++ b/docs/pages/toolpad/core/components/dashboard-layout.js
@@ -1,6 +1,6 @@
import * as React from 'react';
import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs';
-import * as pageProps from '../../../../data/toolpad/core/components/dashboard.md?muiMarkdown';
+import * as pageProps from '../../../../data/toolpad/core/components/dashboard-layout/dashboard-layout.md?muiMarkdown';
export default function Page() {
return ;
diff --git a/docs/scripts/formattedTSDemos.js b/docs/scripts/formattedTSDemos.js
index 5f620df7f8e..909f03e01f3 100644
--- a/docs/scripts/formattedTSDemos.js
+++ b/docs/scripts/formattedTSDemos.js
@@ -154,8 +154,6 @@ async function main(argv) {
...(await getFiles(path.join(workspaceRoot, 'docs/data'))), // new structure
].filter((fileName) => filePattern.test(fileName));
- console.log(tsxFiles);
-
const buildProject = createTypeScriptProjectBuilder(CORE_TYPESCRIPT_PROJECTS);
const project = buildProject('docs', { files: tsxFiles });
diff --git a/packages/create-toolpad-app/src/generateProject.ts b/packages/create-toolpad-app/src/generateProject.ts
index 3355f0debd9..dada0be290d 100644
--- a/packages/create-toolpad-app/src/generateProject.ts
+++ b/packages/create-toolpad-app/src/generateProject.ts
@@ -9,14 +9,30 @@ export default function generateProject(
options: GenerateProjectOptions,
): Map {
const rootLayoutContent = `
- import { AppProvider } from '@toolpad/core';
+ import { AppProvider } from '@toolpad/core/AppProvider';
+ import DashboardIcon from "@mui/icons-material/Dashboard";
+ import type { Navigation } from "@toolpad/core";
import theme from '../theme';
+
+ const NAVIGATION: Navigation = [
+ {
+ kind: 'header',
+ title: 'Main items',
+ },
+ {
+ slug: '/page',
+ title: 'Page',
+ icon: ,
+ },
+ ];
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
- {children}
+
+ {children}
+
);
@@ -24,69 +40,13 @@ export default function generateProject(
`;
const dashboardLayoutContent = `
- import {
- AppBar,
- Badge,
- Box,
- Container,
- Divider,
- Drawer,
- IconButton,
- List,
- ListItemButton,
- ListItemIcon,
- Toolbar,
- } from "@mui/material";
-
- import HomeIcon from "@mui/icons-material/Home";
- import SettingsIcon from "@mui/icons-material/Settings";
- import NotificationsIcon from "@mui/icons-material/Notifications";
+ import { DashboardLayout } from '@toolpad/core/DashboardLayout';
export default function Layout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {children}
-
-
+ {children}
);
}
`;
@@ -139,25 +99,31 @@ export default function generateProject(
`;
const themeContent = `
- "use client"
- import { Roboto } from "next/font/google";
+ "use client";
import { createTheme } from "@mui/material/styles";
-
- const roboto = Roboto({
- weight: ["300", "400", "500", "700"],
- subsets: ["latin"],
- display: "swap",
- });
-
- const theme = createTheme({
+
+ const defaultTheme = createTheme();
+
+ const theme = createTheme(defaultTheme, {
+ palette: {
+ background: {
+ default: defaultTheme.palette.grey['50'],
+ },
+ },
typography: {
- fontFamily: roboto.style.fontFamily,
+ h6: {
+ fontWeight: '700',
+ },
},
components: {
MuiAppBar: {
styleOverrides: {
root: {
- boxShadow: "none",
+ borderWidth: 0,
+ borderBottomWidth: 1,
+ borderStyle: 'solid',
+ borderColor: defaultTheme.palette.divider,
+ boxShadow: 'none',
},
},
},
@@ -168,16 +134,85 @@ export default function generateProject(
},
},
},
+ 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: "28px",
+ minWidth: 34,
+ },
+ },
+ },
+ MuiDivider: {
+ styleOverrides: {
+ root: {
+ borderBottomWidth: 2,
+ marginLeft: '16px',
+ marginRight: '16px',
},
},
},
},
});
-
+
export default theme;
`;
diff --git a/packages/toolpad-core/src/AppProvider/AppProvider.test.tsx b/packages/toolpad-core/src/AppProvider/AppProvider.test.tsx
index c0601331241..7f40c298cd5 100644
--- a/packages/toolpad-core/src/AppProvider/AppProvider.test.tsx
+++ b/packages/toolpad-core/src/AppProvider/AppProvider.test.tsx
@@ -4,18 +4,15 @@
import * as React from 'react';
import { describe, test, expect, afterEach } from 'vitest';
-import { render, cleanup, screen } from '@testing-library/react';
-import { createTheme } from '@mui/material/styles';
+import { render, cleanup } from '@testing-library/react';
import { AppProvider } from './AppProvider';
describe('AppProvider', () => {
afterEach(cleanup);
test('renders content correctly', async () => {
- const theme = createTheme();
+ const { getByText } = render(Hello world );
- render(hello );
-
- expect(screen.getByText('hello')).toBeTruthy();
+ expect(getByText('Hello world')).toBeTruthy();
});
});
diff --git a/packages/toolpad-core/src/AppProvider/AppProvider.tsx b/packages/toolpad-core/src/AppProvider/AppProvider.tsx
index 7dc7768cad1..d202839ba59 100644
--- a/packages/toolpad-core/src/AppProvider/AppProvider.tsx
+++ b/packages/toolpad-core/src/AppProvider/AppProvider.tsx
@@ -1,20 +1,58 @@
import * as React from 'react';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
-
import type { Theme } from '@emotion/react';
+import { baseTheme } from '../themes';
+
+export interface Branding {
+ title?: string;
+ logo?: React.ReactNode;
+}
+
+export interface NavigationPageItem {
+ kind?: 'page';
+ title: string;
+ slug?: string;
+ icon?: React.ReactNode;
+ children?: Navigation;
+}
+
+export interface NavigationSubheaderItem {
+ kind: 'header';
+ title: string;
+}
+
+export interface NavigationDividerItem {
+ kind: 'divider';
+}
+
+export type NavigationItem = NavigationPageItem | NavigationSubheaderItem | NavigationDividerItem;
+
+export type Navigation = NavigationItem[];
+
+export const BrandingContext = React.createContext(null);
+
+export const NavigationContext = React.createContext([]);
interface AppProviderProps {
children: React.ReactNode;
- theme: Theme;
+ theme?: Theme;
+ branding?: Branding | null;
+ navigation?: Navigation;
}
-export function AppProvider({ children, theme }: AppProviderProps) {
+export function AppProvider({
+ children,
+ theme = baseTheme,
+ branding = null,
+ navigation = [],
+}: AppProviderProps) {
return (
- {/* CssBaseline kickstarts an elegant, consistent, and simple baseline to build upon. */}
- {children}
+
+ {children}
+
);
}
diff --git a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx
new file mode 100644
index 00000000000..eef83aeb0f8
--- /dev/null
+++ b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx
@@ -0,0 +1,129 @@
+/**
+ * @vitest-environment jsdom
+ */
+
+import * as React from 'react';
+import { describe, test, expect, afterEach } from 'vitest';
+import { render, cleanup, within, waitFor } from '@testing-library/react';
+import DashboardIcon from '@mui/icons-material/Dashboard';
+import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
+import BarChartIcon from '@mui/icons-material/BarChart';
+import DescriptionIcon from '@mui/icons-material/Description';
+import LayersIcon from '@mui/icons-material/Layers';
+import { DashboardLayout } from './DashboardLayout';
+import { BrandingContext, Navigation, NavigationContext } from '../AppProvider';
+
+describe('DashboardLayout', () => {
+ afterEach(cleanup);
+
+ test('renders content correctly', async () => {
+ const { getByText } = render(Hello world );
+
+ expect(getByText('Hello world')).toBeTruthy();
+ });
+
+ test('renders branding correctly in header', async () => {
+ const BRANDING = {
+ title: 'My Company',
+ logo: ,
+ };
+
+ const { getByRole } = render(
+
+ Hello world
+ ,
+ );
+
+ const header = getByRole('banner');
+
+ expect(within(header).getByText('My Company')).toBeTruthy();
+ expect(within(header).getByAltText('Placeholder Logo')).toBeTruthy();
+ });
+
+ test('navigation works correctly', async () => {
+ const NAVIGATION: Navigation = [
+ {
+ kind: 'header',
+ title: 'Main items',
+ },
+ {
+ title: 'Dashboard',
+ slug: '/dashboard',
+ icon: ,
+ },
+ {
+ title: 'Orders',
+ slug: '/orders',
+ icon: ,
+ },
+ {
+ kind: 'divider',
+ },
+ {
+ kind: 'header',
+ title: 'Analytics',
+ },
+ {
+ title: 'Reports',
+ icon: ,
+ children: [
+ {
+ title: 'Sales',
+ icon: ,
+ },
+ {
+ title: 'Traffic',
+ icon: ,
+ },
+ ],
+ },
+ {
+ title: 'Integrations',
+ icon: ,
+ },
+ ];
+
+ const { getByRole } = render(
+
+ Hello world
+ ,
+ );
+
+ const navigation = getByRole('navigation');
+
+ // Check list subheaders
+
+ expect(within(navigation).getByText('Main items')).toBeTruthy();
+ expect(within(navigation).getByText('Analytics')).toBeTruthy();
+
+ // Check list items and their links
+
+ const dashboardItem = within(navigation).getByText('Dashboard');
+ const ordersItem = within(navigation).getByText('Orders');
+
+ expect(dashboardItem).toBeTruthy();
+ expect(ordersItem).toBeTruthy();
+
+ const dashboardItemLink = dashboardItem.closest('a') as HTMLElement;
+ expect(dashboardItemLink.getAttribute('href')).toBe('/dashboard');
+ const ordersItemLink = ordersItem.closest('a') as HTMLElement;
+ expect(ordersItemLink.getAttribute('href')).toBe('/orders');
+
+ const reportsItem = within(navigation).getByText('Reports');
+
+ expect(reportsItem).toBeTruthy();
+ expect(within(navigation).getByText('Integrations')).toBeTruthy();
+
+ expect(within(navigation).queryByText('Sales')).toBeNull();
+ expect(within(navigation).queryByText('Traffic')).toBeNull();
+
+ // Check nested list items
+
+ reportsItem.click();
+
+ await waitFor(async () => {
+ expect(within(navigation).getByText('Sales')).toBeTruthy();
+ expect(within(navigation).getByText('Traffic')).toBeTruthy();
+ });
+ });
+});
diff --git a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx
index 9cf3fcbbece..8bc1def1159 100644
--- a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx
+++ b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx
@@ -1,9 +1,246 @@
import * as React from 'react';
+import { styled } from '@mui/material';
+import AppBar 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 List from '@mui/material/List';
+import ListItem from '@mui/material/ListItem';
+import ListItemButton from '@mui/material/ListItemButton';
+import ListItemIcon from '@mui/material/ListItemIcon';
+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 Typography from '@mui/material/Typography';
+import ExpandLessIcon from '@mui/icons-material/ExpandLess';
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import { BrandingContext, Navigation, NavigationContext, NavigationPageItem } from '../AppProvider';
+import ToolpadLogo from './ToolpadLogo';
+
+const DRAWER_WIDTH = 320;
+
+// @TODO: Remove temporary usePathname once navigation adapter is implemented
+
+function subscribe() {
+ return () => {};
+}
+
+function getSnapshot() {
+ return new URL(window.location.href).pathname;
+}
+
+function getServerSnapshot() {
+ return '/';
+}
+
+function usePathname() {
+ return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
+}
+
+const LogoContainer = styled('div')({
+ position: 'relative',
+ height: 40,
+ '& img': {
+ maxHeight: 40,
+ },
+});
+
+interface DashboardSidebarSubNavigationProps {
+ subNavigation: Navigation;
+ basePath?: string;
+ depth?: number;
+}
+
+function DashboardSidebarSubNavigation({
+ subNavigation,
+ basePath = '',
+ depth = 0,
+}: DashboardSidebarSubNavigationProps) {
+ const pathname = usePathname();
+
+ const initialExpandedSidebarItemIds = React.useMemo(
+ () =>
+ subNavigation
+ .map((navigationItem, navigationItemIndex) => ({
+ navigationItem,
+ originalIndex: navigationItemIndex,
+ }))
+ .filter(
+ ({ navigationItem }) =>
+ (!navigationItem.kind || navigationItem.kind === 'page') &&
+ navigationItem.children &&
+ navigationItem.children.some((nestedNavigationItem) => {
+ const navigationItemFullPath = `${basePath}${(nestedNavigationItem as NavigationPageItem).slug ?? ''}`;
+
+ return (
+ (!nestedNavigationItem.kind || nestedNavigationItem.kind === 'page') &&
+ navigationItemFullPath === pathname
+ );
+ }),
+ )
+ .map(
+ ({ navigationItem, originalIndex }) =>
+ `${(navigationItem as NavigationPageItem).title}-${depth}-${originalIndex}`,
+ ),
+ [basePath, depth, pathname, subNavigation],
+ );
+
+ const [expandedSidebarItemIds, setExpandedSidebarItemIds] = React.useState(
+ initialExpandedSidebarItemIds,
+ );
+
+ const handleSidebarItemClick = React.useCallback(
+ (itemId: string) => () => {
+ setExpandedSidebarItemIds((previousValue) =>
+ previousValue.includes(itemId)
+ ? previousValue.filter((previousValueItemId) => previousValueItemId !== itemId)
+ : [...previousValue, itemId],
+ );
+ },
+ [],
+ );
+
+ return (
+
+ {subNavigation.map((navigationItem, navigationItemIndex) => {
+ if (navigationItem.kind === 'header') {
+ return (
+
+ {navigationItem.title}
+
+ );
+ }
+
+ if (navigationItem.kind === 'divider') {
+ const nextItem = subNavigation[navigationItemIndex + 1];
+
+ return (
+
+ );
+ }
+
+ const navigationItemFullPath = `${basePath}${navigationItem.slug ?? ''}`;
+
+ const navigationItemId = `${navigationItem.title}-${depth}-${navigationItemIndex}`;
+
+ const isNestedNavigationExpanded = expandedSidebarItemIds.includes(navigationItemId);
+
+ const nestedNavigationCollapseIcon = isNestedNavigationExpanded ? (
+
+ ) : (
+
+ );
+
+ const listItem = (
+
+
+ {navigationItem.icon}
+
+ {navigationItem.children ? nestedNavigationCollapseIcon : null}
+
+
+ );
+
+ return (
+
+ {navigationItem.slug && !navigationItem.children ? (
+
+ {listItem}
+
+ ) : (
+ listItem
+ )}
+ {navigationItem.children ? (
+
+
+
+ ) : null}
+
+ );
+ })}
+
+ );
+}
interface DashboardLayoutProps {
children: React.ReactNode;
}
export function DashboardLayout({ children }: DashboardLayoutProps) {
- return children;
+ const branding = React.useContext(BrandingContext);
+ const navigation = React.useContext(NavigationContext);
+
+ return (
+
+ theme.zIndex.drawer + 1,
+ }}
+ >
+
+
+
+
+ {branding?.logo ?? }
+
+ theme.palette.primary.main }}>
+ {branding?.title ?? 'Toolpad'}
+
+
+
+
+ {/*
+ `1px solid ${theme.palette.divider}`,
+ }}
+ >
+
+
+ */}
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+ );
}
diff --git a/packages/toolpad-core/src/DashboardLayout/ToolpadLogo.tsx b/packages/toolpad-core/src/DashboardLayout/ToolpadLogo.tsx
new file mode 100644
index 00000000000..f5ed7d6a1d8
--- /dev/null
+++ b/packages/toolpad-core/src/DashboardLayout/ToolpadLogo.tsx
@@ -0,0 +1,55 @@
+import * as React from 'react';
+
+export default function ToolpadLogo({ size = 40 }: { size?: number }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/toolpad-core/src/index.ts b/packages/toolpad-core/src/index.ts
index 0e09b0381d8..f71afdb4dcf 100644
--- a/packages/toolpad-core/src/index.ts
+++ b/packages/toolpad-core/src/index.ts
@@ -1,3 +1,4 @@
export { AppProvider } from './AppProvider';
+export type { Branding, Navigation } from './AppProvider';
export { DashboardLayout } from './DashboardLayout';
diff --git a/packages/toolpad-core/src/themes/baseTheme.ts b/packages/toolpad-core/src/themes/baseTheme.ts
new file mode 100644
index 00000000000..8ef32b02c81
--- /dev/null
+++ b/packages/toolpad-core/src/themes/baseTheme.ts
@@ -0,0 +1,112 @@
+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
new file mode 100644
index 00000000000..4ed30a92200
--- /dev/null
+++ b/packages/toolpad-core/src/themes/index.ts
@@ -0,0 +1,2 @@
+'use client';
+export * from './baseTheme';
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 228e8267565..6b1216dae67 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -291,6 +291,9 @@ importers:
'@mui/x-license':
specifier: 7.2.0
version: 7.2.0(@types/react@18.3.1)(react@18.2.0)
+ '@toolpad/core':
+ specifier: workspace:*
+ version: link:../packages/toolpad-core
'@toolpad/studio':
specifier: workspace:*
version: link:../packages/toolpad-studio
@@ -2625,7 +2628,7 @@ packages:
resolution: {integrity: sha512-eFa8up2/8cZXLIpkafhaADTXSnl7IsUFCYenRWrARBz0/qZwcT0RBXpys0LJU4+WfPoF2ZG6ew6s2V6izMCwRA==}
engines: {node: '>=6.9.0'}
peerDependencies:
- '@babel/core': ^7.24.5
+ '@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.24.5
'@babel/helper-plugin-utils': 7.24.5
@@ -2743,7 +2746,7 @@ packages:
/@docsearch/react@3.6.0(@algolia/client-search@4.23.3)(@types/react@18.3.1)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.13.0):
resolution: {integrity: sha512-HUFut4ztcVNmqy9gp/wxNbC7pTOHhgVVkHVGCACTuLhUKUhKAF9KYHJtMiLUJxEqiFLQiuri1fWF8zqwM/cu1w==}
peerDependencies:
- '@types/react': 18.2.55
+ '@types/react': '>= 16.8.0 < 19.0.0'
react: '>= 16.8.0 < 19.0.0'
react-dom: '>= 16.8.0 < 19.0.0'
search-insights: '>= 1 < 3'
@@ -4321,7 +4324,7 @@ packages:
/@mui/types@7.2.14(@types/react@18.3.1):
resolution: {integrity: sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==}
peerDependencies:
- '@types/react': 18.2.55
+ '@types/react': ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
@@ -7032,6 +7035,7 @@ packages:
/are-we-there-yet@3.0.1:
resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+ deprecated: This package is no longer supported.
dependencies:
delegates: 1.0.0
readable-stream: 3.6.2
@@ -9978,6 +9982,7 @@ packages:
/fstream@1.0.12:
resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==}
engines: {node: '>=0.6'}
+ deprecated: This package is no longer supported.
dependencies:
graceful-fs: 4.2.11
inherits: 2.0.4
@@ -10003,6 +10008,7 @@ packages:
/gauge@4.0.4:
resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+ deprecated: This package is no longer supported.
dependencies:
aproba: 2.0.0
color-support: 1.1.3
@@ -12747,6 +12753,7 @@ packages:
/npmlog@6.0.2:
resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+ deprecated: This package is no longer supported.
dependencies:
are-we-there-yet: 3.0.1
console-control-strings: 1.1.0
@@ -14089,6 +14096,7 @@ packages:
/read-package-json@6.0.4:
resolution: {integrity: sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+ deprecated: This package is no longer supported. Please use @npmcli/package-json instead.
dependencies:
glob: 10.3.15
json-parse-even-better-errors: 3.0.1