@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,201 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import PropTypes from "prop-types";
4
+ import Breadcrumbs from "@mui/material/Breadcrumbs";
5
+ import Container, { ContainerProps } from "@mui/material/Container";
6
+ import Link from "@mui/material/Link";
7
+ import Stack from "@mui/material/Stack";
8
+ import Typography from "@mui/material/Typography";
9
+ import useSlotProps from "@mui/utils/useSlotProps";
10
+ import { styled } from "@mui/material";
11
+ import { Link as ToolpadLink } from "../shared/Link";
12
+ import {
13
+ PageContainerToolbar,
14
+ PageContainerToolbarProps
15
+ } from "./PageContainerToolbar";
16
+ import { getItemTitle } from "../shared/navigation";
17
+ import { useActivePage } from "../useActivePage";
18
+ import warnOnce from "../utils/warnOnce";
19
+
20
+ const PageContentHeader = styled("div")(({ theme }) => ({
21
+ display: "flex",
22
+ flexDirection: "row",
23
+ justifyContent: "space-between",
24
+ gap: theme.spacing(2)
25
+ }));
26
+
27
+ export interface PageContainerSlotProps {
28
+ toolbar: PageContainerToolbarProps;
29
+ }
30
+
31
+ export interface PageContainerSlots {
32
+ /**
33
+ * The component that renders the actions toolbar.
34
+ * @default Snackbar
35
+ */
36
+ toolbar: React.ElementType;
37
+ }
38
+
39
+ export interface Breadcrumb {
40
+ /**
41
+ * The title of the breadcrumb segment.
42
+ */
43
+ title: string;
44
+ /**
45
+ * The path the breadcrumb links to.
46
+ */
47
+ path: string;
48
+ }
49
+
50
+ // TODO: Remove in the next major version
51
+ /**
52
+ * @deprecated Use `Breadcrumb` instead.
53
+ */
54
+ export type BreadCrumb = Breadcrumb;
55
+
56
+ export interface PageContainerProps extends ContainerProps {
57
+ children?: React.ReactNode;
58
+ /**
59
+ * The title of the page. Leave blank to use the active page title.
60
+ */
61
+ title?: string;
62
+ /**
63
+ * The breadcrumbs of the page. Leave blank to use the active page breadcrumbs.
64
+ */
65
+ breadcrumbs?: Breadcrumb[];
66
+ // TODO: Remove in the next major version
67
+ /**
68
+ * @deprecated Use `breadcrumbs` instead.
69
+ */
70
+ breadCrumbs?: Breadcrumb[];
71
+ /**
72
+ * The components used for each slot inside.
73
+ */
74
+ slots?: PageContainerSlots;
75
+ /**
76
+ * The props used for each slot inside.
77
+ */
78
+ slotProps?: PageContainerSlotProps;
79
+ }
80
+
81
+ /**
82
+ * A container component to provide a title and breadcrumbs for your pages.
83
+ *
84
+ * Demos:
85
+ *
86
+ * - [Page Container](https://mui.com/toolpad/core/react-page-container/)
87
+ *
88
+ * API:
89
+ *
90
+ * - [PageContainer API](https://mui.com/toolpad/core/api/page-container)
91
+ */
92
+ function PageContainer(props: PageContainerProps) {
93
+ const { children, slots, slotProps, breadcrumbs, breadCrumbs, ...rest } =
94
+ props;
95
+
96
+ if (process.env.NODE_ENV !== "production" && breadCrumbs) {
97
+ warnOnce(
98
+ "The PageContainer `breadCrumbs` prop is deprecated. Use `breadcrumbs` instead."
99
+ );
100
+ }
101
+
102
+ const activePage = useActivePage();
103
+
104
+ // TODO: Remove `props.breadCrumbs` in the next major version
105
+ const resolvedBreadcrumbs =
106
+ breadcrumbs ?? breadCrumbs ?? activePage?.breadcrumbs ?? [];
107
+ const title = props.title ?? activePage?.title ?? "";
108
+
109
+ const ToolbarComponent = props?.slots?.toolbar ?? PageContainerToolbar;
110
+ const toolbarSlotProps = useSlotProps({
111
+ elementType: ToolbarComponent,
112
+ ownerState: props,
113
+ externalSlotProps: props?.slotProps?.toolbar,
114
+ additionalProps: {}
115
+ });
116
+
117
+ return (
118
+ <Container {...rest}>
119
+ <Stack sx={{ my: 2 }} spacing={2}>
120
+ <Stack>
121
+ <Breadcrumbs aria-label="breadcrumb">
122
+ {resolvedBreadcrumbs
123
+ ? resolvedBreadcrumbs.map((item, index) => {
124
+ return index < resolvedBreadcrumbs.length - 1 ? (
125
+ <Link
126
+ key={item.path}
127
+ component={ToolpadLink}
128
+ underline="hover"
129
+ color="inherit"
130
+ href={item.path}
131
+ >
132
+ {getItemTitle(item)}
133
+ </Link>
134
+ ) : (
135
+ <Typography key={item.path} color="text.primary">
136
+ {getItemTitle(item)}
137
+ </Typography>
138
+ );
139
+ })
140
+ : null}
141
+ </Breadcrumbs>
142
+
143
+ <PageContentHeader>
144
+ {title ? <Typography variant="h4">{title}</Typography> : null}
145
+ <ToolbarComponent {...toolbarSlotProps} />
146
+ </PageContentHeader>
147
+ </Stack>
148
+ <div>{children}</div>
149
+ </Stack>
150
+ </Container>
151
+ );
152
+ }
153
+
154
+ PageContainer.propTypes /* remove-proptypes */ = {
155
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
156
+ // │ These PropTypes are generated from the TypeScript type definitions. │
157
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
158
+ // └─────────────────────────────────────────────────────────────────────┘
159
+ /**
160
+ * The breadcrumbs of the page. Leave blank to use the active page breadcrumbs.
161
+ */
162
+ breadcrumbs: PropTypes.arrayOf(
163
+ PropTypes.shape({
164
+ path: PropTypes.string.isRequired,
165
+ title: PropTypes.string.isRequired
166
+ })
167
+ ),
168
+ /**
169
+ * @deprecated Use `breadcrumbs` instead.
170
+ */
171
+ breadCrumbs: PropTypes.arrayOf(
172
+ PropTypes.shape({
173
+ path: PropTypes.string.isRequired,
174
+ title: PropTypes.string.isRequired
175
+ })
176
+ ),
177
+ /**
178
+ * @ignore
179
+ */
180
+ children: PropTypes.node,
181
+ /**
182
+ * The props used for each slot inside.
183
+ */
184
+ slotProps: PropTypes.shape({
185
+ toolbar: PropTypes.shape({
186
+ children: PropTypes.node
187
+ }).isRequired
188
+ }),
189
+ /**
190
+ * The components used for each slot inside.
191
+ */
192
+ slots: PropTypes.shape({
193
+ toolbar: PropTypes.elementType
194
+ }),
195
+ /**
196
+ * The title of the page. Leave blank to use the active page title.
197
+ */
198
+ title: PropTypes.string
199
+ } as any;
200
+
201
+ export { PageContainer };
@@ -0,0 +1,11 @@
1
+ import { describe, test } from "vitest";
2
+ import { PageContainerToolbar } from "./PageContainerToolbar";
3
+ import describeConformance from "../utils/describeConformance";
4
+
5
+ describe("PageContainerToolbar", () => {
6
+ describeConformance(<PageContainerToolbar />, () => ({
7
+ skip: ["themeDefaultProps"]
8
+ }));
9
+
10
+ test("dummy test", () => {});
11
+ });
@@ -0,0 +1,31 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { styled } from "@mui/material";
4
+
5
+ const PageContainerToolbarRoot = styled("div")(({ theme }) => ({
6
+ display: "flex",
7
+ flexDirection: "row",
8
+ gap: theme.spacing(1),
9
+ // Ensure the toolbar is always on the right side, even after wrapping
10
+ marginLeft: "auto"
11
+ }));
12
+
13
+ export interface PageContainerToolbarProps {
14
+ children?: React.ReactNode;
15
+ }
16
+
17
+ /**
18
+ *
19
+ * Demos:
20
+ *
21
+ * - [Page Container](https://mui.com/toolpad/core/react-page-container/)
22
+ *
23
+ * API:
24
+ *
25
+ * - [PageContainerToolbar API](https://mui.com/toolpad/core/api/page-container-toolbar)
26
+ */
27
+ function PageContainerToolbar(props: PageContainerToolbarProps) {
28
+ return <PageContainerToolbarRoot {...props} />;
29
+ }
30
+
31
+ export { PageContainerToolbar };
@@ -0,0 +1,2 @@
1
+ export * from "./PageContainer";
2
+ export * from "./PageContainerToolbar";
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export * from "./AppProvider";
2
+
3
+ export * from "./DashboardLayout";
4
+
5
+ export * from "./Account";
6
+
7
+ export * from "./PageContainer";
8
+
9
+ export * from "./useActivePage";
10
+
11
+ export * from "./useLocalStorageState";
12
+
13
+ export * from "./useSessionStorageState";
14
+
15
+ export * from "./persistence/codec";
@@ -0,0 +1,48 @@
1
+ import * as React from "react";
2
+ import { describe, test, expect, vi } from "vitest";
3
+ import { render, screen } from "@testing-library/react";
4
+ import { AppProvider } from "./AppProvider";
5
+ import { Router } from "../AppProvider";
6
+
7
+ vi.mock("next/navigation", () => {
8
+ const searchParams = new URLSearchParams();
9
+ const push = () => {};
10
+ const replace = () => {};
11
+ const router = { push, replace };
12
+ return {
13
+ usePathname: () => "/",
14
+ useSearchParams: () => searchParams,
15
+ useRouter: () => router
16
+ };
17
+ });
18
+
19
+ vi.mock("next/router", () => ({ useRouter: () => null }));
20
+
21
+ vi.mock("next/compat/router", () => ({ useRouter: () => null }));
22
+
23
+ interface RouterTestProps {
24
+ children: React.ReactNode;
25
+ }
26
+
27
+ function RouterTest({ children }: RouterTestProps) {
28
+ const [pathname, setPathname] = React.useState("/page");
29
+
30
+ const router = React.useMemo<Router>(() => {
31
+ return {
32
+ pathname,
33
+ searchParams: new URLSearchParams(),
34
+ navigate: (path) => setPathname(String(path))
35
+ };
36
+ }, [pathname]);
37
+
38
+ return <AppProvider router={router}>{children}</AppProvider>;
39
+ }
40
+
41
+ describe("Nextjs AppProvider", () => {
42
+ test("renders content correctly", async () => {
43
+ // placeholder test
44
+ render(<RouterTest>Hello</RouterTest>);
45
+
46
+ expect(screen.getByText("Hello")).toBeTruthy();
47
+ });
48
+ });
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import { useRouter } from "next/compat/router";
3
+ import { AppProviderNextApp } from "./AppProviderNextApp";
4
+ import { AppProviderNextPages } from "./AppProviderNextPages";
5
+ import type { AppProviderProps } from "../AppProvider";
6
+
7
+ /**
8
+ * @ignore - internal component.
9
+ */
10
+ function AppProvider(props: AppProviderProps) {
11
+ const router = useRouter();
12
+ const AppProviderComponent = router
13
+ ? AppProviderNextPages
14
+ : AppProviderNextApp;
15
+ return <AppProviderComponent {...props} />;
16
+ }
17
+
18
+ export { AppProvider };
@@ -0,0 +1,37 @@
1
+ import * as React from "react";
2
+ import { usePathname, useSearchParams, useRouter } from "next/navigation";
3
+ import type { AppProviderProps, Navigate, Router } from "../AppProvider";
4
+ import { AppProvider } from "../AppProvider/AppProviderComponent";
5
+
6
+ /**
7
+ * @ignore - internal component.
8
+ */
9
+ export function AppProviderNextApp(props: AppProviderProps) {
10
+ const pathname = usePathname();
11
+ const searchParams = useSearchParams();
12
+ const { push, replace } = useRouter();
13
+
14
+ const navigate = React.useCallback<Navigate>(
15
+ (url, { history = "auto" } = {}) => {
16
+ if (history === "auto" || history === "push") {
17
+ return push(String(url));
18
+ }
19
+ if (history === "replace") {
20
+ return replace(String(url));
21
+ }
22
+ throw new Error(`Invalid history option: ${history}`);
23
+ },
24
+ [push, replace]
25
+ );
26
+
27
+ const routerImpl = React.useMemo<Router>(
28
+ () => ({
29
+ pathname,
30
+ searchParams,
31
+ navigate
32
+ }),
33
+ [pathname, navigate, searchParams]
34
+ );
35
+
36
+ return <AppProvider router={routerImpl} {...props} />;
37
+ }
@@ -0,0 +1,52 @@
1
+ import * as React from "react";
2
+ import { useRouter } from "next/router";
3
+ import type { AppProviderProps, Navigate, Router } from "../AppProvider";
4
+ import { asArray } from "../utils/collections";
5
+ import { AppProvider } from "../AppProvider/AppProviderComponent";
6
+
7
+ /**
8
+ * @ignore - internal component.
9
+ */
10
+ export function AppProviderNextPages(props: AppProviderProps) {
11
+ const { push, replace, asPath, query } = useRouter();
12
+
13
+ const search = React.useMemo(() => {
14
+ const params = new URLSearchParams();
15
+ Object.entries(query ?? {}).forEach(([key, value]) => {
16
+ asArray(value ?? []).forEach((v) => {
17
+ params.append(key, v);
18
+ });
19
+ });
20
+ return params.toString();
21
+ }, [query]);
22
+
23
+ // Stable search params object
24
+ const searchParams = React.useMemo(
25
+ () => new URLSearchParams(search),
26
+ [search]
27
+ );
28
+
29
+ const navigate = React.useCallback<Navigate>(
30
+ (url, { history = "auto" } = {}) => {
31
+ if (history === "auto" || history === "push") {
32
+ return push(String(url));
33
+ }
34
+ if (history === "replace") {
35
+ return replace(String(url));
36
+ }
37
+ throw new Error(`Invalid history option: ${history}`);
38
+ },
39
+ [push, replace]
40
+ );
41
+
42
+ const routerImpl = React.useMemo<Router>(
43
+ () => ({
44
+ pathname: asPath,
45
+ searchParams,
46
+ navigate
47
+ }),
48
+ [asPath, navigate, searchParams]
49
+ );
50
+
51
+ return <AppProvider router={routerImpl} {...props} />;
52
+ }
@@ -0,0 +1 @@
1
+ export * from "./AppProvider";
@@ -0,0 +1,81 @@
1
+ /**
2
+ * A codec that can encode and decode values of type V to and from strings.
3
+ * @typeParam V The type of values that can be encoded and decoded.
4
+ */
5
+ export interface Codec<V> {
6
+ /**
7
+ * Decodes a string value into a value of type V.
8
+ * @param value The value to decode.
9
+ * @returns The decoded value.
10
+ */
11
+ parse: (value: string) => V;
12
+ /**
13
+ * Encodes a value of type V into a string.
14
+ * @param value The value to encode.
15
+ * @returns The encoded value.
16
+ */
17
+ stringify: (value: V) => string;
18
+ }
19
+
20
+ /**
21
+ * A codec that can encode and decode Date objects to and from strings.
22
+ */
23
+ export const CODEC_DATE: Codec<Date> = {
24
+ parse: (value) => new Date(value),
25
+ stringify: (value) => value.toISOString()
26
+ };
27
+
28
+ /**
29
+ * A codec that can encode and decode Date objects to and from strings, but only the date part.
30
+ */
31
+ export const CODEC_DATE_ONLY: Codec<Date> = {
32
+ parse: (value) => new Date(value),
33
+ stringify: (value) => value.toISOString().split("T")[0]
34
+ };
35
+
36
+ /**
37
+ * A codec that can encode and decode numbers to and from strings.
38
+ */
39
+ export const CODEC_NUMBER: Codec<number> = {
40
+ parse: (value) => Number(value),
41
+ stringify: (value) => String(value)
42
+ };
43
+
44
+ /**
45
+ * A codec that can encode and decode boolean values to and from strings.
46
+ */
47
+ export const CODE_BOOLEAN: Codec<boolean> = {
48
+ parse: (value) => value === "true",
49
+ stringify: (value) => String(value)
50
+ };
51
+
52
+ /**
53
+ * A codec that can encode and decode JSON values to and from strings.
54
+ */
55
+ export const CODEC_JSON: Codec<unknown> = {
56
+ parse: (value) => {
57
+ try {
58
+ return JSON.parse(value);
59
+ } catch {
60
+ return null;
61
+ }
62
+ },
63
+ stringify: (value) => JSON.stringify(value)
64
+ };
65
+
66
+ /**
67
+ * A codec that can encode and decode JSON values to and from strings.
68
+ * If the JSON value is invalid, parsing will fail.
69
+ */
70
+ export const CODEC_JSON_STRICT: Codec<unknown> = {
71
+ parse: (value) => JSON.parse(value),
72
+ stringify: (value) => JSON.stringify(value)
73
+ };
74
+
75
+ /**
76
+ * A codec that can encode and decode strings to and from strings.
77
+ */
78
+ export const CODEC_STRING: Codec<string> = {
79
+ parse: (value) => value,
80
+ stringify: (value) => value
81
+ };
@@ -0,0 +1 @@
1
+ export * from "./useStorageState";
@@ -0,0 +1,101 @@
1
+ import { describe, test, expect, beforeEach } from "vitest";
2
+ import { act, renderHook } from "@testing-library/react";
3
+ import { useStorageState } from "./useStorageState";
4
+
5
+ describe("useStorageState", () => {
6
+ beforeEach(() => {
7
+ window.localStorage.clear();
8
+ });
9
+
10
+ test("can do basic local storage with initial value", async () => {
11
+ const { result, rerender } = renderHook(() =>
12
+ useStorageState(window.localStorage, "foo", "bar")
13
+ );
14
+
15
+ expect(result.current[0]).toBe("bar");
16
+
17
+ act(() => {
18
+ result.current[1]("baz");
19
+ });
20
+
21
+ rerender();
22
+
23
+ expect(result.current[0]).toBe("baz");
24
+ });
25
+
26
+ test("can do basic local storage without initial value", async () => {
27
+ const { result, rerender } = renderHook(() =>
28
+ useStorageState(window.localStorage, "foo")
29
+ );
30
+
31
+ expect(result.current[0]).toBe(null);
32
+
33
+ act(() => {
34
+ result.current[1]("baz");
35
+ });
36
+
37
+ rerender();
38
+
39
+ expect(result.current[0]).toBe("baz");
40
+ });
41
+
42
+ test("can clear storage value, and reset to intiial value", async () => {
43
+ const { result, rerender } = renderHook(() =>
44
+ useStorageState(window.localStorage, "foo", "bar")
45
+ );
46
+
47
+ act(() => {
48
+ result.current[1]("baz");
49
+ });
50
+
51
+ rerender();
52
+
53
+ expect(result.current[0]).toBe("baz");
54
+
55
+ act(() => {
56
+ result.current[1](null);
57
+ });
58
+
59
+ rerender();
60
+
61
+ expect(result.current[0]).toBe("bar");
62
+ });
63
+
64
+ test("can clear storage value", async () => {
65
+ const { result, rerender } = renderHook(() =>
66
+ useStorageState(window.localStorage, "foo")
67
+ );
68
+
69
+ act(() => {
70
+ result.current[1]("baz");
71
+ });
72
+
73
+ rerender();
74
+
75
+ expect(result.current[0]).toBe("baz");
76
+
77
+ act(() => {
78
+ result.current[1](null);
79
+ });
80
+
81
+ rerender();
82
+
83
+ expect(result.current[0]).toBe(null);
84
+ });
85
+
86
+ test("can handle complex types", async () => {
87
+ const { result, rerender } = renderHook(() =>
88
+ useStorageState(window.localStorage, "foo", { a: 1 }, { codec: JSON })
89
+ );
90
+
91
+ expect(result.current[0]).toEqual({ a: 1 });
92
+
93
+ act(() => {
94
+ result.current[1]({ b: 2 });
95
+ });
96
+
97
+ rerender();
98
+
99
+ expect(result.current[0]).toEqual({ b: 2 });
100
+ });
101
+ });