Files
TREK/client/src/managed/index.tsx
T
Maurice f2329c9a63 feat(client): let a managed surface attach as an admin tab
The nav-entry seam was the wrong shape for what actually attaches here. A
subscription surface is about the install, not about a trip: /admin is already
behind an admin check, already grouped, and already where somebody looks to
change how their instance works. A top-level nav entry put it beside My Trips
for a screen opened once a month by one person.

Two lines in AdminPage rather than an overlay replacing the file: the tab list
gets the extra entries appended per group, and the body renders whichever one is
active. Appended rather than inserted because PageSidebar needs a group's tabs
contiguous.

The route and nav-entry seams stay. They are the right answer for a surface that
genuinely is a page, and both remain empty here.
2026-08-15 18:07:21 +02:00

99 lines
4.0 KiB
TypeScript

import type { ReactElement } from 'react'
import type { LucideIcon } from 'lucide-react'
/**
* The attachment point for surfaces that only exist on a centrally administered
* install.
*
* Empty here, and empty in every build of this repository. An installation whose
* operator ships additional screens replaces this file at build time; nothing
* else in the client changes, because everything below reads from these two
* exports rather than naming a screen directly.
*
* Why it is a file and not a plugin: what belongs here is not a third-party
* extension living behind a sandbox boundary. It is part of the product for the
* people who get that product operated for them, and it needs the same
* components, the same tokens and the same overlays as everything around it. A
* frame cannot open a dialog past its own edge.
*
* Two rules keep this honest:
* - Never import from here conditionally at a call site. The arrays are always
* read, and they are simply empty, so the public build takes exactly the same
* code path as any other.
* - Nothing here may be required for TREK to work. Everything below is
* additive: routes that are not registered, nav entries that are not shown.
*/
export interface ManagedRoute {
/** Router path, e.g. `/subscription`. Must not collide with an existing one. */
path: string
/** Rendered inside the app's ProtectedRoute wrapper, like every other screen. */
element: ReactElement
/**
* Refuse the route to anyone but an admin, the same way /admin does.
*
* Hiding the nav entry is not access control — the path is still typeable, and
* a screen about how the install is run should not open for a traveller who
* was invited to one trip. Set this alongside `adminOnly` on the matching nav
* entry; the two answer different questions.
*/
adminOnly?: boolean
}
export interface ManagedNavItem {
/** Stable id for React keys; prefixed by the consumer, so keep it bare. */
id: string
path: string
label: string
/** lucide-react, like every other icon in the app — see the theme README. */
Icon: LucideIcon
/**
* Hide the entry from everyone but an admin.
*
* Not cosmetic. What attaches here is the operator's side of the install, and
* a trip's other travellers are on this instance as guests of the person who
* runs it — showing them an entry about how it is run is noise at best and
* confusing at worst. The route behind it still checks for itself; this only
* decides who is offered the door.
*/
adminOnly?: boolean
}
/** Filter for the two nav bars. Exported so both apply the same rule. */
export function visibleManagedNavItems(isAdmin: boolean): ManagedNavItem[] {
return managedNavItems.filter((item) => !item.adminOnly || isAdmin)
}
export interface ManagedAdminTab {
/** Tab id, also the deep-link value of ?tab= on /admin. Keep it bare. */
id: string
label: string
/** lucide-react, like every other icon in the app. */
Icon: LucideIcon
/**
* Which group heading the tab sits under. The admin sidebar groups tabs and
* requires a group's tabs to be contiguous, so this is appended to the end of
* whichever group it names rather than inserted mid-list.
*/
group?: 'users' | 'config' | 'integration' | 'maintenance'
/** Rendered in the tab body. /admin already requires an admin to get here. */
element: ReactElement
}
/** Extra protected routes. Spread into the router next to the built-in ones. */
export const managedRoutes: ManagedRoute[] = []
/**
* Extra tabs in Admin → the left sidebar.
*
* This is where a surface belongs when it is about the install rather than about
* a trip: /admin is already behind an admin check, already grouped, and already
* the place somebody looks when they want to change how their instance works.
* A top-level nav entry would put it next to My Trips for a screen opened once
* a month by one person.
*/
export const managedAdminTabs: ManagedAdminTab[] = []
/** Extra entries for the desktop navbar and the mobile tab bar. */
export const managedNavItems: ManagedNavItem[] = []