@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,244 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { CODEC_STRING, Codec } from "./codec";
4
+
5
+ // storage events only work across tabs, we'll use an event emitter to announce within the current tab
6
+ const currentTabChangeListeners = new Map<string, Set<() => void>>();
7
+
8
+ function onCurrentTabStorageChange(key: string, handler: () => void) {
9
+ let listeners = currentTabChangeListeners.get(key);
10
+
11
+ if (!listeners) {
12
+ listeners = new Set();
13
+ currentTabChangeListeners.set(key, listeners);
14
+ }
15
+
16
+ listeners.add(handler);
17
+ }
18
+
19
+ function offCurrentTabStorageChange(key: string, handler: () => void) {
20
+ const listeners = currentTabChangeListeners.get(key);
21
+ if (!listeners) {
22
+ return;
23
+ }
24
+
25
+ listeners.delete(handler);
26
+
27
+ if (listeners.size === 0) {
28
+ currentTabChangeListeners.delete(key);
29
+ }
30
+ }
31
+
32
+ function emitCurrentTabStorageChange(key: string) {
33
+ const listeners = currentTabChangeListeners.get(key);
34
+ if (listeners) {
35
+ listeners.forEach((listener) => listener());
36
+ }
37
+ }
38
+
39
+ if (typeof window !== "undefined") {
40
+ const origSetItem = window.localStorage.setItem;
41
+ window.localStorage.setItem = function setItem(key, value) {
42
+ const result = origSetItem.call(this, key, value);
43
+ emitCurrentTabStorageChange(key);
44
+ return result;
45
+ };
46
+ }
47
+
48
+ function subscribe(
49
+ area: Storage,
50
+ key: string | null,
51
+ callback: () => void
52
+ ): () => void {
53
+ if (!key) {
54
+ return () => {};
55
+ }
56
+ const storageHandler = (event: StorageEvent) => {
57
+ if (event.storageArea === area && event.key === key) {
58
+ callback();
59
+ }
60
+ };
61
+ window.addEventListener("storage", storageHandler);
62
+ onCurrentTabStorageChange(key, callback);
63
+ return () => {
64
+ window.removeEventListener("storage", storageHandler);
65
+ offCurrentTabStorageChange(key, callback);
66
+ };
67
+ }
68
+
69
+ function getSnapshot(area: Storage, key: string | null): string | null {
70
+ if (!key) {
71
+ return null;
72
+ }
73
+ try {
74
+ return area.getItem(key);
75
+ } catch {
76
+ // ignore
77
+ // See https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#feature-detecting_localstorage
78
+ return null;
79
+ }
80
+ }
81
+
82
+ function setValue(area: Storage, key: string | null, value: string | null) {
83
+ if (!key) {
84
+ return;
85
+ }
86
+ try {
87
+ if (value === null) {
88
+ area.removeItem(key);
89
+ } else {
90
+ area.setItem(key, String(value));
91
+ }
92
+ } catch {
93
+ // ignore
94
+ // See https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#feature-detecting_localstorage
95
+ return;
96
+ }
97
+ emitCurrentTabStorageChange(key);
98
+ }
99
+
100
+ export type StorageStateInitializer<T> = () => T | null;
101
+
102
+ export type UseStorageStateHookResult<T> = [
103
+ T | null,
104
+ React.Dispatch<React.SetStateAction<T | null>>
105
+ ];
106
+
107
+ const serverValue: UseStorageStateHookResult<any> = [null, () => {}];
108
+
109
+ export function useStorageStateServer<
110
+ T = string
111
+ >(): UseStorageStateHookResult<T> {
112
+ return serverValue;
113
+ }
114
+
115
+ export interface DefaultStorageStateoptions<T = string> {
116
+ codec?: Codec<T>;
117
+ }
118
+ export interface StorageStateOptions<T> extends DefaultStorageStateoptions<T> {
119
+ codec: Codec<T>;
120
+ }
121
+
122
+ function encode<V>(codec: Codec<V>, value: V | null): string | null {
123
+ return value === null ? null : codec.stringify(value);
124
+ }
125
+
126
+ function decode<V>(codec: Codec<V>, value: string | null): V | null {
127
+ return value === null ? null : codec.parse(value);
128
+ }
129
+
130
+ // Start with null for the hydration, and then switch to the actual value.
131
+ const getKeyServerSnapshot = () => null;
132
+
133
+ /**
134
+ * Sync state to local storage so that it persists through a page refresh. Usage is
135
+ * similar to useState except we pass in a storage key so that we can default
136
+ * to that value on page load instead of the specified initial value.
137
+ *
138
+ * Since the storage API isn't available in server-rendering environments, we
139
+ * return null during SSR and hydration.
140
+ */
141
+ export function useStorageState(
142
+ area: Storage,
143
+ key: string | null,
144
+ initializer?: string | null | StorageStateInitializer<string>,
145
+ options?: DefaultStorageStateoptions
146
+ ): UseStorageStateHookResult<string>;
147
+ export function useStorageState<T>(
148
+ area: Storage,
149
+ key: string | null,
150
+ initializer: T | null | StorageStateInitializer<T>,
151
+ options: StorageStateOptions<T>
152
+ ): UseStorageStateHookResult<T>;
153
+ export function useStorageState<T = string>(
154
+ area: Storage,
155
+ key: string | null,
156
+ initializer: T | null | StorageStateInitializer<T> = null,
157
+ options?: DefaultStorageStateoptions | StorageStateOptions<T>
158
+ ): UseStorageStateHookResult<T> {
159
+ const codec = (options?.codec ?? CODEC_STRING) as Codec<T>;
160
+
161
+ const [initialValue] = React.useState(initializer);
162
+ const encodedInitialValue = React.useMemo(
163
+ () => encode(codec, initialValue),
164
+ [codec, initialValue]
165
+ );
166
+
167
+ const subscribeKey = React.useCallback(
168
+ (callback: () => void) => subscribe(area, key, callback),
169
+ [area, key]
170
+ );
171
+
172
+ const getKeySnapshot = React.useCallback<() => string | null>(
173
+ () => getSnapshot(area, key) ?? encodedInitialValue,
174
+ [area, encodedInitialValue, key]
175
+ );
176
+
177
+ const encodedStoredValue = React.useSyncExternalStore(
178
+ subscribeKey,
179
+ getKeySnapshot,
180
+ getKeyServerSnapshot
181
+ );
182
+
183
+ const storedValue = React.useMemo(
184
+ () => decode(codec, encodedStoredValue),
185
+ [codec, encodedStoredValue]
186
+ );
187
+
188
+ const setStoredValue = React.useCallback(
189
+ (value: React.SetStateAction<T | null>) => {
190
+ const valueToStore =
191
+ value instanceof Function ? value(storedValue) : value;
192
+ const encodedValueToStore = encode(codec, valueToStore);
193
+ setValue(area, key, encodedValueToStore);
194
+ },
195
+ [area, codec, storedValue, key]
196
+ );
197
+
198
+ const [nonStoredValue, setNonStoredValue] = React.useState(initialValue);
199
+
200
+ if (!key) {
201
+ return [nonStoredValue, setNonStoredValue];
202
+ }
203
+
204
+ return [storedValue, setStoredValue];
205
+ }
206
+
207
+ export interface UseStorageState {
208
+ /**
209
+ * Sync state to local or session storage so that it persists through a page refresh. Usage is
210
+ * similar to useState except we pass in a storage key that uniquely identifies the value.
211
+ * @param key The key to use for storing the value in local or session storage.
212
+ * @param initializer The initial value to use if the key is not present in storage.
213
+ * @param options Additional options for the storage state.
214
+ */
215
+ (
216
+ key: string | null,
217
+ initializer?: string | null | StorageStateInitializer<string>,
218
+ options?: DefaultStorageStateoptions
219
+ ): UseStorageStateHookResult<string>;
220
+ /**
221
+ * Sync state to local or session storage so that it persists through a page refresh. Usage is
222
+ * similar to useState except we pass in a storage key that uniquely identifies the value.
223
+ * @param key The key to use for storing the value in local or session storage.
224
+ * @param initializer The initial value to use if the key is not present in storage.
225
+ * @param options Additional options for the storage state.
226
+ */
227
+ <T>(
228
+ key: string | null,
229
+ initializer: T | null | StorageStateInitializer<T>,
230
+ options: StorageStateOptions<T>
231
+ ): UseStorageStateHookResult<T>;
232
+ /**
233
+ * Sync state to local or session storage so that it persists through a page refresh. Usage is
234
+ * similar to useState except we pass in a storage key that uniquely identifies the value.
235
+ * @param key The key to use for storing the value in local or session storage.
236
+ * @param initializer The initial value to use if the key is not present in storage.
237
+ * @param options Additional options for the storage state.
238
+ */
239
+ <T>(
240
+ key: string | null,
241
+ initializer?: T | null | StorageStateInitializer<T>,
242
+ options?: StorageStateOptions<T>
243
+ ): UseStorageStateHookResult<T>;
244
+ }
@@ -0,0 +1,17 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { BrowserRouter } from "react-router-dom";
4
+ import { AppProvider } from "./AppProvider";
5
+
6
+ describe("React Router AppProvider", () => {
7
+ test("renders content correctly", async () => {
8
+ // placeholder test
9
+ render(
10
+ <BrowserRouter>
11
+ <AppProvider>Hello</AppProvider>
12
+ </BrowserRouter>
13
+ );
14
+
15
+ expect(screen.getByText("Hello")).toBeTruthy();
16
+ });
17
+ });
@@ -0,0 +1,40 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { useSearchParams, useLocation, useNavigate } from "react-router-dom";
4
+ import { type AppProviderProps, Navigate, Router } from "../AppProvider";
5
+ import { AppProvider as AppProviderComponent } from "../AppProvider/AppProviderComponent";
6
+
7
+ /**
8
+ * @ignore - internal component.
9
+ */
10
+ function AppProvider(props: AppProviderProps) {
11
+ const { pathname } = useLocation();
12
+ const [searchParams] = useSearchParams();
13
+ const navigate = useNavigate();
14
+
15
+ const navigateImpl = React.useCallback<Navigate>(
16
+ (url, { history = "auto" } = {}) => {
17
+ if (history === "auto" || history === "push") {
18
+ return navigate(url);
19
+ }
20
+ if (history === "replace") {
21
+ return navigate(url, { replace: true });
22
+ }
23
+ throw new Error(`Invalid history option: ${history}`);
24
+ },
25
+ [navigate]
26
+ );
27
+
28
+ const routerImpl = React.useMemo<Router>(
29
+ () => ({
30
+ pathname,
31
+ searchParams,
32
+ navigate: navigateImpl
33
+ }),
34
+ [pathname, searchParams, navigateImpl]
35
+ );
36
+
37
+ return <AppProviderComponent router={routerImpl} {...props} />;
38
+ }
39
+
40
+ export { AppProvider };
@@ -0,0 +1 @@
1
+ export * from "./AppProvider";
@@ -0,0 +1,37 @@
1
+ import * as React from "react";
2
+ import { RouterContext } from "./context";
3
+
4
+ /**
5
+ * @ignore - internal component.
6
+ */
7
+
8
+ export interface LinkProps
9
+ extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
10
+ history?: "auto" | "push" | "replace";
11
+ }
12
+
13
+ export const Link = React.forwardRef(function Link(
14
+ props: LinkProps,
15
+ ref: React.ForwardedRef<HTMLAnchorElement>
16
+ ) {
17
+ const { children, href, onClick, history, ...rest } = props;
18
+ const routerContext = React.useContext(RouterContext);
19
+
20
+ const handleLinkClick = React.useMemo(() => {
21
+ if (!routerContext) {
22
+ return onClick;
23
+ }
24
+ return (event: React.MouseEvent<HTMLAnchorElement>) => {
25
+ event.preventDefault();
26
+ const url = new URL(event.currentTarget.href);
27
+ routerContext.navigate(url.pathname, { history });
28
+ onClick?.(event);
29
+ };
30
+ }, [routerContext, onClick, history]);
31
+
32
+ return (
33
+ <a ref={ref} href={href} {...rest} onClick={handleLinkClick}>
34
+ {children}
35
+ </a>
36
+ );
37
+ });
@@ -0,0 +1,7 @@
1
+ import * as React from "react";
2
+ import { BrandingContext } from "./context";
3
+
4
+ export function useApplicationTitle() {
5
+ const branding = React.useContext(BrandingContext);
6
+ return branding?.title ?? "Toolpad";
7
+ }
@@ -0,0 +1,46 @@
1
+ import * as React from "react";
2
+ import { CircularProgress, Typography, styled } from "@mui/material";
3
+ import ErrorIcon from "@mui/icons-material/Error";
4
+
5
+ const OverlayRoot = styled("div")(({ theme }) => ({
6
+ position: "absolute",
7
+ inset: "0 0 0 0",
8
+ display: "flex",
9
+ flexDirection: "column",
10
+ alignItems: "center",
11
+ justifyContent: "center",
12
+ padding: theme.spacing(2)
13
+ }));
14
+
15
+ export interface ErrorOverlayProps {
16
+ error?: unknown;
17
+ }
18
+
19
+ export function ErrorOverlay({ error }: ErrorOverlayProps) {
20
+ return (
21
+ <OverlayRoot>
22
+ <Typography
23
+ variant="h6"
24
+ sx={{
25
+ display: "flex",
26
+ flexDirection: "row",
27
+ gap: 1,
28
+ alignItems: "center"
29
+ }}
30
+ >
31
+ <ErrorIcon color="error" /> Error
32
+ </Typography>
33
+ <Typography textAlign="center">
34
+ {(error as any)?.message ?? "Unknown error"}
35
+ </Typography>
36
+ </OverlayRoot>
37
+ );
38
+ }
39
+
40
+ export function LoadingOverlay() {
41
+ return (
42
+ <OverlayRoot>
43
+ <CircularProgress />
44
+ </OverlayRoot>
45
+ );
46
+ }
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+ import type { PaletteMode } from "@mui/material";
3
+ import type { Branding, Navigation, Router } from "../AppProvider";
4
+
5
+ export const BrandingContext = React.createContext<Branding | null>(null);
6
+
7
+ export const NavigationContext = React.createContext<Navigation>([]);
8
+
9
+ export const PaletteModeContext = React.createContext<{
10
+ paletteMode: PaletteMode;
11
+ setPaletteMode: (mode: PaletteMode) => void;
12
+ isDualTheme: boolean;
13
+ }>({
14
+ paletteMode: "light",
15
+ setPaletteMode: () => {},
16
+ isDualTheme: false
17
+ });
18
+
19
+ export const RouterContext = React.createContext<Router | null>(null);
20
+
21
+ export const WindowContext = React.createContext<Window | undefined>(undefined);
@@ -0,0 +1,44 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import DEFAULT_LOCALE_TEXT from "./en";
5
+
6
+ export type LocaleContextType = {
7
+ // Account
8
+ signInLabel?: string;
9
+ signOutLabel?: string;
10
+ // Account Preview
11
+ iconButtonAriaLabel?: string;
12
+ };
13
+
14
+ export const LocaleContext =
15
+ React.createContext<LocaleContextType>(DEFAULT_LOCALE_TEXT);
16
+
17
+ export interface LocaleProviderProps {
18
+ localeText?: Partial<LocaleContextType>;
19
+ children: React.ReactNode;
20
+ }
21
+
22
+ /**
23
+ * @ignore - internal component.
24
+ */
25
+ export function LocaleProvider({ localeText, children }: LocaleProviderProps) {
26
+ const mergedLocaleText = React.useMemo(
27
+ () => ({ ...DEFAULT_LOCALE_TEXT, ...localeText }),
28
+ [localeText]
29
+ );
30
+
31
+ return (
32
+ <LocaleContext.Provider value={mergedLocaleText}>
33
+ {children}
34
+ </LocaleContext.Provider>
35
+ );
36
+ }
37
+
38
+ /**
39
+ * @ignore - internal hook.
40
+ */
41
+
42
+ export function useLocaleText() {
43
+ return React.useContext(LocaleContext);
44
+ }
@@ -0,0 +1,9 @@
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;
@@ -0,0 +1,197 @@
1
+ import { pathToRegexp } from "path-to-regexp";
2
+ import invariant from "invariant";
3
+ import type {
4
+ Navigation,
5
+ NavigationItem,
6
+ NavigationPageItem,
7
+ NavigationSubheaderItem
8
+ } from "../AppProvider";
9
+
10
+ export const getItemKind = (item: NavigationItem) => item.kind ?? "page";
11
+
12
+ export const isPageItem = (item: NavigationItem): item is NavigationPageItem =>
13
+ getItemKind(item) === "page";
14
+
15
+ export const getItemTitle = (
16
+ item: NavigationPageItem | NavigationSubheaderItem
17
+ ) => {
18
+ return isPageItem(item) ? item.title ?? item.segment ?? "" : item.title;
19
+ };
20
+
21
+ export function getPageItemFullPath(
22
+ basePath: string,
23
+ navigationItem: NavigationPageItem
24
+ ) {
25
+ return `${basePath}${basePath && !navigationItem.segment ? "" : "/"}${
26
+ navigationItem.segment ?? ""
27
+ }`;
28
+ }
29
+
30
+ export function isPageItemSelected(
31
+ navigationItem: NavigationPageItem,
32
+ basePath: string,
33
+ pathname: string
34
+ ) {
35
+ return navigationItem.pattern
36
+ ? pathToRegexp(`${basePath}/${navigationItem.pattern}`).test(pathname)
37
+ : getPageItemFullPath(basePath, navigationItem) === pathname;
38
+ }
39
+
40
+ export function hasSelectedNavigationChildren(
41
+ navigationItem: NavigationItem,
42
+ basePath: string,
43
+ pathname: string
44
+ ): boolean {
45
+ if (isPageItem(navigationItem) && navigationItem.children) {
46
+ const navigationItemFullPath = getPageItemFullPath(
47
+ basePath,
48
+ navigationItem
49
+ );
50
+
51
+ return navigationItem.children.some((nestedNavigationItem) => {
52
+ if (!isPageItem(nestedNavigationItem)) {
53
+ return false;
54
+ }
55
+
56
+ if (nestedNavigationItem.children) {
57
+ return hasSelectedNavigationChildren(
58
+ nestedNavigationItem,
59
+ navigationItemFullPath,
60
+ pathname
61
+ );
62
+ }
63
+
64
+ return isPageItemSelected(
65
+ nestedNavigationItem,
66
+ navigationItemFullPath,
67
+ pathname
68
+ );
69
+ });
70
+ }
71
+
72
+ return false;
73
+ }
74
+
75
+ /**
76
+ * Builds a map of navigation page items to their respective paths. This map is used to quickly
77
+ * lookup the path of a navigation item. It will be cached for the lifetime of the navigation.
78
+ */
79
+ function buildItemToPathMap(
80
+ navigation: Navigation
81
+ ): Map<NavigationPageItem, string> {
82
+ const map = new Map<NavigationPageItem, string>();
83
+
84
+ const visit = (item: NavigationItem, base: string) => {
85
+ if (isPageItem(item)) {
86
+ const path = `${base}${item.segment ? `/${item.segment}` : ""}` || "/";
87
+ map.set(item, path);
88
+ if (item.children) {
89
+ for (const child of item.children) {
90
+ visit(child, path);
91
+ }
92
+ }
93
+ }
94
+ };
95
+
96
+ for (const item of navigation) {
97
+ visit(item, "");
98
+ }
99
+
100
+ return map;
101
+ }
102
+
103
+ const itemToPathMapCache = new WeakMap<
104
+ Navigation,
105
+ Map<NavigationPageItem, string>
106
+ >();
107
+
108
+ /**
109
+ * Gets the cached map of navigation page items to their respective paths.
110
+ */
111
+ function getItemToPathMap(navigation: Navigation) {
112
+ let map = itemToPathMapCache.get(navigation);
113
+ if (!map) {
114
+ map = buildItemToPathMap(navigation);
115
+ itemToPathMapCache.set(navigation, map);
116
+ }
117
+ return map;
118
+ }
119
+
120
+ /**
121
+ * Build a lookup map of paths to navigation items. This map is used to match paths against
122
+ * to find the active page.
123
+ */
124
+ function buildItemLookup(navigation: Navigation) {
125
+ const map = new Map<string | RegExp, NavigationPageItem>();
126
+ const visit = (item: NavigationItem) => {
127
+ if (isPageItem(item)) {
128
+ const path = getItemPath(navigation, item);
129
+ if (map.has(path)) {
130
+ console.warn(`Duplicate path in navigation: ${path}`);
131
+ }
132
+ map.set(path, item);
133
+ if (item.pattern) {
134
+ const basePath = item.segment
135
+ ? path.slice(0, -item.segment.length)
136
+ : path;
137
+ map.set(pathToRegexp(basePath + item.pattern), item);
138
+ }
139
+ if (item.children) {
140
+ for (const child of item.children) {
141
+ visit(child);
142
+ }
143
+ }
144
+ }
145
+ };
146
+ for (const item of navigation) {
147
+ visit(item);
148
+ }
149
+ return map;
150
+ }
151
+ const itemLookupMapCache = new WeakMap<
152
+ Navigation,
153
+ Map<string | RegExp, NavigationPageItem>
154
+ >();
155
+ function getItemLookup(navigation: Navigation) {
156
+ let map = itemLookupMapCache.get(navigation);
157
+ if (!map) {
158
+ map = buildItemLookup(navigation);
159
+ itemLookupMapCache.set(navigation, map);
160
+ }
161
+ return map;
162
+ }
163
+
164
+ /**
165
+ * Matches a path against the navigation to find the active page. i.e. the page that should be
166
+ * marked as selected in the navigation.
167
+ */
168
+ export function matchPath(
169
+ navigation: Navigation,
170
+ path: string
171
+ ): NavigationPageItem | null {
172
+ const lookup = getItemLookup(navigation);
173
+
174
+ for (const [key, item] of lookup.entries()) {
175
+ if (typeof key === "string" && key === path) {
176
+ return item;
177
+ }
178
+ if (key instanceof RegExp && key.test(path)) {
179
+ return item;
180
+ }
181
+ }
182
+
183
+ return null;
184
+ }
185
+
186
+ /**
187
+ * Gets the path for a specific navigation page item.
188
+ */
189
+ export function getItemPath(
190
+ navigation: Navigation,
191
+ item: NavigationPageItem
192
+ ): string {
193
+ const map = getItemToPathMap(navigation);
194
+ const path = map.get(item);
195
+ invariant(path, `Item not found in navigation: ${item.title}`);
196
+ return path;
197
+ }
@@ -0,0 +1 @@
1
+ export * from "./useActivePage";