@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.
Files changed (115) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +285 -0
  3. package/dist/app/extension-routes.d.ts +99 -0
  4. package/dist/app/extension-routes.d.ts.map +1 -0
  5. package/dist/app/extension-routes.js +134 -0
  6. package/dist/app/index.d.ts +9 -0
  7. package/dist/app/index.d.ts.map +1 -0
  8. package/dist/app/index.js +4 -0
  9. package/dist/app/root.d.ts +47 -0
  10. package/dist/app/root.d.ts.map +1 -0
  11. package/dist/app/root.js +55 -0
  12. package/dist/app/router.d.ts +30 -0
  13. package/dist/app/router.d.ts.map +1 -0
  14. package/dist/app/router.js +51 -0
  15. package/dist/app/workspace.d.ts +84 -0
  16. package/dist/app/workspace.d.ts.map +1 -0
  17. package/dist/app/workspace.js +87 -0
  18. package/dist/components/admin-breadcrumbs.d.ts +18 -0
  19. package/dist/components/admin-breadcrumbs.d.ts.map +1 -0
  20. package/dist/components/admin-breadcrumbs.js +84 -0
  21. package/dist/components/admin-nav-group.d.ts +11 -0
  22. package/dist/components/admin-nav-group.d.ts.map +1 -0
  23. package/dist/components/admin-nav-group.js +49 -0
  24. package/dist/components/admin-nav-link.d.ts +10 -0
  25. package/dist/components/admin-nav-link.d.ts.map +1 -0
  26. package/dist/components/admin-nav-link.js +5 -0
  27. package/dist/components/admin-page-head.d.ts +17 -0
  28. package/dist/components/admin-page-head.d.ts.map +1 -0
  29. package/dist/components/admin-page-head.js +107 -0
  30. package/dist/components/admin-widget-slot.d.ts +8 -0
  31. package/dist/components/admin-widget-slot.d.ts.map +1 -0
  32. package/dist/components/admin-widget-slot.js +19 -0
  33. package/dist/components/brand/voyant-mark.d.ts +3 -0
  34. package/dist/components/brand/voyant-mark.d.ts.map +1 -0
  35. package/dist/components/brand/voyant-mark.js +4 -0
  36. package/dist/components/brand/voyant-wordmark.d.ts +3 -0
  37. package/dist/components/brand/voyant-wordmark.d.ts.map +1 -0
  38. package/dist/components/brand/voyant-wordmark.js +4 -0
  39. package/dist/components/operator-admin-bootstrap-gate.d.ts +26 -0
  40. package/dist/components/operator-admin-bootstrap-gate.d.ts.map +1 -0
  41. package/dist/components/operator-admin-bootstrap-gate.js +22 -0
  42. package/dist/components/operator-admin-page-shell.d.ts +13 -0
  43. package/dist/components/operator-admin-page-shell.d.ts.map +1 -0
  44. package/dist/components/operator-admin-page-shell.js +6 -0
  45. package/dist/components/operator-admin-sidebar.d.ts +57 -0
  46. package/dist/components/operator-admin-sidebar.d.ts.map +1 -0
  47. package/dist/components/operator-admin-sidebar.js +104 -0
  48. package/dist/components/operator-admin-user-menu.d.ts +10 -0
  49. package/dist/components/operator-admin-user-menu.d.ts.map +1 -0
  50. package/dist/components/operator-admin-user-menu.js +19 -0
  51. package/dist/components/team-settings-page.d.ts +10 -0
  52. package/dist/components/team-settings-page.d.ts.map +1 -0
  53. package/dist/components/team-settings-page.js +149 -0
  54. package/dist/dashboard/dashboard-empty-states.d.ts +67 -0
  55. package/dist/dashboard/dashboard-empty-states.d.ts.map +1 -0
  56. package/dist/dashboard/dashboard-empty-states.js +65 -0
  57. package/dist/dashboard/dashboard-kpi-card.d.ts +13 -0
  58. package/dist/dashboard/dashboard-kpi-card.d.ts.map +1 -0
  59. package/dist/dashboard/dashboard-kpi-card.js +12 -0
  60. package/dist/dashboard/dashboard-page.d.ts +7 -0
  61. package/dist/dashboard/dashboard-page.d.ts.map +1 -0
  62. package/dist/dashboard/dashboard-page.js +150 -0
  63. package/dist/dashboard/dashboard-query-options.d.ts +224 -0
  64. package/dist/dashboard/dashboard-query-options.d.ts.map +1 -0
  65. package/dist/dashboard/dashboard-query-options.js +153 -0
  66. package/dist/dashboard/dashboard-skeleton.d.ts +13 -0
  67. package/dist/dashboard/dashboard-skeleton.d.ts.map +1 -0
  68. package/dist/dashboard/dashboard-skeleton.js +28 -0
  69. package/dist/extensions.d.ts +254 -0
  70. package/dist/extensions.d.ts.map +1 -0
  71. package/dist/extensions.js +139 -0
  72. package/dist/index.d.ts +51 -0
  73. package/dist/index.d.ts.map +1 -0
  74. package/dist/index.js +53 -0
  75. package/dist/lib/i18n.d.ts +2 -0
  76. package/dist/lib/i18n.d.ts.map +1 -0
  77. package/dist/lib/i18n.js +1 -0
  78. package/dist/lib/initials.d.ts +24 -0
  79. package/dist/lib/initials.d.ts.map +1 -0
  80. package/dist/lib/initials.js +45 -0
  81. package/dist/navigation/destinations.d.ts +83 -0
  82. package/dist/navigation/destinations.d.ts.map +1 -0
  83. package/dist/navigation/destinations.js +65 -0
  84. package/dist/navigation/operator-navigation.d.ts +10 -0
  85. package/dist/navigation/operator-navigation.d.ts.map +1 -0
  86. package/dist/navigation/operator-navigation.js +191 -0
  87. package/dist/providers/admin-extensions.d.ts +9 -0
  88. package/dist/providers/admin-extensions.d.ts.map +1 -0
  89. package/dist/providers/admin-extensions.js +10 -0
  90. package/dist/providers/admin-provider.d.ts +53 -0
  91. package/dist/providers/admin-provider.d.ts.map +1 -0
  92. package/dist/providers/admin-provider.js +26 -0
  93. package/dist/providers/locale-preferences.d.ts +12 -0
  94. package/dist/providers/locale-preferences.d.ts.map +1 -0
  95. package/dist/providers/locale-preferences.js +32 -0
  96. package/dist/providers/locale.d.ts +23 -0
  97. package/dist/providers/locale.d.ts.map +1 -0
  98. package/dist/providers/locale.js +98 -0
  99. package/dist/providers/operator-admin-messages.d.ts +14 -0
  100. package/dist/providers/operator-admin-messages.d.ts.map +1 -0
  101. package/dist/providers/operator-admin-messages.js +16 -0
  102. package/dist/providers/operator-admin-shell.d.ts +35 -0
  103. package/dist/providers/operator-admin-shell.d.ts.map +1 -0
  104. package/dist/providers/operator-admin-shell.js +20 -0
  105. package/dist/providers/query-client.d.ts +19 -0
  106. package/dist/providers/query-client.d.ts.map +1 -0
  107. package/dist/providers/query-client.js +34 -0
  108. package/dist/providers/theme.d.ts +29 -0
  109. package/dist/providers/theme.d.ts.map +1 -0
  110. package/dist/providers/theme.js +63 -0
  111. package/dist/types.d.ts +60 -0
  112. package/dist/types.d.ts.map +1 -0
  113. package/dist/types.js +2 -0
  114. package/package.json +222 -0
  115. 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"}
@@ -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
+ }