@voyantjs/admin-app 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/root.d.ts +47 -0
- package/dist/root.d.ts.map +1 -0
- package/dist/root.js +55 -0
- package/dist/router.d.ts +30 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +51 -0
- package/dist/workspace.d.ts +79 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +81 -0
- package/package.json +79 -0
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# @voyantjs/admin-app
|
|
2
|
+
|
|
3
|
+
The Voyant admin application factory: the root document, router defaults,
|
|
4
|
+
auth-guarded workspace shell, and router-aware navigation — the composition
|
|
5
|
+
glue every Voyant admin previously copied from the template, delivered as a
|
|
6
|
+
versioned package.
|
|
7
|
+
|
|
8
|
+
Part of the Packaged Admin direction (`docs/architecture/packaged-admin-rfc.md`,
|
|
9
|
+
Phase 1). `@voyantjs/admin` stays the primitives package (providers, layout,
|
|
10
|
+
extension seam); this package owns the application-level composition on top.
|
|
11
|
+
|
|
12
|
+
## What it provides
|
|
13
|
+
|
|
14
|
+
- **`createAdminRouter({ routeTree })`** / **`createAdminQueryClient()`** —
|
|
15
|
+
TanStack Router + QueryClient with the Voyant defaults: intent preloading
|
|
16
|
+
with matched staleTime, scroll restoration, default not-found page, and
|
|
17
|
+
QueryClient SSR dehydrate/hydrate.
|
|
18
|
+
- **`adminRootHead({ title, ... })`** / **`AdminRootShell`** /
|
|
19
|
+
**`AdminRootErrorBoundary`** — the root route internals, including the
|
|
20
|
+
pre-hydration theme/locale bootstrap script (no theme flash) and an error
|
|
21
|
+
boundary that survives outside the provider tree.
|
|
22
|
+
- **`createAdminWorkspaceBeforeLoad({ getCurrentUser })`** — the auth guard,
|
|
23
|
+
in `beforeLoad` so the redirect short-circuits child loaders instead of
|
|
24
|
+
racing them.
|
|
25
|
+
- **`AdminWorkspaceShell`** — bootstrap gate → per-user message overrides →
|
|
26
|
+
locale sync → workspace layout, with `AdminRouterLink` (Slot-compatible,
|
|
27
|
+
external-URL-aware) as the default nav link. Pass `destinations` (a
|
|
28
|
+
`satisfies AdminDestinationResolvers` map) to mount the semantic-destination
|
|
29
|
+
contract: packaged pages resolve `AdminDestinations` keys to hrefs via
|
|
30
|
+
`useAdminHref`/`useAdminNavigate`, and the shell routes them through the app
|
|
31
|
+
router.
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// src/router.tsx
|
|
37
|
+
export const getRouter = () => createAdminRouter({ routeTree })
|
|
38
|
+
|
|
39
|
+
// src/routes/__root.tsx
|
|
40
|
+
export const Route = createRootRouteWithContext<AdminRouterContext>()({
|
|
41
|
+
head: () => adminRootHead({ title: "Acme Admin" }),
|
|
42
|
+
shellComponent: AdminRootShell,
|
|
43
|
+
component: RootComponent, // app-owned: mounts the app's provider stack
|
|
44
|
+
errorComponent: AdminRootErrorBoundary,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// src/routes/_workspace/route.tsx
|
|
48
|
+
export const Route = createFileRoute("/_workspace")({
|
|
49
|
+
ssr: "data-only",
|
|
50
|
+
beforeLoad: createAdminWorkspaceBeforeLoad({ getCurrentUser }),
|
|
51
|
+
loader: ({ context }) => ({ user: context.user }),
|
|
52
|
+
pendingComponent: AdminWorkspacePendingFallback,
|
|
53
|
+
component: () => (
|
|
54
|
+
<AdminWorkspaceShell
|
|
55
|
+
user={user}
|
|
56
|
+
icons={navigationIcons}
|
|
57
|
+
extensions={(messages) => createMyAdminExtensions(messages)}
|
|
58
|
+
destinations={myAdminDestinations} // key → href resolvers, satisfies AdminDestinationResolvers
|
|
59
|
+
onSignOut={() => signOut({ redirectTo: "/sign-in" })}
|
|
60
|
+
>
|
|
61
|
+
<Outlet />
|
|
62
|
+
</AdminWorkspaceShell>
|
|
63
|
+
),
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
What stays app-owned: the provider list (which domain modules are mounted),
|
|
68
|
+
extension definitions, navigation icons, branding, and the auth client.
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
Apache-2.0
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { AdminRootErrorBoundaryProps, AdminRootHeadOptions } from "./root.js";
|
|
2
|
+
export { AdminRootErrorBoundary, AdminRootShell, adminRootHead } from "./root.js";
|
|
3
|
+
export type { AdminRouterContext, CreateAdminRouterOptions } from "./router.js";
|
|
4
|
+
export { AdminNotFound, createAdminQueryClient, createAdminRouter } from "./router.js";
|
|
5
|
+
export type { AdminWorkspaceShellProps, AdminWorkspaceShellUser, CreateAdminWorkspaceBeforeLoadOptions, } from "./workspace.js";
|
|
6
|
+
export { AdminRouterLink, AdminWorkspacePendingFallback, AdminWorkspaceShell, createAdminWorkspaceBeforeLoad, defaultAdminWorkspaceUser, } from "./workspace.js";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,2BAA2B,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAClF,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AACjF,YAAY,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA;AAC/E,OAAO,EAAE,aAAa,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACtF,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,qCAAqC,GACtC,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACL,eAAe,EACf,6BAA6B,EAC7B,mBAAmB,EACnB,8BAA8B,EAC9B,yBAAyB,GAC1B,MAAM,gBAAgB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { AdminRootErrorBoundary, AdminRootShell, adminRootHead } from "./root.js";
|
|
2
|
+
export { AdminNotFound, createAdminQueryClient, createAdminRouter } from "./router.js";
|
|
3
|
+
export { AdminRouterLink, AdminWorkspacePendingFallback, AdminWorkspaceShell, createAdminWorkspaceBeforeLoad, defaultAdminWorkspaceUser, } from "./workspace.js";
|
package/dist/root.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface AdminRootHeadOptions {
|
|
3
|
+
/** Document/OG title. */
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
faviconHref?: string;
|
|
7
|
+
themeColor?: string;
|
|
8
|
+
/** Extra meta tags appended after the defaults. */
|
|
9
|
+
meta?: Array<Record<string, string>>;
|
|
10
|
+
/** Extra link tags appended after the favicon. */
|
|
11
|
+
links?: Array<Record<string, string>>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The root route `head()` payload for a Voyant admin app: charset/viewport,
|
|
15
|
+
* robots noindex, OG basics, favicon, and the pre-hydration theme/locale
|
|
16
|
+
* bootstrap script.
|
|
17
|
+
*/
|
|
18
|
+
export declare function adminRootHead(options: AdminRootHeadOptions): {
|
|
19
|
+
meta: Record<string, string>[];
|
|
20
|
+
links: Record<string, string>[];
|
|
21
|
+
scripts: {
|
|
22
|
+
children: string;
|
|
23
|
+
}[];
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The SSR'd document shell (`shellComponent` on the root route): bare
|
|
27
|
+
* html/head/body with head content and router scripts. `suppressHydrationWarning`
|
|
28
|
+
* because the bootstrap script mutates `documentElement` before hydration.
|
|
29
|
+
*/
|
|
30
|
+
export declare function AdminRootShell({ children }: {
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
33
|
+
export interface AdminRootErrorBoundaryProps {
|
|
34
|
+
error: unknown;
|
|
35
|
+
reset: () => void;
|
|
36
|
+
/** Fallback copy when the error has no usable message. */
|
|
37
|
+
fallbackMessage?: string;
|
|
38
|
+
homeHref?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Root error boundary. TanStack Router's `errorComponent` replaces the root
|
|
42
|
+
* component entirely, so the app's provider tree (ThemeProvider etc.) isn't
|
|
43
|
+
* above us — mount a local ThemeProvider so <Toaster />'s useTheme() call
|
|
44
|
+
* doesn't crash the boundary.
|
|
45
|
+
*/
|
|
46
|
+
export declare function AdminRootErrorBoundary({ error, reset, fallbackMessage, homeHref, }: AdminRootErrorBoundaryProps): import("react/jsx-runtime").JSX.Element;
|
|
47
|
+
//# sourceMappingURL=root.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root.d.ts","sourceRoot":"","sources":["../src/root.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,MAAM,WAAW,oBAAoB;IACnC,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mDAAmD;IACnD,IAAI,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACpC,kDAAkD;IAClD,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACtC;AAUD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB;;;;;;EAkB1D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,2CAYnE;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,OAAO,CAAA;IACd,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,0DAA0D;IAC1D,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,KAAK,EACL,KAAK,EACL,eAAiE,EACjE,QAAc,GACf,EAAE,2BAA2B,2CA8B7B"}
|
package/dist/root.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { HeadContent, Scripts } from "@tanstack/react-router";
|
|
3
|
+
import { ThemeProvider } from "@voyantjs/admin";
|
|
4
|
+
import { Button, Toaster } from "@voyantjs/ui/components";
|
|
5
|
+
import { Alert, AlertDescription, AlertTitle } from "@voyantjs/ui/components/alert";
|
|
6
|
+
import { Empty, EmptyContent, EmptyHeader, EmptyMedia, EmptyTitle, } from "@voyantjs/ui/components/empty";
|
|
7
|
+
import { RefreshCcw } from "lucide-react";
|
|
8
|
+
/**
|
|
9
|
+
* Inline theme + language detection, run before hydration so the first paint
|
|
10
|
+
* doesn't flash the wrong theme or language. Load-bearing: keep in sync with
|
|
11
|
+
* the ThemeProvider storage key (`theme`) and locale storage key
|
|
12
|
+
* (`admin-locale`).
|
|
13
|
+
*/
|
|
14
|
+
const ADMIN_BOOTSTRAP_SCRIPT = `(function(){var t=localStorage.getItem("theme");if(t==="dark"||(!t||t==="system")&&matchMedia("(prefers-color-scheme:dark)").matches){document.documentElement.classList.add("dark")}var l=localStorage.getItem("admin-locale")||(navigator.language||"en");l=l.toLowerCase().split("-")[0];document.documentElement.lang=l==="ro"?"ro":"en"})()`;
|
|
15
|
+
/**
|
|
16
|
+
* The root route `head()` payload for a Voyant admin app: charset/viewport,
|
|
17
|
+
* robots noindex, OG basics, favicon, and the pre-hydration theme/locale
|
|
18
|
+
* bootstrap script.
|
|
19
|
+
*/
|
|
20
|
+
export function adminRootHead(options) {
|
|
21
|
+
const { title, description, faviconHref = "/fav128.png", themeColor = "#ffffff" } = options;
|
|
22
|
+
return {
|
|
23
|
+
meta: [
|
|
24
|
+
{ charSet: "utf-8" },
|
|
25
|
+
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
|
26
|
+
{ name: "robots", content: "noindex,nofollow" },
|
|
27
|
+
...(description ? [{ name: "description", content: description }] : []),
|
|
28
|
+
{ name: "theme-color", content: themeColor },
|
|
29
|
+
{ property: "og:title", content: title },
|
|
30
|
+
{ property: "og:type", content: "website" },
|
|
31
|
+
{ title },
|
|
32
|
+
...(options.meta ?? []),
|
|
33
|
+
],
|
|
34
|
+
links: [{ rel: "icon", type: "image/png", href: faviconHref }, ...(options.links ?? [])],
|
|
35
|
+
scripts: [{ children: ADMIN_BOOTSTRAP_SCRIPT }],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The SSR'd document shell (`shellComponent` on the root route): bare
|
|
40
|
+
* html/head/body with head content and router scripts. `suppressHydrationWarning`
|
|
41
|
+
* because the bootstrap script mutates `documentElement` before hydration.
|
|
42
|
+
*/
|
|
43
|
+
export function AdminRootShell({ children }) {
|
|
44
|
+
return (_jsxs("html", { lang: "en", suppressHydrationWarning: true, children: [_jsx("head", { children: _jsx(HeadContent, {}) }), _jsxs("body", { className: "min-h-screen bg-background font-sans antialiased", suppressHydrationWarning: true, children: [children, _jsx(Scripts, {})] })] }));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Root error boundary. TanStack Router's `errorComponent` replaces the root
|
|
48
|
+
* component entirely, so the app's provider tree (ThemeProvider etc.) isn't
|
|
49
|
+
* above us — mount a local ThemeProvider so <Toaster />'s useTheme() call
|
|
50
|
+
* doesn't crash the boundary.
|
|
51
|
+
*/
|
|
52
|
+
export function AdminRootErrorBoundary({ error, reset, fallbackMessage = "Something went wrong while loading this page.", homeHref = "/", }) {
|
|
53
|
+
const message = error instanceof Error && error.message ? error.message : fallbackMessage;
|
|
54
|
+
return (_jsx(ThemeProvider, { defaultTheme: "system", storageKey: "theme", children: _jsxs("div", { className: "flex min-h-screen items-center justify-center p-6", children: [_jsxs(Empty, { className: "max-w-xl border border-border bg-card p-8", children: [_jsxs(EmptyHeader, { children: [_jsx(EmptyMedia, { variant: "icon", children: _jsx(RefreshCcw, { className: "size-5" }) }), _jsx(EmptyTitle, { children: "Something went wrong" })] }), _jsxs(EmptyContent, { children: [_jsxs(Alert, { variant: "destructive", className: "text-left", children: [_jsx(AlertTitle, { children: "Request failed" }), _jsx(AlertDescription, { children: message })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Button, { onClick: () => reset(), children: "Try again" }), _jsx(Button, { variant: "outline", onClick: () => window.location.assign(homeHref), children: "Go to dashboard" })] })] })] }), _jsx(Toaster, {})] }) }));
|
|
55
|
+
}
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
2
|
+
import { type AnyRoute } from "@tanstack/react-router";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
export interface AdminRouterContext {
|
|
5
|
+
queryClient: QueryClient;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* QueryClient with the admin defaults: no focus refetch, one retry, and a
|
|
9
|
+
* 30s staleTime that keeps hover-preloaded data fresh long enough for the
|
|
10
|
+
* hover→click navigation to reuse it. Override per-query for hotter data.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createAdminQueryClient(): QueryClient;
|
|
13
|
+
export interface CreateAdminRouterOptions<TRouteTree extends AnyRoute> {
|
|
14
|
+
routeTree: TRouteTree;
|
|
15
|
+
queryClient?: QueryClient;
|
|
16
|
+
notFoundComponent?: () => ReactNode;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The admin router factory: TanStack Router wired with the Voyant defaults —
|
|
20
|
+
* intent preloading with a preload staleTime matching the QueryClient (without
|
|
21
|
+
* it, hover-prefetch considers data immediately stale and re-fires the loader
|
|
22
|
+
* on click), scroll restoration, a default not-found page, and QueryClient
|
|
23
|
+
* SSR dehydrate/hydrate so loader-prefetched queries survive the
|
|
24
|
+
* server→client boundary on routes that opt into SSR.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createAdminRouter<TRouteTree extends AnyRoute>({ routeTree, queryClient, notFoundComponent, }: CreateAdminRouterOptions<TRouteTree>): import("@tanstack/router-core").RouterCore<TRouteTree, "never", false, import("@tanstack/history").RouterHistory, {
|
|
27
|
+
queryClient: object;
|
|
28
|
+
}>;
|
|
29
|
+
export declare function AdminNotFound(): import("react/jsx-runtime").JSX.Element;
|
|
30
|
+
//# sourceMappingURL=router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA4C,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAC7F,OAAO,EAAE,KAAK,QAAQ,EAA8C,MAAM,wBAAwB,CAAA;AAWlG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,WAAW,CAAA;CACzB;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,WAAW,CAUpD;AAED,MAAM,WAAW,wBAAwB,CAAC,UAAU,SAAS,QAAQ;IACnE,SAAS,EAAE,UAAU,CAAA;IACrB,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,iBAAiB,CAAC,EAAE,MAAM,SAAS,CAAA;CACpC;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,SAAS,QAAQ,EAAE,EAC7D,SAAS,EACT,WAAsC,EACtC,iBAAiC,GAClC,EAAE,wBAAwB,CAAC,UAAU,CAAC;iBAYmC,MAAM;GAK/E;AAED,wBAAgB,aAAa,4CAqB5B"}
|
package/dist/router.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { dehydrate, hydrate, QueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { createRouter as createTanStackRouter, Link } from "@tanstack/react-router";
|
|
4
|
+
import { buttonVariants } from "@voyantjs/ui/components/button";
|
|
5
|
+
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from "@voyantjs/ui/components/empty";
|
|
6
|
+
import { SearchX } from "lucide-react";
|
|
7
|
+
/**
|
|
8
|
+
* QueryClient with the admin defaults: no focus refetch, one retry, and a
|
|
9
|
+
* 30s staleTime that keeps hover-preloaded data fresh long enough for the
|
|
10
|
+
* hover→click navigation to reuse it. Override per-query for hotter data.
|
|
11
|
+
*/
|
|
12
|
+
export function createAdminQueryClient() {
|
|
13
|
+
return new QueryClient({
|
|
14
|
+
defaultOptions: {
|
|
15
|
+
queries: {
|
|
16
|
+
refetchOnWindowFocus: false,
|
|
17
|
+
retry: 1,
|
|
18
|
+
staleTime: 30_000,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The admin router factory: TanStack Router wired with the Voyant defaults —
|
|
25
|
+
* intent preloading with a preload staleTime matching the QueryClient (without
|
|
26
|
+
* it, hover-prefetch considers data immediately stale and re-fires the loader
|
|
27
|
+
* on click), scroll restoration, a default not-found page, and QueryClient
|
|
28
|
+
* SSR dehydrate/hydrate so loader-prefetched queries survive the
|
|
29
|
+
* server→client boundary on routes that opt into SSR.
|
|
30
|
+
*/
|
|
31
|
+
export function createAdminRouter({ routeTree, queryClient = createAdminQueryClient(), notFoundComponent = AdminNotFound, }) {
|
|
32
|
+
return createTanStackRouter({
|
|
33
|
+
routeTree,
|
|
34
|
+
context: { queryClient },
|
|
35
|
+
scrollRestoration: true,
|
|
36
|
+
defaultPreload: "intent",
|
|
37
|
+
defaultPreloadStaleTime: 30_000,
|
|
38
|
+
defaultNotFoundComponent: notFoundComponent,
|
|
39
|
+
// Cast around Router's ValidateSerializableInput, which is stricter than
|
|
40
|
+
// DehydratedState's recursive `unknown` slots. Runtime payload is
|
|
41
|
+
// JSON-safe; the official @tanstack/react-router-with-query helper does
|
|
42
|
+
// the same erasure.
|
|
43
|
+
dehydrate: () => ({ queryClient: dehydrate(queryClient) }),
|
|
44
|
+
hydrate: (state) => {
|
|
45
|
+
hydrate(queryClient, state.queryClient);
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export function AdminNotFound() {
|
|
50
|
+
return (_jsx("div", { className: "flex min-h-screen items-center justify-center bg-background p-6", children: _jsxs(Empty, { className: "max-w-xl border border-border bg-card", children: [_jsxs(EmptyHeader, { children: [_jsx(EmptyMedia, { variant: "icon", children: _jsx(SearchX, {}) }), _jsx(EmptyTitle, { children: "Page not found" }), _jsx(EmptyDescription, { children: "The page you requested does not exist or is no longer available." })] }), _jsx(EmptyContent, { children: _jsx(Link, { to: "/", className: buttonVariants({ variant: "default" }), children: "Go to dashboard" }) })] }) }));
|
|
51
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { type AdminDestinationResolvers, type AdminExtension, type AdminNavLinkComponent, type AdminNavLinkProps, type AdminUser, type OperatorAdminMessages, type OperatorAdminNavigationIcons } from "@voyantjs/admin";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Router-aware sidebar link. SidebarMenuButton with `asChild` wraps this in a
|
|
5
|
+
* Slot, which clones the element with merged className, data attributes, and
|
|
6
|
+
* event props — extras not declared on AdminNavLinkProps but arriving at runtime, so
|
|
7
|
+
* spread the rest. Without this, Slot's className is silently dropped and
|
|
8
|
+
* sidebar items render unstyled. External URLs fall back to a plain anchor.
|
|
9
|
+
*/
|
|
10
|
+
export declare const AdminRouterLink: import("react").ForwardRefExoticComponent<AdminNavLinkProps & import("react").RefAttributes<HTMLAnchorElement>>;
|
|
11
|
+
export interface CreateAdminWorkspaceBeforeLoadOptions<TUser> {
|
|
12
|
+
/** Resolve the current user (server fn / cookie-forwarding fetch). */
|
|
13
|
+
getCurrentUser: () => Promise<TUser | null | undefined>;
|
|
14
|
+
/** Where unauthenticated visitors are sent. Default `/sign-in`. */
|
|
15
|
+
signInPath?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The workspace auth guard. MUST run in `beforeLoad`, not `loader`:
|
|
19
|
+
* beforeLoad executes top-down for the whole matched chain BEFORE any loader
|
|
20
|
+
* fires, so an unauthenticated redirect short-circuits the subtree. In a
|
|
21
|
+
* loader it would race child loaders whose 401s surface the root error
|
|
22
|
+
* boundary and beat the redirect, dead-ending logged-out users. Returns
|
|
23
|
+
* `{ user }`, which TanStack merges into route context.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createAdminWorkspaceBeforeLoad<TUser>({ getCurrentUser, signInPath, }: CreateAdminWorkspaceBeforeLoadOptions<TUser>): ({ location }: {
|
|
26
|
+
location: {
|
|
27
|
+
href: string;
|
|
28
|
+
};
|
|
29
|
+
}) => Promise<{
|
|
30
|
+
user: TUser;
|
|
31
|
+
}>;
|
|
32
|
+
export declare function AdminWorkspacePendingFallback({ label }: {
|
|
33
|
+
label?: string;
|
|
34
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
35
|
+
/** Structural slice of the loaded user the shell itself needs. */
|
|
36
|
+
export interface AdminWorkspaceShellUser {
|
|
37
|
+
firstName?: string | null;
|
|
38
|
+
lastName?: string | null;
|
|
39
|
+
email?: string | null;
|
|
40
|
+
profilePictureUrl?: string | null;
|
|
41
|
+
locale?: string | null;
|
|
42
|
+
timeZone?: string | null;
|
|
43
|
+
timezone?: string | null;
|
|
44
|
+
uiPrefs?: unknown;
|
|
45
|
+
}
|
|
46
|
+
/** Default mapping from the loaded user to the layout's AdminUser shape. */
|
|
47
|
+
export declare function defaultAdminWorkspaceUser(user: AdminWorkspaceShellUser): AdminUser;
|
|
48
|
+
export interface AdminWorkspaceShellProps<TUser extends AdminWorkspaceShellUser> {
|
|
49
|
+
user: TUser | null | undefined;
|
|
50
|
+
isUserLoading?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Admin extensions for the navigation/widget seam. Pass a function to
|
|
53
|
+
* derive nav labels from the resolved admin messages.
|
|
54
|
+
*/
|
|
55
|
+
extensions?: ReadonlyArray<AdminExtension> | ((messages: OperatorAdminMessages) => ReadonlyArray<AdminExtension>);
|
|
56
|
+
icons?: OperatorAdminNavigationIcons;
|
|
57
|
+
/** Defaults to the router-aware {@link AdminRouterLink}. */
|
|
58
|
+
linkComponent?: AdminNavLinkComponent;
|
|
59
|
+
/**
|
|
60
|
+
* Host resolver map for the semantic-destination contract (packaged-admin
|
|
61
|
+
* RFC §4.7): one `params → href` resolver per `AdminDestinations` key the
|
|
62
|
+
* mounted packages declare. When provided, the shell mounts an
|
|
63
|
+
* `AdminNavigationProvider` wired to the app router, so packaged pages can
|
|
64
|
+
* navigate to routes they don't own via `useAdminHref`/`useAdminNavigate`.
|
|
65
|
+
*/
|
|
66
|
+
destinations?: AdminDestinationResolvers;
|
|
67
|
+
onSignOut?: () => void | Promise<void>;
|
|
68
|
+
/** Maps the loaded user for the layout; default covers the common fields. */
|
|
69
|
+
mapUser?: (user: TUser) => AdminUser;
|
|
70
|
+
children: ReactNode;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The authenticated workspace shell: bootstrap gate (current-user readiness
|
|
74
|
+
* is the only shell dependency), per-user message overrides, locale
|
|
75
|
+
* preference sync, and the workspace layout with router-aware links — the
|
|
76
|
+
* composition every Voyant admin previously copied from the template.
|
|
77
|
+
*/
|
|
78
|
+
export declare function AdminWorkspaceShell<TUser extends AdminWorkspaceShellUser>({ user, isUserLoading, extensions, icons, linkComponent, destinations, onSignOut, mapUser, children, }: AdminWorkspaceShellProps<TUser>): import("react/jsx-runtime").JSX.Element;
|
|
79
|
+
//# sourceMappingURL=workspace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.tsx"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,yBAAyB,EAC9B,KAAK,cAAc,EAGnB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,SAAS,EAGd,KAAK,qBAAqB,EAE1B,KAAK,4BAA4B,EAGlC,MAAM,iBAAiB,CAAA;AAExB,OAAO,EAAc,KAAK,SAAS,EAAwB,MAAM,OAAO,CAAA;AAExE;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,iHAyB3B,CAAA;AAED,MAAM,WAAW,qCAAqC,CAAC,KAAK;IAC1D,sEAAsE;IACtE,cAAc,EAAE,MAAM,OAAO,CAAC,KAAK,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;IACvD,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,EACpD,cAAc,EACd,UAAuB,GACxB,EAAE,qCAAqC,CAAC,KAAK,CAAC,IAC/B,cAAc;IAAE,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,KAAG,OAAO,CAAC;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC,CAYtF;AAED,wBAAgB,6BAA6B,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,2CAS1E;AAED,kEAAkE;AAClE,MAAM,WAAW,uBAAuB;IACtC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,4EAA4E;AAC5E,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,uBAAuB,GAAG,SAAS,CAUlF;AAED,MAAM,WAAW,wBAAwB,CAAC,KAAK,SAAS,uBAAuB;IAC7E,IAAI,EAAE,KAAK,GAAG,IAAI,GAAG,SAAS,CAAA;IAC9B,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB;;;OAGG;IACH,UAAU,CAAC,EACP,aAAa,CAAC,cAAc,CAAC,GAC7B,CAAC,CAAC,QAAQ,EAAE,qBAAqB,KAAK,aAAa,CAAC,cAAc,CAAC,CAAC,CAAA;IACxE,KAAK,CAAC,EAAE,4BAA4B,CAAA;IACpC,4DAA4D;IAC5D,aAAa,CAAC,EAAE,qBAAqB,CAAA;IACrC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,yBAAyB,CAAA;IACxC,SAAS,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtC,6EAA6E;IAC7E,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,SAAS,CAAA;IACpC,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,SAAS,uBAAuB,EAAE,EACzE,IAAI,EACJ,aAAa,EACb,UAAU,EACV,KAAK,EACL,aAA+B,EAC/B,YAAY,EACZ,SAAS,EACT,OAAmC,EACnC,QAAQ,GACT,EAAE,wBAAwB,CAAC,KAAK,CAAC,2CA6BjC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Link, redirect, useRouter, useRouterState } from "@tanstack/react-router";
|
|
3
|
+
import { AdminLocalePreferenceSync, AdminNavigationProvider, getOperatorAdminMessageOverridesFromUiPrefs, OperatorAdminBootstrapGate, OperatorAdminMessagesProvider, OperatorAdminWorkspaceLayout, useOperatorAdminMessages, } from "@voyantjs/admin";
|
|
4
|
+
import { Loader2 } from "lucide-react";
|
|
5
|
+
import { forwardRef, useCallback, useMemo } from "react";
|
|
6
|
+
/**
|
|
7
|
+
* Router-aware sidebar link. SidebarMenuButton with `asChild` wraps this in a
|
|
8
|
+
* Slot, which clones the element with merged className, data attributes, and
|
|
9
|
+
* event props — extras not declared on AdminNavLinkProps but arriving at runtime, so
|
|
10
|
+
* spread the rest. Without this, Slot's className is silently dropped and
|
|
11
|
+
* sidebar items render unstyled. External URLs fall back to a plain anchor.
|
|
12
|
+
*/
|
|
13
|
+
export const AdminRouterLink = forwardRef(function AdminRouterLink({ children, href, onClick, target, ...rest }, ref) {
|
|
14
|
+
const external = href.startsWith("http://") || href.startsWith("https://");
|
|
15
|
+
if (external) {
|
|
16
|
+
return (_jsx("a", { ref: ref, href: href, target: target, rel: target === "_blank" ? "noopener noreferrer" : undefined, onClick: onClick, ...rest, children: children }));
|
|
17
|
+
}
|
|
18
|
+
return (_jsx(Link, { ref: ref, to: href, target: target, onClick: onClick, ...rest, children: children }));
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* The workspace auth guard. MUST run in `beforeLoad`, not `loader`:
|
|
22
|
+
* beforeLoad executes top-down for the whole matched chain BEFORE any loader
|
|
23
|
+
* fires, so an unauthenticated redirect short-circuits the subtree. In a
|
|
24
|
+
* loader it would race child loaders whose 401s surface the root error
|
|
25
|
+
* boundary and beat the redirect, dead-ending logged-out users. Returns
|
|
26
|
+
* `{ user }`, which TanStack merges into route context.
|
|
27
|
+
*/
|
|
28
|
+
export function createAdminWorkspaceBeforeLoad({ getCurrentUser, signInPath = "/sign-in", }) {
|
|
29
|
+
return async ({ location }) => {
|
|
30
|
+
const user = await getCurrentUser();
|
|
31
|
+
if (!user) {
|
|
32
|
+
throw redirect({
|
|
33
|
+
to: signInPath,
|
|
34
|
+
search: { next: location.href },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return { user };
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function AdminWorkspacePendingFallback({ label }) {
|
|
41
|
+
return (_jsx("div", { className: "flex min-h-screen items-center justify-center bg-background", children: _jsxs("div", { className: "flex flex-col items-center gap-4", children: [_jsx(Loader2, { className: "size-8 animate-spin text-muted-foreground" }), label ? _jsx("p", { className: "text-sm text-muted-foreground", children: label }) : null] }) }));
|
|
42
|
+
}
|
|
43
|
+
/** Default mapping from the loaded user to the layout's AdminUser shape. */
|
|
44
|
+
export function defaultAdminWorkspaceUser(user) {
|
|
45
|
+
return {
|
|
46
|
+
name: [user.firstName, user.lastName].filter(Boolean).join(" "),
|
|
47
|
+
firstName: user.firstName,
|
|
48
|
+
lastName: user.lastName,
|
|
49
|
+
email: user.email ?? "",
|
|
50
|
+
avatar: user.profilePictureUrl,
|
|
51
|
+
locale: user.locale,
|
|
52
|
+
timeZone: user.timeZone ?? user.timezone,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The authenticated workspace shell: bootstrap gate (current-user readiness
|
|
57
|
+
* is the only shell dependency), per-user message overrides, locale
|
|
58
|
+
* preference sync, and the workspace layout with router-aware links — the
|
|
59
|
+
* composition every Voyant admin previously copied from the template.
|
|
60
|
+
*/
|
|
61
|
+
export function AdminWorkspaceShell({ user, isUserLoading, extensions, icons, linkComponent = AdminRouterLink, destinations, onSignOut, mapUser = defaultAdminWorkspaceUser, children, }) {
|
|
62
|
+
const messages = useOperatorAdminMessages();
|
|
63
|
+
return (_jsx(OperatorAdminBootstrapGate, { user: user, isUserLoading: isUserLoading, loadingFallback: _jsx(AdminWorkspacePendingFallback, { label: messages.loading }), children: ({ user: loadedUser }) => (_jsxs(OperatorAdminMessagesProvider, { overrides: getOperatorAdminMessageOverridesFromUiPrefs(loadedUser.uiPrefs), children: [_jsx(AdminLocalePreferenceSync, { source: loadedUser }), _jsx(AdminWorkspaceShellInner, { user: loadedUser, extensions: extensions, icons: icons, linkComponent: linkComponent, destinations: destinations, onSignOut: onSignOut, mapUser: mapUser, children: children })] })) }));
|
|
64
|
+
}
|
|
65
|
+
function AdminWorkspaceShellInner({ user, extensions, icons, linkComponent, destinations, onSignOut, mapUser, children, }) {
|
|
66
|
+
const router = useRouter();
|
|
67
|
+
const currentPath = useRouterState({ select: (s) => s.location.pathname });
|
|
68
|
+
const messages = useOperatorAdminMessages();
|
|
69
|
+
const resolvedExtensions = useMemo(() => (typeof extensions === "function" ? extensions(messages) : extensions), [extensions, messages]);
|
|
70
|
+
// Resolver-built hrefs may carry a query string, so navigate by `href`
|
|
71
|
+
// (which parses it back into search params) rather than `to` (which would
|
|
72
|
+
// treat the whole string as a literal pathname).
|
|
73
|
+
const navigateToHref = useCallback((href) => {
|
|
74
|
+
void router.navigate({ href });
|
|
75
|
+
}, [router]);
|
|
76
|
+
const layout = (_jsx(OperatorAdminWorkspaceLayout, { currentPath: currentPath, extensions: resolvedExtensions, icons: icons, linkComponent: linkComponent, onSignOut: onSignOut, user: mapUser(user), children: children }));
|
|
77
|
+
if (!destinations) {
|
|
78
|
+
return layout;
|
|
79
|
+
}
|
|
80
|
+
return (_jsx(AdminNavigationProvider, { resolvers: destinations, navigate: navigateToHref, children: layout }));
|
|
81
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@voyantjs/admin-app",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./root": "./src/root.tsx",
|
|
10
|
+
"./router": "./src/router.tsx",
|
|
11
|
+
"./workspace": "./src/workspace.tsx"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc -p tsconfig.json",
|
|
15
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
16
|
+
"prepack": "pnpm run build",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"lint": "biome check src/ tests/",
|
|
19
|
+
"test": "vitest run --passWithNoTests"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@tanstack/react-query": "^5.0.0",
|
|
23
|
+
"@tanstack/react-router": "^1.0.0",
|
|
24
|
+
"@voyantjs/admin": "workspace:^",
|
|
25
|
+
"@voyantjs/ui": "workspace:^",
|
|
26
|
+
"lucide-react": "^0.475.0",
|
|
27
|
+
"react": "^19.0.0",
|
|
28
|
+
"react-dom": "^19.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@tanstack/react-query": "^5.100.11",
|
|
32
|
+
"@tanstack/react-router": "^1.170.4",
|
|
33
|
+
"@types/react": "^19.2.14",
|
|
34
|
+
"@types/react-dom": "^19.2.3",
|
|
35
|
+
"@voyantjs/admin": "workspace:^",
|
|
36
|
+
"@voyantjs/ui": "workspace:^",
|
|
37
|
+
"@voyantjs/voyant-typescript-config": "workspace:^",
|
|
38
|
+
"lucide-react": "^1.7.0",
|
|
39
|
+
"react": "^19.2.4",
|
|
40
|
+
"react-dom": "^19.2.4",
|
|
41
|
+
"typescript": "^6.0.2",
|
|
42
|
+
"vitest": "^4.1.2"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist"
|
|
46
|
+
],
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public",
|
|
49
|
+
"exports": {
|
|
50
|
+
".": {
|
|
51
|
+
"types": "./dist/index.d.ts",
|
|
52
|
+
"import": "./dist/index.js",
|
|
53
|
+
"default": "./dist/index.js"
|
|
54
|
+
},
|
|
55
|
+
"./root": {
|
|
56
|
+
"types": "./dist/root.d.ts",
|
|
57
|
+
"import": "./dist/root.js",
|
|
58
|
+
"default": "./dist/root.js"
|
|
59
|
+
},
|
|
60
|
+
"./router": {
|
|
61
|
+
"types": "./dist/router.d.ts",
|
|
62
|
+
"import": "./dist/router.js",
|
|
63
|
+
"default": "./dist/router.js"
|
|
64
|
+
},
|
|
65
|
+
"./workspace": {
|
|
66
|
+
"types": "./dist/workspace.d.ts",
|
|
67
|
+
"import": "./dist/workspace.js",
|
|
68
|
+
"default": "./dist/workspace.js"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"main": "./dist/index.js",
|
|
72
|
+
"types": "./dist/index.d.ts"
|
|
73
|
+
},
|
|
74
|
+
"repository": {
|
|
75
|
+
"type": "git",
|
|
76
|
+
"url": "https://github.com/voyantjs/voyant.git",
|
|
77
|
+
"directory": "packages/admin-app"
|
|
78
|
+
}
|
|
79
|
+
}
|