diff --git a/docs/Admin.md b/docs/Admin.md
index d2f595c4927..d77fa00494b 100644
--- a/docs/Admin.md
+++ b/docs/Admin.md
@@ -819,28 +819,31 @@ Check the [Preferences documentation](./Store.md) for more details.
Material UI supports [theming](https://mui.com/material-ui/customization/theming/). This lets you customize the look and feel of an admin by overriding fonts, colors, and spacing. You can provide a custom Material UI theme by using the `theme` prop.
-For instance, to use a dark theme by default:
+React-admin comes with 4 built-in themes: [Default](./AppTheme.md#default), [Nano](./AppTheme.md#nano), [Radiant](./AppTheme.md#radiant), and [House](./AppTheme.md#house). The [e-commerce demo](https://marmelab.com/react-admin-demo/) contains a theme switcher, so you can test them in a real application.
-```tsx
-import { defaultTheme } from 'react-admin';
+
-const theme = {
- ...defaultTheme,
- palette: { mode: 'dark' },
-};
+For instance, to use the Nano theme instead of the default theme:
+
+```tsx
+import { Admin, nanoLightTheme } from 'react-admin';
+import { dataProvider } from './dataProvider';
const App = () => (
-
+
// ...
);
```
-
+
-If you want to support both a light and a dark theme, check out [the `` prop](#darktheme).
+You can also [write your own theme](./AppTheme.md#writing-a-custom-theme) to fit your company branding. For more details on predefined and custom themes, refer to [the Application Theme chapter](./AppTheme.md).
-For more details on predefined and custom themes, refer to [the Application Theme chapter](./AppTheme.md).
+If you want to support both a light and a dark theme, check out [the `` prop](#darktheme).
## `title`
diff --git a/docs/AppTheme.md b/docs/AppTheme.md
index 270cebffc6b..4b8c1e8a0ce 100644
--- a/docs/AppTheme.md
+++ b/docs/AppTheme.md
@@ -7,11 +7,16 @@ title: "Application Theme"
If you want to override some styles across the entire application, you can use a custom theme, leveraging [the Material UI Theming support](https://mui.com/material-ui/customization/theming/). Custom themes let you override colors, fonts, spacing, and even the style of individual components.
-
+The [e-commerce demo](https://marmelab.com/react-admin-demo/) contains a theme switcher, so you can test them in a real application.
-## Using A Custom Theme
+
-Pass a custom `theme` to the `` component to override the style of the entire application:
+## Setting The Application Theme
+
+You can override the style of the entire application by passing a custom `theme` to the `` component:
```jsx
import { Admin, defaultTheme } from 'react-admin';
@@ -41,8 +46,174 @@ const App = () => (
);
```
+You can either use [built-in themes](#built-in-themes), or [write your own](#writing-a-custom-theme).
+
Note that you don't need to call Material-UI's `createTheme` yourself. React-admin will do it for you.
+## Light And Dark Themes
+
+It's a common practice to support both a light theme and a dark theme in an application, and let users choose which one they prefer.
+
+
+
+
+React-admin's `` component accepts a `darkTheme` prop in addition to the `theme` prop.
+
+```jsx
+import { Admin, defaultTheme } from 'react-admin';
+
+const lightTheme = defaultTheme;
+const darkTheme = { ...defaultTheme, palette: { mode: 'dark' } };
+
+const App = () => (
+
+ // ...
+
+);
+```
+
+With this setup, the default application theme depends on the user's system settings. If the user has chosen a dark mode in their OS, react-admin will use the dark theme. Otherwise, it will use the light theme.
+
+In addition, users can switch from one theme to the other using [the `` component](./ToggleThemeButton.md), which appears in the AppBar as soon as you define a `darkTheme` prop.
+
+## Built-In Themes
+
+React-admin comes with 4 built-in themes, each one having a light and a dark variant. You can use them as a starting point for your custom theme, or use them as-is.
+
+### Default
+
+The default theme is a good fit for every application, and works equally well on desktop and mobile.
+
+[](./img/defaultLightTheme1.jpg)
+[](./img/defaultLightTheme2.jpg)
+[](./img/defaultDarkTheme1.jpg)
+[](./img/defaultDarkTheme2.jpg)
+
+You don't need to configure anything to use the default theme - it comes out of the box with react-admin.
+
+### Nano
+
+A dense theme with minimal chrome, ideal for complex apps. It uses a small font size, reduced spacing, text buttons, standard variant inputs, pale colors. Only fit for desktop apps.
+
+[](./img/nanoLightTheme1.jpg)
+[](./img/nanoLightTheme2.jpg)
+[](./img/nanoDarkTheme1.jpg)
+[](./img/nanoDarkTheme2.jpg)
+
+To use the Nano theme, import the `nanoLightTheme` and `nanoDarkTheme` objects, and pass them to the `` component:
+
+```jsx
+import { Admin, nanoLightTheme, nanoDarkTheme } from 'react-admin';
+import { dataProvider } from './dataProvider';
+
+export const App = () => (
+
+ // ...
+
+);
+```
+
+You must also import the Onest font in your `index.html` file:
+
+```html
+
+```
+
+### Radiant
+
+A theme emphasizing clarity and ease of use. It uses generous margins, outlined inputs and buttons, no uppercase, and an acid color palette.
+
+[](./img/radiantLightTheme1.jpg)
+[](./img/radiantLightTheme2.jpg)
+[](./img/radiantDarkTheme1.jpg)
+[](./img/radiantDarkTheme2.jpg)
+
+To use the Radiant theme, import the `radiantLightTheme` and `radiantDarkTheme` objects, and pass them to the `` component:
+
+```jsx
+import { Admin, radiantLightTheme, radiantDarkTheme } from 'react-admin';
+import { dataProvider } from './dataProvider';
+
+export const App = () => (
+
+ // ...
+
+);
+```
+
+You must also import the Gabarito font in your `index.html` file:
+
+```html
+
+```
+
+### House
+
+A young and joyful theme. It uses rounded corners, blurry backdrop, large padding, and a bright color palette.
+
+[](./img/houseLightTheme1.jpg)
+[](./img/houseLightTheme2.jpg)
+[](./img/houseDarkTheme1.jpg)
+[](./img/houseDarkTheme2.jpg)
+
+To use the House theme, import the `houseLightTheme` and `houseDarkTheme` objects, and pass them to the `` component:
+
+```jsx
+import { Admin, houseLightTheme, houseDarkTheme } from 'react-admin';
+import { dataProvider } from './dataProvider';
+
+export const App = () => (
+
+ // ...
+
+);
+```
+
+You must also import the Open Sans font in your `index.html` file:
+
+```html
+
+```
+
+## Changing the Theme Programmatically
+
+React-admin provides the `useTheme` hook to read and update the theme programmatically. It uses the same syntax as `useState`. Its used internally by [the `` component](./ToggleThemeButton.md).
+
+```jsx
+import { defaultTheme, useTheme } from 'react-admin';
+import { Button } from '@mui/material';
+
+const ThemeToggler = () => {
+ const [theme, setTheme] = useTheme();
+
+ return (
+
+ );
+}
+```
+
## Theming Individual Components
In a custom theme, you can override the style of a component for the entire application using the `components` key.
@@ -133,92 +304,6 @@ const theme = {
};
```
-## Using A Dark Theme
-
-React-admin ships two base themes: light and dark. To use the dark theme, import the `darkTheme` and pass it as the `` prop:
-
-```jsx
-import { darkTheme } from 'react-admin';
-
-const App = () => (
-
- // ...
-
-);
-```
-
-
-
-Alternatively, you can create a custom theme object with a `mode: 'dark'` palette:
-
-```jsx
-import { defaultTheme } from 'react-admin';
-
-const darkTheme = {
- ...defaultTheme,
- palette: { mode: 'dark' }
-};
-
-const App = () => (
-
- // ...
-
-);
-```
-
-## Letting Users Choose The Theme
-
-It's a common practice to support both a light theme and a dark theme in an application, and let users choose which one they prefer.
-
-
-
-
-React-admin's `` component accepts a `darkTheme` prop in addition to the `theme` prop.
-
-```jsx
-import { Admin, defaultTheme } from 'react-admin';
-
-const lightTheme = defaultTheme;
-const darkTheme = { ...defaultTheme, palette: { mode: 'dark' } };
-
-const App = () => (
-
- // ...
-
-);
-```
-
-With this setup, the default application theme depends on the user's system settings. If the user has chosen a dark mode in their OS, react-admin will use the dark theme. Otherwise, it will use the light theme.
-
-In addition, users can switch from one theme to the other using [the `` component](./ToggleThemeButton.md), which appears in the AppBar as soon as you define a `darkTheme` prop.
-
-## Changing the Theme Programmatically
-
-React-admin provides the `useTheme` hook to read and update the theme programmatically. It uses the same syntax as `useState`. Its used internally by [the `` component](./ToggleThemeButton.md).
-
-```jsx
-import { defaultTheme, useTheme } from 'react-admin';
-import { Button } from '@mui/material';
-
-const ThemeToggler = () => {
- const [theme, setTheme] = useTheme();
-
- return (
-
- );
-}
-```
-
## Customizing The Sidebar Width
You can specify the `Sidebar` width by setting the `width` and `closedWidth` properties on your custom Material UI theme:
diff --git a/docs/Features.md b/docs/Features.md
index 2ef4c40de36..352fdbe7b1e 100644
--- a/docs/Features.md
+++ b/docs/Features.md
@@ -1257,7 +1257,31 @@ Check the following components for details:
The default [Material Design](https://material.io/) look and feel is nice, but a bit... Google-y. If this bothers you, or if you need to brand your app, rest assured: react-admin is fully themeable.
-For instance, you can use react-admin to build a [Music Player](https://demo.navidrome.org/app/):
+React-admin comes with 4 built-in themes: [Default](./AppTheme.md#default), [Nano](./AppTheme.md#nano), [Radiant](./AppTheme.md#radiant), and [House](./AppTheme.md#house). The [e-commerce demo](https://marmelab.com/react-admin-demo/) contains a theme switcher, so you can test them in a real application.
+
+
+
+To use a custom theme, pass a theme object to the `` [`theme`](./Admin.md#theme) and [`darkTheme`](./Admin.md#darktheme) props:
+
+```jsx
+import { Admin, nanoLightTheme, nanoDarkTheme } from 'react-admin';
+import { dataProvider } from './dataProvider';
+
+export const App = () => (
+
+ // ...
+
+);
+```
+
+Theming is so powerful that you can even use react-admin to build a [Music Player](https://demo.navidrome.org/app/):

@@ -1357,9 +1381,12 @@ const App = () => (
To learn more about theming in react-admin, check the following sections:
+- [Introduction to Theming](./Theming.md)
+- [Page Layouts](./Theming.md#customizing-the-page-layout)
- [The `sx` prop](./SX.md)
-- [App-wide component overrides](./AppTheme.md#theming-individual-components)
-- [Writing a custom theme](./AppTheme.md)
+- [Built-In Themes](./AppTheme.md#built-in-themes)
+- [App-wide theming](./AppTheme.md#theming-individual-components)
+- [Helper Components For Layouts](./BoxStackGrid.md)
- [``](./ToggleThemeButton.md)
- [`useTheme`](./useTheme.md)
- [`useMediaQuery`](./useMediaQuery.md)
diff --git a/docs/Theming.md b/docs/Theming.md
index 2fa26138cfe..b9be1c828da 100644
--- a/docs/Theming.md
+++ b/docs/Theming.md
@@ -119,6 +119,13 @@ const App = () => (
);
```
+React-admin comes with 4 built-in themes: [Default](./AppTheme.md#default), [Nano](./AppTheme.md#nano), [Radiant](./AppTheme.md#radiant), and [House](./AppTheme.md#house). The [e-commerce demo](https://marmelab.com/react-admin-demo/) contains a theme switcher, so you can test them in a real application.
+
+
+
The application theme lets you customize color, typography, spacing, and component defaults. Check the [dedicated Application Theme chapter](./AppTheme.md) for more information.
## Customizing The Page Layout
diff --git a/docs/img/defaultDarkTheme1.jpg b/docs/img/defaultDarkTheme1.jpg
new file mode 100644
index 00000000000..2279fe194af
Binary files /dev/null and b/docs/img/defaultDarkTheme1.jpg differ
diff --git a/docs/img/defaultDarkTheme2.jpg b/docs/img/defaultDarkTheme2.jpg
new file mode 100644
index 00000000000..5037f70b9cf
Binary files /dev/null and b/docs/img/defaultDarkTheme2.jpg differ
diff --git a/docs/img/defaultLightTheme1.jpg b/docs/img/defaultLightTheme1.jpg
new file mode 100644
index 00000000000..63acbd540d2
Binary files /dev/null and b/docs/img/defaultLightTheme1.jpg differ
diff --git a/docs/img/defaultLightTheme2.jpg b/docs/img/defaultLightTheme2.jpg
new file mode 100644
index 00000000000..95db234d151
Binary files /dev/null and b/docs/img/defaultLightTheme2.jpg differ
diff --git a/docs/img/demo-themes.mp4 b/docs/img/demo-themes.mp4
new file mode 100644
index 00000000000..2a615c927ac
Binary files /dev/null and b/docs/img/demo-themes.mp4 differ
diff --git a/docs/img/houseDarkTheme1.jpg b/docs/img/houseDarkTheme1.jpg
new file mode 100644
index 00000000000..d7adb01e811
Binary files /dev/null and b/docs/img/houseDarkTheme1.jpg differ
diff --git a/docs/img/houseDarkTheme2.jpg b/docs/img/houseDarkTheme2.jpg
new file mode 100644
index 00000000000..e3b843280a4
Binary files /dev/null and b/docs/img/houseDarkTheme2.jpg differ
diff --git a/docs/img/houseLightTheme1.jpg b/docs/img/houseLightTheme1.jpg
new file mode 100644
index 00000000000..b3a53308880
Binary files /dev/null and b/docs/img/houseLightTheme1.jpg differ
diff --git a/docs/img/houseLightTheme2.jpg b/docs/img/houseLightTheme2.jpg
new file mode 100644
index 00000000000..b1c4fe95df5
Binary files /dev/null and b/docs/img/houseLightTheme2.jpg differ
diff --git a/docs/img/nanoDarkTheme1.jpg b/docs/img/nanoDarkTheme1.jpg
new file mode 100644
index 00000000000..6874e55d9cf
Binary files /dev/null and b/docs/img/nanoDarkTheme1.jpg differ
diff --git a/docs/img/nanoDarkTheme2.jpg b/docs/img/nanoDarkTheme2.jpg
new file mode 100644
index 00000000000..1509576038b
Binary files /dev/null and b/docs/img/nanoDarkTheme2.jpg differ
diff --git a/docs/img/nanoLightTheme1.jpg b/docs/img/nanoLightTheme1.jpg
new file mode 100644
index 00000000000..f0aa5a98d24
Binary files /dev/null and b/docs/img/nanoLightTheme1.jpg differ
diff --git a/docs/img/nanoLightTheme2.jpg b/docs/img/nanoLightTheme2.jpg
new file mode 100644
index 00000000000..36f1f653f4a
Binary files /dev/null and b/docs/img/nanoLightTheme2.jpg differ
diff --git a/docs/img/radiantDarkTheme1.jpg b/docs/img/radiantDarkTheme1.jpg
new file mode 100644
index 00000000000..ef9afcdec20
Binary files /dev/null and b/docs/img/radiantDarkTheme1.jpg differ
diff --git a/docs/img/radiantDarkTheme2.jpg b/docs/img/radiantDarkTheme2.jpg
new file mode 100644
index 00000000000..ed9d25c459b
Binary files /dev/null and b/docs/img/radiantDarkTheme2.jpg differ
diff --git a/docs/img/radiantLightTheme1.jpg b/docs/img/radiantLightTheme1.jpg
new file mode 100644
index 00000000000..09fd9e3923f
Binary files /dev/null and b/docs/img/radiantLightTheme1.jpg differ
diff --git a/docs/img/radiantLightTheme2.jpg b/docs/img/radiantLightTheme2.jpg
new file mode 100644
index 00000000000..228f29bb8a2
Binary files /dev/null and b/docs/img/radiantLightTheme2.jpg differ
diff --git a/examples/demo/index.html b/examples/demo/index.html
index 3812163d880..f52b4ec50d2 100644
--- a/examples/demo/index.html
+++ b/examples/demo/index.html
@@ -111,6 +111,10 @@
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
rel="stylesheet"
/>
+
+
+
+
diff --git a/examples/demo/src/App.tsx b/examples/demo/src/App.tsx
index 04ddb66c240..6a9fa80a319 100644
--- a/examples/demo/src/App.tsx
+++ b/examples/demo/src/App.tsx
@@ -1,5 +1,12 @@
import polyglotI18nProvider from 'ra-i18n-polyglot';
-import { Admin, CustomRoutes, Resource, localStorageStore } from 'react-admin';
+import {
+ Admin,
+ CustomRoutes,
+ Resource,
+ localStorageStore,
+ useStore,
+ StoreContextProvider,
+} from 'react-admin';
import { Route } from 'react-router';
import authProvider from './authProvider';
@@ -9,12 +16,12 @@ import dataProviderFactory from './dataProvider';
import englishMessages from './i18n/en';
import invoices from './invoices';
import { Layout, Login } from './layout';
-import { darkTheme, lightTheme } from './layout/themes';
import orders from './orders';
import products from './products';
import reviews from './reviews';
import Segments from './segments/Segments';
import visitors from './visitors';
+import { themes, ThemeName } from './themes/themes';
const i18nProvider = polyglotI18nProvider(
locale => {
@@ -32,33 +39,50 @@ const i18nProvider = polyglotI18nProvider(
]
);
-const App = () => (
-
-
- } />
-
-
-
-
-
-
-
-
+const store = localStorageStore(undefined, 'ECommerce');
+
+const App = () => {
+ const [themeName] = useStore('themeName', 'soft');
+ const lightTheme = themes.find(theme => theme.name === themeName)?.light;
+ const darkTheme = themes.find(theme => theme.name === themeName)?.dark;
+ return (
+
+
+ } />
+
+
+
+
+
+
+
+
+ );
+};
+
+const AppWrapper = () => (
+
+
+
);
-export default App;
+export default AppWrapper;
diff --git a/examples/demo/src/dashboard/CardWithIcon.tsx b/examples/demo/src/dashboard/CardWithIcon.tsx
index 03125739afc..3878b17eeeb 100644
--- a/examples/demo/src/dashboard/CardWithIcon.tsx
+++ b/examples/demo/src/dashboard/CardWithIcon.tsx
@@ -4,9 +4,6 @@ import { Card, Box, Typography, Divider } from '@mui/material';
import { Link, To } from 'react-router-dom';
import { ReactNode } from 'react';
-import cartouche from './cartouche.png';
-import cartoucheDark from './cartoucheDark.png';
-
interface Props {
icon: FC;
to: To;
@@ -31,22 +28,27 @@ const CardWithIcon = ({ icon, title, subtitle, to, children }: Props) => (
- `url(${
- theme.palette.mode === 'dark'
- ? cartoucheDark
- : cartouche
- }) no-repeat`,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
'& .icon': {
- color: theme =>
- theme.palette.mode === 'dark'
- ? 'inherit'
- : '#dc2440',
+ color: 'secondary.main',
+ },
+ '&:before': {
+ position: 'absolute',
+ top: '50%',
+ left: 0,
+ display: 'block',
+ content: `''`,
+ height: '200%',
+ aspectRatio: '1',
+ transform: 'translate(-30%, -60%)',
+ borderRadius: '50%',
+ backgroundColor: 'secondary.main',
+ opacity: 0.15,
},
}}
>
diff --git a/examples/demo/src/dashboard/Welcome.tsx b/examples/demo/src/dashboard/Welcome.tsx
index 123b67988c8..f48bc089839 100644
--- a/examples/demo/src/dashboard/Welcome.tsx
+++ b/examples/demo/src/dashboard/Welcome.tsx
@@ -8,16 +8,12 @@ import publishArticleImage from './welcome_illustration.svg';
const Welcome = () => {
const translate = useTranslate();
-
return (
- theme.palette.mode === 'dark'
- ? '#535353'
- : `linear-gradient(to right, #8975fb 0%, #746be7 35%), linear-gradient(to bottom, #8975fb 0%, #6f4ceb 50%), #6f4ceb`,
-
- color: '#fff',
+ `linear-gradient(45deg, ${theme.palette.secondary.dark} 0%, ${theme.palette.secondary.light} 50%, ${theme.palette.primary.dark} 100%)`,
+ color: theme => theme.palette.primary.contrastText,
padding: '20px',
marginTop: 2,
marginBottom: '1em',
diff --git a/examples/demo/src/dashboard/cartouche.png b/examples/demo/src/dashboard/cartouche.png
deleted file mode 100644
index 06de31c0a16..00000000000
Binary files a/examples/demo/src/dashboard/cartouche.png and /dev/null differ
diff --git a/examples/demo/src/dashboard/cartoucheDark.png b/examples/demo/src/dashboard/cartoucheDark.png
deleted file mode 100644
index 98d1f76e8ea..00000000000
Binary files a/examples/demo/src/dashboard/cartoucheDark.png and /dev/null differ
diff --git a/examples/demo/src/layout/AppBar.tsx b/examples/demo/src/layout/AppBar.tsx
index 735a7defd8d..0fa7a5231ed 100644
--- a/examples/demo/src/layout/AppBar.tsx
+++ b/examples/demo/src/layout/AppBar.tsx
@@ -3,13 +3,14 @@ import { AppBar, TitlePortal } from 'react-admin';
import { Box, useMediaQuery, Theme } from '@mui/material';
import Logo from './Logo';
+import { AppBarToolbar } from './AppBarToolbar';
const CustomAppBar = () => {
const isLargeEnough = useMediaQuery(theme =>
theme.breakpoints.up('sm')
);
return (
-
+ }>
{isLargeEnough && }
{isLargeEnough && }
diff --git a/examples/demo/src/layout/AppBarToolbar.tsx b/examples/demo/src/layout/AppBarToolbar.tsx
new file mode 100644
index 00000000000..ea23bef063b
--- /dev/null
+++ b/examples/demo/src/layout/AppBarToolbar.tsx
@@ -0,0 +1,11 @@
+import { LoadingIndicator, LocalesMenuButton } from 'react-admin';
+
+import { ThemeSwapper } from '../themes/ThemeSwapper';
+
+export const AppBarToolbar = () => (
+ <>
+
+
+
+ >
+);
diff --git a/examples/demo/src/layout/SubMenu.tsx b/examples/demo/src/layout/SubMenu.tsx
index d72d57ef878..86442c66dbc 100644
--- a/examples/demo/src/layout/SubMenu.tsx
+++ b/examples/demo/src/layout/SubMenu.tsx
@@ -52,10 +52,13 @@ const SubMenu = (props: Props) => {
component="div"
disablePadding
sx={{
- '& a': {
+ '& .MuiMenuItem-root': {
transition:
'padding-left 195ms cubic-bezier(0.4, 0, 0.6, 1) 0ms',
- paddingLeft: sidebarIsOpen ? 4 : 2,
+ paddingLeft: theme =>
+ sidebarIsOpen
+ ? theme.spacing(4)
+ : theme.spacing(2),
},
}}
>
diff --git a/examples/demo/src/reviews/AcceptButton.tsx b/examples/demo/src/reviews/AcceptButton.tsx
index 454a11dc05f..f7acbcf3b23 100644
--- a/examples/demo/src/reviews/AcceptButton.tsx
+++ b/examples/demo/src/reviews/AcceptButton.tsx
@@ -45,7 +45,10 @@ const AcceptButton = () => {
color="primary"
size="small"
onClick={() => approve()}
- startIcon={}
+ sx={{ borderColor: theme => theme.palette.success.main }}
+ startIcon={
+ theme.palette.success.main }} />
+ }
disabled={isLoading}
>
{translate('resources.reviews.action.accept')}
diff --git a/examples/demo/src/reviews/RejectButton.tsx b/examples/demo/src/reviews/RejectButton.tsx
index 9ec62738f4a..6a70bfb7df4 100644
--- a/examples/demo/src/reviews/RejectButton.tsx
+++ b/examples/demo/src/reviews/RejectButton.tsx
@@ -46,7 +46,10 @@ const RejectButton = () => {
color="primary"
size="small"
onClick={() => reject()}
- startIcon={}
+ sx={{ borderColor: theme => theme.palette.error.main }}
+ startIcon={
+ theme.palette.error.main }} />
+ }
disabled={isLoading}
>
{translate('resources.reviews.action.reject')}
diff --git a/examples/demo/src/reviews/ReviewEditToolbar.tsx b/examples/demo/src/reviews/ReviewEditToolbar.tsx
index edc8944d412..424f4b931be 100644
--- a/examples/demo/src/reviews/ReviewEditToolbar.tsx
+++ b/examples/demo/src/reviews/ReviewEditToolbar.tsx
@@ -25,7 +25,6 @@ const ReviewEditToolbar = (props: ToolbarProps) => {
return (
{
+ const [anchorEl, setAnchorEl] = useState(null);
+ const open = Boolean(anchorEl);
+ const handleClick = (event: React.MouseEvent) => {
+ setAnchorEl(event.currentTarget);
+ };
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+
+ const [themeName, setThemeName] = useStore('themeName', 'soft');
+ const handleChange = (_: React.MouseEvent, index: number) => {
+ const newTheme = themes[index];
+ setThemeName(newTheme.name);
+ setAnchorEl(null);
+ };
+ const currentTheme = themes.find(theme => theme.name === themeName);
+
+ const translate = useTranslate();
+ const toggleThemeTitle = translate('pos.action.change_theme', {
+ _: 'Change Theme',
+ });
+
+ return (
+ <>
+
+
+
+
+
+ {currentTheme?.dark ? : null}
+
+ >
+ );
+};
+
+const ucFirst = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
diff --git a/examples/demo/src/themes/chiptuneTheme.ts b/examples/demo/src/themes/chiptuneTheme.ts
new file mode 100644
index 00000000000..a702483d917
--- /dev/null
+++ b/examples/demo/src/themes/chiptuneTheme.ts
@@ -0,0 +1,19 @@
+import { RaThemeOptions } from 'react-admin';
+
+/** Just for fun */
+
+export const chiptuneTheme: RaThemeOptions = {
+ palette: {
+ mode: 'dark' as 'dark',
+ primary: {
+ main: '#0f0',
+ },
+ background: {
+ default: '#111111',
+ paper: '#212121',
+ },
+ },
+ typography: {
+ fontFamily: `'Pixelify Sans', cursive`,
+ },
+};
diff --git a/examples/demo/src/layout/themes.ts b/examples/demo/src/themes/softTheme.ts
similarity index 86%
rename from examples/demo/src/layout/themes.ts
rename to examples/demo/src/themes/softTheme.ts
index e1311694370..d94fa763dd8 100644
--- a/examples/demo/src/layout/themes.ts
+++ b/examples/demo/src/themes/softTheme.ts
@@ -1,6 +1,12 @@
import { defaultTheme } from 'react-admin';
-export const darkTheme = {
+/**
+ * Soft: A gentle theme for apps with rich content (images, charts, maps, etc).
+ *
+ * Uses white app bar, rounder corners, light colors.
+ */
+
+export const softDarkTheme = {
palette: {
primary: {
main: '#90caf9',
@@ -29,14 +35,17 @@ export const darkTheme = {
styleOverrides: {
colorSecondary: {
color: '#ffffffb3',
- backgroundColor: '#616161e6',
+ backgroundColor: '#616161',
},
},
+ defaultProps: {
+ elevation: 1,
+ },
},
},
};
-export const lightTheme = {
+export const softLightTheme = {
palette: {
primary: {
main: '#4f3cc9',
@@ -88,6 +97,9 @@ export const lightTheme = {
backgroundColor: '#fff',
},
},
+ defaultProps: {
+ elevation: 1,
+ },
},
MuiLinearProgress: {
styleOverrides: {
diff --git a/examples/demo/src/themes/themes.tsx b/examples/demo/src/themes/themes.tsx
new file mode 100644
index 00000000000..f4a2a203258
--- /dev/null
+++ b/examples/demo/src/themes/themes.tsx
@@ -0,0 +1,37 @@
+import {
+ RaThemeOptions,
+ defaultLightTheme,
+ defaultDarkTheme,
+ nanoDarkTheme,
+ nanoLightTheme,
+ radiantDarkTheme,
+ radiantLightTheme,
+ houseDarkTheme,
+ houseLightTheme,
+} from 'react-admin';
+
+import { softDarkTheme, softLightTheme } from './softTheme';
+import { chiptuneTheme } from './chiptuneTheme';
+
+export type ThemeName =
+ | 'soft'
+ | 'default'
+ | 'nano'
+ | 'radiant'
+ | 'house'
+ | 'chiptune';
+
+export interface Theme {
+ name: ThemeName;
+ light: RaThemeOptions;
+ dark?: RaThemeOptions;
+}
+
+export const themes: Theme[] = [
+ { name: 'soft', light: softLightTheme, dark: softDarkTheme },
+ { name: 'default', light: defaultLightTheme, dark: defaultDarkTheme },
+ { name: 'nano', light: nanoLightTheme, dark: nanoDarkTheme },
+ { name: 'radiant', light: radiantLightTheme, dark: radiantDarkTheme },
+ { name: 'house', light: houseLightTheme, dark: houseDarkTheme },
+ { name: 'chiptune', light: chiptuneTheme },
+];
diff --git a/examples/demo/src/visitors/VisitorList.tsx b/examples/demo/src/visitors/VisitorList.tsx
index dea9ef06a37..8ad74f73a11 100644
--- a/examples/demo/src/visitors/VisitorList.tsx
+++ b/examples/demo/src/visitors/VisitorList.tsx
@@ -56,7 +56,6 @@ const VisitorList = () => {
{
const {
diff --git a/packages/ra-ui-materialui/src/auth/AuthError.stories.tsx b/packages/ra-ui-materialui/src/auth/AuthError.stories.tsx
index 4c512914d63..41379ede21c 100644
--- a/packages/ra-ui-materialui/src/auth/AuthError.stories.tsx
+++ b/packages/ra-ui-materialui/src/auth/AuthError.stories.tsx
@@ -3,7 +3,7 @@ import { I18nContextProvider } from 'ra-core';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import { AuthError } from './AuthError';
-import { defaultTheme } from '../defaultTheme';
+import { defaultTheme } from '../theme/defaultTheme';
import { createTheme, ThemeProvider } from '@mui/material';
export default { title: 'ra-ui-materialui/auth/AuthError' };
diff --git a/packages/ra-ui-materialui/src/button/LocalesMenuButton.tsx b/packages/ra-ui-materialui/src/button/LocalesMenuButton.tsx
index 8112a364f02..f60e345f23a 100644
--- a/packages/ra-ui-materialui/src/button/LocalesMenuButton.tsx
+++ b/packages/ra-ui-materialui/src/button/LocalesMenuButton.tsx
@@ -48,6 +48,7 @@ export const LocalesMenuButton = (props: LocalesMenuButtonProps) => {
@@ -171,8 +172,8 @@ const Root = styled('div', {
},
[`& .${UserMenuClasses.avatar}`]: {
- width: theme.spacing(4),
- height: theme.spacing(4),
+ width: theme.spacing(3),
+ height: theme.spacing(3),
},
}));
diff --git a/packages/ra-ui-materialui/src/layout/index.ts b/packages/ra-ui-materialui/src/layout/index.ts
index 980cba31b9f..9317450f70f 100644
--- a/packages/ra-ui-materialui/src/layout/index.ts
+++ b/packages/ra-ui-materialui/src/layout/index.ts
@@ -21,7 +21,6 @@ export * from './ResourceMenuItem';
export * from './ResourceMenuItems';
export * from './Sidebar';
export * from './SidebarToggleButton';
-export * from './Theme';
export * from './Title';
export * from './TitlePortal';
export * from './TopToolbar';
diff --git a/packages/ra-ui-materialui/src/list/List.spec.tsx b/packages/ra-ui-materialui/src/list/List.spec.tsx
index b83617119f9..2cbd719954a 100644
--- a/packages/ra-ui-materialui/src/list/List.spec.tsx
+++ b/packages/ra-ui-materialui/src/list/List.spec.tsx
@@ -5,7 +5,7 @@ import { CoreAdminContext, testDataProvider, useListContext } from 'ra-core';
import { createMemoryHistory } from 'history';
import { createTheme, ThemeProvider } from '@mui/material/styles';
-import { defaultTheme } from '../defaultTheme';
+import { defaultTheme } from '../theme/defaultTheme';
import { List } from './List';
import { Filter } from './filter';
import { TextInput } from '../input';
diff --git a/packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx b/packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx
index cce02e14231..252a7cf230e 100644
--- a/packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx
+++ b/packages/ra-ui-materialui/src/list/ListGuesser.spec.tsx
@@ -4,7 +4,7 @@ import { render, screen, waitFor } from '@testing-library/react';
import { CoreAdminContext } from 'ra-core';
import { ListGuesser } from './ListGuesser';
-import { ThemeProvider } from '../layout';
+import { ThemeProvider } from '../theme/ThemeProvider';
describe('', () => {
it('should log the guessed List view based on the fetched records', async () => {
diff --git a/packages/ra-ui-materialui/src/list/filter/FilterLiveSearch.stories.tsx b/packages/ra-ui-materialui/src/list/filter/FilterLiveSearch.stories.tsx
index 38e23f8fe34..25a885ba107 100644
--- a/packages/ra-ui-materialui/src/list/filter/FilterLiveSearch.stories.tsx
+++ b/packages/ra-ui-materialui/src/list/filter/FilterLiveSearch.stories.tsx
@@ -10,7 +10,7 @@ import {
} from '@mui/material';
import { FilterLiveSearch } from './FilterLiveSearch';
-import { defaultTheme } from '../../defaultTheme';
+import { defaultTheme } from '../../theme/defaultTheme';
export default {
title: 'ra-ui-materialui/list/filter/FilterLiveSearch',
diff --git a/packages/ra-ui-materialui/src/layout/Theme/ThemeProvider.spec.tsx b/packages/ra-ui-materialui/src/theme/ThemeProvider.spec.tsx
similarity index 98%
rename from packages/ra-ui-materialui/src/layout/Theme/ThemeProvider.spec.tsx
rename to packages/ra-ui-materialui/src/theme/ThemeProvider.spec.tsx
index 27795700cf9..1488d592da4 100644
--- a/packages/ra-ui-materialui/src/layout/Theme/ThemeProvider.spec.tsx
+++ b/packages/ra-ui-materialui/src/theme/ThemeProvider.spec.tsx
@@ -6,7 +6,7 @@ import { Button, ThemeOptions } from '@mui/material';
import { ThemeProvider } from './ThemeProvider';
import { ThemesContext } from './ThemesContext';
-import { ThemeTestWrapper } from '../ThemeTestWrapper';
+import { ThemeTestWrapper } from '../layout/ThemeTestWrapper';
const lightTheme: ThemeOptions = {};
const darkTheme: ThemeOptions = { palette: { mode: 'dark' } };
diff --git a/packages/ra-ui-materialui/src/layout/Theme/ThemeProvider.tsx b/packages/ra-ui-materialui/src/theme/ThemeProvider.tsx
similarity index 100%
rename from packages/ra-ui-materialui/src/layout/Theme/ThemeProvider.tsx
rename to packages/ra-ui-materialui/src/theme/ThemeProvider.tsx
diff --git a/packages/ra-ui-materialui/src/layout/Theme/ThemesContext.ts b/packages/ra-ui-materialui/src/theme/ThemesContext.ts
similarity index 100%
rename from packages/ra-ui-materialui/src/layout/Theme/ThemesContext.ts
rename to packages/ra-ui-materialui/src/theme/ThemesContext.ts
diff --git a/packages/ra-ui-materialui/src/theme/defaultTheme.stories.tsx b/packages/ra-ui-materialui/src/theme/defaultTheme.stories.tsx
new file mode 100644
index 00000000000..e0ceefc9338
--- /dev/null
+++ b/packages/ra-ui-materialui/src/theme/defaultTheme.stories.tsx
@@ -0,0 +1,47 @@
+import * as React from 'react';
+import { Resource } from 'ra-core';
+import fakerestDataProvider from 'ra-data-fakerest';
+import englishTranslations from 'ra-language-english';
+import polyglotI18nProvider from 'ra-i18n-polyglot';
+
+import { AdminContext } from '../AdminContext';
+import { AdminUI } from '../AdminUI';
+import { ListGuesser } from '../list';
+import { EditGuesser } from '../detail';
+import { defaultLightTheme, defaultDarkTheme } from './defaultTheme';
+import { testData } from './testData';
+
+export default {
+ title: 'ra-ui-materialui/theme/Default',
+};
+
+export const Default = () => (
+ englishTranslations, 'en')}
+ >
+
+
+
+
+
+
+);
diff --git a/packages/ra-ui-materialui/src/defaultTheme.ts b/packages/ra-ui-materialui/src/theme/defaultTheme.ts
similarity index 97%
rename from packages/ra-ui-materialui/src/defaultTheme.ts
rename to packages/ra-ui-materialui/src/theme/defaultTheme.ts
index fb140cd80c9..5ef653048ab 100644
--- a/packages/ra-ui-materialui/src/defaultTheme.ts
+++ b/packages/ra-ui-materialui/src/theme/defaultTheme.ts
@@ -1,4 +1,4 @@
-import { RaThemeOptions } from './layout/Theme';
+import { RaThemeOptions } from './types';
const defaultThemeInvariants = {
typography: {
diff --git a/packages/ra-ui-materialui/src/theme/houseTheme.stories.tsx b/packages/ra-ui-materialui/src/theme/houseTheme.stories.tsx
new file mode 100644
index 00000000000..4c149e7afcf
--- /dev/null
+++ b/packages/ra-ui-materialui/src/theme/houseTheme.stories.tsx
@@ -0,0 +1,47 @@
+import * as React from 'react';
+import { Resource } from 'ra-core';
+import fakerestDataProvider from 'ra-data-fakerest';
+import englishTranslations from 'ra-language-english';
+import polyglotI18nProvider from 'ra-i18n-polyglot';
+
+import { AdminContext } from '../AdminContext';
+import { AdminUI } from '../AdminUI';
+import { ListGuesser } from '../list';
+import { EditGuesser } from '../detail';
+import { houseLightTheme, houseDarkTheme } from './houseTheme';
+import { testData } from './testData';
+
+export default {
+ title: 'ra-ui-materialui/theme/House',
+};
+
+export const House = () => (
+ englishTranslations, 'en')}
+ >
+
+
+
+
+
+
+);
diff --git a/packages/ra-ui-materialui/src/theme/houseTheme.ts b/packages/ra-ui-materialui/src/theme/houseTheme.ts
new file mode 100644
index 00000000000..76a1c51e32f
--- /dev/null
+++ b/packages/ra-ui-materialui/src/theme/houseTheme.ts
@@ -0,0 +1,189 @@
+import {
+ alpha,
+ createTheme,
+ darken,
+ Theme,
+ PaletteOptions,
+} from '@mui/material';
+import { RaThemeOptions } from './types';
+
+/**
+ * House: A young and joyful theme.
+ *
+ * Uses rounded corners, blurry backdrop, large padding, and a bright color palette.
+ */
+
+const componentsOverrides = (theme: Theme) => ({
+ MuiBackdrop: {
+ styleOverrides: {
+ root: {
+ backgroundColor: alpha(darken('#000C57', 0.4), 0.2),
+ backdropFilter: 'blur(2px)',
+ '&.MuiBackdrop-invisible': {
+ backgroundColor: 'transparent',
+ backdropFilter: 'blur(2px)',
+ },
+ },
+ },
+ },
+ MuiFormControl: {
+ defaultProps: {
+ margin: 'dense' as const,
+ },
+ },
+ MuiOutlinedInput: {
+ styleOverrides: {
+ input: {
+ padding: `${theme.spacing(1)} ${theme.spacing(2)}`,
+ },
+ },
+ },
+ MuiTab: {
+ styleOverrides: {
+ root: {
+ padding: 0,
+ height: 38,
+ minHeight: 38,
+ borderRadius: 6,
+ transition: 'color .2s',
+
+ '&.MuiButtonBase-root': {
+ minWidth: 'auto',
+ paddingLeft: 20,
+ paddingRight: 20,
+ marginRight: 4,
+ },
+ '&.Mui-selected, &.Mui-selected:hover': {
+ color: theme.palette.primary.contrastText,
+ zIndex: 5,
+ },
+ '&:hover': {
+ color: theme.palette.primary.main,
+ },
+ },
+ },
+ },
+ MuiTableRow: {
+ styleOverrides: {
+ root: {
+ '&:last-child td': { border: 0 },
+ },
+ },
+ },
+ MuiTableCell: {
+ styleOverrides: {
+ root: {
+ padding: theme.spacing(2),
+ '&.MuiTableCell-sizeSmall': {
+ padding: theme.spacing(1.5),
+ },
+ '&.MuiTableCell-paddingNone': {
+ padding: theme.spacing(0.5),
+ },
+ },
+ },
+ },
+ MuiTabs: {
+ styleOverrides: {
+ root: {
+ height: 38,
+ minHeight: 38,
+ overflow: 'visible',
+ },
+ indicator: {
+ height: 38,
+ minHeight: 38,
+ borderRadius: 6,
+ border: `1px solid ${theme.palette.primary.light}`,
+ boxShadow: theme.shadows[1],
+ },
+ scrollableX: {
+ overflow: 'visible !important',
+ },
+ },
+ },
+ MuiTextField: {
+ defaultProps: {
+ variant: 'outlined' as const,
+ },
+ },
+ RaAppBar: {
+ styleOverrides: {
+ root: {
+ color: theme.palette.text.primary,
+ '& .RaAppBar-toolbar': {
+ backgroundColor: theme.palette.primary.main,
+ color: theme.palette.background.default,
+ backgroundImage: `linear-gradient(310deg, ${theme.palette.primary.light}, ${theme.palette.secondary.main})`,
+ },
+ },
+ },
+ },
+ RaMenuItemLink: {
+ styleOverrides: {
+ root: {
+ padding: 10,
+ marginRight: 10,
+ marginLeft: 10,
+ '&:hover': {
+ borderRadius: 5,
+ },
+ '&.RaMenuItemLink-active': {
+ borderRadius: 10,
+ backgroundColor: theme.palette.common.white,
+ color: theme.palette.primary.main,
+ '&:before': {
+ content: '""',
+ position: 'absolute',
+ top: '0; right: 0; bottom: 0; left: 0',
+ zIndex: '-1',
+ margin: '-2px',
+ borderRadius: '12px',
+ background: `linear-gradient(310deg, ${theme.palette.primary.light}, ${theme.palette.secondary.main})`,
+ },
+ '& .MuiSvgIcon-root': {
+ fill: theme.palette.primary.main,
+ },
+ },
+ },
+ },
+ },
+});
+
+const alert = {
+ error: { main: '#DB488B' },
+ warning: { main: '#8C701B' },
+ info: { main: '#3ED0EB' },
+ success: { main: '#0FBF9F' },
+};
+
+const darkPalette: PaletteOptions = {
+ primary: { main: '#ec7a77', light: '#fbcf33' },
+ background: { default: '#363D40', paper: '#2B3033' },
+ ...alert,
+ mode: 'dark' as 'dark',
+};
+
+const lightPalette: PaletteOptions = {
+ primary: { main: '#344767', light: '#7928ca' },
+ secondary: { main: '#f90283' },
+ background: { default: '#f7f8f9', paper: '#ffffff' },
+ ...alert,
+ mode: 'light' as 'light',
+};
+
+const createHouseTheme = (palette: RaThemeOptions['palette']) => {
+ const themeOptions = {
+ palette,
+ shape: { borderRadius: 20 },
+ sidebar: { width: 250 },
+ spacing: 9,
+ typography: { fontFamily: `'Open Sans', sans-serif` },
+ };
+ const theme = createTheme(themeOptions);
+ theme.components = componentsOverrides(theme);
+ return theme;
+};
+
+export const houseLightTheme = createHouseTheme(lightPalette);
+export const houseDarkTheme = createHouseTheme(darkPalette);
diff --git a/packages/ra-ui-materialui/src/layout/Theme/index.ts b/packages/ra-ui-materialui/src/theme/index.ts
similarity index 55%
rename from packages/ra-ui-materialui/src/layout/Theme/index.ts
rename to packages/ra-ui-materialui/src/theme/index.ts
index 384268cb02d..cc150799c0e 100644
--- a/packages/ra-ui-materialui/src/layout/Theme/index.ts
+++ b/packages/ra-ui-materialui/src/theme/index.ts
@@ -3,3 +3,7 @@ export * from './ThemeProvider';
export * from './ThemesContext';
export * from './useThemesContext';
export * from './types';
+export * from './defaultTheme';
+export * from './nanoTheme';
+export * from './radiantTheme';
+export * from './houseTheme';
diff --git a/packages/ra-ui-materialui/src/theme/nanoTheme.stories.tsx b/packages/ra-ui-materialui/src/theme/nanoTheme.stories.tsx
new file mode 100644
index 00000000000..8f2f3be7952
--- /dev/null
+++ b/packages/ra-ui-materialui/src/theme/nanoTheme.stories.tsx
@@ -0,0 +1,47 @@
+import * as React from 'react';
+import { Resource } from 'ra-core';
+import fakerestDataProvider from 'ra-data-fakerest';
+import englishTranslations from 'ra-language-english';
+import polyglotI18nProvider from 'ra-i18n-polyglot';
+
+import { AdminContext } from '../AdminContext';
+import { AdminUI } from '../AdminUI';
+import { ListGuesser } from '../list';
+import { EditGuesser } from '../detail';
+import { nanoLightTheme, nanoDarkTheme } from './nanoTheme';
+import { testData } from './testData';
+
+export default {
+ title: 'ra-ui-materialui/theme/Nano',
+};
+
+export const Nano = () => (
+ englishTranslations, 'en')}
+ >
+
+
+
+
+
+
+);
diff --git a/packages/ra-ui-materialui/src/theme/nanoTheme.ts b/packages/ra-ui-materialui/src/theme/nanoTheme.ts
new file mode 100644
index 00000000000..391b21085dd
--- /dev/null
+++ b/packages/ra-ui-materialui/src/theme/nanoTheme.ts
@@ -0,0 +1,358 @@
+import { createTheme, PaletteOptions, Theme } from '@mui/material';
+import { RaThemeOptions } from './types';
+
+/**
+ * Nano: A dense theme with minimal chrome, ideal for complex apps.
+ *
+ * Uses a small font size, reduced spacing, text buttons, standard variant inputs, pale colors.
+ */
+
+const componentsOverrides = (theme: Theme) => ({
+ MuiAlert: {
+ defaultProps: {
+ variant: 'outlined' as const,
+ },
+ },
+ MuiAppBar: {
+ defaultProps: {
+ elevation: 1,
+ },
+ },
+ MuiAutocomplete: {
+ variants: [
+ {
+ props: {},
+ style: ({ theme }: { theme: Theme }) => ({
+ [theme.breakpoints.down('sm')]: { width: '100%' },
+ }),
+ },
+ ],
+ styleOverrides: {
+ root: {
+ '& label+.MuiInput-root.MuiInputBase-root': {
+ marginTop: theme.spacing(1.5),
+ },
+ '& label[data-shrink=false]+.MuiInput-root.MuiInputBase-root': {
+ marginTop: 0,
+ paddingBottom: theme.spacing(2),
+ },
+ },
+ input: {
+ padding: theme.spacing(0.5),
+ },
+ },
+ },
+ MuiButton: {
+ defaultProps: {
+ variant: 'text' as const,
+ size: 'small' as const,
+ },
+ styleOverrides: {
+ root: {
+ paddingTop: theme.spacing(0.2),
+ paddingBottom: theme.spacing(0.2),
+ },
+ },
+ },
+ MuiCard: {
+ defaultProps: {
+ square: true,
+ },
+ },
+ MuiChip: {
+ defaultProps: {
+ variant: 'outlined' as const,
+ },
+ },
+ MuiFormControl: {
+ defaultProps: {
+ variant: 'standard' as const,
+ margin: 'dense' as const,
+ size: 'small' as const,
+ },
+ },
+ MuiFormHelperText: {
+ defaultProps: {
+ margin: 'dense' as const,
+ },
+ },
+ MuiIconButton: {
+ defaultProps: {
+ size: 'small' as const,
+ },
+ },
+ MuiInputBase: {
+ styleOverrides: {
+ root: {
+ 'label+&.MuiInputBase-root': {
+ marginTop: theme.spacing(1.5),
+ },
+ 'label[data-shrink=false]+&.MuiInputBase-root': {
+ marginTop: 0,
+ paddingBottom: theme.spacing(1.5),
+ },
+ },
+ input: {
+ padding: theme.spacing(0.5),
+ },
+ },
+ },
+ MuiInputLabel: {
+ styleOverrides: {
+ root: {
+ paddingLeft: theme.spacing(0.5),
+ },
+ },
+ defaultProps: {
+ margin: 'dense' as const,
+ },
+ },
+ MuiListItem: {
+ defaultProps: {
+ dense: true,
+ },
+ },
+ MuiListItemIcon: {
+ styleOverrides: {
+ root: {
+ '&.MuiListItemIcon-root': {
+ minWidth: theme.spacing(3.5),
+ },
+ },
+ },
+ },
+ MuiMenuItem: {
+ styleOverrides: {
+ root: {
+ paddingTop: theme.spacing(0.5),
+ paddingBottom: theme.spacing(0.5),
+ paddingLeft: theme.spacing(1),
+ paddingRight: theme.spacing(1),
+ },
+ },
+ },
+ MuiOutlinedInput: {
+ defaultProps: {
+ margin: 'dense' as const,
+ },
+ styleOverrides: {
+ input: {
+ padding: 16,
+ },
+ },
+ },
+ MuiPaper: {
+ styleOverrides: {
+ elevation1: {
+ boxShadow: theme.shadows[1],
+ },
+ root: {
+ backgroundColor: theme.palette.background.default,
+ },
+ },
+ },
+ MuiSnackbar: {
+ styleOverrides: {
+ root: {
+ '& .RaNotification-error': {
+ border: `1px solid ${theme.palette.error.main}`,
+ backgroundColor: `${theme.palette.common.white} !important`,
+ color: `${theme.palette.error.main} !important`,
+ },
+ '& .RaNotification-warning': {
+ border: `1px solid ${theme.palette.warning.main}`,
+ backgroundColor: `${theme.palette.common.white} !important`,
+ color: `${theme.palette.warning.main} !important`,
+ },
+ '& .RaNotification-info': {
+ border: `1px solid ${theme.palette.info.main}`,
+ backgroundColor: `${theme.palette.common.white} !important`,
+ color: `${theme.palette.info.main} !important`,
+ },
+ '& .RaNotification-success': {
+ border: `1px solid ${theme.palette.success.main}`,
+ backgroundColor: `${theme.palette.common.white} !important`,
+ color: `${theme.palette.success.main} !important`,
+ },
+ },
+ },
+ },
+ MuiTabs: {
+ styleOverrides: {
+ root: {
+ '&.MuiTabs-root': {
+ minHeight: theme.spacing(3.5),
+ },
+ },
+ },
+ },
+ MuiTab: {
+ styleOverrides: {
+ root: {
+ '&.MuiTab-root': {
+ padding: `${theme.spacing(0.5)} ${theme.spacing(1)}`,
+ minHeight: theme.spacing(3.5),
+ minWidth: theme.spacing(10),
+ },
+ },
+ },
+ },
+ MuiTable: {
+ defaultProps: {
+ size: 'small' as const,
+ },
+ },
+ MuiTableCell: {
+ styleOverrides: {
+ root: {
+ padding: theme.spacing(1),
+ '&.MuiTableCell-sizeSmall': {
+ padding: theme.spacing(0.5),
+ },
+ '&.MuiTableCell-paddingNone': {
+ padding: 0,
+ },
+ },
+ },
+ },
+ MuiTextField: {
+ defaultProps: {
+ variant: 'standard' as const,
+ margin: 'dense' as const,
+ size: 'small' as const,
+ },
+ variants: [
+ {
+ props: {},
+ style: ({ theme }: { theme: Theme }) => ({
+ [theme.breakpoints.down('sm')]: { width: '100%' },
+ }),
+ },
+ ],
+ },
+ MuiToolbar: {
+ defaultProps: {
+ variant: 'dense' as const,
+ },
+ styleOverrides: {
+ root: {
+ minHeight: theme.spacing(4.5),
+ },
+ regular: {
+ backgroundColor: theme.palette.background.paper,
+ },
+ },
+ },
+ RaDatagrid: {
+ styleOverrides: {
+ root: {
+ '& .RaDatagrid-headerCell': {
+ color: theme.palette.primary.main,
+ },
+ },
+ },
+ },
+ RaFilterForm: {
+ styleOverrides: {
+ root: {
+ [theme.breakpoints.up('sm')]: {
+ minHeight: theme.spacing(7.1),
+ },
+ },
+ },
+ },
+ RaFilterFormInput: {
+ styleOverrides: {
+ root: {
+ '& .RaFilterFormInput-hideButton': {
+ marginBottom: theme.spacing(0.5),
+ },
+ },
+ },
+ },
+ RaLayout: {
+ styleOverrides: {
+ root: {
+ '& .RaLayout-appFrame': {
+ marginTop: theme.spacing(5),
+ },
+ },
+ },
+ },
+ RaLoadingIndicator: {
+ styleOverrides: {
+ root: {
+ '& .RaLoadingIndicator-loader': {
+ top: '20%',
+ left: '20%',
+ },
+ },
+ },
+ },
+ RaMenuItemLink: {
+ styleOverrides: {
+ root: {
+ paddingLeft: theme.spacing(1),
+ paddingRight: theme.spacing(1),
+ '&.RaMenuItemLink-active': {
+ color: theme.palette.primary.dark,
+ fontWeight: 700,
+ '& .MuiSvgIcon-root': {
+ fill: theme.palette.primary.dark,
+ },
+ },
+ },
+ },
+ },
+});
+
+const alert = {
+ error: { main: '#B57185' },
+ warning: { main: '#F2CB05' },
+ info: { main: '#39AEA9' },
+ success: { main: '#00745F' },
+};
+
+const darkPalette: PaletteOptions = {
+ mode: 'dark' as 'dark',
+ primary: { main: '#f9fafb' },
+ secondary: { main: '#a0a0a0' },
+ background: { default: '#363D40' },
+ ...alert,
+};
+
+const lightPalette: PaletteOptions = {
+ mode: 'light' as 'light',
+ primary: { main: '#00585C' },
+ secondary: { main: '#64B4B8' },
+ background: { default: '#f9fafb' },
+ text: { primary: '#212b36' },
+ ...alert,
+};
+
+const createNanoTheme = (palette: RaThemeOptions['palette']) => {
+ const themeOptions = {
+ palette,
+ shape: { borderRadius: 0 },
+ sidebar: {
+ width: 200,
+ closedWidth: 36,
+ },
+ spacing: 8,
+ typography: {
+ fontFamily: 'Onest, sans-serif',
+ fontSize: 12,
+ h1: { fontSize: '7rem' },
+ h2: { fontWeight: 400 },
+ h3: { fontWeight: 500 },
+ h4: { fontWeight: 700 },
+ h5: { fontWeight: 700 },
+ },
+ };
+ const theme = createTheme(themeOptions);
+ theme.components = componentsOverrides(theme);
+ return theme;
+};
+
+export const nanoLightTheme = createNanoTheme(lightPalette);
+export const nanoDarkTheme = createNanoTheme(darkPalette);
diff --git a/packages/ra-ui-materialui/src/theme/radiantTheme.stories.tsx b/packages/ra-ui-materialui/src/theme/radiantTheme.stories.tsx
new file mode 100644
index 00000000000..e6ab8c0a13f
--- /dev/null
+++ b/packages/ra-ui-materialui/src/theme/radiantTheme.stories.tsx
@@ -0,0 +1,47 @@
+import * as React from 'react';
+import { Resource } from 'ra-core';
+import fakerestDataProvider from 'ra-data-fakerest';
+import englishTranslations from 'ra-language-english';
+import polyglotI18nProvider from 'ra-i18n-polyglot';
+
+import { AdminContext } from '../AdminContext';
+import { AdminUI } from '../AdminUI';
+import { ListGuesser } from '../list';
+import { EditGuesser } from '../detail';
+import { radiantLightTheme, radiantDarkTheme } from './radiantTheme';
+import { testData } from './testData';
+
+export default {
+ title: 'ra-ui-materialui/theme/Radiant',
+};
+
+export const Radiant = () => (
+ englishTranslations, 'en')}
+ >
+
+
+
+
+
+
+);
diff --git a/packages/ra-ui-materialui/src/theme/radiantTheme.ts b/packages/ra-ui-materialui/src/theme/radiantTheme.ts
new file mode 100644
index 00000000000..a04e4a3f107
--- /dev/null
+++ b/packages/ra-ui-materialui/src/theme/radiantTheme.ts
@@ -0,0 +1,180 @@
+import { alpha, createTheme, PaletteOptions, Theme } from '@mui/material';
+import { RaThemeOptions } from './types';
+
+/**
+ * Radiant: A theme emphasizing clarity and ease of use.
+ *
+ * Uses generous margins, outlined inputs and buttons, no uppercase, and an acid color palette.
+ */
+
+const componentsOverrides = (theme: Theme) => {
+ const shadows = [
+ alpha(theme.palette.primary.main, 0.2),
+ alpha(theme.palette.primary.main, 0.1),
+ alpha(theme.palette.primary.main, 0.05),
+ ];
+ return {
+ MuiAppBar: {
+ styleOverrides: {
+ colorSecondary: {
+ backgroundColor: theme.palette.background.default,
+ color: theme.palette.text.primary,
+ },
+ },
+ },
+ MuiButton: {
+ defaultProps: {
+ variant: 'outlined' as const,
+ },
+ styleOverrides: {
+ sizeSmall: {
+ padding: `${theme.spacing(0.5)} ${theme.spacing(1.5)}`,
+ },
+ },
+ },
+ MuiFormControl: {
+ defaultProps: {
+ variant: 'outlined' as const,
+ margin: 'dense' as const,
+ size: 'small' as const,
+ },
+ },
+ MuiPaper: {
+ styleOverrides: {
+ elevation1: {
+ boxShadow: `${shadows[0]} -2px 2px, ${shadows[1]} -4px 4px,${shadows[2]} -6px 6px`,
+ },
+ root: {
+ backgroundClip: 'padding-box',
+ },
+ },
+ },
+ MuiTableCell: {
+ styleOverrides: {
+ root: {
+ padding: theme.spacing(1.5),
+ '&.MuiTableCell-sizeSmall': {
+ padding: theme.spacing(1),
+ },
+ '&.MuiTableCell-paddingNone': {
+ padding: 0,
+ },
+ },
+ },
+ },
+ MuiTableRow: {
+ styleOverrides: {
+ root: {
+ '&:last-child td': { border: 0 },
+ },
+ },
+ },
+ MuiTextField: {
+ defaultProps: {
+ variant: 'outlined' as const,
+ margin: 'dense' as const,
+ size: 'small' as const,
+ },
+ },
+ RaDatagrid: {
+ styleOverrides: {
+ root: {
+ '& .RaDatagrid-headerCell': {
+ color: theme.palette.primary.main,
+ },
+ },
+ },
+ },
+ RaFilterForm: {
+ styleOverrides: {
+ root: {
+ [theme.breakpoints.up('sm')]: {
+ minHeight: theme.spacing(6),
+ },
+ },
+ },
+ },
+ RaLayout: {
+ styleOverrides: {
+ root: {
+ '& .RaLayout-appFrame': { marginTop: theme.spacing(5) },
+ },
+ },
+ },
+ RaMenuItemLink: {
+ styleOverrides: {
+ root: {
+ borderLeft: `3px solid ${theme.palette.primary.contrastText}`,
+ '&:hover': {
+ borderRadius: '0px 100px 100px 0px',
+ },
+ '&.RaMenuItemLink-active': {
+ borderLeft: `3px solid ${theme.palette.primary.main}`,
+ borderRadius: '0px 100px 100px 0px',
+ backgroundImage: `linear-gradient(98deg, ${theme.palette.primary.light}, ${theme.palette.primary.dark} 94%)`,
+ boxShadow: theme.shadows[1],
+ color: theme.palette.primary.contrastText,
+
+ '& .MuiSvgIcon-root': {
+ fill: theme.palette.primary.contrastText,
+ },
+ },
+ },
+ },
+ },
+ };
+};
+
+const alert = {
+ error: { main: '#DB488B' },
+ warning: { main: '#F2E963' },
+ info: { main: '#3ED0EB' },
+ success: { main: '#0FBF9F' },
+};
+
+const darkPalette: PaletteOptions = {
+ primary: { main: '#9055fd' },
+ secondary: { main: '#FF83F6' },
+ background: { default: '#110e1c', paper: '#151221' },
+ ...alert,
+ mode: 'dark' as 'dark',
+};
+
+const lightPalette: PaletteOptions = {
+ primary: { main: '#9055fd' },
+ secondary: { main: '#A270FF' },
+ background: { default: '#f0f1f6' },
+ text: {
+ primary: '#544f5a',
+ secondary: '#89868D',
+ },
+ ...alert,
+ mode: 'light' as 'light',
+};
+
+const createRadiantTheme = (palette: RaThemeOptions['palette']) => {
+ const themeOptions = {
+ palette,
+ shape: { borderRadius: 6 },
+ sidebar: { width: 250 },
+ spacing: 10,
+ typography: {
+ fontFamily: 'Gabarito, tahoma, sans-serif',
+ h1: {
+ fontWeight: 500,
+ fontSize: '6rem',
+ },
+ h2: { fontWeight: 600 },
+ h3: { fontWeight: 700 },
+ h4: { fontWeight: 800 },
+ h5: { fontWeight: 900 },
+ button: { textTransform: undefined, fontWeight: 700 },
+ },
+ };
+ const theme = createTheme(themeOptions);
+ theme.components = componentsOverrides(theme);
+ return theme;
+};
+
+export const radiantLightTheme = createRadiantTheme(lightPalette);
+export const radiantDarkTheme = createRadiantTheme(darkPalette);
diff --git a/packages/ra-ui-materialui/src/theme/testData.ts b/packages/ra-ui-materialui/src/theme/testData.ts
new file mode 100644
index 00000000000..2f166084289
--- /dev/null
+++ b/packages/ra-ui-materialui/src/theme/testData.ts
@@ -0,0 +1,62 @@
+export const testData = {
+ products: [
+ {
+ id: 1,
+ name: 'Office jeans',
+ price: 45.99,
+ category_id: 1,
+ tags_ids: [1],
+ },
+ {
+ id: 2,
+ name: 'Black elegance jeans',
+ price: 69.99,
+ category_id: 1,
+ tags_ids: [2, 3],
+ },
+ {
+ id: 3,
+ name: 'Slim fit jeans',
+ price: 55.99,
+ category_id: 1,
+ tags_ids: [2, 4],
+ },
+ {
+ id: 4,
+ name: 'Basic T-shirt',
+ price: 15.99,
+ category_id: 2,
+ tags_ids: [1, 4, 3],
+ },
+ {
+ id: 5,
+ name: 'Basic cap',
+ price: 19.99,
+ category_id: 6,
+ tags_ids: [1, 4, 3],
+ },
+ ],
+ categories: [
+ { id: 1, name: 'Jeans' },
+ { id: 2, name: 'T-Shirts' },
+ { id: 3, name: 'Jackets' },
+ { id: 4, name: 'Shoes' },
+ { id: 5, name: 'Accessories' },
+ { id: 6, name: 'Hats' },
+ { id: 7, name: 'Socks' },
+ { id: 8, name: 'Shirts' },
+ { id: 9, name: 'Sweaters' },
+ { id: 10, name: 'Trousers' },
+ { id: 11, name: 'Coats' },
+ { id: 12, name: 'Dresses' },
+ { id: 13, name: 'Skirts' },
+ { id: 14, name: 'Swimwear' },
+ { id: 15, name: 'Bags' },
+ ],
+ tags: [
+ { id: 1, name: 'top seller' },
+ { id: 2, name: 'new' },
+ { id: 3, name: 'sale' },
+ { id: 4, name: 'promotion' },
+ ],
+};
diff --git a/packages/ra-ui-materialui/src/layout/Theme/types.ts b/packages/ra-ui-materialui/src/theme/types.ts
similarity index 100%
rename from packages/ra-ui-materialui/src/layout/Theme/types.ts
rename to packages/ra-ui-materialui/src/theme/types.ts
diff --git a/packages/ra-ui-materialui/src/layout/Theme/useTheme.spec.tsx b/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx
similarity index 100%
rename from packages/ra-ui-materialui/src/layout/Theme/useTheme.spec.tsx
rename to packages/ra-ui-materialui/src/theme/useTheme.spec.tsx
diff --git a/packages/ra-ui-materialui/src/layout/Theme/useTheme.ts b/packages/ra-ui-materialui/src/theme/useTheme.ts
similarity index 100%
rename from packages/ra-ui-materialui/src/layout/Theme/useTheme.ts
rename to packages/ra-ui-materialui/src/theme/useTheme.ts
diff --git a/packages/ra-ui-materialui/src/layout/Theme/useThemesContext.ts b/packages/ra-ui-materialui/src/theme/useThemesContext.ts
similarity index 100%
rename from packages/ra-ui-materialui/src/layout/Theme/useThemesContext.ts
rename to packages/ra-ui-materialui/src/theme/useThemesContext.ts