Files
webmail/components/admin/page-header.tsx
T
Linus Rath 10b4d2a9db feat: rework the admin panel around a settings-style sidebar and one URL per page
The old panel kept its active tab in a store, so every page sat at
/admin, the sidebar duplicated the webmail rail by hand, and each page
was a stack of boxed cards with their own save buttons. Plugin settings
were only reachable through a gear icon in a row of five.

- one catch-all route for the whole panel; pages switch through
  history.pushState, so `/admin/plugins/<id>`, the back button, bookmarks
  and connector links all work without a route transition
- sidebar laid out like the user settings: back to mail, search over
  pages and their sections, two group headings, change password and
  sign out at the bottom; the phone shows a list, then the page
- pages use SettingsSection rows instead of nested cards, with one page
  header, one flash message and one save bar at the bottom (Discard,
  Save, Ctrl+S); the unsaved-changes prompt still guards leaving
- plugins and themes are lists of rows with a toggle each; a row opens
  the extension's own page with its settings, force-on, permissions,
  kept versions and removal
- overview shows stat tiles, server facts and recent activity
- old `/admin?tab=x` links redirect; the tab store is gone and the
  connector registry resolves admin links to the page URL
- `NEXT_TURBOPACK_ROOT` overrides the Turbopack root so `next dev` runs
  from a worktree with a junctioned node_modules
2026-10-20 15:16:05 +02:00

51 lines
1.8 KiB
TypeScript

'use client';
import type { ReactNode } from 'react';
import { ArrowLeft } from '@/components/icons';
import { cn } from '@/lib/utils';
import { AdminLink } from './admin-link';
interface AdminPageHeaderProps {
title: ReactNode;
description?: ReactNode;
/** Buttons on the end side of the header. */
actions?: ReactNode;
/** A link back to the parent page, for detail pages. */
back?: { href: string; label: string };
/** Something between the back link and the title, such as an icon. */
leading?: ReactNode;
className?: string;
}
/**
* The header of an admin page. The mobile shell already shows the page name
* in its top bar, so the title itself is desktop-only there; the
* description and the actions stay.
*/
export function AdminPageHeader({ title, description, actions, back, leading, className }: AdminPageHeaderProps) {
return (
<div className={cn('mb-6 space-y-3', className)}>
{back && (
<AdminLink
href={back.href}
// The phone shell's top bar already has the back arrow.
className="hidden lg:inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<ArrowLeft className="w-4 h-4" />
{back.label}
</AdminLink>
)}
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="flex items-start gap-3 min-w-0">
{leading}
<div className="min-w-0">
<h1 className={cn('text-xl font-semibold text-foreground leading-tight', !back && 'hidden lg:block')}>{title}</h1>
{description && <p className="text-sm text-muted-foreground mt-1">{description}</p>}
</div>
</div>
{actions && <div className="flex flex-wrap items-center gap-2 shrink-0">{actions}</div>}
</div>
</div>
);
}