@vexcms/next 0.1.0-alpha.27 → 0.1.0-alpha.28
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/dist/HydrationBoundaryClient.d.ts +17 -0
- package/dist/HydrationBoundaryClient.d.ts.map +1 -0
- package/dist/HydrationBoundaryClient.js +9 -0
- package/dist/HydrationBoundaryClient.js.map +1 -0
- package/dist/NextAdminLayout.d.ts +21 -13
- package/dist/NextAdminLayout.d.ts.map +1 -1
- package/dist/NextAdminLayout.js +1 -1
- package/dist/NextAdminLayoutClient.d.ts +10 -4
- package/dist/NextAdminLayoutClient.d.ts.map +1 -1
- package/dist/NextAdminLayoutClient.js +104 -7
- package/dist/NextAdminLayoutClient.js.map +1 -1
- package/dist/NextAdminPage.d.ts +52 -5
- package/dist/NextAdminPage.d.ts.map +1 -1
- package/dist/NextAdminPage.js +1 -1
- package/dist/auth/VexAdminAuthAdapterContext.d.ts +66 -0
- package/dist/auth/VexAdminAuthAdapterContext.d.ts.map +1 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +12 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/chunk-JB3GMZPF.js +525 -0
- package/dist/chunk-JB3GMZPF.js.map +1 -0
- package/dist/chunk-NE2VUFS2.js +15 -0
- package/dist/chunk-NE2VUFS2.js.map +1 -0
- package/dist/chunk-T7DNJLSX.js +21 -0
- package/dist/chunk-T7DNJLSX.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/navigation/index.d.ts +3 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/useNextNavigate.d.ts +19 -0
- package/dist/navigation/useNextNavigate.d.ts.map +1 -0
- package/dist/navigation/useNextNavigationBlocker.d.ts +32 -0
- package/dist/navigation/useNextNavigationBlocker.d.ts.map +1 -0
- package/dist/serverQueryClient.d.ts +48 -0
- package/dist/serverQueryClient.d.ts.map +1 -0
- package/package.json +11 -4
- package/dist/chunk-DDH2TJTP.js +0 -195
- package/dist/chunk-DDH2TJTP.js.map +0 -1
- package/dist/chunk-UMJTAD5A.js +0 -22
- package/dist/chunk-UMJTAD5A.js.map +0 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { HydrationBoundary } from "@tanstack/react-query";
|
|
2
|
+
/**
|
|
3
|
+
* Re-exports `@tanstack/react-query`'s own `HydrationBoundary` through an explicit
|
|
4
|
+
* `"use client"` module boundary, in its own tsup entry (`tsup.config.ts`'s client build
|
|
5
|
+
* target, `@tanstack/react-query` marked `external` there) — `NextAdminPage.tsx` (a Server
|
|
6
|
+
* Component, no banner) imports it from HERE, not directly from `@tanstack/react-query`.
|
|
7
|
+
*
|
|
8
|
+
* Importing `HydrationBoundary` directly into `NextAdminPage.tsx`'s own no-banner tsup
|
|
9
|
+
* bundle let tsup inline/bundle it into the SAME output chunk as the server component code
|
|
10
|
+
* — losing the "use client" boundary Next.js's RSC compiler needs to see at the MODULE
|
|
11
|
+
* level (boundary detection is per-file, not per-component-within-a-bundled-chunk),
|
|
12
|
+
* confirmed live: "createContext only works in Client Components" from inside
|
|
13
|
+
* `HydrationBoundary`'s own internals. The same class of issue `NextAdminLayoutClient.tsx`
|
|
14
|
+
* exists to avoid for `NextAdminLayout`'s client leaf.
|
|
15
|
+
*/
|
|
16
|
+
export { HydrationBoundary as HydrationBoundaryClient };
|
|
17
|
+
//# sourceMappingURL=HydrationBoundaryClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HydrationBoundaryClient.d.ts","sourceRoot":"","sources":["../src/HydrationBoundaryClient.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE1D;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,iBAAiB,IAAI,uBAAuB,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/HydrationBoundaryClient.tsx"],"sourcesContent":["\"use client\";\n\nimport { HydrationBoundary } from \"@tanstack/react-query\";\n\n/**\n * Re-exports `@tanstack/react-query`'s own `HydrationBoundary` through an explicit\n * `\"use client\"` module boundary, in its own tsup entry (`tsup.config.ts`'s client build\n * target, `@tanstack/react-query` marked `external` there) — `NextAdminPage.tsx` (a Server\n * Component, no banner) imports it from HERE, not directly from `@tanstack/react-query`.\n *\n * Importing `HydrationBoundary` directly into `NextAdminPage.tsx`'s own no-banner tsup\n * bundle let tsup inline/bundle it into the SAME output chunk as the server component code\n * — losing the \"use client\" boundary Next.js's RSC compiler needs to see at the MODULE\n * level (boundary detection is per-file, not per-component-within-a-bundled-chunk),\n * confirmed live: \"createContext only works in Client Components\" from inside\n * `HydrationBoundary`'s own internals. The same class of issue `NextAdminLayoutClient.tsx`\n * exists to avoid for `NextAdminLayout`'s client leaf.\n */\nexport { HydrationBoundary as HydrationBoundaryClient };\n"],"mappings":";;;;AAEA,SAAS,yBAAyB;","names":[]}
|
|
@@ -1,36 +1,45 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import type
|
|
2
|
+
import { type VexApiAuth } from "@vexcms/core";
|
|
3
3
|
/**
|
|
4
4
|
* Next.js admin layout shell for VexCMS.
|
|
5
5
|
*
|
|
6
|
-
* A **server component** that reads the `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* A **server component** that reads the consolidated `vex-admin` cookie's
|
|
7
|
+
* `sidebarOpen` field server-side (Decision 51; previously a dedicated
|
|
8
|
+
* `sidebar_state` cookie) so the sidebar's open/closed state is correct on
|
|
9
|
+
* first paint, with no client flash, and passes it, plus `authPromise`/
|
|
10
|
+
* `children`, to the
|
|
9
11
|
* client leaf `NextAdminLayoutClient`. Config no longer crosses this
|
|
10
12
|
* boundary at all — the admin panel's client tree gets its config from the
|
|
11
13
|
* app's own root `VexConfigProvider` mount
|
|
12
14
|
* (`components/providers/client.tsx`), not from a prop threaded through this
|
|
13
15
|
* layout.
|
|
14
16
|
*
|
|
17
|
+
* Does **not** await `authPromise` itself (perf fix, Decision 44/streaming) — only
|
|
18
|
+
* `cookies()` (fast, no network). The sidebar resolves `authPromise` inside its
|
|
19
|
+
* OWN `<Suspense>` boundary (`AdminLayout`, `@vexcms/react`), so a slow
|
|
20
|
+
* `getUserOrg()` round trip only delays the sidebar nav, never `children` or
|
|
21
|
+
* this layout's own static chrome.
|
|
22
|
+
*
|
|
15
23
|
* Render it from your admin `layout.tsx`. Because this is a server component,
|
|
16
|
-
* your `layout.tsx` may itself
|
|
17
|
-
*
|
|
24
|
+
* your `layout.tsx` may itself call server-only auth helpers to build
|
|
25
|
+
* `authPromise` (e.g. `getUserOrg()` from `~/auth/server`) — started, not
|
|
26
|
+
* awaited, so `layout.tsx` itself stays synchronous and streams immediately.
|
|
18
27
|
*
|
|
19
28
|
* @param props - Layout props.
|
|
20
29
|
* @param props.children - The page content from `[[...slug]]/page.tsx`.
|
|
21
|
-
* @param props.
|
|
30
|
+
* @param props.authPromise - The in-flight `{ user, organization }` fetch (Decision 44),
|
|
31
|
+
* optional — omit it entirely when `config.access` is absent/disabled.
|
|
22
32
|
* @returns The rendered admin shell, wrapping `children` in the client boundary
|
|
23
33
|
* ({@link NextAdminLayoutClient}) with the server-read sidebar cookie state.
|
|
24
34
|
* @example
|
|
25
35
|
* ```tsx
|
|
26
36
|
* // app/admin/layout.tsx
|
|
27
37
|
* import { NextAdminLayout } from "@vexcms/next/client";
|
|
28
|
-
* import {
|
|
38
|
+
* import { getUserOrg } from "~/auth/server";
|
|
29
39
|
*
|
|
30
|
-
* export default
|
|
31
|
-
* const user = await getCurrentUser();
|
|
40
|
+
* export default function AdminLayout({ children }: { children: ReactNode }) {
|
|
32
41
|
* return (
|
|
33
|
-
* <NextAdminLayout
|
|
42
|
+
* <NextAdminLayout authPromise={getUserOrg()}>
|
|
34
43
|
* {children}
|
|
35
44
|
* </NextAdminLayout>
|
|
36
45
|
* );
|
|
@@ -39,7 +48,6 @@ import type { AdminUser } from "@vexcms/react";
|
|
|
39
48
|
*/
|
|
40
49
|
export declare function NextAdminLayout(props: {
|
|
41
50
|
children: ReactNode;
|
|
42
|
-
|
|
43
|
-
organization?: Record<string, unknown>;
|
|
51
|
+
authPromise?: Promise<VexApiAuth>;
|
|
44
52
|
}): Promise<import("react").JSX.Element>;
|
|
45
53
|
//# sourceMappingURL=NextAdminLayout.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NextAdminLayout.d.ts","sourceRoot":"","sources":["../src/NextAdminLayout.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"NextAdminLayout.d.ts","sourceRoot":"","sources":["../src/NextAdminLayout.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,EAAuC,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAIpF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE;IAC3C,QAAQ,EAAE,SAAS,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;CACnC,wCASA"}
|
package/dist/NextAdminLayout.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import type { VexApiAuth } from "@vexcms/core";
|
|
3
3
|
/**
|
|
4
4
|
* Client leaf for the Next.js admin layout.
|
|
5
5
|
*
|
|
@@ -10,6 +10,11 @@ import { type AdminUser } from "@vexcms/react";
|
|
|
10
10
|
* admin routes.
|
|
11
11
|
* - `NextLink` / `NextImage` passed as framework components so every
|
|
12
12
|
* `VexLink`/`VexImage` uses Next.js routing and image optimisation.
|
|
13
|
+
* - `useVexAdminAuthAdapter()` (`./auth/VexAdminAuthAdapterContext`) read here and
|
|
14
|
+
* forwarded into `components.auth`, so `AdminTopNav`'s sign-in/out control picks up
|
|
15
|
+
* whatever adapter the app mounted via `VexAdminAuthAdapterProvider`
|
|
16
|
+
* (`@vexcms/next/auth`) — `undefined` when the app hasn't mounted one, same as the
|
|
17
|
+
* control simply not rendering.
|
|
13
18
|
*
|
|
14
19
|
* Config reaches `AdminLayout`'s children through the app's own
|
|
15
20
|
* `VexConfigProvider` mount, not through a prop on this component — this
|
|
@@ -20,15 +25,16 @@ import { type AdminUser } from "@vexcms/react";
|
|
|
20
25
|
*
|
|
21
26
|
* @param props - Layout props.
|
|
22
27
|
* @param props.children - The page content from `[[...slug]]/page.tsx`.
|
|
23
|
-
* @param props.
|
|
28
|
+
* @param props.authPromise - The in-flight `{ user, organization }` fetch (Decision 44),
|
|
29
|
+
* forwarded to `AdminLayout` so the sidebar resolves it in its OWN `<Suspense>` boundary —
|
|
30
|
+
* a slow auth round trip streams in the sidebar nav without blocking `children`.
|
|
24
31
|
* @returns The admin shell rendered from `children`, wrapped in `NuqsAdapter`
|
|
25
32
|
* and the shared `AdminLayout`, wired with the active slug/document id derived
|
|
26
33
|
* from `usePathname()` and Next.js `Link`/`Image` components.
|
|
27
34
|
*/
|
|
28
35
|
export declare function NextAdminLayoutClient(props: {
|
|
29
36
|
children: ReactNode;
|
|
30
|
-
|
|
37
|
+
authPromise?: Promise<VexApiAuth>;
|
|
31
38
|
sidebarOpen?: boolean;
|
|
32
|
-
user?: AdminUser;
|
|
33
39
|
}): import("react").JSX.Element;
|
|
34
40
|
//# sourceMappingURL=NextAdminLayoutClient.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NextAdminLayoutClient.d.ts","sourceRoot":"","sources":["../src/NextAdminLayoutClient.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"NextAdminLayoutClient.d.ts","sourceRoot":"","sources":["../src/NextAdminLayoutClient.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAY/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAC3C,QAAQ,EAAE,SAAS,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAClC,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,+BA8BA"}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
"use client";
|
|
3
|
+
import {
|
|
4
|
+
useVexAdminAuthAdapter
|
|
5
|
+
} from "./chunk-T7DNJLSX.js";
|
|
3
6
|
|
|
4
7
|
// src/NextAdminLayoutClient.tsx
|
|
5
8
|
import { AdminLayout } from "@vexcms/react";
|
|
@@ -7,22 +10,116 @@ import NextImage from "next/image";
|
|
|
7
10
|
import NextLink from "next/link";
|
|
8
11
|
import { usePathname } from "next/navigation";
|
|
9
12
|
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
|
13
|
+
|
|
14
|
+
// src/navigation/useNextNavigationBlocker.ts
|
|
15
|
+
import { useEffect, useRef } from "react";
|
|
16
|
+
import { useRouter } from "next/navigation";
|
|
17
|
+
function useNextNavigationBlocker(props) {
|
|
18
|
+
const router = useRouter();
|
|
19
|
+
const routerRef = useRef(router);
|
|
20
|
+
routerRef.current = router;
|
|
21
|
+
const whenRef = useRef(props.when);
|
|
22
|
+
whenRef.current = props.when;
|
|
23
|
+
const onBlockRef = useRef(props.onBlock);
|
|
24
|
+
onBlockRef.current = props.onBlock;
|
|
25
|
+
const popstateReplayRef = useRef(false);
|
|
26
|
+
const sentinelActiveRef = useRef(false);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
function onClick(event) {
|
|
29
|
+
const anchor = event.target.closest("a");
|
|
30
|
+
if (!anchor || event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || anchor.target || anchor.hasAttribute("download")) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const url = new URL(anchor.href, window.location.href);
|
|
34
|
+
if (url.origin !== window.location.origin) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (url.pathname + url.search === window.location.pathname + window.location.search) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!whenRef.current) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
onBlockRef.current(() => {
|
|
45
|
+
const href = url.pathname + url.search;
|
|
46
|
+
if (isSentinelEntry()) {
|
|
47
|
+
sentinelActiveRef.current = false;
|
|
48
|
+
routerRef.current.replace(href);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
routerRef.current.push(href);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function onPopState() {
|
|
55
|
+
if (popstateReplayRef.current) {
|
|
56
|
+
popstateReplayRef.current = false;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const leftSentinel = sentinelActiveRef.current;
|
|
60
|
+
sentinelActiveRef.current = false;
|
|
61
|
+
if (!whenRef.current || !leftSentinel) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
pushSentinel();
|
|
65
|
+
onBlockRef.current(() => {
|
|
66
|
+
sentinelActiveRef.current = false;
|
|
67
|
+
popstateReplayRef.current = true;
|
|
68
|
+
window.history.go(-2);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
document.addEventListener("click", onClick, true);
|
|
72
|
+
window.addEventListener("popstate", onPopState);
|
|
73
|
+
return () => {
|
|
74
|
+
document.removeEventListener("click", onClick, true);
|
|
75
|
+
window.removeEventListener("popstate", onPopState);
|
|
76
|
+
sentinelActiveRef.current = false;
|
|
77
|
+
};
|
|
78
|
+
}, []);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!props.when || sentinelActiveRef.current) return;
|
|
81
|
+
pushSentinel();
|
|
82
|
+
}, [props.when]);
|
|
83
|
+
function pushSentinel() {
|
|
84
|
+
window.history.pushState({ [SENTINEL_STATE_KEY]: true }, "", window.location.href);
|
|
85
|
+
sentinelActiveRef.current = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
var SENTINEL_STATE_KEY = "__vexNavigationSentinel";
|
|
89
|
+
function isSentinelEntry() {
|
|
90
|
+
const state = window.history.state;
|
|
91
|
+
return typeof state === "object" && state !== null && SENTINEL_STATE_KEY in state;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/navigation/useNextNavigate.ts
|
|
95
|
+
import { useRouter as useRouter2 } from "next/navigation";
|
|
96
|
+
function useNextNavigate() {
|
|
97
|
+
const router = useRouter2();
|
|
98
|
+
return router.push;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/NextAdminLayoutClient.tsx
|
|
10
102
|
import { jsx } from "react/jsx-runtime";
|
|
11
103
|
function NextAdminLayoutClient(props) {
|
|
12
104
|
const pathname = usePathname();
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
105
|
+
const auth = useVexAdminAuthAdapter();
|
|
106
|
+
const [, section, slug, documentId] = pathname.split("/").filter(Boolean);
|
|
107
|
+
const activeSlug = section === "collections" ? slug : section;
|
|
108
|
+
const activeDocID = section === "collections" ? documentId : slug;
|
|
16
109
|
return /* @__PURE__ */ jsx(NuqsAdapter, { children: /* @__PURE__ */ jsx(
|
|
17
110
|
AdminLayout,
|
|
18
111
|
{
|
|
19
112
|
activeDocID,
|
|
20
113
|
activeSlug,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
114
|
+
authPromise: props.authPromise,
|
|
115
|
+
components: {
|
|
116
|
+
Link: NextLink,
|
|
117
|
+
Image: NextImage,
|
|
118
|
+
navigationBlocker: { useBlocker: useNextNavigationBlocker },
|
|
119
|
+
useNavigate: useNextNavigate,
|
|
120
|
+
auth
|
|
121
|
+
},
|
|
24
122
|
sidebarOpen: props.sidebarOpen,
|
|
25
|
-
user: props.user,
|
|
26
123
|
children: props.children
|
|
27
124
|
}
|
|
28
125
|
) });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/NextAdminLayoutClient.tsx"],"sourcesContent":["\"use client\";\n// \"use client\" is injected by the tsup build banner for this entry (see\n// tsup.config.ts) so the emitted module carries the client boundary.\nimport type { ReactNode } from \"react\";\n\nimport { AdminLayout, type AdminUser } from \"@vexcms/react\";\nimport NextImage from \"next/image\";\nimport NextLink from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport { NuqsAdapter } from \"nuqs/adapters/next/app\";\n\n/**\n * Client leaf for the Next.js admin layout.\n *\n * Owns all client-only wiring that cannot run on the server:\n * - `usePathname()` to derive the active collection slug / document id for\n * sidebar highlighting (Next.js layouts do not receive route params).\n * - `NuqsAdapter` (`nuqs/adapters/next/app`) so URL state works inside the\n * admin routes.\n * - `NextLink` / `NextImage` passed as framework components so every\n * `VexLink`/`VexImage` uses Next.js routing and image optimisation.\n *\n * Config reaches `AdminLayout`'s children through the app's own\n * `VexConfigProvider` mount, not through a prop on this component — this\n * leaf never touches config at all.\n *\n * This component is internal to `@vexcms/next` — applications render\n * {@link NextAdminLayout} instead.\n *\n * @param props - Layout props.\n * @param props.children - The page content from `[[...slug]]/page.tsx`.\n * @param props.user - Current user for the admin shell.\n * @returns The admin shell rendered from `children`, wrapped in `NuqsAdapter`\n * and the shared `AdminLayout`, wired with the active slug/document id derived\n * from `usePathname()` and Next.js `Link`/`Image` components.\n */\nexport function NextAdminLayoutClient(props: {\n children: ReactNode;\n organization?: Record<string, unknown>;\n sidebarOpen?: boolean;\n user?: AdminUser;\n}) {\n const pathname = usePathname();\n // pathname: \"/admin\", \"/admin/posts\", \"/admin/posts/123\"\n // Split on \"/\" and take the segment after \"admin\"\n const segments = pathname.split(\"/\").filter(Boolean);\n const activeSlug = segments[1]; // undefined on /admin, \"posts\" on /admin/posts\n const activeDocID = segments[2];\n\n return (\n <NuqsAdapter>\n <AdminLayout\n activeDocID={activeDocID}\n activeSlug={activeSlug}\n components={{ Link: NextLink, Image: NextImage }}\n organization={props.organization}\n pathname={pathname}\n sidebarOpen={props.sidebarOpen}\n user={props.user}\n >\n {props.children}\n </AdminLayout>\n </NuqsAdapter>\n );\n}\n"],"mappings":";;;;AAKA,SAAS,mBAAmC;AAC5C,OAAO,eAAe;AACtB,OAAO,cAAc;AACrB,SAAS,mBAAmB;AAC5B,SAAS,mBAAmB;AA0CtB;AAfC,SAAS,sBAAsB,OAKnC;AACD,QAAM,WAAW,YAAY;AAG7B,QAAM,WAAW,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO;AACnD,QAAM,aAAa,SAAS,CAAC;AAC7B,QAAM,cAAc,SAAS,CAAC;AAE9B,SACE,oBAAC,eACC;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA,YAAY,EAAE,MAAM,UAAU,OAAO,UAAU;AAAA,MAC/C,cAAc,MAAM;AAAA,MACpB;AAAA,MACA,aAAa,MAAM;AAAA,MACnB,MAAM,MAAM;AAAA,MAEX,gBAAM;AAAA;AAAA,EACT,GACF;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/NextAdminLayoutClient.tsx","../src/navigation/useNextNavigationBlocker.ts","../src/navigation/useNextNavigate.ts"],"sourcesContent":["\"use client\";\n// \"use client\" is injected by the tsup build banner for this entry (see\n// tsup.config.ts) so the emitted module carries the client boundary.\nimport type { ReactNode } from \"react\";\nimport type { VexApiAuth } from \"@vexcms/core\";\n\nimport { AdminLayout } from \"@vexcms/react\";\nimport NextImage from \"next/image\";\nimport NextLink from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport { NuqsAdapter } from \"nuqs/adapters/next/app\";\n\nimport { useNextNavigationBlocker } from \"./navigation/useNextNavigationBlocker\";\nimport { useNextNavigate } from \"./navigation/useNextNavigate\";\nimport { useVexAdminAuthAdapter } from \"./auth/VexAdminAuthAdapterContext\";\n\n/**\n * Client leaf for the Next.js admin layout.\n *\n * Owns all client-only wiring that cannot run on the server:\n * - `usePathname()` to derive the active collection slug / document id for\n * sidebar highlighting (Next.js layouts do not receive route params).\n * - `NuqsAdapter` (`nuqs/adapters/next/app`) so URL state works inside the\n * admin routes.\n * - `NextLink` / `NextImage` passed as framework components so every\n * `VexLink`/`VexImage` uses Next.js routing and image optimisation.\n * - `useVexAdminAuthAdapter()` (`./auth/VexAdminAuthAdapterContext`) read here and\n * forwarded into `components.auth`, so `AdminTopNav`'s sign-in/out control picks up\n * whatever adapter the app mounted via `VexAdminAuthAdapterProvider`\n * (`@vexcms/next/auth`) — `undefined` when the app hasn't mounted one, same as the\n * control simply not rendering.\n *\n * Config reaches `AdminLayout`'s children through the app's own\n * `VexConfigProvider` mount, not through a prop on this component — this\n * leaf never touches config at all.\n *\n * This component is internal to `@vexcms/next` — applications render\n * {@link NextAdminLayout} instead.\n *\n * @param props - Layout props.\n * @param props.children - The page content from `[[...slug]]/page.tsx`.\n * @param props.authPromise - The in-flight `{ user, organization }` fetch (Decision 44),\n * forwarded to `AdminLayout` so the sidebar resolves it in its OWN `<Suspense>` boundary —\n * a slow auth round trip streams in the sidebar nav without blocking `children`.\n * @returns The admin shell rendered from `children`, wrapped in `NuqsAdapter`\n * and the shared `AdminLayout`, wired with the active slug/document id derived\n * from `usePathname()` and Next.js `Link`/`Image` components.\n */\nexport function NextAdminLayoutClient(props: {\n children: ReactNode;\n authPromise?: Promise<VexApiAuth>;\n sidebarOpen?: boolean;\n}) {\n const pathname = usePathname();\n const auth = useVexAdminAuthAdapter();\n // pathname: \"/admin\", \"/admin/collections/posts[/<id>]\", \"/admin/globals[/<slug>]\"\n // Split on \"/\" and read the segments after \"admin\". Collections report their\n // slug + document id; globals keep the \"globals\" section as the active slug\n // and the global's slug as the doc id (the sidebar/top nav contract).\n const [, section, slug, documentId] = pathname.split(\"/\").filter(Boolean);\n const activeSlug = section === \"collections\" ? slug : section;\n const activeDocID = section === \"collections\" ? documentId : slug;\n\n return (\n <NuqsAdapter>\n <AdminLayout\n activeDocID={activeDocID}\n activeSlug={activeSlug}\n authPromise={props.authPromise}\n components={{\n Link: NextLink,\n Image: NextImage,\n navigationBlocker: { useBlocker: useNextNavigationBlocker },\n useNavigate: useNextNavigate,\n auth,\n }}\n sidebarOpen={props.sidebarOpen}\n >\n {props.children}\n </AdminLayout>\n </NuqsAdapter>\n );\n}\n","\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport type { NavigationBlockerAdapter } from \"@vexcms/react\";\n\n/**\n * Next.js App Router implementation of `NavigationBlockerAdapter[\"useBlocker\"]`.\n *\n * The App Router has no `routeChangeStart`-equivalent event and (spec decision 3)\n * programmatic navigation cannot be intercepted by patching\n * `history.pushState`/`replaceState` — Next's own `HistoryUpdater` calls\n * `pushState` from a `useInsertionEffect` AFTER the route has already\n * committed, and patches those methods itself. This hook therefore covers\n * only the two surfaces that fire BEFORE a route change commits: same-origin\n * `<a>` clicks (capture-phase `click` listener) and browser back/forward (a\n * `popstate` listener plus a duplicate-history-entry trap). Programmatic\n * navigation is guarded separately by `@vexcms/react`'s `useVexNavigate`,\n * which consults the guard itself before calling this adapter's\n * `useNavigate` (`useNextNavigate`, this package). `beforeunload` (tab close\n * / hard reload) is also NOT handled here — `@vexcms/react`'s\n * `useUnsavedChangesGuard` registers it directly, framework-independent.\n *\n * Provide the resulting adapter from `NextAdminLayoutClient`:\n * `components={{ …, navigationBlocker: { useBlocker: useNextNavigationBlocker } }}`.\n * `useUnsavedChangesGuard` then calls it once per mounted edit view with the\n * live dirty/autosave state.\n *\n * @param props - See `NavigationBlockerAdapter[\"useBlocker\"]` in\n * `@vexcms/react` (`packages/react/src/config/useFrameworkComponents.ts`).\n * @returns Nothing — every intercepted navigation surfaces through\n * `props.onBlock`, never a return value.\n * @throws Never under normal operation; a blocked navigation reports through\n * `onBlock`, not a throw.\n */\nexport function useNextNavigationBlocker(\n props: Parameters<NavigationBlockerAdapter[\"useBlocker\"]>[0],\n): void {\n const router = useRouter();\n\n // Refs, not state: the click/popstate listeners below are installed ONCE\n // per mount (empty dep array) so `when` flipping dirty<->clean never tears\n // down and reinstalls them, and the router's method identity (not\n // guaranteed stable across renders) never matters. Every handler reads\n // these refs instead of closing over the render's values.\n const routerRef = useRef(router);\n routerRef.current = router;\n const whenRef = useRef(props.when);\n whenRef.current = props.when;\n const onBlockRef = useRef(props.onBlock);\n onBlockRef.current = props.onBlock;\n\n // Set just before `history.go(-2)` on a blocked back/forward's Leave, so\n // the `popstate` that fires as a result of `history.go` is treated as the\n // replay instead of re-blocking it.\n const popstateReplayRef = useRef(false);\n // Whether the top-of-stack entry is our same-URL sentinel (pushed when\n // `when` turns true, re-pushed on a blocked popstate).\n const sentinelActiveRef = useRef(false);\n\n useEffect(() => {\n function onClick(event: MouseEvent) {\n const anchor = (event.target as Element).closest(\"a\");\n if (\n !anchor ||\n event.defaultPrevented ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey ||\n event.altKey ||\n anchor.target ||\n anchor.hasAttribute(\"download\")\n ) {\n return;\n }\n\n const url = new URL(anchor.href, window.location.href);\n if (url.origin !== window.location.origin) {\n // External link — let the browser handle it.\n return;\n }\n\n if (\n url.pathname + url.search ===\n window.location.pathname + window.location.search\n ) {\n // Same-page `#hash` anchor — not a route change.\n return;\n }\n\n if (!whenRef.current) {\n return;\n }\n\n event.preventDefault();\n onBlockRef.current(() => {\n const href = url.pathname + url.search;\n if (isSentinelEntry()) {\n // The sentinel is a same-URL duplicate of the guarded page: replace\n // it, so history matches a navigation made with no guard mounted.\n sentinelActiveRef.current = false;\n routerRef.current.replace(href);\n return;\n }\n routerRef.current.push(href);\n });\n }\n\n function onPopState() {\n if (popstateReplayRef.current) {\n popstateReplayRef.current = false;\n return;\n }\n\n const leftSentinel = sentinelActiveRef.current;\n sentinelActiveRef.current = false;\n // Without a sentinel the browser already left the guarded URL — the\n // traversal can no longer be undone, so let it through.\n if (!whenRef.current || !leftSentinel) {\n return;\n }\n\n // Back/forward only stepped off the sentinel onto the guarded page's own\n // entry (same URL). Re-push the sentinel so a Stay leaves history as it was.\n pushSentinel();\n onBlockRef.current(() => {\n // \"Leave\": step past the re-pushed sentinel AND the guarded page's own\n // entry, landing where the original back press intended.\n sentinelActiveRef.current = false;\n popstateReplayRef.current = true;\n window.history.go(-2);\n });\n }\n\n document.addEventListener(\"click\", onClick, true);\n window.addEventListener(\"popstate\", onPopState);\n\n return () => {\n document.removeEventListener(\"click\", onClick, true);\n window.removeEventListener(\"popstate\", onPopState);\n // The sentinel is deliberately left in place: it is a traversable\n // same-URL entry (Next merged its router state into it), and popping it\n // here would start an async traversal that races — and cancels — the\n // navigation that usually caused this unmount (e.g. Delete's redirect\n // after the document query already returned null).\n sentinelActiveRef.current = false;\n };\n // Installs once per mount; handlers read refs.\n }, []);\n\n // A same-URL sentinel entry, pushed as soon as the guard starts blocking, so\n // the first back press lands on the guarded page's own entry instead of\n // leaving it. Never popped (popping triggers a traversal); a Leave through a\n // link replaces it instead.\n useEffect(() => {\n if (!props.when || sentinelActiveRef.current) return;\n pushSentinel();\n }, [props.when]);\n\n function pushSentinel(): void {\n // Next's patched `pushState` merges its own router state into this object,\n // so the entry stays traversable by the App Router.\n window.history.pushState({ [SENTINEL_STATE_KEY]: true }, \"\", window.location.href);\n sentinelActiveRef.current = true;\n }\n}\n\n/** History-state key marking the guard's same-URL sentinel entry. */\nconst SENTINEL_STATE_KEY = \"__vexNavigationSentinel\";\n\nfunction isSentinelEntry(): boolean {\n const state: unknown = window.history.state;\n return typeof state === \"object\" && state !== null && SENTINEL_STATE_KEY in state;\n}\n","\"use client\";\n\nimport { useRouter } from \"next/navigation\";\n\n/**\n * Next.js App Router implementation of `FrameworkComponents[\"useNavigate\"]`\n * (`packages/react/src/config/useFrameworkComponents.ts`, Step 7).\n *\n * Returns the App Router's own `router.push`, unwrapped — guarding is not\n * this hook's job. `@vexcms/react`'s `useVexNavigate`\n * (`packages/react/src/config/useVexNavigate.ts`) is the only caller: it\n * consults `useUnsavedChangesGuard`'s blocking state itself before invoking\n * the function this hook returns, so every built-in post-mutation\n * navigation (after delete, after a duplicate's create resolves) is guarded\n * without this hook knowing anything about dirty state.\n *\n * Provide it from `NextAdminLayoutClient`:\n * `components={{ …, useNavigate: useNextNavigate }}`.\n *\n * @returns A function that navigates to `href` via the App Router.\n */\nexport function useNextNavigate(): (href: string) => void {\n const router = useRouter();\n return router.push;\n}\n"],"mappings":"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","names":["useRouter"]}
|
package/dist/NextAdminPage.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { VexConfig } from "@vexcms/core";
|
|
1
|
+
import type { VexApiAuth, VexConfig } from "@vexcms/core";
|
|
2
2
|
/**
|
|
3
3
|
* VexCMS admin page server component for Next.js.
|
|
4
4
|
*
|
|
@@ -15,25 +15,65 @@ import type { VexConfig } from "@vexcms/core";
|
|
|
15
15
|
* | `[collectionSlug, "new"]` | `CollectionEditView` (empty form) |
|
|
16
16
|
* | `[collectionSlug, documentId]` | `CollectionEditView` with preloaded doc |
|
|
17
17
|
*
|
|
18
|
+
* Takes `tokenPromise`/`authPromise`, not resolved values (perf fix, developer-binding): the
|
|
19
|
+
* caller (`page.tsx`) starts both without awaiting them immediately, so `NextAdminPage`'s own
|
|
20
|
+
* data fetches can begin as soon as `tokenPromise` resolves (what every `fetchQuery` call
|
|
21
|
+
* actually needs) rather than waiting on the slower `authPromise` (which itself depends on
|
|
22
|
+
* the token, plus one more Convex round trip). The caller's own page-level access gate
|
|
23
|
+
* (`canAccessAdminPanel`) should `await authPromise` and redirect BEFORE rendering
|
|
24
|
+
* `NextAdminPage` at all (Polish Decision 8, developer-reported — a generic page-level
|
|
25
|
+
* skeleton rendered while the gate was still in flight made the loading state worse than no
|
|
26
|
+
* gate-level fallback at all: shifts, row-selection spinners showing where real checkboxes
|
|
27
|
+
* belong, skeletons with no matching settled element, content popping in unannounced).
|
|
28
|
+
* `NextAdminPage` streams its own real chrome (title/tabs/toolbar/controls) immediately once
|
|
29
|
+
* rendered and wraps only its OWN data-dependent regions (e.g. table rows) in their own
|
|
30
|
+
* `<Suspense>` boundaries — no page-level fallback needed or wanted.
|
|
31
|
+
*
|
|
18
32
|
* @param props - Component props
|
|
19
33
|
* @param props.config - The resolved server `VexConfig`, typically the default export of
|
|
20
34
|
* `vex.config.server.ts` — not the client-safe `vex.config.ts`.
|
|
21
35
|
* @param props.params - Next.js 15 async params `{ path?: string[] }`
|
|
22
|
-
* @param props.
|
|
36
|
+
* @param props.tokenPromise - The in-flight (or already-resolved) session token (e.g.
|
|
37
|
+
* `getToken()`), forwarded to every `fetchQuery` call for authenticated reads. Optional —
|
|
38
|
+
* a project with no auth at all omits it, every `fetchQuery` call runs unauthenticated.
|
|
39
|
+
* @param props.authPromise - The in-flight (or already-resolved — e.g. `Promise.resolve(auth)`
|
|
40
|
+
* after the caller's own gate already awaited it) server-resolved caller (Decision 44),
|
|
41
|
+
* OPTIONAL. Wraps the entire return in `VexAuthProvider` and narrows the collection list
|
|
42
|
+
* route's search field by read permission. Omit it when `config.access` is absent or
|
|
43
|
+
* disabled — nothing is restricted either way. An `access`-enabled project that omits it is
|
|
44
|
+
* treated as an anonymous caller (fail-closed) with a one-time dev console warning.
|
|
23
45
|
* @returns The appropriate admin view for the current URL path.
|
|
24
46
|
*
|
|
25
47
|
* @example
|
|
26
48
|
* ```tsx
|
|
27
49
|
* // app/admin/[[...slug]]/page.tsx
|
|
50
|
+
* import { canAccessAdminPanel } from "@vexcms/core";
|
|
28
51
|
* import { NextAdminPage } from "@vexcms/next/server";
|
|
52
|
+
* import { redirect } from "next/navigation";
|
|
53
|
+
* import { getToken, getUserOrg } from "~/auth/server";
|
|
29
54
|
* import config from "../../../../vex.config.server";
|
|
30
55
|
*
|
|
31
|
-
* export default function AdminPage({
|
|
56
|
+
* export default async function AdminPage({
|
|
32
57
|
* params,
|
|
33
58
|
* }: {
|
|
34
59
|
* params: Promise<{ path?: string[] }>;
|
|
35
60
|
* }) {
|
|
36
|
-
*
|
|
61
|
+
* const token = await getToken();
|
|
62
|
+
* if (!token) redirect("/auth/sign-in?redirectTo=/admin");
|
|
63
|
+
* const auth = await getUserOrg();
|
|
64
|
+
* if (
|
|
65
|
+
* !canAccessAdminPanel({ access: config.access, user: auth.user, organization: auth.organization })
|
|
66
|
+
* ) {
|
|
67
|
+
* redirect("/unauthorized");
|
|
68
|
+
* }
|
|
69
|
+
* return (
|
|
70
|
+
* <NextAdminPage
|
|
71
|
+
* config={config}
|
|
72
|
+
* params={params}
|
|
73
|
+
* tokenPromise={Promise.resolve(token)}
|
|
74
|
+
* authPromise={Promise.resolve(auth)}
|
|
75
|
+
* />
|
|
76
|
+
* );
|
|
37
77
|
* }
|
|
38
78
|
* ```
|
|
39
79
|
*/
|
|
@@ -42,6 +82,13 @@ export declare function NextAdminPage(props: {
|
|
|
42
82
|
params: Promise<{
|
|
43
83
|
path?: string[];
|
|
44
84
|
}>;
|
|
45
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Next.js's own `searchParams` prop, forwarded by the app route (`AdminPage`). Only the
|
|
87
|
+
* collection list route reads it (`parseListViewUrlState`, Design Decision 40) — every other
|
|
88
|
+
* route ignores it.
|
|
89
|
+
*/
|
|
90
|
+
searchParams?: Promise<Record<string, string | string[] | undefined>>;
|
|
91
|
+
tokenPromise?: Promise<string | undefined>;
|
|
92
|
+
authPromise?: Promise<VexApiAuth>;
|
|
46
93
|
}): Promise<import("react").JSX.Element>;
|
|
47
94
|
//# sourceMappingURL=NextAdminPage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NextAdminPage.d.ts","sourceRoot":"","sources":["../src/NextAdminPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"NextAdminPage.d.ts","sourceRoot":"","sources":["../src/NextAdminPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,SAAS,EAIV,MAAM,cAAc,CAAC;AA4KtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6EG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE;IACzC,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACrC;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC;IACtE,YAAY,CAAC,EAAE,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC3C,WAAW,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;CACnC,wCAonBA"}
|
package/dist/NextAdminPage.js
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { FrameworkAuthAdapter } from "@vexcms/react";
|
|
3
|
+
/**
|
|
4
|
+
* Carries the app's sign-in/out actions (`@vexcms/react`'s `FrameworkComponents.auth`,
|
|
5
|
+
* Next.js adapter) from wherever the app mounts {@link VexAdminAuthAdapterProvider} down
|
|
6
|
+
* to `NextAdminLayoutClient`, which forwards it into `AdminLayout`'s `components` prop.
|
|
7
|
+
*
|
|
8
|
+
* A context, not a prop threaded through `NextAdminLayout`/your admin `layout.tsx`,
|
|
9
|
+
* because `layout.tsx` and `NextAdminLayout` are BOTH server components — the sign-in
|
|
10
|
+
* href builder and `signOut` function are app-specific client values (closures over your
|
|
11
|
+
* own `~/auth/client` module) that cannot cross a Server→Client Component boundary as
|
|
12
|
+
* props. Mounting this provider anywhere above `NextAdminLayout` in your own CLIENT tree
|
|
13
|
+
* (e.g. alongside `VexConfigProvider`/your Better Auth client provider) sidesteps that
|
|
14
|
+
* boundary entirely — both ends are client components.
|
|
15
|
+
*
|
|
16
|
+
* `undefined` default (not an empty object) distinguishes "no provider mounted" from "a
|
|
17
|
+
* provider mounted with no adapter" — `AdminTopNav`'s own control renders nothing either
|
|
18
|
+
* way, but the distinction matters for anyone debugging why it's missing.
|
|
19
|
+
*/
|
|
20
|
+
export declare const VexAdminAuthAdapterContext: import("react").Context<FrameworkAuthAdapter | undefined>;
|
|
21
|
+
/**
|
|
22
|
+
* Mounts the sign-in/out adapter for the admin panel's top-bar control.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* // ~/components/providers/client.tsx ("use client")
|
|
27
|
+
* import { VexAdminAuthAdapterProvider } from "@vexcms/next/auth";
|
|
28
|
+
* import { useRouter } from "next/navigation";
|
|
29
|
+
* import { signOut } from "~/auth/client";
|
|
30
|
+
*
|
|
31
|
+
* function AuthAdapterProvider({ children }: { children: React.ReactNode }) {
|
|
32
|
+
* const router = useRouter();
|
|
33
|
+
* return (
|
|
34
|
+
* <VexAdminAuthAdapterProvider
|
|
35
|
+
* value={{
|
|
36
|
+
* signInHref: (redirectTo) => `/auth/sign-in?redirectTo=${encodeURIComponent(redirectTo)}`,
|
|
37
|
+
* signOut: () =>
|
|
38
|
+
* signOut()
|
|
39
|
+
* .then(() => router.refresh())
|
|
40
|
+
* .catch(() => router.refresh()), // a stale client cache after a failed
|
|
41
|
+
* // sign-out is worse than a redundant refresh — refresh either way.
|
|
42
|
+
* }}
|
|
43
|
+
* >
|
|
44
|
+
* {children}
|
|
45
|
+
* </VexAdminAuthAdapterProvider>
|
|
46
|
+
* );
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @param props.value - The sign-in href builder and `signOut` function.
|
|
51
|
+
* @param props.children - The subtree that should see this adapter — must contain
|
|
52
|
+
* `NextAdminLayout` for `AdminTopNav`'s control to pick it up.
|
|
53
|
+
* @returns The context provider wrapping `props.children`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function VexAdminAuthAdapterProvider(props: {
|
|
56
|
+
value: FrameworkAuthAdapter;
|
|
57
|
+
children: ReactNode;
|
|
58
|
+
}): import("react").JSX.Element;
|
|
59
|
+
/**
|
|
60
|
+
* Reads the sign-in/out adapter mounted by {@link VexAdminAuthAdapterProvider}, if any.
|
|
61
|
+
* Consumed internally by `NextAdminLayoutClient` — not typically called from app code.
|
|
62
|
+
*
|
|
63
|
+
* @returns The adapter, or `undefined` when no provider is mounted above this point.
|
|
64
|
+
*/
|
|
65
|
+
export declare function useVexAdminAuthAdapter(): FrameworkAuthAdapter | undefined;
|
|
66
|
+
//# sourceMappingURL=VexAdminAuthAdapterContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VexAdminAuthAdapterContext.d.ts","sourceRoot":"","sources":["../../src/auth/VexAdminAuthAdapterContext.tsx"],"names":[],"mappings":"AAKA,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAClE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAE1D;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,0BAA0B,2DAEtC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE;IACjD,KAAK,EAAE,oBAAoB,CAAC;IAC5B,QAAQ,EAAE,SAAS,CAAC;CACrB,+BAMA;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,IAAI,oBAAoB,GAAG,SAAS,CAEzE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,2BAA2B,EAC3B,sBAAsB,GACvB,MAAM,8BAA8B,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
VexAdminAuthAdapterContext,
|
|
4
|
+
VexAdminAuthAdapterProvider,
|
|
5
|
+
useVexAdminAuthAdapter
|
|
6
|
+
} from "../chunk-T7DNJLSX.js";
|
|
7
|
+
export {
|
|
8
|
+
VexAdminAuthAdapterContext,
|
|
9
|
+
VexAdminAuthAdapterProvider,
|
|
10
|
+
useVexAdminAuthAdapter
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|