@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 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
@@ -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
+ }
@@ -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
+ }