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 = () => ( - + // ... ); ``` -![Dark theme](./img/dark-theme.png) +![Nano light theme](./img/nanoLightTheme1.jpg) -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. -![Music Player](./img/navidrome.png) +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. + +[![Default light theme](./img/defaultLightTheme1.jpg)](./img/defaultLightTheme1.jpg) +[![Default light theme](./img/defaultLightTheme2.jpg)](./img/defaultLightTheme2.jpg) +[![Default dark theme](./img/defaultDarkTheme1.jpg)](./img/defaultDarkTheme1.jpg) +[![Default dark theme](./img/defaultDarkTheme2.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. + +[![Nano light theme](./img/nanoLightTheme1.jpg)](./img/nanoLightTheme1.jpg) +[![Nano light theme](./img/nanoLightTheme2.jpg)](./img/nanoLightTheme2.jpg) +[![Nano dark theme](./img/nanoDarkTheme1.jpg)](./img/nanoDarkTheme1.jpg) +[![Nano dark theme](./img/nanoDarkTheme2.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. + +[![Radiant light theme](./img/radiantLightTheme1.jpg)](./img/radiantLightTheme1.jpg) +[![Radiant light theme](./img/radiantLightTheme2.jpg)](./img/radiantLightTheme2.jpg) +[![Radiant dark theme](./img/radiantDarkTheme1.jpg)](./img/radiantDarkTheme1.jpg) +[![Radiant dark theme](./img/radiantDarkTheme2.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. + +[![House light theme](./img/houseLightTheme1.jpg)](./img/houseLightTheme1.jpg) +[![House light theme](./img/houseLightTheme2.jpg)](./img/houseLightTheme2.jpg) +[![House dark theme](./img/houseDarkTheme1.jpg)](./img/houseDarkTheme1.jpg) +[![House dark theme](./img/houseDarkTheme2.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 = () => ( - - // ... - -); -``` - -![Dark theme](./img/dark-theme.png) - -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/): ![Music Player](./img/navidrome.png) @@ -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} + + {themes.map((theme, index: number) => ( + handleChange(event, index)} + value={theme.name} + key={theme.name} + selected={theme.name === themeName} + > + {ucFirst(theme.name)} + + ))} + + + ); +}; + +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