@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,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { NavigationContext, RouterContext } from "../shared/context";
|
|
4
|
+
import { getItemPath, getItemTitle, matchPath } from "../shared/navigation";
|
|
5
|
+
import type { Breadcrumb } from "../PageContainer";
|
|
6
|
+
|
|
7
|
+
export interface ActivePage {
|
|
8
|
+
title: string;
|
|
9
|
+
path: string;
|
|
10
|
+
/**
|
|
11
|
+
* @deprecated Use `breadcrumbs` instead.
|
|
12
|
+
*/
|
|
13
|
+
breadCrumbs: Breadcrumb[];
|
|
14
|
+
breadcrumbs: Breadcrumb[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useActivePage(): ActivePage | null {
|
|
18
|
+
const navigationContext = React.useContext(NavigationContext);
|
|
19
|
+
const routerContext = React.useContext(RouterContext);
|
|
20
|
+
const pathname = routerContext?.pathname ?? "/";
|
|
21
|
+
const activeItem = matchPath(navigationContext, pathname);
|
|
22
|
+
|
|
23
|
+
const rootItem = matchPath(navigationContext, "/");
|
|
24
|
+
|
|
25
|
+
return React.useMemo(() => {
|
|
26
|
+
if (!activeItem) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const breadcrumbs: Breadcrumb[] = [];
|
|
31
|
+
|
|
32
|
+
if (rootItem) {
|
|
33
|
+
breadcrumbs.push({
|
|
34
|
+
title: getItemTitle(rootItem),
|
|
35
|
+
path: "/"
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
40
|
+
let prefix = "";
|
|
41
|
+
for (const segment of segments) {
|
|
42
|
+
const path = `${prefix}/${segment}`;
|
|
43
|
+
prefix = path;
|
|
44
|
+
const item = matchPath(navigationContext, path);
|
|
45
|
+
if (!item) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const itemPath = getItemPath(navigationContext, item);
|
|
49
|
+
const lastCrumb = breadcrumbs[breadcrumbs.length - 1];
|
|
50
|
+
if (lastCrumb?.path !== itemPath) {
|
|
51
|
+
breadcrumbs.push({
|
|
52
|
+
title: getItemTitle(item),
|
|
53
|
+
path: itemPath
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
title: getItemTitle(activeItem),
|
|
60
|
+
path: getItemPath(navigationContext, activeItem),
|
|
61
|
+
breadcrumbs,
|
|
62
|
+
// TODO: Remove in the next major version
|
|
63
|
+
breadCrumbs: breadcrumbs
|
|
64
|
+
};
|
|
65
|
+
}, [activeItem, rootItem, pathname, navigationContext]);
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./useLocalStorageState";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
UseStorageState,
|
|
4
|
+
useStorageState,
|
|
5
|
+
useStorageStateServer
|
|
6
|
+
} from "../persistence";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sync state to local storage so that it persists through a page refresh. Usage is
|
|
10
|
+
* similar to useState except we pass in a storage key so that we can default
|
|
11
|
+
* to that value on page load instead of the specified initial value.
|
|
12
|
+
*
|
|
13
|
+
* Since the storage API isn't available in server-rendering environments, we
|
|
14
|
+
* return null during SSR and hydration.
|
|
15
|
+
*/
|
|
16
|
+
const useLocalStorageStateBrowser: UseStorageState = (
|
|
17
|
+
...args: [any, any, any]
|
|
18
|
+
) => useStorageState(window.localStorage, ...args);
|
|
19
|
+
|
|
20
|
+
export const useLocalStorageState: UseStorageState =
|
|
21
|
+
typeof window === "undefined"
|
|
22
|
+
? useStorageStateServer
|
|
23
|
+
: useLocalStorageStateBrowser;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./useSessionStorageState";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
UseStorageState,
|
|
4
|
+
useStorageState,
|
|
5
|
+
useStorageStateServer
|
|
6
|
+
} from "../persistence";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sync state to session storage so that it persists through a page refresh. Usage is
|
|
10
|
+
* similar to useState except we pass in a storage key so that we can default
|
|
11
|
+
* to that value on page load instead of the specified initial value.
|
|
12
|
+
*
|
|
13
|
+
* Since the storage API isn't available in server-rendering environments, we
|
|
14
|
+
* return null during SSR and hydration.
|
|
15
|
+
*/
|
|
16
|
+
const useSessionStorageStateBrowser: UseStorageState = (
|
|
17
|
+
...args: [any, any, any]
|
|
18
|
+
) => useStorageState(window.sessionStorage, ...args);
|
|
19
|
+
|
|
20
|
+
export const useSessionStorageState: UseStorageState =
|
|
21
|
+
typeof window === "undefined"
|
|
22
|
+
? useStorageStateServer
|
|
23
|
+
: useSessionStorageStateBrowser;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export function asArray<T>(maybeArray: T | T[]): T[] {
|
|
2
|
+
return Array.isArray(maybeArray) ? maybeArray : [maybeArray];
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
type PropertiesOf<P> = Extract<keyof P, string>;
|
|
6
|
+
|
|
7
|
+
type Require<T, K extends keyof T> = T & { [P in K]-?: T[P] };
|
|
8
|
+
|
|
9
|
+
type Ensure<U, K extends PropertyKey> = K extends keyof U
|
|
10
|
+
? Require<U, K>
|
|
11
|
+
: U & Record<K, unknown>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Type aware version of Object.protoype.hasOwnProperty.
|
|
15
|
+
* See https://fettblog.eu/typescript-hasownproperty/
|
|
16
|
+
*/
|
|
17
|
+
export function hasOwnProperty<X extends {}, Y extends PropertyKey>(
|
|
18
|
+
obj: X,
|
|
19
|
+
prop: Y
|
|
20
|
+
): obj is Ensure<X, Y> {
|
|
21
|
+
return obj.hasOwnProperty(prop);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Maps `obj` to a new object. The `mapper` function receives an entry array of key and value and
|
|
26
|
+
* is allowed to manipulate both. It can also return `null` to omit a property from the result.
|
|
27
|
+
*/
|
|
28
|
+
export function mapProperties<P, L extends PropertyKey, U>(
|
|
29
|
+
obj: P,
|
|
30
|
+
mapper: <K extends PropertiesOf<P>>(old: [K, P[K]]) => [L, U] | null
|
|
31
|
+
): Record<L, U>;
|
|
32
|
+
export function mapProperties<U, V>(
|
|
33
|
+
obj: Record<string, U>,
|
|
34
|
+
mapper: (old: [string, U]) => [string, V] | null
|
|
35
|
+
): Record<string, V> {
|
|
36
|
+
return Object.fromEntries(
|
|
37
|
+
Object.entries(obj).flatMap((entry) => {
|
|
38
|
+
const mapped = mapper(entry);
|
|
39
|
+
return mapped ? [mapped] : [];
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Maps an objects' property keys. The result is a new object with the mapped keys, but the same values.
|
|
46
|
+
*/
|
|
47
|
+
export function mapKeys<U>(
|
|
48
|
+
obj: Record<string, U>,
|
|
49
|
+
mapper: (old: string) => string
|
|
50
|
+
): Record<string, U> {
|
|
51
|
+
return mapProperties(obj, ([key, value]) => [mapper(key), value]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Maps an objects' property values. The result is a new object with the same keys, but mapped values.
|
|
56
|
+
*/
|
|
57
|
+
export function mapValues<P, V>(
|
|
58
|
+
obj: P,
|
|
59
|
+
mapper: (old: P[PropertiesOf<P>], key: PropertiesOf<P>) => V
|
|
60
|
+
): Record<PropertiesOf<P>, V> {
|
|
61
|
+
return mapProperties(obj, ([key, value]) => [key, mapper(value, key)]);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Filters an objects' property values. Similar to `array.filter` but for objects. The result is a new
|
|
65
|
+
* object with all the properties removed for which `filter` returned `false`.
|
|
66
|
+
*/
|
|
67
|
+
export function filterValues<K extends PropertyKey, P, Q extends P>(
|
|
68
|
+
obj: Record<K, P>,
|
|
69
|
+
filter: (old: P) => old is Q
|
|
70
|
+
): Record<K, Q>;
|
|
71
|
+
export function filterValues<P>(
|
|
72
|
+
obj: P,
|
|
73
|
+
filter: (old: P[keyof P]) => boolean
|
|
74
|
+
): Partial<P>;
|
|
75
|
+
export function filterValues<U>(
|
|
76
|
+
obj: Record<string, U>,
|
|
77
|
+
filter: (old: U) => boolean
|
|
78
|
+
): Record<string, U>;
|
|
79
|
+
export function filterValues<U>(
|
|
80
|
+
obj: Record<string, U>,
|
|
81
|
+
filter: (old: U) => boolean
|
|
82
|
+
): Record<string, U> {
|
|
83
|
+
return mapProperties(obj, ([key, value]) =>
|
|
84
|
+
filter(value) ? [key, value] : null
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Filters an objects' property keys. Similar to `array.filter` but for objects. The result is a new
|
|
90
|
+
* object with all the properties removed for which `filter` returned `false`.
|
|
91
|
+
*/
|
|
92
|
+
export function filterKeys<P>(
|
|
93
|
+
obj: P,
|
|
94
|
+
filter: (old: keyof P) => boolean
|
|
95
|
+
): Partial<P>;
|
|
96
|
+
export function filterKeys<U>(
|
|
97
|
+
obj: Record<string, U>,
|
|
98
|
+
filter: (old: string) => boolean
|
|
99
|
+
): Record<string, U>;
|
|
100
|
+
export function filterKeys<U>(
|
|
101
|
+
obj: Record<string, U>,
|
|
102
|
+
filter: (old: string) => boolean
|
|
103
|
+
): Record<string, U> {
|
|
104
|
+
return mapProperties(obj, ([key, value]) =>
|
|
105
|
+
filter(key) ? [key, value] : null
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Compares the properties of two objects. Returns `true` if all properties are strictly equal, `false`
|
|
111
|
+
* otherwise.
|
|
112
|
+
* Pass a subset of properties to only compare those.
|
|
113
|
+
*/
|
|
114
|
+
export function equalProperties<P extends object>(
|
|
115
|
+
obj1: P,
|
|
116
|
+
obj2: P,
|
|
117
|
+
subset?: (keyof P)[]
|
|
118
|
+
): boolean {
|
|
119
|
+
const keysToCheck = new Set(
|
|
120
|
+
subset ?? ([...Object.keys(obj1), ...Object.keys(obj2)] as (keyof P)[])
|
|
121
|
+
);
|
|
122
|
+
return Array.from(keysToCheck).every((key) =>
|
|
123
|
+
Object.is(obj1[key], obj2[key])
|
|
124
|
+
);
|
|
125
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
export interface ConformanceOptions {
|
|
4
|
+
refInstanceof?: unknown;
|
|
5
|
+
inheritComponent?: React.ElementType;
|
|
6
|
+
skip?: Array<"themeDefaultProps">;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function describeConformance(
|
|
10
|
+
minimalElement: React.ReactElement<unknown>,
|
|
11
|
+
getOptions: () => ConformanceOptions
|
|
12
|
+
) {
|
|
13
|
+
getOptions();
|
|
14
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export type EventName = string | symbol;
|
|
2
|
+
|
|
3
|
+
export type EventHandlers = Record<EventName, unknown>;
|
|
4
|
+
|
|
5
|
+
export type EventHandler<
|
|
6
|
+
T extends EventHandlers,
|
|
7
|
+
K extends keyof T = keyof T
|
|
8
|
+
> = (event: T[K]) => void;
|
|
9
|
+
|
|
10
|
+
export type AllEventsHandler<
|
|
11
|
+
T extends EventHandlers,
|
|
12
|
+
K extends keyof T = keyof T
|
|
13
|
+
> = (type: K, event: T[K]) => void;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Lightweight event emitter
|
|
17
|
+
*/
|
|
18
|
+
export class Emitter<T extends EventHandlers = {}> {
|
|
19
|
+
private handlers = new Map<
|
|
20
|
+
keyof T,
|
|
21
|
+
Set<EventHandler<T> | AllEventsHandler<T>>
|
|
22
|
+
>();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Add a listener for an event
|
|
26
|
+
*/
|
|
27
|
+
on(name: "*", handler: AllEventsHandler<T>): void;
|
|
28
|
+
on<K extends keyof T>(name: K, handler: EventHandler<T, K>): void;
|
|
29
|
+
on<K extends keyof T>(
|
|
30
|
+
name: K | "*",
|
|
31
|
+
handler: EventHandler<T, K> | AllEventsHandler<T>
|
|
32
|
+
): void {
|
|
33
|
+
let eventHandlers = this.handlers.get(name);
|
|
34
|
+
if (!eventHandlers) {
|
|
35
|
+
eventHandlers = new Set();
|
|
36
|
+
this.handlers.set(name, eventHandlers);
|
|
37
|
+
}
|
|
38
|
+
eventHandlers.add(handler as EventHandler<T> | AllEventsHandler<T>);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Remove a listener from an event
|
|
43
|
+
*/
|
|
44
|
+
off<K extends keyof T>(name: K, handler: EventHandler<T, K>) {
|
|
45
|
+
const eventHandlers = this.handlers.get(name);
|
|
46
|
+
if (eventHandlers) {
|
|
47
|
+
eventHandlers.delete(handler as EventHandler<T> | AllEventsHandler<T>);
|
|
48
|
+
if (eventHandlers.size <= 0) {
|
|
49
|
+
this.handlers.delete(name);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Subscribe to an event and return an unsubscribe function.
|
|
56
|
+
*/
|
|
57
|
+
subscribe<K extends keyof T>(name: K, handler: EventHandler<T, K>) {
|
|
58
|
+
this.on(name, handler);
|
|
59
|
+
return () => {
|
|
60
|
+
this.off(name, handler);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Emit an event.
|
|
66
|
+
*/
|
|
67
|
+
emit<K extends keyof T>(name: K, event: T[K]) {
|
|
68
|
+
const eventHandlers = this.handlers.get(name);
|
|
69
|
+
if (eventHandlers) {
|
|
70
|
+
for (const eventHandler of eventHandlers) {
|
|
71
|
+
(eventHandler as EventHandler<T, K>)(event);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const allHandlers = this.handlers.get("*");
|
|
75
|
+
if (allHandlers) {
|
|
76
|
+
for (const eventHandler of allHandlers) {
|
|
77
|
+
(eventHandler as AllEventsHandler<T>)(name, event);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A utility with shortcuts to manipulate boolean values.
|
|
5
|
+
*/
|
|
6
|
+
export default function useBoolean(initialValue: boolean) {
|
|
7
|
+
const [value, setValue] = React.useState(initialValue);
|
|
8
|
+
const toggle = React.useCallback(() => setValue((existing) => !existing), []);
|
|
9
|
+
const setTrue = React.useCallback(() => setValue(true), []);
|
|
10
|
+
const setFalse = React.useCallback(() => setValue(false), []);
|
|
11
|
+
return { value, setValue, toggle, setTrue, setFalse };
|
|
12
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This hook allows you to debounce any fast changing value. The debounced value will only
|
|
5
|
+
* reflect the latest value when the useDebounce hook has not been called for the specified
|
|
6
|
+
* time period.
|
|
7
|
+
*
|
|
8
|
+
* Inspired by https://usehooks.com/useDebounce/
|
|
9
|
+
*/
|
|
10
|
+
export default function useDebounced<T>(value: T, delay: number): T {
|
|
11
|
+
const [debouncedValue, setDebouncedValue] = React.useState(() => value);
|
|
12
|
+
const timeoutIdRef = React.useRef<NodeJS.Timeout | null>(null);
|
|
13
|
+
|
|
14
|
+
React.useEffect(
|
|
15
|
+
() => () => {
|
|
16
|
+
if (timeoutIdRef.current) {
|
|
17
|
+
clearTimeout(timeoutIdRef.current);
|
|
18
|
+
timeoutIdRef.current = null;
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
[]
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
React.useEffect(() => {
|
|
25
|
+
timeoutIdRef.current = setTimeout(
|
|
26
|
+
() => setDebouncedValue(() => value),
|
|
27
|
+
delay
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return () => {
|
|
31
|
+
if (timeoutIdRef.current) {
|
|
32
|
+
clearTimeout(timeoutIdRef.current);
|
|
33
|
+
timeoutIdRef.current = null;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}, [value, delay]);
|
|
37
|
+
|
|
38
|
+
return debouncedValue;
|
|
39
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
interface Handler<P extends unknown[]> {
|
|
4
|
+
(...params: P): void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface DelayedInvocation<P extends unknown[]> {
|
|
8
|
+
startTime: number;
|
|
9
|
+
timeout: NodeJS.Timeout;
|
|
10
|
+
params: P;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function defer<P extends unknown[]>(
|
|
14
|
+
fn: React.MutableRefObject<Handler<P>>,
|
|
15
|
+
params: P,
|
|
16
|
+
delay: number
|
|
17
|
+
) {
|
|
18
|
+
const timeout = setTimeout(() => {
|
|
19
|
+
fn.current(...params);
|
|
20
|
+
}, delay);
|
|
21
|
+
|
|
22
|
+
return { startTime: Date.now(), timeout, params };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates a debounced version of the handler that is passed. The invocation of [fn] is
|
|
27
|
+
* delayed for [delay] milliseconds from the last invocation of the debounced function.
|
|
28
|
+
*
|
|
29
|
+
* This implementation adds on the lodash implementation in that it handles updates to the
|
|
30
|
+
* delay value.
|
|
31
|
+
*/
|
|
32
|
+
export default function useDebouncedHandler<P extends unknown[]>(
|
|
33
|
+
fn: Handler<P>,
|
|
34
|
+
delay: number
|
|
35
|
+
): Handler<P> {
|
|
36
|
+
const fnRef = React.useRef(fn);
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
fnRef.current = fn;
|
|
39
|
+
}, [fn]);
|
|
40
|
+
|
|
41
|
+
const delayedInvocation = React.useRef<DelayedInvocation<P> | null>(null);
|
|
42
|
+
|
|
43
|
+
const clearCurrent = React.useCallback(() => {
|
|
44
|
+
if (delayedInvocation.current) {
|
|
45
|
+
clearTimeout(delayedInvocation.current.timeout);
|
|
46
|
+
delayedInvocation.current = null;
|
|
47
|
+
}
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
if (!delayedInvocation.current) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { startTime, params } = delayedInvocation.current;
|
|
56
|
+
|
|
57
|
+
const elapsed = Date.now() - startTime;
|
|
58
|
+
const newDelay = Math.max(delay - elapsed, 0);
|
|
59
|
+
|
|
60
|
+
clearCurrent();
|
|
61
|
+
delayedInvocation.current = defer(fnRef, params, newDelay);
|
|
62
|
+
}, [delay, clearCurrent]);
|
|
63
|
+
|
|
64
|
+
return React.useCallback(
|
|
65
|
+
(...params: P) => {
|
|
66
|
+
clearCurrent();
|
|
67
|
+
delayedInvocation.current = defer(fnRef, params, delay);
|
|
68
|
+
},
|
|
69
|
+
[delay, clearCurrent]
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the latest non-null, non-undefined value that has been passed to it.
|
|
5
|
+
*/
|
|
6
|
+
function useLatest<T>(value: T): T;
|
|
7
|
+
function useLatest<T>(value: T | null | undefined): T | null | undefined;
|
|
8
|
+
function useLatest<T>(value: T | null | undefined): T | null | undefined {
|
|
9
|
+
const [latest, setLatest] = React.useState<T | null | undefined>(value);
|
|
10
|
+
if (latest !== value && value !== null && value !== undefined) {
|
|
11
|
+
setLatest(value);
|
|
12
|
+
}
|
|
13
|
+
return value ?? latest;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default useLatest;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sets the current document title.
|
|
5
|
+
*/
|
|
6
|
+
export default function usePageTitle(title: string) {
|
|
7
|
+
React.useEffect(() => {
|
|
8
|
+
const original = document.title;
|
|
9
|
+
document.title = title;
|
|
10
|
+
return () => {
|
|
11
|
+
document.title = original;
|
|
12
|
+
};
|
|
13
|
+
}, [title]);
|
|
14
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
function subscribe() {
|
|
4
|
+
return () => {};
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function getSnapshot() {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getServerSnapshot() {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Returns true when serverside rendering, or when hydrating.
|
|
17
|
+
*/
|
|
18
|
+
export default function useSsr() {
|
|
19
|
+
return React.useSyncExternalStore<boolean>(
|
|
20
|
+
subscribe,
|
|
21
|
+
getSnapshot,
|
|
22
|
+
getServerSnapshot
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Emitter } from "../events";
|
|
3
|
+
|
|
4
|
+
// storage events only work across windows, we'll use an event emitter to announce within the window
|
|
5
|
+
const emitter = new Emitter<Record<string, null>>();
|
|
6
|
+
|
|
7
|
+
function subscribe(area: Storage, key: string, cb: () => void): () => void {
|
|
8
|
+
const storageHandler = (event: StorageEvent) => {
|
|
9
|
+
if (event.storageArea === area && event.key === key) {
|
|
10
|
+
cb();
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
window.addEventListener("storage", storageHandler);
|
|
14
|
+
emitter.on(key, cb);
|
|
15
|
+
return () => {
|
|
16
|
+
window.removeEventListener("storage", storageHandler);
|
|
17
|
+
emitter.off(key, cb);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getSnapshot(area: Storage, key: string): string | null {
|
|
22
|
+
return area.getItem(key);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function setValue(area: Storage, key: string, value: string | null) {
|
|
26
|
+
if (typeof window !== "undefined") {
|
|
27
|
+
if (value === null) {
|
|
28
|
+
area.removeItem(key);
|
|
29
|
+
} else {
|
|
30
|
+
area.setItem(key, String(value));
|
|
31
|
+
}
|
|
32
|
+
emitter.emit(key, null);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type Initializer<T> = () => T;
|
|
37
|
+
|
|
38
|
+
type UseStorageStateHookResult<T> = [
|
|
39
|
+
T,
|
|
40
|
+
React.Dispatch<React.SetStateAction<T>>
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function useStorageStateServer(
|
|
44
|
+
kind: "session" | "local",
|
|
45
|
+
key: string,
|
|
46
|
+
initializer: string | Initializer<string>
|
|
47
|
+
): UseStorageStateHookResult<string>;
|
|
48
|
+
function useStorageStateServer(
|
|
49
|
+
kind: "session" | "local",
|
|
50
|
+
key: string,
|
|
51
|
+
initializer?: string | null | Initializer<string | null>
|
|
52
|
+
): UseStorageStateHookResult<string | null>;
|
|
53
|
+
function useStorageStateServer(
|
|
54
|
+
kind: "session" | "local",
|
|
55
|
+
key: string,
|
|
56
|
+
initializer: string | null | Initializer<string | null> = null
|
|
57
|
+
):
|
|
58
|
+
| UseStorageStateHookResult<string | null>
|
|
59
|
+
| UseStorageStateHookResult<string> {
|
|
60
|
+
const [initialValue] = React.useState(initializer);
|
|
61
|
+
return [initialValue, () => {}];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Sync state to local/session storage so that it persists through a page refresh. Usage is
|
|
66
|
+
* similar to useState except we pass in a storage key so that we can default
|
|
67
|
+
* to that value on page load instead of the specified initial value.
|
|
68
|
+
*
|
|
69
|
+
* Since the storage API isn't available in server-rendering environments, we
|
|
70
|
+
* return initialValue during SSR and hydration.
|
|
71
|
+
*
|
|
72
|
+
* Things this hook does different from existing solutions:
|
|
73
|
+
* - SSR-capable: it shows initial value during SSR and hydration, but immediately
|
|
74
|
+
* initializes when clientside mounted.
|
|
75
|
+
* - Sync state across tabs: When another tab changes the value in the storage area, the
|
|
76
|
+
* current tab follows suit.
|
|
77
|
+
*/
|
|
78
|
+
function useStorageStateBrowser(
|
|
79
|
+
kind: "session" | "local",
|
|
80
|
+
key: string,
|
|
81
|
+
initializer: string | Initializer<string>
|
|
82
|
+
): UseStorageStateHookResult<string>;
|
|
83
|
+
function useStorageStateBrowser(
|
|
84
|
+
kind: "session" | "local",
|
|
85
|
+
key: string,
|
|
86
|
+
initializer?: string | null | Initializer<string | null>
|
|
87
|
+
): UseStorageStateHookResult<string | null>;
|
|
88
|
+
function useStorageStateBrowser(
|
|
89
|
+
kind: "session" | "local",
|
|
90
|
+
key: string,
|
|
91
|
+
initializer: string | null | Initializer<string | null> = null
|
|
92
|
+
):
|
|
93
|
+
| UseStorageStateHookResult<string | null>
|
|
94
|
+
| UseStorageStateHookResult<string> {
|
|
95
|
+
const [initialValue] = React.useState(initializer);
|
|
96
|
+
const area = kind === "session" ? window.sessionStorage : window.localStorage;
|
|
97
|
+
const subscribeKey = React.useCallback(
|
|
98
|
+
(cb: () => void) => subscribe(area, key, cb),
|
|
99
|
+
[area, key]
|
|
100
|
+
);
|
|
101
|
+
const getKeySnapshot = React.useCallback(
|
|
102
|
+
() => getSnapshot(area, key) ?? initialValue,
|
|
103
|
+
[area, initialValue, key]
|
|
104
|
+
);
|
|
105
|
+
const getKeyServerSnapshot = React.useCallback(
|
|
106
|
+
() => initialValue,
|
|
107
|
+
[initialValue]
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const storedValue = React.useSyncExternalStore(
|
|
111
|
+
subscribeKey,
|
|
112
|
+
getKeySnapshot,
|
|
113
|
+
getKeyServerSnapshot
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const setStoredValue = React.useCallback(
|
|
117
|
+
(value: React.SetStateAction<string | null>) => {
|
|
118
|
+
const valueToStore =
|
|
119
|
+
value instanceof Function ? value(storedValue) : value;
|
|
120
|
+
setValue(area, key, valueToStore);
|
|
121
|
+
},
|
|
122
|
+
[area, key, storedValue]
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return [storedValue, setStoredValue];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default typeof window === "undefined"
|
|
129
|
+
? useStorageStateServer
|
|
130
|
+
: useStorageStateBrowser;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
|
4
|
+
"target": "ES2020",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Node10",
|
|
7
|
+
"isolatedModules": true,
|
|
8
|
+
"outDir": "build",
|
|
9
|
+
"noEmit": false,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"forceConsistentCasingInFileNames": true,
|
|
16
|
+
"lib": ["dom", "dom.iterable", "esnext"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["src", "vitest.setup.ts"]
|
|
19
|
+
}
|