@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.
- package/.github/workflows/main.yml +49 -0
- package/.vscode/settings.json +3 -0
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/build/src/Account/Account.d.ts +71 -0
- package/build/src/Account/Account.js +117 -0
- package/build/src/Account/Account.test.d.ts +1 -0
- package/build/src/Account/Account.test.js +35 -0
- package/build/src/Account/AccountPopoverFooter.d.ts +6 -0
- package/build/src/Account/AccountPopoverFooter.js +21 -0
- package/build/src/Account/AccountPopoverHeader.d.ts +6 -0
- package/build/src/Account/AccountPopoverHeader.js +15 -0
- package/build/src/Account/AccountPreview.d.ts +67 -0
- package/build/src/Account/AccountPreview.js +76 -0
- package/build/src/Account/AccountPreview.test.d.ts +1 -0
- package/build/src/Account/AccountPreview.test.js +47 -0
- package/build/src/Account/SignInButton.d.ts +2 -0
- package/build/src/Account/SignInButton.js +26 -0
- package/build/src/Account/SignOutButton.d.ts +3 -0
- package/build/src/Account/SignOutButton.js +28 -0
- package/build/src/Account/index.d.ts +6 -0
- package/build/src/Account/index.js +6 -0
- package/build/src/AppProvider/AppProvider.d.ts +98 -0
- package/build/src/AppProvider/AppProvider.js +4 -0
- package/build/src/AppProvider/AppProvider.test.d.ts +4 -0
- package/build/src/AppProvider/AppProvider.test.js +16 -0
- package/build/src/AppProvider/AppProviderComponent.d.ts +17 -0
- package/build/src/AppProvider/AppProviderComponent.js +120 -0
- package/build/src/AppProvider/AppThemeProvider.d.ts +12 -0
- package/build/src/AppProvider/AppThemeProvider.js +73 -0
- package/build/src/AppProvider/index.d.ts +1 -0
- package/build/src/AppProvider/index.js +1 -0
- package/build/src/DashboardLayout/DashboardLayout.d.ts +83 -0
- package/build/src/DashboardLayout/DashboardLayout.js +286 -0
- package/build/src/DashboardLayout/DashboardLayout.test.d.ts +1 -0
- package/build/src/DashboardLayout/DashboardLayout.test.js +291 -0
- package/build/src/DashboardLayout/DashboardSidebarSubNavigation.d.ts +16 -0
- package/build/src/DashboardLayout/DashboardSidebarSubNavigation.js +149 -0
- package/build/src/DashboardLayout/ThemeSwitcher.d.ts +5 -0
- package/build/src/DashboardLayout/ThemeSwitcher.js +40 -0
- package/build/src/DashboardLayout/ToolbarActions.d.ts +5 -0
- package/build/src/DashboardLayout/ToolbarActions.js +9 -0
- package/build/src/DashboardLayout/index.d.ts +2 -0
- package/build/src/DashboardLayout/index.js +3 -0
- package/build/src/DashboardLayout/utils.d.ts +8 -0
- package/build/src/DashboardLayout/utils.js +16 -0
- package/build/src/PageContainer/PageContainer.d.ts +66 -0
- package/build/src/PageContainer/PageContainer.js +97 -0
- package/build/src/PageContainer/PageContainer.test.d.ts +1 -0
- package/build/src/PageContainer/PageContainer.test.js +127 -0
- package/build/src/PageContainer/PageContainerToolbar.d.ts +16 -0
- package/build/src/PageContainer/PageContainerToolbar.js +24 -0
- package/build/src/PageContainer/PageContainerToolbar.test.d.ts +1 -0
- package/build/src/PageContainer/PageContainerToolbar.test.js +10 -0
- package/build/src/PageContainer/index.d.ts +2 -0
- package/build/src/PageContainer/index.js +2 -0
- package/build/src/index.d.ts +8 -0
- package/build/src/index.js +8 -0
- package/build/src/nextjs/AppProvider.d.ts +6 -0
- package/build/src/nextjs/AppProvider.js +16 -0
- package/build/src/nextjs/AppProvider.test.d.ts +1 -0
- package/build/src/nextjs/AppProvider.test.js +36 -0
- package/build/src/nextjs/AppProviderNextApp.d.ts +5 -0
- package/build/src/nextjs/AppProviderNextApp.js +27 -0
- package/build/src/nextjs/AppProviderNextPages.d.ts +5 -0
- package/build/src/nextjs/AppProviderNextPages.js +37 -0
- package/build/src/nextjs/index.d.ts +1 -0
- package/build/src/nextjs/index.js +1 -0
- package/build/src/persistence/codec.d.ts +47 -0
- package/build/src/persistence/codec.js +57 -0
- package/build/src/persistence/index.d.ts +1 -0
- package/build/src/persistence/index.js +1 -0
- package/build/src/persistence/useStorageState.d.ts +50 -0
- package/build/src/persistence/useStorageState.js +116 -0
- package/build/src/persistence/useStorageState.test.d.ts +1 -0
- package/build/src/persistence/useStorageState.test.js +61 -0
- package/build/src/react-router-dom/AppProvider.d.ts +6 -0
- package/build/src/react-router-dom/AppProvider.js +29 -0
- package/build/src/react-router-dom/AppProvider.test.d.ts +1 -0
- package/build/src/react-router-dom/AppProvider.test.js +12 -0
- package/build/src/react-router-dom/index.d.ts +1 -0
- package/build/src/react-router-dom/index.js +1 -0
- package/build/src/shared/Link.d.ts +8 -0
- package/build/src/shared/Link.js +19 -0
- package/build/src/shared/branding.d.ts +1 -0
- package/build/src/shared/branding.js +6 -0
- package/build/src/shared/components.d.ts +5 -0
- package/build/src/shared/components.js +23 -0
- package/build/src/shared/context.d.ts +12 -0
- package/build/src/shared/context.js +10 -0
- package/build/src/shared/locales/LocaleContext.d.ts +19 -0
- package/build/src/shared/locales/LocaleContext.js +18 -0
- package/build/src/shared/locales/en.d.ts +6 -0
- package/build/src/shared/locales/en.js +8 -0
- package/build/src/shared/navigation.d.ts +16 -0
- package/build/src/shared/navigation.js +129 -0
- package/build/src/useActivePage/index.d.ts +1 -0
- package/build/src/useActivePage/index.js +1 -0
- package/build/src/useActivePage/useActivePage.d.ts +11 -0
- package/build/src/useActivePage/useActivePage.js +48 -0
- package/build/src/useLocalStorageState/index.d.ts +1 -0
- package/build/src/useLocalStorageState/index.js +1 -0
- package/build/src/useLocalStorageState/useLocalStorageState.d.ts +2 -0
- package/build/src/useLocalStorageState/useLocalStorageState.js +14 -0
- package/build/src/useSessionStorageState/index.d.ts +1 -0
- package/build/src/useSessionStorageState/index.js +1 -0
- package/build/src/useSessionStorageState/useSessionStorageState.d.ts +2 -0
- package/build/src/useSessionStorageState/useSessionStorageState.js +14 -0
- package/build/src/utils/collections.d.ts +44 -0
- package/build/src/utils/collections.js +43 -0
- package/build/src/utils/describeConformance.d.ts +7 -0
- package/build/src/utils/describeConformance.js +3 -0
- package/build/src/utils/events.d.ts +27 -0
- package/build/src/utils/events.js +54 -0
- package/build/src/utils/hooks/index.d.ts +2 -0
- package/build/src/utils/hooks/index.js +2 -0
- package/build/src/utils/hooks/useBoolean.d.ts +11 -0
- package/build/src/utils/hooks/useBoolean.js +11 -0
- package/build/src/utils/hooks/useDebounced.d.ts +8 -0
- package/build/src/utils/hooks/useDebounced.js +28 -0
- package/build/src/utils/hooks/useDebouncedHandler.d.ts +12 -0
- package/build/src/utils/hooks/useDebouncedHandler.js +41 -0
- package/build/src/utils/hooks/useLatest.d.ts +6 -0
- package/build/src/utils/hooks/useLatest.js +9 -0
- package/build/src/utils/hooks/usePageTitle.d.ts +4 -0
- package/build/src/utils/hooks/usePageTitle.js +13 -0
- package/build/src/utils/hooks/useSsr.d.ts +4 -0
- package/build/src/utils/hooks/useSsr.js +16 -0
- package/build/src/utils/hooks/useStorageState.d.ts +10 -0
- package/build/src/utils/hooks/useStorageState.js +51 -0
- package/build/src/utils/warnOnce.d.ts +5 -0
- package/build/src/utils/warnOnce.js +11 -0
- package/build/vitest.setup.d.ts +1 -0
- package/build/vitest.setup.js +19 -0
- package/package.json +60 -0
- package/src/Account/Account.test.tsx +56 -0
- package/src/Account/Account.tsx +226 -0
- package/src/Account/AccountPopoverFooter.tsx +33 -0
- package/src/Account/AccountPopoverHeader.tsx +20 -0
- package/src/Account/AccountPreview.test.tsx +79 -0
- package/src/Account/AccountPreview.tsx +189 -0
- package/src/Account/SignInButton.tsx +38 -0
- package/src/Account/SignOutButton.tsx +44 -0
- package/src/Account/index.ts +6 -0
- package/src/AppProvider/AppProvider.test.tsx +25 -0
- package/src/AppProvider/AppProvider.tsx +116 -0
- package/src/AppProvider/AppProviderComponent.tsx +162 -0
- package/src/AppProvider/AppThemeProvider.tsx +177 -0
- package/src/AppProvider/index.ts +1 -0
- package/src/DashboardLayout/DashboardLayout.test.tsx +429 -0
- package/src/DashboardLayout/DashboardLayout.tsx +558 -0
- package/src/DashboardLayout/DashboardSidebarSubNavigation.tsx +301 -0
- package/src/DashboardLayout/ThemeSwitcher.tsx +81 -0
- package/src/DashboardLayout/ToolbarActions.tsx +11 -0
- package/src/DashboardLayout/index.ts +4 -0
- package/src/DashboardLayout/utils.ts +23 -0
- package/src/PageContainer/PageContainer.test.tsx +194 -0
- package/src/PageContainer/PageContainer.tsx +201 -0
- package/src/PageContainer/PageContainerToolbar.test.tsx +11 -0
- package/src/PageContainer/PageContainerToolbar.tsx +31 -0
- package/src/PageContainer/index.ts +2 -0
- package/src/index.ts +15 -0
- package/src/nextjs/AppProvider.test.tsx +48 -0
- package/src/nextjs/AppProvider.tsx +18 -0
- package/src/nextjs/AppProviderNextApp.tsx +37 -0
- package/src/nextjs/AppProviderNextPages.tsx +52 -0
- package/src/nextjs/index.tsx +1 -0
- package/src/persistence/codec.tsx +81 -0
- package/src/persistence/index.ts +1 -0
- package/src/persistence/useStorageState.test.tsx +101 -0
- package/src/persistence/useStorageState.tsx +244 -0
- package/src/react-router-dom/AppProvider.test.tsx +17 -0
- package/src/react-router-dom/AppProvider.tsx +40 -0
- package/src/react-router-dom/index.tsx +1 -0
- package/src/shared/Link.tsx +37 -0
- package/src/shared/branding.ts +7 -0
- package/src/shared/components.tsx +46 -0
- package/src/shared/context.ts +21 -0
- package/src/shared/locales/LocaleContext.tsx +44 -0
- package/src/shared/locales/en.tsx +9 -0
- package/src/shared/navigation.tsx +197 -0
- package/src/useActivePage/index.ts +1 -0
- package/src/useActivePage/useActivePage.ts +66 -0
- package/src/useLocalStorageState/index.ts +1 -0
- package/src/useLocalStorageState/useLocalStorageState.tsx +23 -0
- package/src/useSessionStorageState/index.tsx +1 -0
- package/src/useSessionStorageState/useSessionStorageState.tsx +23 -0
- package/src/utils/collections.ts +125 -0
- package/src/utils/describeConformance.ts +14 -0
- package/src/utils/events.ts +81 -0
- package/src/utils/hooks/index.ts +2 -0
- package/src/utils/hooks/useBoolean.ts +12 -0
- package/src/utils/hooks/useDebounced.ts +39 -0
- package/src/utils/hooks/useDebouncedHandler.ts +71 -0
- package/src/utils/hooks/useLatest.ts +16 -0
- package/src/utils/hooks/usePageTitle.ts +14 -0
- package/src/utils/hooks/useSsr.ts +24 -0
- package/src/utils/hooks/useStorageState.ts +130 -0
- package/src/utils/warnOnce.ts +12 -0
- package/tsconfig.json +19 -0
- package/vite.config.ts +27 -0
- 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,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,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";
|