@etsoo/toolpad 1.0.0

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 (202) hide show
  1. package/.github/workflows/main.yml +49 -0
  2. package/.vscode/settings.json +3 -0
  3. package/LICENSE +21 -0
  4. package/README.md +11 -0
  5. package/build/src/Account/Account.d.ts +71 -0
  6. package/build/src/Account/Account.js +117 -0
  7. package/build/src/Account/Account.test.d.ts +1 -0
  8. package/build/src/Account/Account.test.js +35 -0
  9. package/build/src/Account/AccountPopoverFooter.d.ts +6 -0
  10. package/build/src/Account/AccountPopoverFooter.js +21 -0
  11. package/build/src/Account/AccountPopoverHeader.d.ts +6 -0
  12. package/build/src/Account/AccountPopoverHeader.js +15 -0
  13. package/build/src/Account/AccountPreview.d.ts +67 -0
  14. package/build/src/Account/AccountPreview.js +76 -0
  15. package/build/src/Account/AccountPreview.test.d.ts +1 -0
  16. package/build/src/Account/AccountPreview.test.js +47 -0
  17. package/build/src/Account/SignInButton.d.ts +2 -0
  18. package/build/src/Account/SignInButton.js +26 -0
  19. package/build/src/Account/SignOutButton.d.ts +3 -0
  20. package/build/src/Account/SignOutButton.js +28 -0
  21. package/build/src/Account/index.d.ts +6 -0
  22. package/build/src/Account/index.js +6 -0
  23. package/build/src/AppProvider/AppProvider.d.ts +98 -0
  24. package/build/src/AppProvider/AppProvider.js +4 -0
  25. package/build/src/AppProvider/AppProvider.test.d.ts +4 -0
  26. package/build/src/AppProvider/AppProvider.test.js +16 -0
  27. package/build/src/AppProvider/AppProviderComponent.d.ts +17 -0
  28. package/build/src/AppProvider/AppProviderComponent.js +120 -0
  29. package/build/src/AppProvider/AppThemeProvider.d.ts +12 -0
  30. package/build/src/AppProvider/AppThemeProvider.js +73 -0
  31. package/build/src/AppProvider/index.d.ts +1 -0
  32. package/build/src/AppProvider/index.js +1 -0
  33. package/build/src/DashboardLayout/DashboardLayout.d.ts +83 -0
  34. package/build/src/DashboardLayout/DashboardLayout.js +286 -0
  35. package/build/src/DashboardLayout/DashboardLayout.test.d.ts +1 -0
  36. package/build/src/DashboardLayout/DashboardLayout.test.js +291 -0
  37. package/build/src/DashboardLayout/DashboardSidebarSubNavigation.d.ts +16 -0
  38. package/build/src/DashboardLayout/DashboardSidebarSubNavigation.js +149 -0
  39. package/build/src/DashboardLayout/ThemeSwitcher.d.ts +5 -0
  40. package/build/src/DashboardLayout/ThemeSwitcher.js +40 -0
  41. package/build/src/DashboardLayout/ToolbarActions.d.ts +5 -0
  42. package/build/src/DashboardLayout/ToolbarActions.js +9 -0
  43. package/build/src/DashboardLayout/index.d.ts +2 -0
  44. package/build/src/DashboardLayout/index.js +3 -0
  45. package/build/src/DashboardLayout/utils.d.ts +8 -0
  46. package/build/src/DashboardLayout/utils.js +16 -0
  47. package/build/src/PageContainer/PageContainer.d.ts +66 -0
  48. package/build/src/PageContainer/PageContainer.js +97 -0
  49. package/build/src/PageContainer/PageContainer.test.d.ts +1 -0
  50. package/build/src/PageContainer/PageContainer.test.js +127 -0
  51. package/build/src/PageContainer/PageContainerToolbar.d.ts +16 -0
  52. package/build/src/PageContainer/PageContainerToolbar.js +24 -0
  53. package/build/src/PageContainer/PageContainerToolbar.test.d.ts +1 -0
  54. package/build/src/PageContainer/PageContainerToolbar.test.js +10 -0
  55. package/build/src/PageContainer/index.d.ts +2 -0
  56. package/build/src/PageContainer/index.js +2 -0
  57. package/build/src/index.d.ts +8 -0
  58. package/build/src/index.js +8 -0
  59. package/build/src/nextjs/AppProvider.d.ts +6 -0
  60. package/build/src/nextjs/AppProvider.js +16 -0
  61. package/build/src/nextjs/AppProvider.test.d.ts +1 -0
  62. package/build/src/nextjs/AppProvider.test.js +36 -0
  63. package/build/src/nextjs/AppProviderNextApp.d.ts +5 -0
  64. package/build/src/nextjs/AppProviderNextApp.js +27 -0
  65. package/build/src/nextjs/AppProviderNextPages.d.ts +5 -0
  66. package/build/src/nextjs/AppProviderNextPages.js +37 -0
  67. package/build/src/nextjs/index.d.ts +1 -0
  68. package/build/src/nextjs/index.js +1 -0
  69. package/build/src/persistence/codec.d.ts +47 -0
  70. package/build/src/persistence/codec.js +57 -0
  71. package/build/src/persistence/index.d.ts +1 -0
  72. package/build/src/persistence/index.js +1 -0
  73. package/build/src/persistence/useStorageState.d.ts +50 -0
  74. package/build/src/persistence/useStorageState.js +116 -0
  75. package/build/src/persistence/useStorageState.test.d.ts +1 -0
  76. package/build/src/persistence/useStorageState.test.js +61 -0
  77. package/build/src/react-router-dom/AppProvider.d.ts +6 -0
  78. package/build/src/react-router-dom/AppProvider.js +29 -0
  79. package/build/src/react-router-dom/AppProvider.test.d.ts +1 -0
  80. package/build/src/react-router-dom/AppProvider.test.js +12 -0
  81. package/build/src/react-router-dom/index.d.ts +1 -0
  82. package/build/src/react-router-dom/index.js +1 -0
  83. package/build/src/shared/Link.d.ts +8 -0
  84. package/build/src/shared/Link.js +19 -0
  85. package/build/src/shared/branding.d.ts +1 -0
  86. package/build/src/shared/branding.js +6 -0
  87. package/build/src/shared/components.d.ts +5 -0
  88. package/build/src/shared/components.js +23 -0
  89. package/build/src/shared/context.d.ts +12 -0
  90. package/build/src/shared/context.js +10 -0
  91. package/build/src/shared/locales/LocaleContext.d.ts +19 -0
  92. package/build/src/shared/locales/LocaleContext.js +18 -0
  93. package/build/src/shared/locales/en.d.ts +6 -0
  94. package/build/src/shared/locales/en.js +8 -0
  95. package/build/src/shared/navigation.d.ts +16 -0
  96. package/build/src/shared/navigation.js +129 -0
  97. package/build/src/useActivePage/index.d.ts +1 -0
  98. package/build/src/useActivePage/index.js +1 -0
  99. package/build/src/useActivePage/useActivePage.d.ts +11 -0
  100. package/build/src/useActivePage/useActivePage.js +48 -0
  101. package/build/src/useLocalStorageState/index.d.ts +1 -0
  102. package/build/src/useLocalStorageState/index.js +1 -0
  103. package/build/src/useLocalStorageState/useLocalStorageState.d.ts +2 -0
  104. package/build/src/useLocalStorageState/useLocalStorageState.js +14 -0
  105. package/build/src/useSessionStorageState/index.d.ts +1 -0
  106. package/build/src/useSessionStorageState/index.js +1 -0
  107. package/build/src/useSessionStorageState/useSessionStorageState.d.ts +2 -0
  108. package/build/src/useSessionStorageState/useSessionStorageState.js +14 -0
  109. package/build/src/utils/collections.d.ts +44 -0
  110. package/build/src/utils/collections.js +43 -0
  111. package/build/src/utils/describeConformance.d.ts +7 -0
  112. package/build/src/utils/describeConformance.js +3 -0
  113. package/build/src/utils/events.d.ts +27 -0
  114. package/build/src/utils/events.js +54 -0
  115. package/build/src/utils/hooks/index.d.ts +2 -0
  116. package/build/src/utils/hooks/index.js +2 -0
  117. package/build/src/utils/hooks/useBoolean.d.ts +11 -0
  118. package/build/src/utils/hooks/useBoolean.js +11 -0
  119. package/build/src/utils/hooks/useDebounced.d.ts +8 -0
  120. package/build/src/utils/hooks/useDebounced.js +28 -0
  121. package/build/src/utils/hooks/useDebouncedHandler.d.ts +12 -0
  122. package/build/src/utils/hooks/useDebouncedHandler.js +41 -0
  123. package/build/src/utils/hooks/useLatest.d.ts +6 -0
  124. package/build/src/utils/hooks/useLatest.js +9 -0
  125. package/build/src/utils/hooks/usePageTitle.d.ts +4 -0
  126. package/build/src/utils/hooks/usePageTitle.js +13 -0
  127. package/build/src/utils/hooks/useSsr.d.ts +4 -0
  128. package/build/src/utils/hooks/useSsr.js +16 -0
  129. package/build/src/utils/hooks/useStorageState.d.ts +10 -0
  130. package/build/src/utils/hooks/useStorageState.js +51 -0
  131. package/build/src/utils/warnOnce.d.ts +5 -0
  132. package/build/src/utils/warnOnce.js +11 -0
  133. package/build/vitest.setup.d.ts +1 -0
  134. package/build/vitest.setup.js +19 -0
  135. package/package.json +60 -0
  136. package/src/Account/Account.test.tsx +56 -0
  137. package/src/Account/Account.tsx +226 -0
  138. package/src/Account/AccountPopoverFooter.tsx +33 -0
  139. package/src/Account/AccountPopoverHeader.tsx +20 -0
  140. package/src/Account/AccountPreview.test.tsx +79 -0
  141. package/src/Account/AccountPreview.tsx +189 -0
  142. package/src/Account/SignInButton.tsx +38 -0
  143. package/src/Account/SignOutButton.tsx +44 -0
  144. package/src/Account/index.ts +6 -0
  145. package/src/AppProvider/AppProvider.test.tsx +25 -0
  146. package/src/AppProvider/AppProvider.tsx +116 -0
  147. package/src/AppProvider/AppProviderComponent.tsx +162 -0
  148. package/src/AppProvider/AppThemeProvider.tsx +177 -0
  149. package/src/AppProvider/index.ts +1 -0
  150. package/src/DashboardLayout/DashboardLayout.test.tsx +429 -0
  151. package/src/DashboardLayout/DashboardLayout.tsx +558 -0
  152. package/src/DashboardLayout/DashboardSidebarSubNavigation.tsx +301 -0
  153. package/src/DashboardLayout/ThemeSwitcher.tsx +81 -0
  154. package/src/DashboardLayout/ToolbarActions.tsx +11 -0
  155. package/src/DashboardLayout/index.ts +4 -0
  156. package/src/DashboardLayout/utils.ts +23 -0
  157. package/src/PageContainer/PageContainer.test.tsx +194 -0
  158. package/src/PageContainer/PageContainer.tsx +201 -0
  159. package/src/PageContainer/PageContainerToolbar.test.tsx +11 -0
  160. package/src/PageContainer/PageContainerToolbar.tsx +31 -0
  161. package/src/PageContainer/index.ts +2 -0
  162. package/src/index.ts +15 -0
  163. package/src/nextjs/AppProvider.test.tsx +48 -0
  164. package/src/nextjs/AppProvider.tsx +18 -0
  165. package/src/nextjs/AppProviderNextApp.tsx +37 -0
  166. package/src/nextjs/AppProviderNextPages.tsx +52 -0
  167. package/src/nextjs/index.tsx +1 -0
  168. package/src/persistence/codec.tsx +81 -0
  169. package/src/persistence/index.ts +1 -0
  170. package/src/persistence/useStorageState.test.tsx +101 -0
  171. package/src/persistence/useStorageState.tsx +244 -0
  172. package/src/react-router-dom/AppProvider.test.tsx +17 -0
  173. package/src/react-router-dom/AppProvider.tsx +40 -0
  174. package/src/react-router-dom/index.tsx +1 -0
  175. package/src/shared/Link.tsx +37 -0
  176. package/src/shared/branding.ts +7 -0
  177. package/src/shared/components.tsx +46 -0
  178. package/src/shared/context.ts +21 -0
  179. package/src/shared/locales/LocaleContext.tsx +44 -0
  180. package/src/shared/locales/en.tsx +9 -0
  181. package/src/shared/navigation.tsx +197 -0
  182. package/src/useActivePage/index.ts +1 -0
  183. package/src/useActivePage/useActivePage.ts +66 -0
  184. package/src/useLocalStorageState/index.ts +1 -0
  185. package/src/useLocalStorageState/useLocalStorageState.tsx +23 -0
  186. package/src/useSessionStorageState/index.tsx +1 -0
  187. package/src/useSessionStorageState/useSessionStorageState.tsx +23 -0
  188. package/src/utils/collections.ts +125 -0
  189. package/src/utils/describeConformance.ts +14 -0
  190. package/src/utils/events.ts +81 -0
  191. package/src/utils/hooks/index.ts +2 -0
  192. package/src/utils/hooks/useBoolean.ts +12 -0
  193. package/src/utils/hooks/useDebounced.ts +39 -0
  194. package/src/utils/hooks/useDebouncedHandler.ts +71 -0
  195. package/src/utils/hooks/useLatest.ts +16 -0
  196. package/src/utils/hooks/usePageTitle.ts +14 -0
  197. package/src/utils/hooks/useSsr.ts +24 -0
  198. package/src/utils/hooks/useStorageState.ts +130 -0
  199. package/src/utils/warnOnce.ts +12 -0
  200. package/tsconfig.json +19 -0
  201. package/vite.config.ts +27 -0
  202. package/vitest.setup.ts +22 -0
@@ -0,0 +1,301 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { styled } from "@mui/material";
4
+ import Avatar from "@mui/material/Avatar";
5
+ import Collapse from "@mui/material/Collapse";
6
+ import Divider from "@mui/material/Divider";
7
+ import List from "@mui/material/List";
8
+ import ListItem from "@mui/material/ListItem";
9
+ import ListItemButton from "@mui/material/ListItemButton";
10
+ import ListItemIcon from "@mui/material/ListItemIcon";
11
+ import ListItemText from "@mui/material/ListItemText";
12
+ import ListSubheader from "@mui/material/ListSubheader";
13
+ import Tooltip from "@mui/material/Tooltip";
14
+ import type {} from "@mui/material/themeCssVarsAugmentation";
15
+ import ExpandLessIcon from "@mui/icons-material/ExpandLess";
16
+ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
17
+ import { Link } from "../shared/Link";
18
+ import { RouterContext } from "../shared/context";
19
+ import type { Navigation } from "../AppProvider";
20
+ import {
21
+ getItemTitle,
22
+ getPageItemFullPath,
23
+ hasSelectedNavigationChildren,
24
+ isPageItemSelected
25
+ } from "../shared/navigation";
26
+ import { getDrawerSxTransitionMixin } from "./utils";
27
+
28
+ const NavigationListItemButton = styled(ListItemButton)(({ theme }) => ({
29
+ borderRadius: 8,
30
+ "&.Mui-selected": {
31
+ "& .MuiListItemIcon-root": {
32
+ color: (theme.vars ?? theme).palette.primary.dark
33
+ },
34
+ "& .MuiTypography-root": {
35
+ color: (theme.vars ?? theme).palette.primary.dark
36
+ },
37
+ "& .MuiSvgIcon-root": {
38
+ color: (theme.vars ?? theme).palette.primary.dark
39
+ },
40
+ "& .MuiAvatar-root": {
41
+ backgroundColor: (theme.vars ?? theme).palette.primary.dark
42
+ },
43
+ "& .MuiTouchRipple-child": {
44
+ backgroundColor: (theme.vars ?? theme).palette.primary.dark
45
+ }
46
+ },
47
+ "& .MuiSvgIcon-root": {
48
+ color: (theme.vars ?? theme).palette.action.active
49
+ },
50
+ "& .MuiAvatar-root": {
51
+ backgroundColor: (theme.vars ?? theme).palette.action.active
52
+ }
53
+ }));
54
+
55
+ interface DashboardSidebarSubNavigationProps {
56
+ subNavigation: Navigation;
57
+ basePath?: string;
58
+ depth?: number;
59
+ onLinkClick: () => void;
60
+ isMini?: boolean;
61
+ isFullyExpanded?: boolean;
62
+ hasDrawerTransitions?: boolean;
63
+ selectedItemId: string;
64
+ }
65
+
66
+ /**
67
+ * @ignore - internal component.
68
+ */
69
+ function DashboardSidebarSubNavigation({
70
+ subNavigation,
71
+ basePath = "",
72
+ depth = 0,
73
+ onLinkClick,
74
+ isMini = false,
75
+ isFullyExpanded = true,
76
+ hasDrawerTransitions = false,
77
+ selectedItemId
78
+ }: DashboardSidebarSubNavigationProps) {
79
+ const routerContext = React.useContext(RouterContext);
80
+
81
+ const pathname = routerContext?.pathname ?? "/";
82
+
83
+ const initialExpandedSidebarItemIds = React.useMemo(
84
+ () =>
85
+ subNavigation
86
+ .map((navigationItem, navigationItemIndex) => ({
87
+ navigationItem,
88
+ originalIndex: navigationItemIndex
89
+ }))
90
+ .filter(({ navigationItem }) =>
91
+ hasSelectedNavigationChildren(navigationItem, basePath, pathname)
92
+ )
93
+ .map(({ originalIndex }) => `${depth}-${originalIndex}`),
94
+ [basePath, depth, pathname, subNavigation]
95
+ );
96
+
97
+ const [expandedSidebarItemIds, setExpandedSidebarItemIds] = React.useState(
98
+ initialExpandedSidebarItemIds
99
+ );
100
+
101
+ const handleOpenFolderClick = React.useCallback(
102
+ (itemId: string) => () => {
103
+ setExpandedSidebarItemIds((previousValue) =>
104
+ previousValue.includes(itemId)
105
+ ? previousValue.filter(
106
+ (previousValueItemId) => previousValueItemId !== itemId
107
+ )
108
+ : [...previousValue, itemId]
109
+ );
110
+ },
111
+ []
112
+ );
113
+
114
+ return (
115
+ <List sx={{ padding: 0, mb: depth === 0 ? 4 : 1, pl: 2 * depth }}>
116
+ {subNavigation.map((navigationItem, navigationItemIndex) => {
117
+ if (navigationItem.kind === "header") {
118
+ return (
119
+ <ListSubheader
120
+ key={`subheader-${depth}-${navigationItemIndex}`}
121
+ component="div"
122
+ sx={{
123
+ fontSize: 12,
124
+ fontWeight: "700",
125
+ height: isMini ? 0 : 40,
126
+ ...(hasDrawerTransitions
127
+ ? getDrawerSxTransitionMixin(isFullyExpanded, "height")
128
+ : {}),
129
+ px: 2,
130
+ overflow: "hidden",
131
+ textOverflow: "ellipsis",
132
+ whiteSpace: "nowrap",
133
+ zIndex: 2
134
+ }}
135
+ >
136
+ {getItemTitle(navigationItem)}
137
+ </ListSubheader>
138
+ );
139
+ }
140
+
141
+ if (navigationItem.kind === "divider") {
142
+ const nextItem = subNavigation[navigationItemIndex + 1];
143
+
144
+ return (
145
+ <Divider
146
+ key={`divider-${depth}-${navigationItemIndex}`}
147
+ sx={{
148
+ borderBottomWidth: 2,
149
+ mx: 1,
150
+ mt: 1,
151
+ mb: nextItem?.kind === "header" && !isMini ? 0 : 1,
152
+ ...(hasDrawerTransitions
153
+ ? getDrawerSxTransitionMixin(isFullyExpanded, "margin")
154
+ : {})
155
+ }}
156
+ />
157
+ );
158
+ }
159
+
160
+ const navigationItemFullPath = getPageItemFullPath(
161
+ basePath,
162
+ navigationItem
163
+ );
164
+ const navigationItemId = `${depth}-${navigationItemIndex}`;
165
+ const navigationItemTitle = getItemTitle(navigationItem);
166
+
167
+ const isNestedNavigationExpanded =
168
+ expandedSidebarItemIds.includes(navigationItemId);
169
+
170
+ const nestedNavigationCollapseIcon = isNestedNavigationExpanded ? (
171
+ <ExpandLessIcon />
172
+ ) : (
173
+ <ExpandMoreIcon />
174
+ );
175
+
176
+ const listItemIconSize = 34;
177
+
178
+ const isSelected = isPageItemSelected(
179
+ navigationItem,
180
+ basePath,
181
+ pathname
182
+ );
183
+
184
+ if (
185
+ process.env.NODE_ENV !== "production" &&
186
+ isSelected &&
187
+ selectedItemId
188
+ ) {
189
+ console.warn(
190
+ `Duplicate selected path in navigation: ${navigationItemFullPath}`
191
+ );
192
+ }
193
+
194
+ if (isSelected && !selectedItemId) {
195
+ selectedItemId = navigationItemId;
196
+ }
197
+
198
+ const listItem = (
199
+ <ListItem
200
+ sx={{
201
+ py: 0,
202
+ px: 1,
203
+ overflowX: "hidden"
204
+ }}
205
+ >
206
+ <NavigationListItemButton
207
+ selected={isSelected && (!navigationItem.children || isMini)}
208
+ sx={{
209
+ px: 1.4,
210
+ height: 48
211
+ }}
212
+ {...(navigationItem.children && !isMini
213
+ ? {
214
+ onClick: handleOpenFolderClick(navigationItemId)
215
+ }
216
+ : {
217
+ LinkComponent: Link,
218
+ href: navigationItemFullPath,
219
+ onClick: onLinkClick
220
+ })}
221
+ >
222
+ {navigationItem.icon || isMini ? (
223
+ <ListItemIcon
224
+ sx={{
225
+ minWidth: listItemIconSize,
226
+ mr: 1.2
227
+ }}
228
+ >
229
+ {navigationItem.icon ?? null}
230
+ {!navigationItem.icon && isMini ? (
231
+ <Avatar
232
+ sx={{
233
+ width: listItemIconSize - 7,
234
+ height: listItemIconSize - 7,
235
+ fontSize: 12,
236
+ ml: "-2px"
237
+ }}
238
+ >
239
+ {navigationItemTitle
240
+ .split(" ")
241
+ .slice(0, 2)
242
+ .map((itemTitleWord) =>
243
+ itemTitleWord.charAt(0).toUpperCase()
244
+ )}
245
+ </Avatar>
246
+ ) : null}
247
+ </ListItemIcon>
248
+ ) : null}
249
+ <ListItemText
250
+ primary={navigationItemTitle}
251
+ sx={{
252
+ whiteSpace: "nowrap",
253
+ zIndex: 1,
254
+ "& .MuiTypography-root": {
255
+ fontWeight: "500"
256
+ }
257
+ }}
258
+ />
259
+ {navigationItem.action && !isMini && isFullyExpanded
260
+ ? navigationItem.action
261
+ : null}
262
+ {navigationItem.children && !isMini && isFullyExpanded
263
+ ? nestedNavigationCollapseIcon
264
+ : null}
265
+ </NavigationListItemButton>
266
+ </ListItem>
267
+ );
268
+
269
+ return (
270
+ <React.Fragment key={navigationItemId}>
271
+ {isMini ? (
272
+ <Tooltip title={navigationItemTitle} placement="right">
273
+ {listItem}
274
+ </Tooltip>
275
+ ) : (
276
+ listItem
277
+ )}
278
+
279
+ {navigationItem.children && !isMini ? (
280
+ <Collapse
281
+ in={isNestedNavigationExpanded}
282
+ timeout="auto"
283
+ unmountOnExit
284
+ >
285
+ <DashboardSidebarSubNavigation
286
+ subNavigation={navigationItem.children}
287
+ basePath={navigationItemFullPath}
288
+ depth={depth + 1}
289
+ onLinkClick={onLinkClick}
290
+ selectedItemId={selectedItemId}
291
+ />
292
+ </Collapse>
293
+ ) : null}
294
+ </React.Fragment>
295
+ );
296
+ })}
297
+ </List>
298
+ );
299
+ }
300
+
301
+ export { DashboardSidebarSubNavigation };
@@ -0,0 +1,81 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { useTheme } from "@mui/material";
4
+ import IconButton from "@mui/material/IconButton";
5
+ import Tooltip from "@mui/material/Tooltip";
6
+ import DarkModeIcon from "@mui/icons-material/DarkMode";
7
+ import LightModeIcon from "@mui/icons-material/LightMode";
8
+ import { PaletteModeContext } from "../shared/context";
9
+ import useSsr from "../utils/hooks/useSsr";
10
+
11
+ // TODO: When we use this component as the default for a slot, make it non-internal
12
+ /**
13
+ * @ignore - internal component.
14
+ */
15
+ function ThemeSwitcher() {
16
+ const isSsr = useSsr();
17
+ const theme = useTheme();
18
+
19
+ const { paletteMode, setPaletteMode, isDualTheme } =
20
+ React.useContext(PaletteModeContext);
21
+
22
+ const toggleMode = React.useCallback(() => {
23
+ setPaletteMode(paletteMode === "dark" ? "light" : "dark");
24
+ }, [paletteMode, setPaletteMode]);
25
+
26
+ return isDualTheme ? (
27
+ <Tooltip
28
+ title={
29
+ isSsr
30
+ ? "Switch mode"
31
+ : `${paletteMode === "dark" ? "Light" : "Dark"} mode`
32
+ }
33
+ enterDelay={1000}
34
+ >
35
+ <div>
36
+ <IconButton
37
+ aria-label={
38
+ isSsr
39
+ ? "Switch theme mode"
40
+ : `Switch to ${paletteMode === "dark" ? "light" : "dark"} mode`
41
+ }
42
+ onClick={toggleMode}
43
+ sx={{
44
+ color: (theme.vars ?? theme).palette.primary.dark
45
+ }}
46
+ >
47
+ {theme.getColorSchemeSelector ? (
48
+ <React.Fragment>
49
+ <DarkModeIcon
50
+ sx={{
51
+ display: "inline",
52
+ [theme.getColorSchemeSelector("dark")]: {
53
+ display: "none"
54
+ }
55
+ }}
56
+ />
57
+ <LightModeIcon
58
+ sx={{
59
+ display: "none",
60
+ [theme.getColorSchemeSelector("dark")]: {
61
+ display: "inline"
62
+ }
63
+ }}
64
+ />
65
+ </React.Fragment>
66
+ ) : (
67
+ <React.Fragment>
68
+ {isSsr || paletteMode !== "dark" ? (
69
+ <DarkModeIcon />
70
+ ) : (
71
+ <LightModeIcon />
72
+ )}
73
+ </React.Fragment>
74
+ )}
75
+ </IconButton>
76
+ </div>
77
+ </Tooltip>
78
+ ) : null;
79
+ }
80
+
81
+ export { ThemeSwitcher };
@@ -0,0 +1,11 @@
1
+ "use client";
2
+
3
+ // TODO: When we add content to this component, make it non-internal
4
+ /**
5
+ * @ignore - internal component.
6
+ */
7
+ function ToolbarActions() {
8
+ return null;
9
+ }
10
+
11
+ export { ToolbarActions };
@@ -0,0 +1,4 @@
1
+ export * from "./DashboardLayout";
2
+
3
+ // Default slots components
4
+ export * from "./ToolbarActions";
@@ -0,0 +1,23 @@
1
+ import type { Theme } from "@mui/material";
2
+
3
+ export function getDrawerSxTransitionMixin(
4
+ isExpanded: boolean,
5
+ property: string
6
+ ) {
7
+ return {
8
+ transition: (theme: Theme) =>
9
+ theme.transitions.create(property, {
10
+ easing: theme.transitions.easing.sharp,
11
+ duration: isExpanded
12
+ ? theme.transitions.duration.enteringScreen
13
+ : theme.transitions.duration.leavingScreen
14
+ })
15
+ };
16
+ }
17
+
18
+ export function getDrawerWidthTransitionMixin(isExpanded: boolean) {
19
+ return {
20
+ ...getDrawerSxTransitionMixin(isExpanded, "width"),
21
+ overflowX: "hidden"
22
+ };
23
+ }
@@ -0,0 +1,194 @@
1
+ import { expect, describe, test, vi } from "vitest";
2
+ import { render, within, screen } from "@testing-library/react";
3
+ import { userEvent } from "@testing-library/user-event";
4
+ import { PageContainer } from "./PageContainer";
5
+ import describeConformance from "../utils/describeConformance";
6
+ import { AppProvider } from "../AppProvider/AppProviderComponent";
7
+
8
+ describe("PageContainer", () => {
9
+ describeConformance(<PageContainer />, () => ({
10
+ skip: ["themeDefaultProps"],
11
+ slots: {
12
+ toolbar: {}
13
+ }
14
+ }));
15
+
16
+ test("renders page container correctly", async () => {
17
+ const user = await userEvent.setup();
18
+ const router = {
19
+ pathname: "/orders",
20
+ searchParams: new URLSearchParams(),
21
+ navigate: vi.fn()
22
+ };
23
+ render(
24
+ <AppProvider
25
+ navigation={[
26
+ { segment: "", title: "Home" },
27
+ { segment: "orders", title: "Orders" }
28
+ ]}
29
+ router={router}
30
+ >
31
+ <PageContainer />
32
+ </AppProvider>
33
+ );
34
+
35
+ const breadcrumbs = screen.getByRole("navigation", { name: "breadcrumb" });
36
+
37
+ const homeLink = within(breadcrumbs).getByRole("link", { name: "Home" });
38
+ await user.click(homeLink);
39
+
40
+ expect(router.navigate).toHaveBeenCalledWith(
41
+ "/",
42
+ expect.objectContaining({})
43
+ );
44
+ router.navigate.mockClear();
45
+
46
+ expect(within(breadcrumbs).getByText("Orders")).toBeTruthy();
47
+
48
+ expect(screen.getByText("Orders", { ignore: "nav *" }));
49
+ });
50
+
51
+ test("renders nested", async () => {
52
+ const navigation = [
53
+ { segment: "", title: "ACME" },
54
+ {
55
+ segment: "home",
56
+ title: "Home",
57
+ children: [
58
+ {
59
+ segment: "orders",
60
+ title: "Orders"
61
+ }
62
+ ]
63
+ }
64
+ ];
65
+
66
+ const router = {
67
+ pathname: "/home/orders",
68
+ searchParams: new URLSearchParams(),
69
+ navigate: vi.fn()
70
+ };
71
+
72
+ const branding = { title: "ACME" };
73
+ render(
74
+ <AppProvider branding={branding} navigation={navigation} router={router}>
75
+ <PageContainer />
76
+ </AppProvider>
77
+ );
78
+
79
+ const breadcrumbs = screen.getByRole("navigation", { name: "breadcrumb" });
80
+
81
+ expect(within(breadcrumbs).getByText("ACME")).toBeTruthy();
82
+ expect(within(breadcrumbs).getByText("Home")).toBeTruthy();
83
+ expect(within(breadcrumbs).getByText("Orders")).toBeTruthy();
84
+ });
85
+
86
+ test("renders dynamic correctly", async () => {
87
+ const user = await userEvent.setup();
88
+ const router = {
89
+ pathname: "/orders/123",
90
+ searchParams: new URLSearchParams(),
91
+ navigate: vi.fn()
92
+ };
93
+ render(
94
+ <AppProvider
95
+ navigation={[
96
+ { segment: "", title: "Home" },
97
+ { segment: "orders", title: "Orders", pattern: "orders/:id" }
98
+ ]}
99
+ router={router}
100
+ >
101
+ <PageContainer />
102
+ </AppProvider>
103
+ );
104
+
105
+ const breadcrumbs = screen.getByRole("navigation", { name: "breadcrumb" });
106
+
107
+ const homeLink = within(breadcrumbs).getByRole("link", { name: "Home" });
108
+ await user.click(homeLink);
109
+
110
+ expect(router.navigate).toHaveBeenCalledWith(
111
+ "/",
112
+ expect.objectContaining({})
113
+ );
114
+ router.navigate.mockClear();
115
+
116
+ expect(within(breadcrumbs).getByText("Orders")).toBeTruthy();
117
+
118
+ expect(screen.getByText("Orders", { ignore: "nav *" }));
119
+ });
120
+
121
+ test("renders nested dynamic correctly", async () => {
122
+ const router = {
123
+ pathname: "/users/invoices/123",
124
+ searchParams: new URLSearchParams(),
125
+ navigate: vi.fn()
126
+ };
127
+ render(
128
+ <AppProvider
129
+ navigation={[
130
+ {
131
+ segment: "users",
132
+ title: "Users",
133
+ children: [
134
+ {
135
+ segment: "invoices",
136
+ title: "Invoices",
137
+ pattern: "invoices/:id"
138
+ }
139
+ ]
140
+ }
141
+ ]}
142
+ router={router}
143
+ >
144
+ <PageContainer />
145
+ </AppProvider>
146
+ );
147
+
148
+ const breadcrumbs = screen.getByRole("navigation", { name: "breadcrumb" });
149
+
150
+ const homeLink = within(breadcrumbs).getByRole("link", { name: "Users" });
151
+ expect(homeLink.getAttribute("href")).toBe("/users");
152
+ expect(within(breadcrumbs).getByText("Invoices")).toBeTruthy();
153
+ });
154
+
155
+ test("renders custom breadcrumbs", async () => {
156
+ render(
157
+ <PageContainer
158
+ breadcrumbs={[
159
+ { title: "Hello", path: "/hello" },
160
+ { title: "World", path: "/world" }
161
+ ]}
162
+ />
163
+ );
164
+
165
+ const breadcrumbs = screen.getByRole("navigation", { name: "breadcrumb" });
166
+
167
+ const helloLink = within(breadcrumbs).getByRole("link", { name: "Hello" });
168
+ expect(helloLink.getAttribute("href")).toBe("/hello");
169
+ expect(within(breadcrumbs).getByText("World")).toBeTruthy();
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
+ });