@voyant-travel/admin 0.111.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/LICENSE +201 -0
- package/README.md +285 -0
- package/dist/app/extension-routes.d.ts +99 -0
- package/dist/app/extension-routes.d.ts.map +1 -0
- package/dist/app/extension-routes.js +134 -0
- package/dist/app/index.d.ts +9 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +4 -0
- package/dist/app/root.d.ts +47 -0
- package/dist/app/root.d.ts.map +1 -0
- package/dist/app/root.js +55 -0
- package/dist/app/router.d.ts +30 -0
- package/dist/app/router.d.ts.map +1 -0
- package/dist/app/router.js +51 -0
- package/dist/app/workspace.d.ts +84 -0
- package/dist/app/workspace.d.ts.map +1 -0
- package/dist/app/workspace.js +87 -0
- package/dist/components/admin-breadcrumbs.d.ts +18 -0
- package/dist/components/admin-breadcrumbs.d.ts.map +1 -0
- package/dist/components/admin-breadcrumbs.js +84 -0
- package/dist/components/admin-nav-group.d.ts +11 -0
- package/dist/components/admin-nav-group.d.ts.map +1 -0
- package/dist/components/admin-nav-group.js +49 -0
- package/dist/components/admin-nav-link.d.ts +10 -0
- package/dist/components/admin-nav-link.d.ts.map +1 -0
- package/dist/components/admin-nav-link.js +5 -0
- package/dist/components/admin-page-head.d.ts +17 -0
- package/dist/components/admin-page-head.d.ts.map +1 -0
- package/dist/components/admin-page-head.js +107 -0
- package/dist/components/admin-widget-slot.d.ts +8 -0
- package/dist/components/admin-widget-slot.d.ts.map +1 -0
- package/dist/components/admin-widget-slot.js +19 -0
- package/dist/components/brand/voyant-mark.d.ts +3 -0
- package/dist/components/brand/voyant-mark.d.ts.map +1 -0
- package/dist/components/brand/voyant-mark.js +4 -0
- package/dist/components/brand/voyant-wordmark.d.ts +3 -0
- package/dist/components/brand/voyant-wordmark.d.ts.map +1 -0
- package/dist/components/brand/voyant-wordmark.js +4 -0
- package/dist/components/operator-admin-bootstrap-gate.d.ts +26 -0
- package/dist/components/operator-admin-bootstrap-gate.d.ts.map +1 -0
- package/dist/components/operator-admin-bootstrap-gate.js +22 -0
- package/dist/components/operator-admin-page-shell.d.ts +13 -0
- package/dist/components/operator-admin-page-shell.d.ts.map +1 -0
- package/dist/components/operator-admin-page-shell.js +6 -0
- package/dist/components/operator-admin-sidebar.d.ts +57 -0
- package/dist/components/operator-admin-sidebar.d.ts.map +1 -0
- package/dist/components/operator-admin-sidebar.js +104 -0
- package/dist/components/operator-admin-user-menu.d.ts +10 -0
- package/dist/components/operator-admin-user-menu.d.ts.map +1 -0
- package/dist/components/operator-admin-user-menu.js +19 -0
- package/dist/components/team-settings-page.d.ts +10 -0
- package/dist/components/team-settings-page.d.ts.map +1 -0
- package/dist/components/team-settings-page.js +149 -0
- package/dist/dashboard/dashboard-empty-states.d.ts +67 -0
- package/dist/dashboard/dashboard-empty-states.d.ts.map +1 -0
- package/dist/dashboard/dashboard-empty-states.js +65 -0
- package/dist/dashboard/dashboard-kpi-card.d.ts +13 -0
- package/dist/dashboard/dashboard-kpi-card.d.ts.map +1 -0
- package/dist/dashboard/dashboard-kpi-card.js +12 -0
- package/dist/dashboard/dashboard-page.d.ts +7 -0
- package/dist/dashboard/dashboard-page.d.ts.map +1 -0
- package/dist/dashboard/dashboard-page.js +150 -0
- package/dist/dashboard/dashboard-query-options.d.ts +224 -0
- package/dist/dashboard/dashboard-query-options.d.ts.map +1 -0
- package/dist/dashboard/dashboard-query-options.js +153 -0
- package/dist/dashboard/dashboard-skeleton.d.ts +13 -0
- package/dist/dashboard/dashboard-skeleton.d.ts.map +1 -0
- package/dist/dashboard/dashboard-skeleton.js +28 -0
- package/dist/extensions.d.ts +254 -0
- package/dist/extensions.d.ts.map +1 -0
- package/dist/extensions.js +139 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/lib/i18n.d.ts +2 -0
- package/dist/lib/i18n.d.ts.map +1 -0
- package/dist/lib/i18n.js +1 -0
- package/dist/lib/initials.d.ts +24 -0
- package/dist/lib/initials.d.ts.map +1 -0
- package/dist/lib/initials.js +45 -0
- package/dist/navigation/destinations.d.ts +83 -0
- package/dist/navigation/destinations.d.ts.map +1 -0
- package/dist/navigation/destinations.js +65 -0
- package/dist/navigation/operator-navigation.d.ts +10 -0
- package/dist/navigation/operator-navigation.d.ts.map +1 -0
- package/dist/navigation/operator-navigation.js +191 -0
- package/dist/providers/admin-extensions.d.ts +9 -0
- package/dist/providers/admin-extensions.d.ts.map +1 -0
- package/dist/providers/admin-extensions.js +10 -0
- package/dist/providers/admin-provider.d.ts +53 -0
- package/dist/providers/admin-provider.d.ts.map +1 -0
- package/dist/providers/admin-provider.js +26 -0
- package/dist/providers/locale-preferences.d.ts +12 -0
- package/dist/providers/locale-preferences.d.ts.map +1 -0
- package/dist/providers/locale-preferences.js +32 -0
- package/dist/providers/locale.d.ts +23 -0
- package/dist/providers/locale.d.ts.map +1 -0
- package/dist/providers/locale.js +98 -0
- package/dist/providers/operator-admin-messages.d.ts +14 -0
- package/dist/providers/operator-admin-messages.d.ts.map +1 -0
- package/dist/providers/operator-admin-messages.js +16 -0
- package/dist/providers/operator-admin-shell.d.ts +35 -0
- package/dist/providers/operator-admin-shell.d.ts.map +1 -0
- package/dist/providers/operator-admin-shell.js +20 -0
- package/dist/providers/query-client.d.ts +19 -0
- package/dist/providers/query-client.d.ts.map +1 -0
- package/dist/providers/query-client.js +34 -0
- package/dist/providers/theme.d.ts +29 -0
- package/dist/providers/theme.d.ts.map +1 -0
- package/dist/providers/theme.js +63 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/package.json +222 -0
- package/src/styles.css +11 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createRoute, lazyRouteComponent, redirect, useNavigate, useParams, useSearch, } from "@tanstack/react-router";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { findAdminRouteContribution, requireImplementedAdminRoute, } from "../extensions.js";
|
|
4
|
+
function resolveRuntime(runtime) {
|
|
5
|
+
return typeof runtime === "function" ? runtime() : runtime;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Wrap a lazy `page` contribution into a route component that injects route
|
|
9
|
+
* state ({@link AdminRoutePageProps}) read from the matched route. The page
|
|
10
|
+
* chunk stays code-split (the router's lazy-component machinery), and the
|
|
11
|
+
* wrapper forwards `preload` so hover/intent preloading fetches the chunk
|
|
12
|
+
* ahead of navigation.
|
|
13
|
+
*/
|
|
14
|
+
function createAdminRoutePageComponent(route) {
|
|
15
|
+
const page = route.page;
|
|
16
|
+
if (!page) {
|
|
17
|
+
throw new Error(`[voyant-admin] Route contribution "${route.id}" has no \`page\` loader to bind.`);
|
|
18
|
+
}
|
|
19
|
+
const LazyPage = lazyRouteComponent(page);
|
|
20
|
+
function AdminExtensionRoutePage() {
|
|
21
|
+
const params = useParams({ strict: false });
|
|
22
|
+
const search = useSearch({ strict: false });
|
|
23
|
+
const navigate = useNavigate();
|
|
24
|
+
const updateSearch = React.useCallback((updater, options) => {
|
|
25
|
+
void navigate({
|
|
26
|
+
// Same-route navigation: patch search state in place.
|
|
27
|
+
to: ".",
|
|
28
|
+
search: (prev) => updater(prev),
|
|
29
|
+
replace: options?.replace ?? true,
|
|
30
|
+
});
|
|
31
|
+
}, [navigate]);
|
|
32
|
+
return React.createElement(LazyPage, {
|
|
33
|
+
params,
|
|
34
|
+
search,
|
|
35
|
+
updateSearch,
|
|
36
|
+
title: route.title,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
AdminExtensionRoutePage.displayName = `AdminExtensionRoutePage(${route.id})`;
|
|
40
|
+
AdminExtensionRoutePage.preload = LazyPage.preload;
|
|
41
|
+
return AdminExtensionRoutePage;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolve an extension route contribution by id and return the route
|
|
45
|
+
* options the host's generated admin route module spreads into a
|
|
46
|
+
* code-based `createRoute({...})`.
|
|
47
|
+
*
|
|
48
|
+
* Path and typed search contract stay literal in the generated module (they
|
|
49
|
+
* are what gives the host typed links); everything else — page, loader,
|
|
50
|
+
* SSR mode, boundaries — comes from the contribution.
|
|
51
|
+
*/
|
|
52
|
+
export function adminExtensionRouteOptions(extension, routeId, runtime) {
|
|
53
|
+
const route = requireImplementedAdminRoute(extension, routeId);
|
|
54
|
+
return adminRouteOptionsFromContribution(route, runtime);
|
|
55
|
+
}
|
|
56
|
+
function adminRouteOptionsFromContribution(route, runtime) {
|
|
57
|
+
const redirectTo = route.redirectTo;
|
|
58
|
+
const component = route.page
|
|
59
|
+
? createAdminRoutePageComponent(route)
|
|
60
|
+
: route.component;
|
|
61
|
+
return {
|
|
62
|
+
component,
|
|
63
|
+
beforeLoad: redirectTo
|
|
64
|
+
? () => {
|
|
65
|
+
throw redirect({ to: redirectTo, replace: true });
|
|
66
|
+
}
|
|
67
|
+
: undefined,
|
|
68
|
+
loader: ({ context, params }) => route.loader?.({
|
|
69
|
+
queryClient: context.queryClient,
|
|
70
|
+
runtime: resolveRuntime(runtime),
|
|
71
|
+
params,
|
|
72
|
+
}),
|
|
73
|
+
ssr: route.ssr,
|
|
74
|
+
wrapInSuspense: route.page ? true : undefined,
|
|
75
|
+
pendingComponent: route.pendingComponent,
|
|
76
|
+
errorComponent: route.errorComponent,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Build code-based child routes for a layout contribution's
|
|
81
|
+
* `children` that are NOT statically emitted by the host's generated
|
|
82
|
+
* module (packaged-admin RFC §4.8 + core-extension nesting).
|
|
83
|
+
*
|
|
84
|
+
* Static children keep literal paths in the generated module for typed
|
|
85
|
+
* links; dynamic children (app-supplied at factory time) bind here and are
|
|
86
|
+
* reachable via plain string navigation only.
|
|
87
|
+
*/
|
|
88
|
+
export function adminExtensionChildRoutes(extension, parentRouteId, getParentRoute, runtime, options = {}) {
|
|
89
|
+
const parent = findAdminRouteContribution(extension.routes, parentRouteId);
|
|
90
|
+
if (!parent) {
|
|
91
|
+
throw new Error(`[voyant-admin] Extension "${extension.id}" has no route contribution "${parentRouteId}".`);
|
|
92
|
+
}
|
|
93
|
+
const exclude = new Set(options.exclude ?? []);
|
|
94
|
+
return (parent.children ?? [])
|
|
95
|
+
.filter((child) => !exclude.has(child.path))
|
|
96
|
+
.map((child) => {
|
|
97
|
+
const options = {
|
|
98
|
+
getParentRoute,
|
|
99
|
+
path: child.path,
|
|
100
|
+
validateSearch: child.validateSearch,
|
|
101
|
+
...adminRouteOptionsFromContribution(requireChildImplementation(extension, child), runtime),
|
|
102
|
+
};
|
|
103
|
+
// Runtime-built routes carry no typed-link contract (they are invisible
|
|
104
|
+
// to the host's generated typed-link maps), so the loose cast is sound.
|
|
105
|
+
return createRoute(options);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function requireChildImplementation(extension, child) {
|
|
109
|
+
if (!child.page && !child.component && !child.redirectTo) {
|
|
110
|
+
throw new Error(`[voyant-admin] Child route contribution "${child.id}" of extension ` +
|
|
111
|
+
`"${extension.id}" carries no implementation (neither \`page\`, \`component\`, ` +
|
|
112
|
+
`nor \`redirectTo\`).`);
|
|
113
|
+
}
|
|
114
|
+
return child;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Graft code-built extension routes under a file-based parent route
|
|
118
|
+
* (typically the workspace layout) and return the tree for `_addFileTypes`
|
|
119
|
+
* re-typing. Idempotent: an extension route replaces any previously grafted
|
|
120
|
+
* route with the same path, so dev-server re-evaluation of the generated
|
|
121
|
+
* module never duplicates children.
|
|
122
|
+
*/
|
|
123
|
+
export function attachAdminExtensionRoutes(routeTree, parentRoute, extensionRoutes) {
|
|
124
|
+
const existing = Array.isArray(parentRoute.children)
|
|
125
|
+
? parentRoute.children
|
|
126
|
+
: [];
|
|
127
|
+
const extensionPaths = new Set(extensionRoutes.map((route) => route.options.path));
|
|
128
|
+
const children = [
|
|
129
|
+
...existing.filter((route) => !extensionPaths.has(route.options.path)),
|
|
130
|
+
...extensionRoutes,
|
|
131
|
+
];
|
|
132
|
+
parentRoute.addChildren(children);
|
|
133
|
+
return routeTree;
|
|
134
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type { AdminExtensionChildRoutesOptions, AdminExtensionRouteLoaderArgs, AdminExtensionRouteOptions, AdminExtensionRouteRuntime, } from "./extension-routes.js";
|
|
2
|
+
export { adminExtensionChildRoutes, adminExtensionRouteOptions, attachAdminExtensionRoutes, } from "./extension-routes.js";
|
|
3
|
+
export type { AdminRootErrorBoundaryProps, AdminRootHeadOptions } from "./root.js";
|
|
4
|
+
export { AdminRootErrorBoundary, AdminRootShell, adminRootHead } from "./root.js";
|
|
5
|
+
export type { AdminRouterContext, CreateAdminRouterOptions } from "./router.js";
|
|
6
|
+
export { AdminNotFound, createAdminQueryClient, createAdminRouter } from "./router.js";
|
|
7
|
+
export type { AdminWorkspaceShellProps, AdminWorkspaceShellUser, CreateAdminWorkspaceBeforeLoadOptions, } from "./workspace.js";
|
|
8
|
+
export { AdminRouterLink, AdminWorkspacePendingFallback, AdminWorkspaceShell, createAdminWorkspaceBeforeLoad, defaultAdminWorkspaceUser, } from "./workspace.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/app/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,gCAAgC,EAChC,6BAA6B,EAC7B,0BAA0B,EAC1B,0BAA0B,GAC3B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,yBAAyB,EACzB,0BAA0B,EAC1B,0BAA0B,GAC3B,MAAM,uBAAuB,CAAA;AAC9B,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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { adminExtensionChildRoutes, adminExtensionRouteOptions, attachAdminExtensionRoutes, } from "./extension-routes.js";
|
|
2
|
+
export { AdminRootErrorBoundary, AdminRootShell, adminRootHead } from "./root.js";
|
|
3
|
+
export { AdminNotFound, createAdminQueryClient, createAdminRouter } from "./router.js";
|
|
4
|
+
export { AdminRouterLink, AdminWorkspacePendingFallback, AdminWorkspaceShell, createAdminWorkspaceBeforeLoad, defaultAdminWorkspaceUser, } from "./workspace.js";
|
|
@@ -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/app/root.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,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/app/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 { Button, Toaster } from "@voyant-travel/ui/components";
|
|
4
|
+
import { Alert, AlertDescription, AlertTitle } from "@voyant-travel/ui/components/alert";
|
|
5
|
+
import { Empty, EmptyContent, EmptyHeader, EmptyMedia, EmptyTitle, } from "@voyant-travel/ui/components/empty";
|
|
6
|
+
import { RefreshCcw } from "lucide-react";
|
|
7
|
+
import { ThemeProvider } from "../providers/theme.js";
|
|
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/app/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;iBAYwB,MAAM;GAKpE;AAED,wBAAgB,aAAa,4CAqB5B"}
|
|
@@ -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 "@voyant-travel/ui/components/button";
|
|
5
|
+
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from "@voyant-travel/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,84 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { AdminNavLinkComponent, AdminNavLinkProps } from "../components/admin-nav-link.js";
|
|
3
|
+
import type { AdminExtension } from "../extensions.js";
|
|
4
|
+
import { type AdminDestinationResolvers } from "../navigation/destinations.js";
|
|
5
|
+
import type { OperatorAdminNavigationIcons } from "../navigation/operator-navigation.js";
|
|
6
|
+
import { type OperatorAdminMessages } from "../providers/operator-admin-messages.js";
|
|
7
|
+
import type { AdminUser } from "../types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Router-aware sidebar link. SidebarMenuButton with `asChild` wraps this in a
|
|
10
|
+
* Slot, which clones the element with merged className, data attributes, and
|
|
11
|
+
* event props — extras not declared on AdminNavLinkProps but arriving at runtime, so
|
|
12
|
+
* spread the rest. Without this, Slot's className is silently dropped and
|
|
13
|
+
* sidebar items render unstyled. External URLs fall back to a plain anchor.
|
|
14
|
+
*/
|
|
15
|
+
export declare const AdminRouterLink: import("react").ForwardRefExoticComponent<AdminNavLinkProps & import("react").RefAttributes<HTMLAnchorElement>>;
|
|
16
|
+
export interface CreateAdminWorkspaceBeforeLoadOptions<TUser> {
|
|
17
|
+
/** Resolve the current user (server fn / cookie-forwarding fetch). */
|
|
18
|
+
getCurrentUser: () => Promise<TUser | null | undefined>;
|
|
19
|
+
/** Where unauthenticated visitors are sent. Default `/sign-in`. */
|
|
20
|
+
signInPath?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The workspace auth guard. MUST run in `beforeLoad`, not `loader`:
|
|
24
|
+
* beforeLoad executes top-down for the whole matched chain BEFORE any loader
|
|
25
|
+
* fires, so an unauthenticated redirect short-circuits the subtree. In a
|
|
26
|
+
* loader it would race child loaders whose 401s surface the root error
|
|
27
|
+
* boundary and beat the redirect, dead-ending logged-out users. Returns
|
|
28
|
+
* `{ user }`, which TanStack merges into route context.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createAdminWorkspaceBeforeLoad<TUser>({ getCurrentUser, signInPath, }: CreateAdminWorkspaceBeforeLoadOptions<TUser>): ({ location }: {
|
|
31
|
+
location: {
|
|
32
|
+
href: string;
|
|
33
|
+
};
|
|
34
|
+
}) => Promise<{
|
|
35
|
+
user: TUser;
|
|
36
|
+
}>;
|
|
37
|
+
export declare function AdminWorkspacePendingFallback({ label }: {
|
|
38
|
+
label?: string;
|
|
39
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
40
|
+
/** Structural slice of the loaded user the shell itself needs. */
|
|
41
|
+
export interface AdminWorkspaceShellUser {
|
|
42
|
+
firstName?: string | null;
|
|
43
|
+
lastName?: string | null;
|
|
44
|
+
email?: string | null;
|
|
45
|
+
profilePictureUrl?: string | null;
|
|
46
|
+
locale?: string | null;
|
|
47
|
+
timeZone?: string | null;
|
|
48
|
+
timezone?: string | null;
|
|
49
|
+
uiPrefs?: unknown;
|
|
50
|
+
}
|
|
51
|
+
/** Default mapping from the loaded user to the layout's AdminUser shape. */
|
|
52
|
+
export declare function defaultAdminWorkspaceUser(user: AdminWorkspaceShellUser): AdminUser;
|
|
53
|
+
export interface AdminWorkspaceShellProps<TUser extends AdminWorkspaceShellUser> {
|
|
54
|
+
user: TUser | null | undefined;
|
|
55
|
+
isUserLoading?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Admin extensions for the navigation/widget seam. Pass a function to
|
|
58
|
+
* derive nav labels from the resolved admin messages.
|
|
59
|
+
*/
|
|
60
|
+
extensions?: ReadonlyArray<AdminExtension> | ((messages: OperatorAdminMessages) => ReadonlyArray<AdminExtension>);
|
|
61
|
+
icons?: OperatorAdminNavigationIcons;
|
|
62
|
+
/** Defaults to the router-aware {@link AdminRouterLink}. */
|
|
63
|
+
linkComponent?: AdminNavLinkComponent;
|
|
64
|
+
/**
|
|
65
|
+
* Host resolver map for the semantic-destination contract (packaged-admin
|
|
66
|
+
* RFC §4.7): one `params → href` resolver per `AdminDestinations` key the
|
|
67
|
+
* mounted packages declare. When provided, the shell mounts an
|
|
68
|
+
* `AdminNavigationProvider` wired to the app router, so packaged pages can
|
|
69
|
+
* navigate to routes they don't own via `useAdminHref`/`useAdminNavigate`.
|
|
70
|
+
*/
|
|
71
|
+
destinations?: AdminDestinationResolvers;
|
|
72
|
+
onSignOut?: () => void | Promise<void>;
|
|
73
|
+
/** Maps the loaded user for the layout; default covers the common fields. */
|
|
74
|
+
mapUser?: (user: TUser) => AdminUser;
|
|
75
|
+
children: ReactNode;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The authenticated workspace shell: bootstrap gate (current-user readiness
|
|
79
|
+
* is the only shell dependency), per-user message overrides, locale
|
|
80
|
+
* preference sync, and the workspace layout with router-aware links — the
|
|
81
|
+
* composition every Voyant admin previously copied from the starter.
|
|
82
|
+
*/
|
|
83
|
+
export declare function AdminWorkspaceShell<TUser extends AdminWorkspaceShellUser>({ user, isUserLoading, extensions, icons, linkComponent, destinations, onSignOut, mapUser, children, }: AdminWorkspaceShellProps<TUser>): import("react/jsx-runtime").JSX.Element;
|
|
84
|
+
//# sourceMappingURL=workspace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../src/app/workspace.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Link, redirect, useRouter, useRouterState } from "@tanstack/react-router";
|
|
3
|
+
import { Loader2 } from "lucide-react";
|
|
4
|
+
import { forwardRef, useCallback, useMemo } from "react";
|
|
5
|
+
import { OperatorAdminBootstrapGate } from "../components/operator-admin-bootstrap-gate.js";
|
|
6
|
+
import { OperatorAdminWorkspaceLayout } from "../components/operator-admin-sidebar.js";
|
|
7
|
+
import { AdminNavigationProvider, } from "../navigation/destinations.js";
|
|
8
|
+
import { AdminLocalePreferenceSync } from "../providers/locale-preferences.js";
|
|
9
|
+
import { getOperatorAdminMessageOverridesFromUiPrefs, OperatorAdminMessagesProvider, useOperatorAdminMessages, } from "../providers/operator-admin-messages.js";
|
|
10
|
+
/**
|
|
11
|
+
* Router-aware sidebar link. SidebarMenuButton with `asChild` wraps this in a
|
|
12
|
+
* Slot, which clones the element with merged className, data attributes, and
|
|
13
|
+
* event props — extras not declared on AdminNavLinkProps but arriving at runtime, so
|
|
14
|
+
* spread the rest. Without this, Slot's className is silently dropped and
|
|
15
|
+
* sidebar items render unstyled. External URLs fall back to a plain anchor.
|
|
16
|
+
*/
|
|
17
|
+
export const AdminRouterLink = forwardRef(function AdminRouterLink({ children, href, onClick, target, ...rest }, ref) {
|
|
18
|
+
const external = href.startsWith("http://") || href.startsWith("https://");
|
|
19
|
+
if (external) {
|
|
20
|
+
return (_jsx("a", { ref: ref, href: href, target: target, rel: target === "_blank" ? "noopener noreferrer" : undefined, onClick: onClick, ...rest, children: children }));
|
|
21
|
+
}
|
|
22
|
+
return (_jsx(Link, { ref: ref, to: href, target: target, onClick: onClick, ...rest, children: children }));
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* The workspace auth guard. MUST run in `beforeLoad`, not `loader`:
|
|
26
|
+
* beforeLoad executes top-down for the whole matched chain BEFORE any loader
|
|
27
|
+
* fires, so an unauthenticated redirect short-circuits the subtree. In a
|
|
28
|
+
* loader it would race child loaders whose 401s surface the root error
|
|
29
|
+
* boundary and beat the redirect, dead-ending logged-out users. Returns
|
|
30
|
+
* `{ user }`, which TanStack merges into route context.
|
|
31
|
+
*/
|
|
32
|
+
export function createAdminWorkspaceBeforeLoad({ getCurrentUser, signInPath = "/sign-in", }) {
|
|
33
|
+
return async ({ location }) => {
|
|
34
|
+
const user = await getCurrentUser();
|
|
35
|
+
if (!user) {
|
|
36
|
+
throw redirect({
|
|
37
|
+
to: signInPath,
|
|
38
|
+
search: { next: location.href },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return { user };
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function AdminWorkspacePendingFallback({ label }) {
|
|
45
|
+
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] }) }));
|
|
46
|
+
}
|
|
47
|
+
/** Default mapping from the loaded user to the layout's AdminUser shape. */
|
|
48
|
+
export function defaultAdminWorkspaceUser(user) {
|
|
49
|
+
return {
|
|
50
|
+
name: [user.firstName, user.lastName].filter(Boolean).join(" "),
|
|
51
|
+
firstName: user.firstName,
|
|
52
|
+
lastName: user.lastName,
|
|
53
|
+
email: user.email ?? "",
|
|
54
|
+
avatar: user.profilePictureUrl,
|
|
55
|
+
locale: user.locale,
|
|
56
|
+
timeZone: user.timeZone ?? user.timezone,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The authenticated workspace shell: bootstrap gate (current-user readiness
|
|
61
|
+
* is the only shell dependency), per-user message overrides, locale
|
|
62
|
+
* preference sync, and the workspace layout with router-aware links — the
|
|
63
|
+
* composition every Voyant admin previously copied from the starter.
|
|
64
|
+
*/
|
|
65
|
+
export function AdminWorkspaceShell({ user, isUserLoading, extensions, icons, linkComponent = AdminRouterLink, destinations, onSignOut, mapUser = defaultAdminWorkspaceUser, children, }) {
|
|
66
|
+
const messages = useOperatorAdminMessages();
|
|
67
|
+
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 })] })) }));
|
|
68
|
+
}
|
|
69
|
+
function AdminWorkspaceShellInner({ user, extensions, icons, linkComponent, destinations, onSignOut, mapUser, children, }) {
|
|
70
|
+
const router = useRouter();
|
|
71
|
+
const currentPath = useRouterState({ select: (s) => s.location.pathname });
|
|
72
|
+
const messages = useOperatorAdminMessages();
|
|
73
|
+
const resolvedExtensions = useMemo(() => (typeof extensions === "function" ? extensions(messages) : extensions), [extensions, messages]);
|
|
74
|
+
// Resolver-built hrefs may carry a query string, so navigate by `href`
|
|
75
|
+
// (which parses it back into search params) rather than `to` (which would
|
|
76
|
+
// treat the whole string as a literal pathname). `replace` forwards so
|
|
77
|
+
// packaged redirect pages (alias routes, deep-link forwards) keep
|
|
78
|
+
// route-redirect history semantics.
|
|
79
|
+
const navigateToHref = useCallback((href, options) => {
|
|
80
|
+
void router.navigate({ href, replace: options?.replace });
|
|
81
|
+
}, [router]);
|
|
82
|
+
const layout = (_jsx(OperatorAdminWorkspaceLayout, { currentPath: currentPath, extensions: resolvedExtensions, icons: icons, linkComponent: linkComponent, onSignOut: onSignOut, user: mapUser(user), children: children }));
|
|
83
|
+
if (!destinations) {
|
|
84
|
+
return layout;
|
|
85
|
+
}
|
|
86
|
+
return (_jsx(AdminNavigationProvider, { resolvers: destinations, navigate: navigateToHref, children: layout }));
|
|
87
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import { type AdminNavLinkComponent } from "./admin-nav-link.js";
|
|
3
|
+
export interface AdminBreadcrumbSegment {
|
|
4
|
+
label: string;
|
|
5
|
+
href?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface AdminBreadcrumbsProviderProps {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
export declare function AdminBreadcrumbsProvider({ children }: AdminBreadcrumbsProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export declare function useAdminBreadcrumbs(segments: ReadonlyArray<AdminBreadcrumbSegment>): void;
|
|
12
|
+
export declare function useAdminBreadcrumbsValue(): ReadonlyArray<AdminBreadcrumbSegment>;
|
|
13
|
+
export interface AdminBreadcrumbsTrailProps {
|
|
14
|
+
linkComponent?: AdminNavLinkComponent;
|
|
15
|
+
segments: ReadonlyArray<AdminBreadcrumbSegment>;
|
|
16
|
+
}
|
|
17
|
+
export declare function AdminBreadcrumbsTrail({ linkComponent: LinkComponent, segments, }: AdminBreadcrumbsTrailProps): import("react/jsx-runtime").JSX.Element | null;
|
|
18
|
+
//# sourceMappingURL=admin-breadcrumbs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-breadcrumbs.d.ts","sourceRoot":"","sources":["../../src/components/admin-breadcrumbs.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAA;AAanC,OAAO,EAAE,KAAK,qBAAqB,EAAuB,MAAM,qBAAqB,CAAA;AAErF,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AASD,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B;AAED,wBAAgB,wBAAwB,CAAC,EAAE,QAAQ,EAAE,EAAE,6BAA6B,2CAyCnF;AAUD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,aAAa,CAAC,sBAAsB,CAAC,QAwBlF;AAED,wBAAgB,wBAAwB,IAAI,aAAa,CAAC,sBAAsB,CAAC,CAEhF;AAED,MAAM,WAAW,0BAA0B;IACzC,aAAa,CAAC,EAAE,qBAAqB,CAAA;IACrC,QAAQ,EAAE,aAAa,CAAC,sBAAsB,CAAC,CAAA;CAChD;AAED,wBAAgB,qBAAqB,CAAC,EACpC,aAAa,EAAE,aAAmC,EAClD,QAAQ,GACT,EAAE,0BAA0B,kDA+B5B"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, } from "@voyant-travel/ui/components";
|
|
4
|
+
import { createContext, Fragment, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, } from "react";
|
|
5
|
+
import { DefaultAdminNavLink } from "./admin-nav-link.js";
|
|
6
|
+
const AdminBreadcrumbsContext = createContext(null);
|
|
7
|
+
export function AdminBreadcrumbsProvider({ children }) {
|
|
8
|
+
const [overrides, setOverrides] = useState(() => new Map());
|
|
9
|
+
const segments = useMemo(() => Array.from(overrides.values()).at(-1) ?? [], [overrides]);
|
|
10
|
+
// Stable `setSegments` reference — the function reads / writes through
|
|
11
|
+
// the state setter only, so it never needs to capture `segments`. With
|
|
12
|
+
// a stable setter and content-hash short-circuit, consumer effects
|
|
13
|
+
// that depend on `context.setSegments` don't fire in a loop just
|
|
14
|
+
// because some other consumer pushed segments.
|
|
15
|
+
const setSegments = useCallback((id, next) => {
|
|
16
|
+
setOverrides((current) => {
|
|
17
|
+
const existing = current.get(id);
|
|
18
|
+
const desired = next && next.length > 0 ? next : undefined;
|
|
19
|
+
// Short-circuit: if the entry is already structurally identical
|
|
20
|
+
// (same content hash) — or already absent — leave the map
|
|
21
|
+
// untouched so React skips the re-render that would otherwise
|
|
22
|
+
// re-trigger every consumer's effect.
|
|
23
|
+
if (!desired && !existing)
|
|
24
|
+
return current;
|
|
25
|
+
if (desired &&
|
|
26
|
+
existing &&
|
|
27
|
+
existing.length === desired.length &&
|
|
28
|
+
serializeSegments(existing) === serializeSegments(desired)) {
|
|
29
|
+
return current;
|
|
30
|
+
}
|
|
31
|
+
const merged = new Map(current);
|
|
32
|
+
if (desired)
|
|
33
|
+
merged.set(id, desired);
|
|
34
|
+
else
|
|
35
|
+
merged.delete(id);
|
|
36
|
+
return merged;
|
|
37
|
+
});
|
|
38
|
+
}, []);
|
|
39
|
+
const context = useMemo(() => ({ segments, setSegments }), [segments, setSegments]);
|
|
40
|
+
return (_jsx(AdminBreadcrumbsContext.Provider, { value: context, children: children }));
|
|
41
|
+
}
|
|
42
|
+
function serializeSegments(segments) {
|
|
43
|
+
let out = "";
|
|
44
|
+
for (const s of segments) {
|
|
45
|
+
out += `${s.label}|${s.href ?? ""}\n`;
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
export function useAdminBreadcrumbs(segments) {
|
|
50
|
+
const context = useContext(AdminBreadcrumbsContext);
|
|
51
|
+
const id = useId();
|
|
52
|
+
// Hold the latest segments in a ref so the effect can read them without
|
|
53
|
+
// depending on array identity — callers can pass a fresh array each render.
|
|
54
|
+
const segmentsRef = useRef(segments);
|
|
55
|
+
segmentsRef.current = segments;
|
|
56
|
+
const key = serializeSegments(segments);
|
|
57
|
+
// Hold the context in a ref too so we don't have to put it in the
|
|
58
|
+
// effect's deps. The provider's `setSegments` is stable, but `context`
|
|
59
|
+
// itself re-allocates when `segments` (a different field on the same
|
|
60
|
+
// context) changes — which would otherwise re-fire this effect on
|
|
61
|
+
// every push from any consumer and cascade into an infinite loop.
|
|
62
|
+
const contextRef = useRef(context);
|
|
63
|
+
contextRef.current = context;
|
|
64
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional content gate via `key`; context read via ref so it doesn't re-fire the effect
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const ctx = contextRef.current;
|
|
67
|
+
if (!ctx)
|
|
68
|
+
return;
|
|
69
|
+
const snapshot = segmentsRef.current.map((s) => ({ label: s.label, href: s.href }));
|
|
70
|
+
ctx.setSegments(id, snapshot);
|
|
71
|
+
return () => ctx.setSegments(id, null);
|
|
72
|
+
}, [id, key]);
|
|
73
|
+
}
|
|
74
|
+
export function useAdminBreadcrumbsValue() {
|
|
75
|
+
return useContext(AdminBreadcrumbsContext)?.segments ?? [];
|
|
76
|
+
}
|
|
77
|
+
export function AdminBreadcrumbsTrail({ linkComponent: LinkComponent = DefaultAdminNavLink, segments, }) {
|
|
78
|
+
if (segments.length === 0)
|
|
79
|
+
return null;
|
|
80
|
+
return (_jsx(Breadcrumb, { children: _jsx(BreadcrumbList, { children: segments.map((segment, index) => {
|
|
81
|
+
const isLast = index === segments.length - 1;
|
|
82
|
+
return (_jsxs(Fragment, { children: [_jsx(BreadcrumbItem, { children: isLast || !segment.href ? (_jsx(BreadcrumbPage, { children: segment.label })) : (_jsx(BreadcrumbLink, { render: _jsx(LinkComponent, { href: segment.href, target: "_self", children: segment.label }) })) }), !isLast && _jsx(BreadcrumbSeparator, {})] }, `${index}-${segment.label}`));
|
|
83
|
+
}) }) }));
|
|
84
|
+
}
|