@etsoo/toolpad 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +1 -1
  2. package/build/src/Account/Account.d.ts +0 -5
  3. package/build/src/Account/Account.js +2 -12
  4. package/build/src/Account/AccountPreview.js +21 -2
  5. package/build/src/Account/SignInButton.js +1 -1
  6. package/build/src/AppProvider/AppProvider.d.ts +6 -0
  7. package/build/src/AppProvider/AppProviderComponent.js +3 -2
  8. package/build/src/DashboardLayout/DashboardLayout.d.ts +11 -0
  9. package/build/src/DashboardLayout/DashboardLayout.js +12 -23
  10. package/build/src/DashboardLayout/DashboardLayout.test.js +1 -1
  11. package/build/src/DashboardLayout/ThemeSwitcher.js +10 -4
  12. package/build/src/DashboardLayout/TitleBar.d.ts +1 -0
  13. package/build/src/DashboardLayout/TitleBar.js +27 -0
  14. package/build/src/PageContainer/PageContainer.d.ts +0 -4
  15. package/build/src/PageContainer/PageContainer.js +2 -13
  16. package/build/src/PageContainer/PageContainer.test.js +0 -14
  17. package/build/src/shared/locales/LocaleContext.d.ts +46 -9
  18. package/build/src/shared/locales/LocaleContext.js +19 -4
  19. package/package.json +15 -16
  20. package/src/Account/Account.tsx +6 -24
  21. package/src/Account/AccountPreview.tsx +26 -3
  22. package/src/Account/SignInButton.tsx +1 -1
  23. package/src/AppProvider/AppProvider.tsx +6 -0
  24. package/src/AppProvider/AppProviderComponent.tsx +9 -5
  25. package/src/DashboardLayout/DashboardLayout.test.tsx +1 -1
  26. package/src/DashboardLayout/DashboardLayout.tsx +29 -44
  27. package/src/DashboardLayout/ThemeSwitcher.tsx +10 -4
  28. package/src/DashboardLayout/TitleBar.tsx +43 -0
  29. package/src/PageContainer/PageContainer.test.tsx +0 -23
  30. package/src/PageContainer/PageContainer.tsx +2 -25
  31. package/src/shared/locales/LocaleContext.tsx +18 -9
  32. package/tsconfig.json +1 -1
  33. package/build/src/shared/locales/en.d.ts +0 -6
  34. package/build/src/shared/locales/en.js +0 -8
  35. package/src/shared/locales/en.tsx +0 -9
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ## Toolpad
2
2
 
3
- It's a project originally cloned from Toolpad Core (https://github.com/mui/toolpad/tree/master/packages/toolpad-core).Toolpad Core is a set of full-stack components for building dashboards, internal tools, and B2B web applications with React. Additional simplicity and new features have been implemented for ETSOO SmartERP SaaS platform development.
3
+ It's a project originally cloned from Toolpad Core (https://github.com/mui/toolpad/tree/master/packages/toolpad-core). Toolpad Core is a set of full-stack components for building dashboards, internal tools, and B2B web applications with React. Additional simplicity and new features have been implemented for ETSOO SmartERP SaaS platform development.
4
4
 
5
5
  ## Installation
6
6
 
@@ -4,7 +4,6 @@ import Popover from "@mui/material/Popover";
4
4
  import Stack from "@mui/material/Stack";
5
5
  import { SignInButton } from "./SignInButton";
6
6
  import { AccountPreviewProps } from "./AccountPreview";
7
- import { useLocaleText } from "../shared/locales/LocaleContext";
8
7
  export interface AccountSlots {
9
8
  /**
10
9
  * The component used for the account preview
@@ -47,10 +46,6 @@ export interface AccountProps {
47
46
  signInButton?: React.ComponentProps<typeof SignInButton>;
48
47
  signOutButton?: React.ComponentProps<typeof Button>;
49
48
  };
50
- /**
51
- * The labels for the account component.
52
- */
53
- localeText?: Partial<ReturnType<typeof useLocaleText>>;
54
49
  }
55
50
  /**
56
51
  *
@@ -10,7 +10,6 @@ import { AccountPreview } from "./AccountPreview";
10
10
  import { AccountPopoverHeader } from "./AccountPopoverHeader";
11
11
  import { AccountPopoverFooter } from "./AccountPopoverFooter";
12
12
  import { SessionContext, AuthenticationContext } from "../AppProvider/AppProvider";
13
- import { LocaleProvider } from "../shared/locales/LocaleContext";
14
13
  /**
15
14
  *
16
15
  * Demos:
@@ -24,7 +23,6 @@ import { LocaleProvider } from "../shared/locales/LocaleContext";
24
23
  * - [Account API](https://mui.com/toolpad/core/api/account)
25
24
  */
26
25
  function Account(props) {
27
- const { localeText } = props;
28
26
  const { slots, slotProps } = props;
29
27
  const [anchorEl, setAnchorEl] = React.useState(null);
30
28
  const session = React.useContext(SessionContext);
@@ -40,9 +38,9 @@ function Account(props) {
40
38
  return null;
41
39
  }
42
40
  if (!session?.user) {
43
- return (_jsx(LocaleProvider, { localeText: localeText, children: slots?.signInButton ? (_jsx(slots.signInButton, { onClick: authentication.signIn })) : (_jsx(SignInButton, { ...slotProps?.signInButton })) }));
41
+ return slots?.signInButton ? (_jsx(slots.signInButton, { onClick: authentication.signIn })) : (_jsx(SignInButton, { ...slotProps?.signInButton }));
44
42
  }
45
- return (_jsxs(LocaleProvider, { localeText: localeText, children: [slots?.preview ? (_jsx(slots.preview, { handleClick: handleClick, open: open })) : (_jsx(AccountPreview, { variant: "condensed", handleClick: handleClick, open: open, ...slotProps?.preview })), slots?.popover ? (_jsx(slots.popover, { ...slotProps?.popover })) : (_jsx(Popover, { anchorEl: anchorEl, id: "account-menu", open: open, onClose: handleClose, onClick: handleClose, transformOrigin: { horizontal: "right", vertical: "top" }, anchorOrigin: { horizontal: "right", vertical: "bottom" }, ...slotProps?.popover, slotProps: {
43
+ return (_jsxs(React.Fragment, { children: [slots?.preview ? (_jsx(slots.preview, { handleClick: handleClick, open: open })) : (_jsx(AccountPreview, { variant: "condensed", handleClick: handleClick, open: open, ...slotProps?.preview })), slots?.popover ? (_jsx(slots.popover, { ...slotProps?.popover })) : (_jsx(Popover, { anchorEl: anchorEl, id: "account-menu", open: open, onClose: handleClose, onClick: handleClose, transformOrigin: { horizontal: "right", vertical: "top" }, anchorOrigin: { horizontal: "right", vertical: "bottom" }, ...slotProps?.popover, slotProps: {
46
44
  paper: {
47
45
  elevation: 0,
48
46
  sx: {
@@ -71,14 +69,6 @@ Account.propTypes /* remove-proptypes */ = {
71
69
  // │ These PropTypes are generated from the TypeScript type definitions. │
72
70
  // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
73
71
  // └─────────────────────────────────────────────────────────────────────┘
74
- /**
75
- * The labels for the account component.
76
- */
77
- localeText: PropTypes.shape({
78
- iconButtonAriaLabel: PropTypes.string,
79
- signInLabel: PropTypes.string,
80
- signOutLabel: PropTypes.string
81
- }),
82
72
  /**
83
73
  * The props used for each slot inside.
84
74
  */
@@ -9,6 +9,25 @@ import IconButton from "@mui/material/IconButton";
9
9
  import MoreVertIcon from "@mui/icons-material/MoreVert";
10
10
  import { SessionContext } from "../AppProvider";
11
11
  import { useLocaleText } from "../shared/locales/LocaleContext";
12
+ function formatNameInitials(user) {
13
+ if (user == null)
14
+ return null;
15
+ const name = user.latinName || user.name;
16
+ if (name == null)
17
+ return null;
18
+ const chars = name
19
+ .trim()
20
+ .split(/\s+/g)
21
+ .map((part) => {
22
+ const firstChar = part.charAt(0).toUpperCase();
23
+ return firstChar >= "A" && firstChar <= "Z" ? firstChar : "";
24
+ })
25
+ .filter((c) => c)
26
+ .slice(-2);
27
+ if (chars.length === 0)
28
+ return null;
29
+ return chars.join("");
30
+ }
12
31
  /**
13
32
  * The AccountPreview component displays user account information.
14
33
  *
@@ -30,12 +49,12 @@ function AccountPreview(props) {
30
49
  const avatarContent = slots?.avatar ? (_jsx(slots.avatar, {})) : (_jsx(Avatar, { src: session.user?.image || "", alt: session.user?.name || session.user?.email || "", sx: {
31
50
  height: variant === "expanded" ? 48 : 32,
32
51
  width: variant === "expanded" ? 48 : 32
33
- }, ...slotProps?.avatar }));
52
+ }, ...slotProps?.avatar, children: session.user?.image ? null : formatNameInitials(session.user) }));
34
53
  if (variant === "expanded") {
35
54
  return (_jsxs(Stack, { direction: "row", justifyContent: "flex-start", spacing: 2, padding: 2, children: [avatarContent, _jsxs(Stack, { direction: "column", justifyContent: "space-evenly", children: [_jsx(Typography, { variant: "body2", fontWeight: "bolder", noWrap: true, children: session.user?.name }), _jsx(Typography, { variant: "caption", noWrap: true, children: session.user?.email })] }), handleClick &&
36
55
  (slots?.moreIconButton ? (_jsx(slots.moreIconButton, {})) : (_jsx(IconButton, { size: "small", onClick: handleClick, ...slotProps?.moreIconButton, sx: { alignSelf: "flex-start", ...slotProps?.moreIconButton?.sx }, children: _jsx(MoreVertIcon, { fontSize: "small" }) })))] }));
37
56
  }
38
- return (_jsx(Tooltip, { title: session.user.name ?? "Account", children: slots?.avatarIconButton ? (_jsx(slots.avatarIconButton, {})) : (_jsx(IconButton, { onClick: handleClick, "aria-label": localeText.iconButtonAriaLabel || "Current User", size: "small", "aria-controls": open ? "account-menu" : undefined, "aria-haspopup": "true", "aria-expanded": open ? "true" : undefined, ...slotProps?.avatarIconButton, children: avatarContent })) }));
57
+ return (_jsx(Tooltip, { title: session.user.name ?? "Account", children: slots?.avatarIconButton ? (_jsx(slots.avatarIconButton, {})) : (_jsx(IconButton, { onClick: handleClick, "aria-label": localeText.accountIconButtonAriaLabel, size: "small", "aria-controls": open ? "account-menu" : undefined, "aria-haspopup": "true", "aria-expanded": open ? "true" : undefined, ...slotProps?.avatarIconButton, children: avatarContent })) }));
39
58
  }
40
59
  AccountPreview.propTypes /* remove-proptypes */ = {
41
60
  // ┌────────────────────────────── Warning ──────────────────────────────┐
@@ -22,5 +22,5 @@ export /**
22
22
  "&:hover": {
23
23
  filter: "opacity(1)"
24
24
  }
25
- }, ...props, children: localeText?.signInLabel || "Sign In" }));
25
+ }, ...props, children: localeText.signInLabel }));
26
26
  }
@@ -1,5 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { Theme } from "@mui/material/styles";
3
+ import { LocaleContextType } from "../shared/locales/LocaleContext";
3
4
  export interface NavigateOptions {
4
5
  history?: "auto" | "push" | "replace";
5
6
  }
@@ -40,6 +41,7 @@ export interface Session {
40
41
  user?: {
41
42
  id?: string | null;
42
43
  name?: string | null;
44
+ latinName?: string | null;
43
45
  image?: string | null;
44
46
  email?: string | null;
45
47
  };
@@ -69,6 +71,10 @@ export interface AppProviderProps {
69
71
  * @default null
70
72
  */
71
73
  branding?: Branding | null;
74
+ /**
75
+ * The labels for the component.
76
+ */
77
+ localeText?: Partial<LocaleContextType>;
72
78
  /**
73
79
  * Navigation definition for the app.
74
80
  * @default []
@@ -5,6 +5,7 @@ import { BrandingContext, NavigationContext, RouterContext, WindowContext } from
5
5
  import { AppThemeProvider } from "./AppThemeProvider";
6
6
  import { createTheme as createMuiTheme } from "@mui/material/styles";
7
7
  import { AuthenticationContext, SessionContext } from "./AppProvider";
8
+ import { LocaleProvider } from "../shared/locales/LocaleContext";
8
9
  function createTheme() {
9
10
  return createMuiTheme({
10
11
  cssVariables: {
@@ -25,8 +26,8 @@ function createTheme() {
25
26
  * - [AppProvider API](https://mui.com/toolpad/core/api/app-provider)
26
27
  */
27
28
  function AppProvider(props) {
28
- const { children, theme = createTheme(), branding = null, navigation = [], router = null, authentication = null, session = null, window: appWindow } = props;
29
- return (_jsx(WindowContext.Provider, { value: appWindow, children: _jsx(AuthenticationContext.Provider, { value: authentication, children: _jsx(SessionContext.Provider, { value: session, children: _jsx(RouterContext.Provider, { value: router, children: _jsx(AppThemeProvider, { theme: theme, window: appWindow, children: _jsx(BrandingContext.Provider, { value: branding, children: _jsx(NavigationContext.Provider, { value: navigation, children: children }) }) }) }) }) }) }));
29
+ const { children, theme = createTheme(), branding = null, localeText, navigation = [], router = null, authentication = null, session = null, window: appWindow } = props;
30
+ return (_jsx(WindowContext.Provider, { value: appWindow, children: _jsx(AuthenticationContext.Provider, { value: authentication, children: _jsx(SessionContext.Provider, { value: session, children: _jsx(RouterContext.Provider, { value: router, children: _jsx(AppThemeProvider, { theme: theme, window: appWindow, children: _jsx(LocaleProvider, { localeText: localeText, children: _jsx(BrandingContext.Provider, { value: branding, children: _jsx(NavigationContext.Provider, { value: navigation, children: children }) }) }) }) }) }) }) }));
30
31
  }
31
32
  AppProvider.propTypes /* remove-proptypes */ = {
32
33
  // ┌────────────────────────────── Warning ──────────────────────────────┐
@@ -5,11 +5,17 @@ export interface SidebarFooterProps {
5
5
  mini: boolean;
6
6
  }
7
7
  export interface DashboardLayoutSlotProps {
8
+ titlebar?: {};
8
9
  toolbarActions?: {};
9
10
  toolbarAccount?: AccountProps;
10
11
  sidebarFooter?: SidebarFooterProps;
11
12
  }
12
13
  export interface DashboardLayoutSlots {
14
+ /**
15
+ * The title bar component used in the layout header.
16
+ * @default TitleBar
17
+ */
18
+ titlebar?: React.JSXElementConstructor<{}>;
13
19
  /**
14
20
  * The toolbar actions component used in the layout header.
15
21
  * @default ToolbarActions
@@ -46,6 +52,11 @@ export interface DashboardLayoutProps {
46
52
  * @default false
47
53
  */
48
54
  hideNavigation?: boolean;
55
+ /**
56
+ * Whether the theme switcher should be shown in the toolbar.
57
+ * @default false
58
+ */
59
+ showThemeSwitcher?: boolean;
49
60
  /**
50
61
  * Width of the sidebar when expanded.
51
62
  * @default 320
@@ -10,18 +10,17 @@ import IconButton from "@mui/material/IconButton";
10
10
  import Stack from "@mui/material/Stack";
11
11
  import Toolbar from "@mui/material/Toolbar";
12
12
  import Tooltip from "@mui/material/Tooltip";
13
- import Typography from "@mui/material/Typography";
14
13
  import useMediaQuery from "@mui/material/useMediaQuery";
15
14
  import MenuIcon from "@mui/icons-material/Menu";
16
15
  import MenuOpenIcon from "@mui/icons-material/MenuOpen";
17
- import { Link } from "../shared/Link";
18
- import { BrandingContext, NavigationContext, WindowContext } from "../shared/context";
16
+ import { NavigationContext, WindowContext } from "../shared/context";
19
17
  import { Account } from "../Account";
20
- import { useApplicationTitle } from "../shared/branding";
21
18
  import { DashboardSidebarSubNavigation } from "./DashboardSidebarSubNavigation";
22
19
  import { ToolbarActions } from "./ToolbarActions";
23
20
  import { ThemeSwitcher } from "./ThemeSwitcher";
24
21
  import { getDrawerSxTransitionMixin, getDrawerWidthTransitionMixin } from "./utils";
22
+ import { TitleBar } from "./TitleBar";
23
+ import { useLocaleText } from "../shared/locales/LocaleContext";
25
24
  const AppBar = styled(MuiAppBar)(({ theme }) => ({
26
25
  borderWidth: 0,
27
26
  borderBottomWidth: 1,
@@ -30,13 +29,6 @@ const AppBar = styled(MuiAppBar)(({ theme }) => ({
30
29
  boxShadow: "none",
31
30
  zIndex: theme.zIndex.drawer + 1
32
31
  }));
33
- const LogoContainer = styled("div")({
34
- position: "relative",
35
- height: 40,
36
- "& img": {
37
- maxHeight: 40
38
- }
39
- });
40
32
  /**
41
33
  *
42
34
  * Demos:
@@ -48,12 +40,11 @@ const LogoContainer = styled("div")({
48
40
  * - [DashboardLayout API](https://mui.com/toolpad/core/api/dashboard-layout)
49
41
  */
50
42
  function DashboardLayout(props) {
51
- const { children, disableCollapsibleSidebar = false, defaultSidebarCollapsed = false, hideNavigation = false, sidebarExpandedWidth = 320, slots, slotProps, sx } = props;
43
+ const { children, disableCollapsibleSidebar = false, defaultSidebarCollapsed = false, hideNavigation = false, showThemeSwitcher = false, sidebarExpandedWidth = 320, slots, slotProps, sx } = props;
52
44
  const theme = useTheme();
53
- const branding = React.useContext(BrandingContext);
54
45
  const navigation = React.useContext(NavigationContext);
55
46
  const appWindow = React.useContext(WindowContext);
56
- const applicationTitle = useApplicationTitle();
47
+ const localeText = useLocaleText();
57
48
  const [isDesktopNavigationExpanded, setIsDesktopNavigationExpanded] = React.useState(!defaultSidebarCollapsed);
58
49
  const [isMobileNavigationExpanded, setIsMobileNavigationExpanded] = React.useState(false);
59
50
  const isUnderMdViewport = useMediaQuery(theme.breakpoints.down("md"), appWindow && {
@@ -104,11 +95,14 @@ function DashboardLayout(props) {
104
95
  const isDesktopMini = !disableCollapsibleSidebar && !isDesktopNavigationExpanded;
105
96
  const isMobileMini = !disableCollapsibleSidebar && !isMobileNavigationExpanded;
106
97
  const getMenuIcon = React.useCallback((isExpanded) => {
107
- const expandMenuActionText = "Expand";
108
- const collapseMenuActionText = "Collapse";
109
- return (_jsx(Tooltip, { title: `${isExpanded ? collapseMenuActionText : expandMenuActionText} menu`, enterDelay: 1000, children: _jsx("div", { children: _jsx(IconButton, { "aria-label": `${isExpanded ? collapseMenuActionText : expandMenuActionText} navigation menu`, onClick: toggleNavigationExpanded, children: isExpanded ? _jsx(MenuOpenIcon, {}) : _jsx(MenuIcon, {}) }) }) }));
98
+ return (_jsx(Tooltip, { title: isExpanded
99
+ ? localeText.collapseMenuTitle
100
+ : localeText.expandMenuTitle, enterDelay: 1000, children: _jsx("div", { children: _jsx(IconButton, { "aria-label": isExpanded
101
+ ? localeText.collapseNavMenuAriaLabel
102
+ : localeText.expandNavMenuAriaLabel, onClick: toggleNavigationExpanded, children: isExpanded ? _jsx(MenuOpenIcon, {}) : _jsx(MenuIcon, {}) }) }) }));
110
103
  }, [toggleNavigationExpanded]);
111
104
  const hasDrawerTransitions = isOverSmViewport && (disableCollapsibleSidebar || !isUnderMdViewport);
105
+ const TitlebarSlot = slots?.titlebar ?? TitleBar;
112
106
  const ToolbarActionsSlot = slots?.toolbarActions ?? ToolbarActions;
113
107
  const ToolbarAccountSlot = slots?.toolbarAccount ?? Account;
114
108
  const SidebarFooterSlot = slots?.sidebarFooter ?? null;
@@ -167,12 +161,7 @@ function DashboardLayout(props) {
167
161
  position: { xs: "absolute", md: "static" },
168
162
  left: { xs: "50%", md: "auto" },
169
163
  transform: { xs: "translateX(-50%)", md: "none" }
170
- }, children: _jsx(Link, { href: "/", style: { color: "inherit", textDecoration: "none" }, children: _jsxs(Stack, { direction: "row", alignItems: "center", children: [branding?.logo && (_jsx(LogoContainer, { children: branding.logo })), _jsx(Typography, { variant: "h6", sx: {
171
- color: (theme.vars ?? theme).palette.primary.main,
172
- fontWeight: "700",
173
- ml: 0.5,
174
- whiteSpace: "nowrap"
175
- }, children: applicationTitle })] }) }) }), _jsx(Box, { sx: { flexGrow: 1 } }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(ToolbarActionsSlot, { ...slotProps?.toolbarActions }), _jsx(ThemeSwitcher, {}), _jsx(ToolbarAccountSlot, { ...slotProps?.toolbarAccount })] })] }) }), !hideNavigation ? (_jsxs(React.Fragment, { children: [_jsx(Drawer, { container: layoutRef.current, variant: "temporary", open: isMobileNavigationExpanded, onClose: handleSetNavigationExpanded(false), ModalProps: {
164
+ }, children: _jsx(TitlebarSlot, { ...slotProps?.titlebar }) }), _jsx(Box, { sx: { flexGrow: 1 } }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(ToolbarActionsSlot, { ...slotProps?.toolbarActions }), showThemeSwitcher && _jsx(ThemeSwitcher, {}), _jsx(ToolbarAccountSlot, { ...slotProps?.toolbarAccount })] })] }) }), !hideNavigation ? (_jsxs(React.Fragment, { children: [_jsx(Drawer, { container: layoutRef.current, variant: "temporary", open: isMobileNavigationExpanded, onClose: handleSetNavigationExpanded(false), ModalProps: {
176
165
  keepMounted: true // Better open performance on mobile.
177
166
  }, sx: {
178
167
  display: {
@@ -27,7 +27,7 @@ describe("DashboardLayout", () => {
27
27
  });
28
28
  test("can switch theme", async () => {
29
29
  const user = userEvent.setup();
30
- render(_jsx(AppProvider, { children: _jsx(DashboardLayout, { children: "Hello world" }) }));
30
+ render(_jsx(AppProvider, { children: _jsx(DashboardLayout, { showThemeSwitcher: true, children: "Hello world" }) }));
31
31
  const getBackgroundColorCSSVariable = () => getComputedStyle(document.documentElement).getPropertyValue("--mui-palette-common-background");
32
32
  const header = screen.getByRole("banner");
33
33
  const themeSwitcherButton = within(header).getByLabelText("Switch to dark mode");
@@ -8,6 +8,7 @@ import DarkModeIcon from "@mui/icons-material/DarkMode";
8
8
  import LightModeIcon from "@mui/icons-material/LightMode";
9
9
  import { PaletteModeContext } from "../shared/context";
10
10
  import useSsr from "../utils/hooks/useSsr";
11
+ import { useLocaleText } from "../shared/locales/LocaleContext";
11
12
  // TODO: When we use this component as the default for a slot, make it non-internal
12
13
  /**
13
14
  * @ignore - internal component.
@@ -15,15 +16,20 @@ import useSsr from "../utils/hooks/useSsr";
15
16
  function ThemeSwitcher() {
16
17
  const isSsr = useSsr();
17
18
  const theme = useTheme();
19
+ const localeText = useLocaleText();
18
20
  const { paletteMode, setPaletteMode, isDualTheme } = React.useContext(PaletteModeContext);
19
21
  const toggleMode = React.useCallback(() => {
20
22
  setPaletteMode(paletteMode === "dark" ? "light" : "dark");
21
23
  }, [paletteMode, setPaletteMode]);
22
24
  return isDualTheme ? (_jsx(Tooltip, { title: isSsr
23
- ? "Switch mode"
24
- : `${paletteMode === "dark" ? "Light" : "Dark"} mode`, enterDelay: 1000, children: _jsx("div", { children: _jsx(IconButton, { "aria-label": isSsr
25
- ? "Switch theme mode"
26
- : `Switch to ${paletteMode === "dark" ? "light" : "dark"} mode`, onClick: toggleMode, sx: {
25
+ ? localeText.switchModeTitle
26
+ : paletteMode === "dark"
27
+ ? localeText.lightModeTitle
28
+ : localeText.darkModeTitle, enterDelay: 1000, children: _jsx("div", { children: _jsx(IconButton, { "aria-label": isSsr
29
+ ? localeText.switchThemeModeAriaLabel
30
+ : paletteMode === "dark"
31
+ ? localeText.switchToLightModeAriaLabel
32
+ : localeText.switchToDarkModeAriaLabel, onClick: toggleMode, sx: {
27
33
  color: (theme.vars ?? theme).palette.primary.dark
28
34
  }, children: theme.getColorSchemeSelector ? (_jsxs(React.Fragment, { children: [_jsx(DarkModeIcon, { sx: {
29
35
  display: "inline",
@@ -0,0 +1 @@
1
+ export declare function TitleBar(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Stack, styled, Typography, useTheme } from "@mui/material";
3
+ import { Link } from "../shared/Link";
4
+ import { useApplicationTitle } from "../shared/branding";
5
+ import React from "react";
6
+ import { BrandingContext } from "../shared/context";
7
+ const LogoContainer = styled("div")({
8
+ position: "relative",
9
+ height: 40,
10
+ "& img": {
11
+ maxHeight: 40
12
+ }
13
+ });
14
+ export function TitleBar() {
15
+ // Theme
16
+ const theme = useTheme();
17
+ // Application title
18
+ const applicationTitle = useApplicationTitle();
19
+ // Branding
20
+ const branding = React.useContext(BrandingContext);
21
+ return (_jsx(Link, { href: "/", style: { color: "inherit", textDecoration: "none" }, children: _jsxs(Stack, { direction: "row", alignItems: "center", children: [branding?.logo && _jsx(LogoContainer, { children: branding.logo }), _jsx(Typography, { variant: "h6", sx: {
22
+ color: (theme.vars ?? theme).palette.primary.main,
23
+ fontWeight: "700",
24
+ ml: 0.5,
25
+ whiteSpace: "nowrap"
26
+ }, children: applicationTitle })] }) }));
27
+ }
@@ -35,10 +35,6 @@ export interface PageContainerProps extends ContainerProps {
35
35
  * The breadcrumbs of the page. Leave blank to use the active page breadcrumbs.
36
36
  */
37
37
  breadcrumbs?: Breadcrumb[];
38
- /**
39
- * @deprecated Use `breadcrumbs` instead.
40
- */
41
- breadCrumbs?: Breadcrumb[];
42
38
  /**
43
39
  * The components used for each slot inside.
44
40
  */
@@ -12,7 +12,6 @@ import { Link as ToolpadLink } from "../shared/Link";
12
12
  import { PageContainerToolbar } from "./PageContainerToolbar";
13
13
  import { getItemTitle } from "../shared/navigation";
14
14
  import { useActivePage } from "../useActivePage";
15
- import warnOnce from "../utils/warnOnce";
16
15
  const PageContentHeader = styled("div")(({ theme }) => ({
17
16
  display: "flex",
18
17
  flexDirection: "row",
@@ -31,13 +30,10 @@ const PageContentHeader = styled("div")(({ theme }) => ({
31
30
  * - [PageContainer API](https://mui.com/toolpad/core/api/page-container)
32
31
  */
33
32
  function PageContainer(props) {
34
- const { children, slots, slotProps, breadcrumbs, breadCrumbs, ...rest } = props;
35
- if (process.env.NODE_ENV !== "production" && breadCrumbs) {
36
- warnOnce("The PageContainer `breadCrumbs` prop is deprecated. Use `breadcrumbs` instead.");
37
- }
33
+ const { children, slots, slotProps, breadcrumbs, ...rest } = props;
38
34
  const activePage = useActivePage();
39
35
  // TODO: Remove `props.breadCrumbs` in the next major version
40
- const resolvedBreadcrumbs = breadcrumbs ?? breadCrumbs ?? activePage?.breadcrumbs ?? [];
36
+ const resolvedBreadcrumbs = breadcrumbs ?? activePage?.breadcrumbs ?? [];
41
37
  const title = props.title ?? activePage?.title ?? "";
42
38
  const ToolbarComponent = props?.slots?.toolbar ?? PageContainerToolbar;
43
39
  const toolbarSlotProps = useSlotProps({
@@ -64,13 +60,6 @@ PageContainer.propTypes /* remove-proptypes */ = {
64
60
  path: PropTypes.string.isRequired,
65
61
  title: PropTypes.string.isRequired
66
62
  })),
67
- /**
68
- * @deprecated Use `breadcrumbs` instead.
69
- */
70
- breadCrumbs: PropTypes.arrayOf(PropTypes.shape({
71
- path: PropTypes.string.isRequired,
72
- title: PropTypes.string.isRequired
73
- })),
74
63
  /**
75
64
  * @ignore
76
65
  */
@@ -110,18 +110,4 @@ describe("PageContainer", () => {
110
110
  expect(helloLink.getAttribute("href")).toBe("/hello");
111
111
  expect(within(breadcrumbs).getByText("World")).toBeTruthy();
112
112
  });
113
- // TODO: Remove in the next major version
114
- test("renders legacy breadcrumbs prop", async () => {
115
- vi.spyOn(console, "warn").mockImplementation(() => { });
116
- render(_jsx(PageContainer, { breadCrumbs: [
117
- { title: "Hello", path: "/hello" },
118
- { title: "World", path: "/world" }
119
- ] }));
120
- const breadcrumbs = screen.getByRole("navigation", { name: "breadcrumb" });
121
- const helloLink = within(breadcrumbs).getByRole("link", { name: "Hello" });
122
- expect(helloLink.getAttribute("href")).toBe("/hello");
123
- expect(within(breadcrumbs).getByText("World")).toBeTruthy();
124
- expect(console.warn).toHaveBeenCalledOnce();
125
- expect(console.warn).toHaveBeenCalledWith("The PageContainer `breadCrumbs` prop is deprecated. Use `breadcrumbs` instead.");
126
- });
127
113
  });
@@ -1,10 +1,35 @@
1
1
  import * as React from "react";
2
- export type LocaleContextType = {
3
- signInLabel?: string;
4
- signOutLabel?: string;
5
- iconButtonAriaLabel?: string;
2
+ declare const DEFAULT_LOCALE_TEXT: {
3
+ signInLabel: string;
4
+ signOutLabel: string;
5
+ accountIconButtonAriaLabel: string;
6
+ expandMenuTitle: string;
7
+ expandNavMenuAriaLabel: string;
8
+ collapseMenuTitle: string;
9
+ collapseNavMenuAriaLabel: string;
10
+ switchModeTitle: string;
11
+ darkModeTitle: string;
12
+ lightModeTitle: string;
13
+ switchThemeModeAriaLabel: string;
14
+ switchToDarkModeAriaLabel: string;
15
+ switchToLightModeAriaLabel: string;
6
16
  };
7
- export declare const LocaleContext: React.Context<LocaleContextType>;
17
+ export type LocaleContextType = typeof DEFAULT_LOCALE_TEXT;
18
+ export declare const LocaleContext: React.Context<{
19
+ signInLabel: string;
20
+ signOutLabel: string;
21
+ accountIconButtonAriaLabel: string;
22
+ expandMenuTitle: string;
23
+ expandNavMenuAriaLabel: string;
24
+ collapseMenuTitle: string;
25
+ collapseNavMenuAriaLabel: string;
26
+ switchModeTitle: string;
27
+ darkModeTitle: string;
28
+ lightModeTitle: string;
29
+ switchThemeModeAriaLabel: string;
30
+ switchToDarkModeAriaLabel: string;
31
+ switchToLightModeAriaLabel: string;
32
+ }>;
8
33
  export interface LocaleProviderProps {
9
34
  localeText?: Partial<LocaleContextType>;
10
35
  children: React.ReactNode;
@@ -13,7 +38,19 @@ export interface LocaleProviderProps {
13
38
  * @ignore - internal component.
14
39
  */
15
40
  export declare function LocaleProvider({ localeText, children }: LocaleProviderProps): import("react/jsx-runtime").JSX.Element;
16
- /**
17
- * @ignore - internal hook.
18
- */
19
- export declare function useLocaleText(): LocaleContextType;
41
+ export declare function useLocaleText(): {
42
+ signInLabel: string;
43
+ signOutLabel: string;
44
+ accountIconButtonAriaLabel: string;
45
+ expandMenuTitle: string;
46
+ expandNavMenuAriaLabel: string;
47
+ collapseMenuTitle: string;
48
+ collapseNavMenuAriaLabel: string;
49
+ switchModeTitle: string;
50
+ darkModeTitle: string;
51
+ lightModeTitle: string;
52
+ switchThemeModeAriaLabel: string;
53
+ switchToDarkModeAriaLabel: string;
54
+ switchToLightModeAriaLabel: string;
55
+ };
56
+ export {};
@@ -1,7 +1,25 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import DEFAULT_LOCALE_TEXT from "./en";
4
+ const DEFAULT_LOCALE_TEXT = {
5
+ // Account
6
+ signInLabel: "Sign In",
7
+ signOutLabel: "Sign Out",
8
+ // Account Preview
9
+ accountIconButtonAriaLabel: "Current User",
10
+ // Menu
11
+ expandMenuTitle: "Expand menu",
12
+ expandNavMenuAriaLabel: "Expand navigation menu",
13
+ collapseMenuTitle: "Collapse menu",
14
+ collapseNavMenuAriaLabel: "Collapse navigation menu",
15
+ // Theme
16
+ switchModeTitle: "Switch mode",
17
+ darkModeTitle: "Dark mode",
18
+ lightModeTitle: "Light mode",
19
+ switchThemeModeAriaLabel: "Switch theme mode",
20
+ switchToDarkModeAriaLabel: "Switch to dark mode",
21
+ switchToLightModeAriaLabel: "Switch to light mode"
22
+ };
5
23
  export const LocaleContext = React.createContext(DEFAULT_LOCALE_TEXT);
6
24
  /**
7
25
  * @ignore - internal component.
@@ -10,9 +28,6 @@ export function LocaleProvider({ localeText, children }) {
10
28
  const mergedLocaleText = React.useMemo(() => ({ ...DEFAULT_LOCALE_TEXT, ...localeText }), [localeText]);
11
29
  return (_jsx(LocaleContext.Provider, { value: mergedLocaleText, children: children }));
12
30
  }
13
- /**
14
- * @ignore - internal hook.
15
- */
16
31
  export function useLocaleText() {
17
32
  return React.useContext(LocaleContext);
18
33
  }
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "@etsoo/toolpad",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "author": "ETSOO",
5
5
  "description": "Dashboard framework extention based on Toolpad Core",
6
6
  "main": "build/index.js",
7
- "type": "module",
8
7
  "types": "build/index.d.ts",
9
8
  "sideEffects": false,
10
9
  "keywords": [
@@ -16,25 +15,25 @@
16
15
  ],
17
16
  "repository": {
18
17
  "type": "git",
19
- "url": "git+https://github.com/ETSOO/toolpad.git"
18
+ "url": "git+https://github.com/ETSOO/etsoo-toolpad.git"
20
19
  },
21
20
  "license": "MIT",
22
21
  "bugs": {
23
- "url": "https://github.com/ETSOO/toolpad/issues"
22
+ "url": "https://github.com/ETSOO/etsoo-toolpad/issues"
24
23
  },
25
- "homepage": "https://github.com/ETSOO/toolpad#readme",
24
+ "homepage": "https://github.com/ETSOO/etsoo-toolpad#readme",
26
25
  "scripts": {
27
26
  "build": "tsc",
28
27
  "test": "vitest",
29
28
  "testb": "vitest --browser.enabled"
30
29
  },
31
30
  "dependencies": {
32
- "@babel/runtime": "^7.25.7",
31
+ "@babel/runtime": "^7.26.0",
33
32
  "@emotion/css": "^11.13.4",
34
33
  "@emotion/react": "^11.13.3",
35
34
  "@emotion/styled": "^11.13.0",
36
- "@mui/lab": "6.0.0-beta.12",
37
- "@mui/utils": "6.1.4",
35
+ "@mui/lab": "6.0.0-beta.13",
36
+ "@mui/utils": "6.1.5",
38
37
  "@vitejs/plugin-react": "4.3.3",
39
38
  "invariant": "2.2.4",
40
39
  "path-to-regexp": "6.3.0",
@@ -42,19 +41,19 @@
42
41
  "react": "^18.3.1"
43
42
  },
44
43
  "devDependencies": {
45
- "@mui/icons-material": "6.1.4",
46
- "@mui/material": "6.1.4",
44
+ "@mui/icons-material": "6.1.5",
45
+ "@mui/material": "6.1.5",
47
46
  "@testing-library/jest-dom": "^6.6.2",
48
47
  "@testing-library/react": "^16.0.1",
49
48
  "@types/invariant": "2.2.37",
50
49
  "@types/prop-types": "15.7.13",
51
- "@types/react": "18.3.11",
50
+ "@types/react": "18.3.12",
52
51
  "@types/react-dom": "18.3.1",
53
- "@vitest/browser": "2.1.2",
54
- "next": "^14.2.15",
52
+ "@vitest/browser": "2.1.4",
53
+ "next": "^15.0.1",
55
54
  "next-router-mock": "^0.9.13",
56
- "playwright": "^1.47.2",
57
- "react-router-dom": "6.26.2",
58
- "vitest": "2.1.2"
55
+ "playwright": "^1.48.2",
56
+ "react-router-dom": "6.27.0",
57
+ "vitest": "2.1.4"
59
58
  }
60
59
  }
@@ -13,7 +13,6 @@ import {
13
13
  SessionContext,
14
14
  AuthenticationContext
15
15
  } from "../AppProvider/AppProvider";
16
- import { LocaleProvider, useLocaleText } from "../shared/locales/LocaleContext";
17
16
 
18
17
  export interface AccountSlots {
19
18
  /**
@@ -58,10 +57,6 @@ export interface AccountProps {
58
57
  signInButton?: React.ComponentProps<typeof SignInButton>;
59
58
  signOutButton?: React.ComponentProps<typeof Button>;
60
59
  };
61
- /**
62
- * The labels for the account component.
63
- */
64
- localeText?: Partial<ReturnType<typeof useLocaleText>>;
65
60
  }
66
61
  /**
67
62
  *
@@ -76,7 +71,6 @@ export interface AccountProps {
76
71
  * - [Account API](https://mui.com/toolpad/core/api/account)
77
72
  */
78
73
  function Account(props: AccountProps) {
79
- const { localeText } = props;
80
74
  const { slots, slotProps } = props;
81
75
  const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
82
76
  const session = React.useContext(SessionContext);
@@ -96,19 +90,15 @@ function Account(props: AccountProps) {
96
90
  }
97
91
 
98
92
  if (!session?.user) {
99
- return (
100
- <LocaleProvider localeText={localeText}>
101
- {slots?.signInButton ? (
102
- <slots.signInButton onClick={authentication.signIn} />
103
- ) : (
104
- <SignInButton {...slotProps?.signInButton} />
105
- )}
106
- </LocaleProvider>
93
+ return slots?.signInButton ? (
94
+ <slots.signInButton onClick={authentication.signIn} />
95
+ ) : (
96
+ <SignInButton {...slotProps?.signInButton} />
107
97
  );
108
98
  }
109
99
 
110
100
  return (
111
- <LocaleProvider localeText={localeText}>
101
+ <React.Fragment>
112
102
  {slots?.preview ? (
113
103
  <slots.preview handleClick={handleClick} open={open} />
114
104
  ) : (
@@ -170,7 +160,7 @@ function Account(props: AccountProps) {
170
160
  )}
171
161
  </Popover>
172
162
  )}
173
- </LocaleProvider>
163
+ </React.Fragment>
174
164
  );
175
165
  }
176
166
 
@@ -179,14 +169,6 @@ Account.propTypes /* remove-proptypes */ = {
179
169
  // │ These PropTypes are generated from the TypeScript type definitions. │
180
170
  // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
181
171
  // └─────────────────────────────────────────────────────────────────────┘
182
- /**
183
- * The labels for the account component.
184
- */
185
- localeText: PropTypes.shape({
186
- iconButtonAriaLabel: PropTypes.string,
187
- signInLabel: PropTypes.string,
188
- signOutLabel: PropTypes.string
189
- }),
190
172
  /**
191
173
  * The props used for each slot inside.
192
174
  */
@@ -6,7 +6,7 @@ import Tooltip from "@mui/material/Tooltip";
6
6
  import Stack from "@mui/material/Stack";
7
7
  import IconButton, { IconButtonProps } from "@mui/material/IconButton";
8
8
  import MoreVertIcon from "@mui/icons-material/MoreVert";
9
- import { SessionContext } from "../AppProvider";
9
+ import { Session, SessionContext } from "../AppProvider";
10
10
  import { useLocaleText } from "../shared/locales/LocaleContext";
11
11
 
12
12
  export type AccountPreviewVariant = "condensed" | "expanded";
@@ -60,6 +60,27 @@ export interface AccountPreviewProps {
60
60
  open?: boolean;
61
61
  }
62
62
 
63
+ function formatNameInitials(user?: Session["user"]) {
64
+ if (user == null) return null;
65
+
66
+ const name = user.latinName || user.name;
67
+ if (name == null) return null;
68
+
69
+ const chars = name
70
+ .trim()
71
+ .split(/\s+/g)
72
+ .map((part) => {
73
+ const firstChar = part.charAt(0).toUpperCase();
74
+ return firstChar >= "A" && firstChar <= "Z" ? firstChar : "";
75
+ })
76
+ .filter((c) => c)
77
+ .slice(-2);
78
+
79
+ if (chars.length === 0) return null;
80
+
81
+ return chars.join("");
82
+ }
83
+
63
84
  /**
64
85
  * The AccountPreview component displays user account information.
65
86
  *
@@ -91,7 +112,9 @@ function AccountPreview(props: AccountPreviewProps) {
91
112
  width: variant === "expanded" ? 48 : 32
92
113
  }}
93
114
  {...slotProps?.avatar}
94
- />
115
+ >
116
+ {session.user?.image ? null : formatNameInitials(session.user)}
117
+ </Avatar>
95
118
  );
96
119
 
97
120
  if (variant === "expanded") {
@@ -135,7 +158,7 @@ function AccountPreview(props: AccountPreviewProps) {
135
158
  ) : (
136
159
  <IconButton
137
160
  onClick={handleClick}
138
- aria-label={localeText.iconButtonAriaLabel || "Current User"}
161
+ aria-label={localeText.accountIconButtonAriaLabel}
139
162
  size="small"
140
163
  aria-controls={open ? "account-menu" : undefined}
141
164
  aria-haspopup="true"
@@ -32,7 +32,7 @@ export /**
32
32
  }}
33
33
  {...props}
34
34
  >
35
- {localeText?.signInLabel || "Sign In"}
35
+ {localeText.signInLabel}
36
36
  </Button>
37
37
  );
38
38
  }
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
3
  import { Theme } from "@mui/material/styles";
4
+ import { LocaleContextType } from "../shared/locales/LocaleContext";
4
5
 
5
6
  export interface NavigateOptions {
6
7
  history?: "auto" | "push" | "replace";
@@ -54,6 +55,7 @@ export interface Session {
54
55
  user?: {
55
56
  id?: string | null;
56
57
  name?: string | null;
58
+ latinName?: string | null;
57
59
  image?: string | null;
58
60
  email?: string | null;
59
61
  };
@@ -87,6 +89,10 @@ export interface AppProviderProps {
87
89
  * @default null
88
90
  */
89
91
  branding?: Branding | null;
92
+ /**
93
+ * The labels for the component.
94
+ */
95
+ localeText?: Partial<LocaleContextType>;
90
96
  /**
91
97
  * Navigation definition for the app.
92
98
  * @default []
@@ -13,6 +13,7 @@ import {
13
13
  AuthenticationContext,
14
14
  SessionContext
15
15
  } from "./AppProvider";
16
+ import { LocaleProvider } from "../shared/locales/LocaleContext";
16
17
 
17
18
  function createTheme(): Theme {
18
19
  return createMuiTheme({
@@ -39,6 +40,7 @@ function AppProvider(props: AppProviderProps) {
39
40
  children,
40
41
  theme = createTheme(),
41
42
  branding = null,
43
+ localeText,
42
44
  navigation = [],
43
45
  router = null,
44
46
  authentication = null,
@@ -52,11 +54,13 @@ function AppProvider(props: AppProviderProps) {
52
54
  <SessionContext.Provider value={session}>
53
55
  <RouterContext.Provider value={router}>
54
56
  <AppThemeProvider theme={theme} window={appWindow}>
55
- <BrandingContext.Provider value={branding}>
56
- <NavigationContext.Provider value={navigation}>
57
- {children}
58
- </NavigationContext.Provider>
59
- </BrandingContext.Provider>
57
+ <LocaleProvider localeText={localeText}>
58
+ <BrandingContext.Provider value={branding}>
59
+ <NavigationContext.Provider value={navigation}>
60
+ {children}
61
+ </NavigationContext.Provider>
62
+ </BrandingContext.Provider>
63
+ </LocaleProvider>
60
64
  </AppThemeProvider>
61
65
  </RouterContext.Provider>
62
66
  </SessionContext.Provider>
@@ -41,7 +41,7 @@ describe("DashboardLayout", () => {
41
41
 
42
42
  render(
43
43
  <AppProvider>
44
- <DashboardLayout>Hello world</DashboardLayout>
44
+ <DashboardLayout showThemeSwitcher>Hello world</DashboardLayout>
45
45
  </AppProvider>
46
46
  );
47
47
 
@@ -15,13 +15,8 @@ import type {} from "@mui/material/themeCssVarsAugmentation";
15
15
  import MenuIcon from "@mui/icons-material/Menu";
16
16
  import MenuOpenIcon from "@mui/icons-material/MenuOpen";
17
17
  import { Link } from "../shared/Link";
18
- import {
19
- BrandingContext,
20
- NavigationContext,
21
- WindowContext
22
- } from "../shared/context";
18
+ import { NavigationContext, WindowContext } from "../shared/context";
23
19
  import { Account, type AccountProps } from "../Account";
24
- import { useApplicationTitle } from "../shared/branding";
25
20
  import { DashboardSidebarSubNavigation } from "./DashboardSidebarSubNavigation";
26
21
  import { ToolbarActions } from "./ToolbarActions";
27
22
  import { ThemeSwitcher } from "./ThemeSwitcher";
@@ -29,6 +24,8 @@ import {
29
24
  getDrawerSxTransitionMixin,
30
25
  getDrawerWidthTransitionMixin
31
26
  } from "./utils";
27
+ import { TitleBar } from "./TitleBar";
28
+ import { useLocaleText } from "../shared/locales/LocaleContext";
32
29
 
33
30
  const AppBar = styled(MuiAppBar)(({ theme }) => ({
34
31
  borderWidth: 0,
@@ -39,25 +36,23 @@ const AppBar = styled(MuiAppBar)(({ theme }) => ({
39
36
  zIndex: theme.zIndex.drawer + 1
40
37
  }));
41
38
 
42
- const LogoContainer = styled("div")({
43
- position: "relative",
44
- height: 40,
45
- "& img": {
46
- maxHeight: 40
47
- }
48
- });
49
-
50
39
  export interface SidebarFooterProps {
51
40
  mini: boolean;
52
41
  }
53
42
 
54
43
  export interface DashboardLayoutSlotProps {
44
+ titlebar?: {};
55
45
  toolbarActions?: {};
56
46
  toolbarAccount?: AccountProps;
57
47
  sidebarFooter?: SidebarFooterProps;
58
48
  }
59
49
 
60
50
  export interface DashboardLayoutSlots {
51
+ /**
52
+ * The title bar component used in the layout header.
53
+ * @default TitleBar
54
+ */
55
+ titlebar?: React.JSXElementConstructor<{}>;
61
56
  /**
62
57
  * The toolbar actions component used in the layout header.
63
58
  * @default ToolbarActions
@@ -95,6 +90,11 @@ export interface DashboardLayoutProps {
95
90
  * @default false
96
91
  */
97
92
  hideNavigation?: boolean;
93
+ /**
94
+ * Whether the theme switcher should be shown in the toolbar.
95
+ * @default false
96
+ */
97
+ showThemeSwitcher?: boolean;
98
98
  /**
99
99
  * Width of the sidebar when expanded.
100
100
  * @default 320
@@ -132,6 +132,7 @@ function DashboardLayout(props: DashboardLayoutProps) {
132
132
  disableCollapsibleSidebar = false,
133
133
  defaultSidebarCollapsed = false,
134
134
  hideNavigation = false,
135
+ showThemeSwitcher = false,
135
136
  sidebarExpandedWidth = 320,
136
137
  slots,
137
138
  slotProps,
@@ -140,10 +141,9 @@ function DashboardLayout(props: DashboardLayoutProps) {
140
141
 
141
142
  const theme = useTheme();
142
143
 
143
- const branding = React.useContext(BrandingContext);
144
144
  const navigation = React.useContext(NavigationContext);
145
145
  const appWindow = React.useContext(WindowContext);
146
- const applicationTitle = useApplicationTitle();
146
+ const localeText = useLocaleText();
147
147
 
148
148
  const [isDesktopNavigationExpanded, setIsDesktopNavigationExpanded] =
149
149
  React.useState(!defaultSidebarCollapsed);
@@ -227,21 +227,22 @@ function DashboardLayout(props: DashboardLayoutProps) {
227
227
 
228
228
  const getMenuIcon = React.useCallback(
229
229
  (isExpanded: boolean) => {
230
- const expandMenuActionText = "Expand";
231
- const collapseMenuActionText = "Collapse";
232
-
233
230
  return (
234
231
  <Tooltip
235
- title={`${
236
- isExpanded ? collapseMenuActionText : expandMenuActionText
237
- } menu`}
232
+ title={
233
+ isExpanded
234
+ ? localeText.collapseMenuTitle
235
+ : localeText.expandMenuTitle
236
+ }
238
237
  enterDelay={1000}
239
238
  >
240
239
  <div>
241
240
  <IconButton
242
- aria-label={`${
243
- isExpanded ? collapseMenuActionText : expandMenuActionText
244
- } navigation menu`}
241
+ aria-label={
242
+ isExpanded
243
+ ? localeText.collapseNavMenuAriaLabel
244
+ : localeText.expandNavMenuAriaLabel
245
+ }
245
246
  onClick={toggleNavigationExpanded}
246
247
  >
247
248
  {isExpanded ? <MenuOpenIcon /> : <MenuIcon />}
@@ -256,6 +257,7 @@ function DashboardLayout(props: DashboardLayoutProps) {
256
257
  const hasDrawerTransitions =
257
258
  isOverSmViewport && (disableCollapsibleSidebar || !isUnderMdViewport);
258
259
 
260
+ const TitlebarSlot = slots?.titlebar ?? TitleBar;
259
261
  const ToolbarActionsSlot = slots?.toolbarActions ?? ToolbarActions;
260
262
  const ToolbarAccountSlot = slots?.toolbarAccount ?? Account;
261
263
  const SidebarFooterSlot = slots?.sidebarFooter ?? null;
@@ -373,29 +375,12 @@ function DashboardLayout(props: DashboardLayoutProps) {
373
375
  transform: { xs: "translateX(-50%)", md: "none" }
374
376
  }}
375
377
  >
376
- <Link href="/" style={{ color: "inherit", textDecoration: "none" }}>
377
- <Stack direction="row" alignItems="center">
378
- {branding?.logo && (
379
- <LogoContainer>{branding.logo}</LogoContainer>
380
- )}
381
- <Typography
382
- variant="h6"
383
- sx={{
384
- color: (theme.vars ?? theme).palette.primary.main,
385
- fontWeight: "700",
386
- ml: 0.5,
387
- whiteSpace: "nowrap"
388
- }}
389
- >
390
- {applicationTitle}
391
- </Typography>
392
- </Stack>
393
- </Link>
378
+ <TitlebarSlot {...slotProps?.titlebar} />
394
379
  </Box>
395
380
  <Box sx={{ flexGrow: 1 }} />
396
381
  <Stack direction="row" spacing={1}>
397
382
  <ToolbarActionsSlot {...slotProps?.toolbarActions} />
398
- <ThemeSwitcher />
383
+ {showThemeSwitcher && <ThemeSwitcher />}
399
384
  <ToolbarAccountSlot {...slotProps?.toolbarAccount} />
400
385
  </Stack>
401
386
  </Toolbar>
@@ -7,6 +7,7 @@ import DarkModeIcon from "@mui/icons-material/DarkMode";
7
7
  import LightModeIcon from "@mui/icons-material/LightMode";
8
8
  import { PaletteModeContext } from "../shared/context";
9
9
  import useSsr from "../utils/hooks/useSsr";
10
+ import { useLocaleText } from "../shared/locales/LocaleContext";
10
11
 
11
12
  // TODO: When we use this component as the default for a slot, make it non-internal
12
13
  /**
@@ -15,6 +16,7 @@ import useSsr from "../utils/hooks/useSsr";
15
16
  function ThemeSwitcher() {
16
17
  const isSsr = useSsr();
17
18
  const theme = useTheme();
19
+ const localeText = useLocaleText();
18
20
 
19
21
  const { paletteMode, setPaletteMode, isDualTheme } =
20
22
  React.useContext(PaletteModeContext);
@@ -27,8 +29,10 @@ function ThemeSwitcher() {
27
29
  <Tooltip
28
30
  title={
29
31
  isSsr
30
- ? "Switch mode"
31
- : `${paletteMode === "dark" ? "Light" : "Dark"} mode`
32
+ ? localeText.switchModeTitle
33
+ : paletteMode === "dark"
34
+ ? localeText.lightModeTitle
35
+ : localeText.darkModeTitle
32
36
  }
33
37
  enterDelay={1000}
34
38
  >
@@ -36,8 +40,10 @@ function ThemeSwitcher() {
36
40
  <IconButton
37
41
  aria-label={
38
42
  isSsr
39
- ? "Switch theme mode"
40
- : `Switch to ${paletteMode === "dark" ? "light" : "dark"} mode`
43
+ ? localeText.switchThemeModeAriaLabel
44
+ : paletteMode === "dark"
45
+ ? localeText.switchToLightModeAriaLabel
46
+ : localeText.switchToDarkModeAriaLabel
41
47
  }
42
48
  onClick={toggleMode}
43
49
  sx={{
@@ -0,0 +1,43 @@
1
+ import { Stack, styled, Typography, useTheme } from "@mui/material";
2
+ import { Link } from "../shared/Link";
3
+ import { useApplicationTitle } from "../shared/branding";
4
+ import React from "react";
5
+ import { BrandingContext } from "../shared/context";
6
+
7
+ const LogoContainer = styled("div")({
8
+ position: "relative",
9
+ height: 40,
10
+ "& img": {
11
+ maxHeight: 40
12
+ }
13
+ });
14
+
15
+ export function TitleBar() {
16
+ // Theme
17
+ const theme = useTheme();
18
+
19
+ // Application title
20
+ const applicationTitle = useApplicationTitle();
21
+
22
+ // Branding
23
+ const branding = React.useContext(BrandingContext);
24
+
25
+ return (
26
+ <Link href="/" style={{ color: "inherit", textDecoration: "none" }}>
27
+ <Stack direction="row" alignItems="center">
28
+ {branding?.logo && <LogoContainer>{branding.logo}</LogoContainer>}
29
+ <Typography
30
+ variant="h6"
31
+ sx={{
32
+ color: (theme.vars ?? theme).palette.primary.main,
33
+ fontWeight: "700",
34
+ ml: 0.5,
35
+ whiteSpace: "nowrap"
36
+ }}
37
+ >
38
+ {applicationTitle}
39
+ </Typography>
40
+ </Stack>
41
+ </Link>
42
+ );
43
+ }
@@ -168,27 +168,4 @@ describe("PageContainer", () => {
168
168
  expect(helloLink.getAttribute("href")).toBe("/hello");
169
169
  expect(within(breadcrumbs).getByText("World")).toBeTruthy();
170
170
  });
171
-
172
- // TODO: Remove in the next major version
173
- test("renders legacy breadcrumbs prop", async () => {
174
- vi.spyOn(console, "warn").mockImplementation(() => {});
175
- render(
176
- <PageContainer
177
- breadCrumbs={[
178
- { title: "Hello", path: "/hello" },
179
- { title: "World", path: "/world" }
180
- ]}
181
- />
182
- );
183
-
184
- const breadcrumbs = screen.getByRole("navigation", { name: "breadcrumb" });
185
-
186
- const helloLink = within(breadcrumbs).getByRole("link", { name: "Hello" });
187
- expect(helloLink.getAttribute("href")).toBe("/hello");
188
- expect(within(breadcrumbs).getByText("World")).toBeTruthy();
189
- expect(console.warn).toHaveBeenCalledOnce();
190
- expect(console.warn).toHaveBeenCalledWith(
191
- "The PageContainer `breadCrumbs` prop is deprecated. Use `breadcrumbs` instead."
192
- );
193
- });
194
171
  });
@@ -15,7 +15,6 @@ import {
15
15
  } from "./PageContainerToolbar";
16
16
  import { getItemTitle } from "../shared/navigation";
17
17
  import { useActivePage } from "../useActivePage";
18
- import warnOnce from "../utils/warnOnce";
19
18
 
20
19
  const PageContentHeader = styled("div")(({ theme }) => ({
21
20
  display: "flex",
@@ -63,11 +62,6 @@ export interface PageContainerProps extends ContainerProps {
63
62
  * The breadcrumbs of the page. Leave blank to use the active page breadcrumbs.
64
63
  */
65
64
  breadcrumbs?: Breadcrumb[];
66
- // TODO: Remove in the next major version
67
- /**
68
- * @deprecated Use `breadcrumbs` instead.
69
- */
70
- breadCrumbs?: Breadcrumb[];
71
65
  /**
72
66
  * The components used for each slot inside.
73
67
  */
@@ -90,20 +84,12 @@ export interface PageContainerProps extends ContainerProps {
90
84
  * - [PageContainer API](https://mui.com/toolpad/core/api/page-container)
91
85
  */
92
86
  function PageContainer(props: PageContainerProps) {
93
- const { children, slots, slotProps, breadcrumbs, breadCrumbs, ...rest } =
94
- props;
95
-
96
- if (process.env.NODE_ENV !== "production" && breadCrumbs) {
97
- warnOnce(
98
- "The PageContainer `breadCrumbs` prop is deprecated. Use `breadcrumbs` instead."
99
- );
100
- }
87
+ const { children, slots, slotProps, breadcrumbs, ...rest } = props;
101
88
 
102
89
  const activePage = useActivePage();
103
90
 
104
91
  // TODO: Remove `props.breadCrumbs` in the next major version
105
- const resolvedBreadcrumbs =
106
- breadcrumbs ?? breadCrumbs ?? activePage?.breadcrumbs ?? [];
92
+ const resolvedBreadcrumbs = breadcrumbs ?? activePage?.breadcrumbs ?? [];
107
93
  const title = props.title ?? activePage?.title ?? "";
108
94
 
109
95
  const ToolbarComponent = props?.slots?.toolbar ?? PageContainerToolbar;
@@ -165,15 +151,6 @@ PageContainer.propTypes /* remove-proptypes */ = {
165
151
  title: PropTypes.string.isRequired
166
152
  })
167
153
  ),
168
- /**
169
- * @deprecated Use `breadcrumbs` instead.
170
- */
171
- breadCrumbs: PropTypes.arrayOf(
172
- PropTypes.shape({
173
- path: PropTypes.string.isRequired,
174
- title: PropTypes.string.isRequired
175
- })
176
- ),
177
154
  /**
178
155
  * @ignore
179
156
  */
@@ -1,16 +1,29 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
- import DEFAULT_LOCALE_TEXT from "./en";
5
4
 
6
- export type LocaleContextType = {
5
+ const DEFAULT_LOCALE_TEXT = {
7
6
  // Account
8
- signInLabel?: string;
9
- signOutLabel?: string;
7
+ signInLabel: "Sign In",
8
+ signOutLabel: "Sign Out",
10
9
  // Account Preview
11
- iconButtonAriaLabel?: string;
10
+ accountIconButtonAriaLabel: "Current User",
11
+ // Menu
12
+ expandMenuTitle: "Expand menu",
13
+ expandNavMenuAriaLabel: "Expand navigation menu",
14
+ collapseMenuTitle: "Collapse menu",
15
+ collapseNavMenuAriaLabel: "Collapse navigation menu",
16
+ // Theme
17
+ switchModeTitle: "Switch mode",
18
+ darkModeTitle: "Dark mode",
19
+ lightModeTitle: "Light mode",
20
+ switchThemeModeAriaLabel: "Switch theme mode",
21
+ switchToDarkModeAriaLabel: "Switch to dark mode",
22
+ switchToLightModeAriaLabel: "Switch to light mode"
12
23
  };
13
24
 
25
+ export type LocaleContextType = typeof DEFAULT_LOCALE_TEXT;
26
+
14
27
  export const LocaleContext =
15
28
  React.createContext<LocaleContextType>(DEFAULT_LOCALE_TEXT);
16
29
 
@@ -35,10 +48,6 @@ export function LocaleProvider({ localeText, children }: LocaleProviderProps) {
35
48
  );
36
49
  }
37
50
 
38
- /**
39
- * @ignore - internal hook.
40
- */
41
-
42
51
  export function useLocaleText() {
43
52
  return React.useContext(LocaleContext);
44
53
  }
package/tsconfig.json CHANGED
@@ -3,7 +3,7 @@
3
3
  /* Visit https://aka.ms/tsconfig.json to read more about this file */
4
4
  "target": "ES2020",
5
5
  "module": "ESNext",
6
- "moduleResolution": "Node10",
6
+ "moduleResolution": "bundler",
7
7
  "isolatedModules": true,
8
8
  "outDir": "build",
9
9
  "noEmit": false,
@@ -1,6 +0,0 @@
1
- declare const TOOLPAD_CORE_DEFAULT_LOCALE_TEXT: {
2
- signInLabel: string;
3
- signOutLabel: string;
4
- iconButtonAriaLabel: string;
5
- };
6
- export default TOOLPAD_CORE_DEFAULT_LOCALE_TEXT;
@@ -1,8 +0,0 @@
1
- const TOOLPAD_CORE_DEFAULT_LOCALE_TEXT = {
2
- // Account
3
- signInLabel: "Sign In",
4
- signOutLabel: "Sign Out",
5
- // Account Preview
6
- iconButtonAriaLabel: "Current User"
7
- };
8
- export default TOOLPAD_CORE_DEFAULT_LOCALE_TEXT;
@@ -1,9 +0,0 @@
1
- const TOOLPAD_CORE_DEFAULT_LOCALE_TEXT = {
2
- // Account
3
- signInLabel: "Sign In",
4
- signOutLabel: "Sign Out",
5
- // Account Preview
6
- iconButtonAriaLabel: "Current User"
7
- };
8
-
9
- export default TOOLPAD_CORE_DEFAULT_LOCALE_TEXT;