@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,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,2 @@
1
+ export { default as useBoolean } from "./useBoolean";
2
+ export { default as usePageTitle } from "./usePageTitle";
@@ -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;
@@ -0,0 +1,12 @@
1
+ const history = new Set<string>();
2
+
3
+ /**
4
+ * Warns only once for a specific message.
5
+ * @param msg The message to warn.
6
+ */
7
+ export default function warnOnce(msg: string) {
8
+ if (!history.has(msg)) {
9
+ history.add(msg);
10
+ console.warn(msg);
11
+ }
12
+ }
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
+ }