mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
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
75 lines
2.5 KiB
TypeScript
75 lines
2.5 KiB
TypeScript
'use client';
|
|
|
|
import type { ReactNode } from 'react';
|
|
import { ChevronRight } from '@/components/icons';
|
|
import { cn } from '@/lib/utils';
|
|
import { AdminLink } from './admin-link';
|
|
|
|
interface AdminListProps {
|
|
children: ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
/** Rows of things the admin manages: plugins, themes, requests. */
|
|
export function AdminList({ children, className }: AdminListProps) {
|
|
return (
|
|
<div className={cn('divide-y divide-border rounded-lg border border-border', className)}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface AdminListRowProps {
|
|
/** Makes the whole row open this admin path; the title becomes the link. */
|
|
href?: string;
|
|
icon?: ReactNode;
|
|
title: ReactNode;
|
|
/** Badges and other inline bits after the title. */
|
|
titleExtra?: ReactNode;
|
|
description?: ReactNode;
|
|
/** The small line under the description: author, version, dates. */
|
|
meta?: ReactNode;
|
|
/** Controls on the end side; they sit above the row link. */
|
|
control?: ReactNode;
|
|
className?: string;
|
|
'data-testid'?: string;
|
|
}
|
|
|
|
export function AdminListRow({ href, icon, title, titleExtra, description, meta, control, className, ...rest }: AdminListRowProps) {
|
|
return (
|
|
<div
|
|
data-testid={rest['data-testid']}
|
|
className={cn(
|
|
'relative flex items-center gap-3 px-4 py-3 transition-colors',
|
|
href && 'hover:bg-muted/40',
|
|
className,
|
|
)}
|
|
>
|
|
{icon && (
|
|
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground [&>svg]:h-[18px] [&>svg]:w-[18px]">
|
|
{icon}
|
|
</div>
|
|
)}
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
{href ? (
|
|
<AdminLink
|
|
href={href}
|
|
className="text-sm font-medium text-foreground after:absolute after:inset-0 after:rounded-lg focus-visible:outline-none focus-visible:after:ring-2 focus-visible:after:ring-ring"
|
|
>
|
|
{title}
|
|
</AdminLink>
|
|
) : (
|
|
<span className="text-sm font-medium text-foreground">{title}</span>
|
|
)}
|
|
{titleExtra}
|
|
</div>
|
|
{description && <p className="mt-0.5 line-clamp-1 text-xs text-muted-foreground">{description}</p>}
|
|
{meta && <div className="mt-0.5 text-xs text-muted-foreground">{meta}</div>}
|
|
</div>
|
|
{control && <div className="relative z-10 flex shrink-0 items-center gap-2">{control}</div>}
|
|
{href && <ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true" />}
|
|
</div>
|
|
);
|
|
}
|