# @voyant-travel/admin
Packaged staff shell, admin extension surface, and reusable admin dashboard
primitives for Voyant starters.
## Install
```bash
pnpm add @voyant-travel/admin
```
## Usage
```typescript
import { AdminProvider } from "@voyant-travel/admin/providers/admin-provider"
import { OperatorAdminShellProvider } from "@voyant-travel/admin/providers/operator-admin-shell"
import { ThemeProvider, useTheme } from "@voyant-travel/admin/providers/theme"
import { makeQueryClient } from "@voyant-travel/admin/providers/query-client"
import { getInitials, getDisplayName } from "@voyant-travel/admin/lib/initials"
import { OperatorAdminWorkspaceLayout } from "@voyant-travel/admin/components/operator-admin-sidebar"
import {
createAdminExtensionRegistry,
defineAdminExtension,
resolveAdminNavigation,
} from "@voyant-travel/admin"
function App() {
return (
)
}
```
## Exports
| Entry | Description |
| --- | --- |
| `.` | Barrel re-exports |
| `./app` | App-shell barrel: root shell, router defaults, workspace shell, and route binders |
| `./app/root` | Root document shell, root head helper, and root error boundary |
| `./app/router` | TanStack Router and QueryClient defaults for admin apps |
| `./app/workspace` | Auth-guarded workspace shell and router-aware admin links |
| `./app/extension-routes` | TanStack Router binding helpers for admin extension route contributions |
| `./components/admin-nav-group` | Sidebar navigation group renderer |
| `./components/admin-nav-link` | Navigation link adapter types and default anchor link |
| `./components/admin-widget-slot` | Widget slot renderer for admin extension widgets |
| `./components/operator-admin-bootstrap-gate` | Single-tenant-first shell bootstrap gate |
| `./components/operator-admin-sidebar` | Operator sidebar and workspace layout |
| `./components/operator-admin-user-menu` | Operator user/account/theme/locale menu |
| `./extensions` | Admin extension types and helpers |
| `./navigation/operator-navigation` | Base operator admin navigation factory |
| `./providers/admin-provider` | `AdminProvider` composing QueryClient + Theme |
| `./providers/locale-preferences` | `AdminLocalePreferenceSync` for user locale/timezone defaults |
| `./providers/operator-admin-shell` | `OperatorAdminShellProvider` and provider stack helpers |
| `./providers/operator-admin-messages` | Operator admin message provider and hooks |
| `./providers/theme` | `ThemeProvider`, `useTheme` with system-theme support |
| `./providers/query-client` | `makeQueryClient(config?)` factory with Voyant defaults |
| `./lib/initials` | `getInitials`, `getDisplayName` helpers |
| `./types` | `AdminUser`, `NavItem`, `NavSubItem`, `ThemeMode`, `AuthActions` |
## Admin Extensions
Use `defineAdminExtension(...)` to declare shared admin contributions and keep
the extension surface explicit:
```ts
import { defineAdminExtension } from "@voyant-travel/admin"
export const financeExtension = defineAdminExtension({
id: "finance-tools",
navigation: [
{
order: 10,
items: [{ id: "settlements", title: "Settlements", url: "/finance/settlements" }],
},
],
})
```
Starters can merge those contributions into their base navigation with
`resolveAdminNavigation(...)` and expose widget slots with
`resolveAdminWidgets(...)`. When a starter wants one explicit source-controlled
registry, compose it with `createAdminExtensionRegistry(...)`.
The packaged app shell lives under `@voyant-travel/admin/app/*`. First-party
starters should import shell/router helpers from those subpaths. The
domain-backed core extension remains in `@voyant-travel/admin-app/core-extension`
because it imports first-party domain React packages that depend on the admin
extension surface.
Render widgets from a starter-owned registry with `AdminWidgetSlotRenderer`:
```tsx
import { AdminWidgetSlotRenderer, createAdminExtensionRegistry } from "@voyant-travel/admin"
const adminExtensions = createAdminExtensionRegistry(financeExtension)
function DashboardHeader({ dashboard }) {
return (
)
}
```
The operator starter currently exposes these stable slots:
`dashboard.header`, `dashboard.after-kpis`, `dashboard.footer`,
`booking.details.header`, `booking.details.after-summary`,
`invoice.details.header`, and `invoice.details.after-summary`.
## Operator Shell
Operator apps can centralize the standard provider order with
`OperatorAdminShellProvider`. It composes `AdminProvider`,
`VoyantReactProvider`, operator admin messages, optional app-level providers,
and optional domain UI message providers that accept `{ locale, children }`.
```tsx
import {
type AdminDomainMessagesProvider,
OperatorAdminShellProvider,
} from "@voyant-travel/admin"
import { BookingsUiMessagesProvider } from "@voyant-travel/bookings-react/i18n"
const domainMessageProviders = [
BookingsUiMessagesProvider,
] satisfies readonly AdminDomainMessagesProvider[]
function App({ queryClient }: { queryClient: QueryClient }) {
return (
)
}
```
Use `OperatorAdminWorkspaceLayout` to reuse the standard sidebar chrome while
keeping app-owned routing and sign-out behavior explicit:
```tsx
import { Link, useRouterState } from "@tanstack/react-router"
import { OperatorAdminWorkspaceLayout } from "@voyant-travel/admin"
const AdminLink = ({ children, href, onClick, target }) => (
{children}
)
function Workspace({ children, user }) {
const currentPath = useRouterState({ select: (s) => s.location.pathname })
return (
signOut({ redirectTo: "/sign-in" })}
>
{children}
)
}
```
`OperatorAdminShellProvider` and `OperatorAdminWorkspaceLayout` do not fetch or
require Better Auth organizations. First-party Voyant starters are
single-tenant per deployment: load the current authenticated user first, then
render the shell with the `user` prop.
Use `OperatorAdminBootstrapGate` to make that contract explicit:
```tsx
{children}
```
The workspace layout follows the shadcn sidebar composition with a
`SidebarInset` main region and a visible sidebar trigger in the inset header.
Pass `variant="inset"` or `variant="floating"` and `side="right"` when an app
needs one of the modern sidebar variants. The sidebar can also be toggled with
`Cmd+B` on macOS or `Ctrl+B` on Windows and Linux.
Use `OperatorAdminPageShell` when a route needs the standard per-page header:
sidebar trigger, breadcrumbs, page-level actions, and a padded body. Disable the
workspace layout's fallback trigger header so the page shell owns the route
header:
```tsx
import { OperatorAdminPageShell, OperatorAdminWorkspaceLayout } from "@voyant-travel/admin"
function Workspace({ children }) {
return (
{children}
)
}
function BookingsRoute() {
return (
}
actions={}
>
)
}
```
Pass `padded={false}` for full-bleed pages such as maps, workflow timelines, or
canvas-style tools that own their own spacing.
Route-level document titles are derived from `navItems` and `currentPath` by
default, so `/bookings` renders `Bookings ยท Voyant` and tracks the active
locale when the navigation messages change. `AdminPageHead` also keeps
`` synchronized with `useLocale().resolvedLocale` and updates the
description and Open Graph description meta tags when a description is provided.
Apps can override detail routes that are not represented by navigation items
with `useAdminPageHead`:
```tsx
import { useAdminPageHead } from "@voyant-travel/admin"
function ProductDetailPage({ product }) {
useAdminPageHead({
title: product.name,
description: product.summary,
})
return
}
```
Set `pageHead={false}` on `OperatorAdminWorkspaceLayout` only when an app owns
all document metadata itself.
The default brand uses the exported `VoyantMark` and `VoyantWordmark` SVG
components and swaps from wordmark to mark in collapsed icon mode. Apps that
need a custom lockup can pass `brand={}` or compose those exported
brand components directly.
Workspace switching routes remain app-owned opt-ins. Apps that intentionally
implement workspace switching can opt into `mode="organization"` and pass their
own workspace readiness state.
`DashboardPage` renders explicit empty states for charts, upcoming departures,
and outstanding invoices instead of blank card frames. Brand-new tenants see a
first-run onboarding panel. The page fetches each aggregate card itself with
independent queries; apps should render it directly instead of preloading the
dashboard aggregate query options in a route loader. Apps that need different
empty copy or actions can pass `emptyStates` for the affected section:
```tsx
```
## License
Apache-2.0