refactor(admin): migrate section cards onto the shared card primitives

The admin tree hand-rolled its card surfaces and stepped padding at sm:
instead of md:, so it read denser than every other route on a tablet.
Move the sections onto SettingsCard/SettingsCardHeader and the mini-rows
onto ListRow; no behaviour, data, or copy changes.
This commit is contained in:
Marc-André Bombeck committed 2026-07-01 23:44:55 +02:00
1 parent c6dea5eb56
commit 86d80d2c04
23 files changed
+200 -183

No files matched your search

@@ -115,11 +115,12 @@ describe("ApiTokenOverviewSection — responsive", () => {
expect(html).toContain("All");
});
it("uses a smaller card padding on mobile (p-4 sm:p-6)", () => {
it("uses a smaller card padding on mobile (p-4 md:p-6)", () => {
const html = render();
// The card root carries the responsive padding to leave more room
// for content within a 393 CSS-px viewport.
expect(html).toMatch(/class="[^"]*\bp-4\b[^"]*sm:p-6/);
// The card root carries the shared SettingsCard padding contract
// (`p-4 md:p-6`) to leave more room for content within a 393 CSS-px
// viewport while stepping up only from `md`, not `sm`.
expect(html).toMatch(/class="[^"]*\bp-4\b[^"]*md:p-6/);
});
it("desktop date cells drop whitespace-nowrap (v1.4.22 C2 5th-attempt fix)", () => {
@@ -115,9 +115,9 @@ describe("UserManagementSection — mobile responsive", () => {
expect(mobileSlice).toContain("Force logout");
});
it("uses smaller card padding on mobile (p-4 sm:p-6)", () => {
it("uses smaller card padding on mobile (p-4 md:p-6)", () => {
const html = render();
expect(html).toMatch(/class="[^"]*\bp-4\b[^"]*sm:p-6/);
expect(html).toMatch(/class="[^"]*\bp-4\b[^"]*md:p-6/);
});
it("wraps the filter pills below the title on narrow viewports", () => {
+3 -2
View File
@@ -21,6 +21,7 @@
import { useQuery } from "@tanstack/react-query";
import { Sparkles, Loader2 } from "lucide-react";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -75,7 +76,7 @@ export function AiQualitySection() {
// resolves. Mirrors <CoachFeedbackSection> (header outside the
// fetch-state branch).
return (
<div className="bg-card border-border space-y-4 rounded-xl border p-4 sm:p-6">
<SettingsCard className="space-y-4">
<SettingsCardHeader
icon={Sparkles}
title={t("admin.aiQuality.title")}
@@ -178,6 +179,6 @@ export function AiQualitySection() {
</table>
</div>
)}
</div>
</SettingsCard>
);
}
@@ -9,6 +9,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -92,10 +93,7 @@ export function AiServerKeySection() {
}
return (
<div
data-slot="admin-ai-server-key"
className="bg-card border-border rounded-xl border p-4 sm:p-6"
>
<SettingsCard data-slot="admin-ai-server-key">
<SettingsCardHeader
icon={KeyRound}
title={t("admin.aiServerKey.title")}
@@ -183,6 +181,6 @@ export function AiServerKeySection() {
)}
</div>
</div>
</div>
</SettingsCard>
);
}
@@ -11,7 +11,9 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { formatDate, formatDateTime } from "@/lib/format";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { ListRow } from "@/components/ui/list-row";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { type ApiTokenInfo } from "./_shared";
@@ -157,7 +159,7 @@ export function ApiTokenOverviewSection() {
});
return (
<div className="bg-card border-border overflow-hidden rounded-xl border p-4 sm:p-6">
<SettingsCard className="overflow-hidden">
<SettingsCardHeader
icon={Key}
title={t("admin.apiTokens")}
@@ -293,55 +295,60 @@ export function ApiTokenOverviewSection() {
>
{tokens.map((token) => {
return (
<li
<ListRow
asChild
key={token.id}
className="bg-muted/30 border-border overflow-hidden rounded-lg border p-3"
className="bg-muted/30 border-border overflow-hidden"
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<li>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<TruncatedCell
value={formatTokenName(token.name)}
className="min-w-0 flex-1 font-medium"
/>
<TokenStatusBadge token={token} size="xs" />
</div>
<TruncatedCell
value={formatTokenName(token.name)}
className="min-w-0 flex-1 font-medium"
value={token.user.username}
className="text-muted-foreground text-xs"
/>
<TokenStatusBadge token={token} size="xs" />
</div>
<TruncatedCell
value={token.user.username}
className="text-muted-foreground text-xs"
/>
</div>
</div>
<div className="mt-2 flex flex-wrap gap-1">
{token.permissions.map((p) => (
<Badge
key={p}
variant="secondary"
className="max-w-full truncate text-[10px]"
title={
p === "*" ? t("admin.tokenPermissionAllTooltip") : p
}
>
{p === "*" ? t("admin.tokenPermissionAll") : p}
</Badge>
))}
</div>
<p className="text-muted-foreground mt-2 text-[11px]">
{t("admin.tokenLastUsed")}:{" "}
{token.lastUsedAt
? formatDateTime(token.lastUsedAt)
: t("admin.tokenNeverUsed")}
</p>
<p className="text-muted-foreground text-[11px]">
{t("admin.tokenCreated")}: {formatDate(token.createdAt)}
</p>
</li>
<div className="mt-2 flex flex-wrap gap-1">
{token.permissions.map((p) => (
<Badge
key={p}
variant="secondary"
className="max-w-full truncate text-[10px]"
title={
p === "*"
? t("admin.tokenPermissionAllTooltip")
: p
}
>
{p === "*" ? t("admin.tokenPermissionAll") : p}
</Badge>
))}
</div>
<p className="text-muted-foreground mt-2 text-[11px]">
{t("admin.tokenLastUsed")}:{" "}
{token.lastUsedAt
? formatDateTime(token.lastUsedAt)
: t("admin.tokenNeverUsed")}
</p>
<p className="text-muted-foreground text-[11px]">
{t("admin.tokenCreated")}: {formatDate(token.createdAt)}
</p>
</li>
</ListRow>
);
})}
</ul>
</>
)}
</div>
</div>
</SettingsCard>
);
}
@@ -46,6 +46,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { formatDateTime } from "@/lib/format";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -115,7 +116,7 @@ export function AppLogPreviewSection() {
const events = data?.events ?? [];
return (
<div className="bg-card border-border space-y-4 rounded-xl border p-4 sm:p-6">
<SettingsCard className="space-y-4">
<SettingsCardHeader
icon={FileText}
title={t("admin.section.app-logs.title")}
@@ -303,6 +304,6 @@ export function AppLogPreviewSection() {
</DialogClose>
</DialogContent>
</Dialog>
</div>
</SettingsCard>
);
}
+3 -2
View File
@@ -4,6 +4,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Sparkles } from "lucide-react";
import { toast } from "sonner";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -99,7 +100,7 @@ export function AssistantSection() {
const disabledSubs = !masterOn || mutation.isPending;
return (
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
<SettingsCard>
<SettingsCardHeader
icon={Sparkles}
title={t("admin.assistant.title")}
@@ -166,6 +167,6 @@ export function AssistantSection() {
/>
</div>
</div>
</div>
</SettingsCard>
);
}
+47 -42
View File
@@ -39,7 +39,9 @@ import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { ListRow } from "@/components/ui/list-row";
import { useFormatters, useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import type { BackupRow, BackupsList } from "@/types/backups";
@@ -332,7 +334,7 @@ export function BackupsSection() {
}
return (
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
<SettingsCard>
{/* v1.18.1 E3 — the snapshot count leads (numbers first), the
"Run now" button follows, and the explainer + docs link move
into the header's description slot. Previously the description
@@ -550,48 +552,51 @@ export function BackupsSection() {
data-testid="admin-backups-mobile-list"
>
{rows.map((row) => (
<li
<ListRow
asChild
key={row.id}
className="bg-muted/30 border-border rounded-lg border p-3"
className="bg-muted/30 border-border"
>
<div className="flex items-center gap-2">
<span className="truncate font-medium">{row.username}</span>
<Badge variant="secondary" className="text-[10px]">
{formatBackupType(row.type, t)}
</Badge>
</div>
<p className="text-muted-foreground text-[11px]">
{t("admin.section.backups.colSize")}:{" "}
{formatBytes(row.sizeBytes, fmt)} ·{" "}
{fmt.dateTime(row.createdAt)}
</p>
<div className="mt-2 flex flex-wrap items-center gap-2">
<Button
size="sm"
variant="outline"
disabled={downloadingId === row.id}
onClick={() => handleDownload(row)}
aria-label={t("admin.section.backups.downloadAria", {
username: row.username,
})}
className="min-h-11"
>
{downloadingId === row.id ? (
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<Download className="h-3.5 w-3.5" />
)}
{t("admin.section.backups.download")}
</Button>
<RestoreRowDialog
row={row}
pending={
restore.isPending && restore.variables?.id === row.id
}
onConfirm={() => restore.mutate(row)}
/>
</div>
</li>
<li>
<div className="flex items-center gap-2">
<span className="truncate font-medium">{row.username}</span>
<Badge variant="secondary" className="text-[10px]">
{formatBackupType(row.type, t)}
</Badge>
</div>
<p className="text-muted-foreground text-[11px]">
{t("admin.section.backups.colSize")}:{" "}
{formatBytes(row.sizeBytes, fmt)} ·{" "}
{fmt.dateTime(row.createdAt)}
</p>
<div className="mt-2 flex flex-wrap items-center gap-2">
<Button
size="sm"
variant="outline"
disabled={downloadingId === row.id}
onClick={() => handleDownload(row)}
aria-label={t("admin.section.backups.downloadAria", {
username: row.username,
})}
className="min-h-11"
>
{downloadingId === row.id ? (
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<Download className="h-3.5 w-3.5" />
)}
{t("admin.section.backups.download")}
</Button>
<RestoreRowDialog
row={row}
pending={
restore.isPending && restore.variables?.id === row.id
}
onConfirm={() => restore.mutate(row)}
/>
</div>
</li>
</ListRow>
))}
</ul>
@@ -602,6 +607,6 @@ export function BackupsSection() {
</p>
</div>
)}
</div>
</SettingsCard>
);
}
@@ -21,6 +21,7 @@
import { useQuery } from "@tanstack/react-query";
import { Loader2, Sparkles } from "lucide-react";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -70,7 +71,7 @@ export function CoachFeedbackSection() {
// /admin/coach-feedback. Mirrors the canonical structure used by
// <SystemStatusSection> (header outside the fetch-state branch).
return (
<div className="bg-card border-border space-y-4 rounded-xl border p-4 sm:p-6">
<SettingsCard className="space-y-4">
<SettingsCardHeader
icon={Sparkles}
title={t("admin.coachFeedback.title")}
@@ -167,6 +168,6 @@ export function CoachFeedbackSection() {
</table>
</div>
)}
</div>
</SettingsCard>
);
}
@@ -2,6 +2,7 @@
import { useMemo } from "react";
import { Settings } from "lucide-react";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { NativeSelect } from "@/components/ui/native-select";
@@ -26,7 +27,7 @@ export function GeneralSettingsSection() {
const currentDefaultTz = settings?.defaultUserTimezone ?? "";
return (
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
<SettingsCard>
<SettingsCardHeader
icon={Settings}
title={t("admin.appSettings")}
@@ -117,6 +118,6 @@ export function GeneralSettingsSection() {
</NativeSelect>
</div>
</div>
</div>
</SettingsCard>
);
}
+3 -2
View File
@@ -4,6 +4,7 @@ import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { AlertTriangle, Loader2 } from "lucide-react";
import { toast } from "sonner";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -69,7 +70,7 @@ export function GlitchtipSection() {
}
return (
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
<SettingsCard>
<SettingsCardHeader
icon={AlertTriangle}
title={t("admin.glitchtipTitle")}
@@ -152,6 +153,6 @@ export function GlitchtipSection() {
{t("common.save")}
</Button>
</div>
</div>
</SettingsCard>
);
}
+9 -20
View File
@@ -39,6 +39,7 @@ import { useMemo } from "react";
import { useFormatters, useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { Skeleton } from "@/components/ui/skeleton";
import { SettingsCard } from "@/components/settings/settings-card";
import { apiGet } from "@/lib/api/api-fetch";
interface HostMetricsApiSample {
@@ -156,10 +157,7 @@ export function HostMetricsChart() {
// jump when data lands. Animated only when motion isn't reduced.
if (isLoading) {
return (
<section
aria-labelledby="admin-host-metrics-heading"
className="bg-card border-border rounded-xl border p-4 md:p-6"
>
<SettingsCard as="section" aria-labelledby="admin-host-metrics-heading">
<div className="mb-3 flex items-center justify-between">
<h2 id="admin-host-metrics-heading" className="text-sm font-semibold">
{t("admin.hostMetrics.title")}
@@ -169,16 +167,13 @@ export function HostMetricsChart() {
</span>
</div>
<Skeleton className="bg-muted/40 h-[180px] w-full" />
</section>
</SettingsCard>
);
}
if (isError) {
return (
<section
aria-labelledby="admin-host-metrics-heading"
className="bg-card border-border rounded-xl border p-4 md:p-6"
>
<SettingsCard as="section" aria-labelledby="admin-host-metrics-heading">
<h2 id="admin-host-metrics-heading" className="text-sm font-semibold">
{t("admin.hostMetrics.title")}
</h2>
@@ -188,7 +183,7 @@ export function HostMetricsChart() {
>
{t("admin.hostMetrics.loadError")}
</p>
</section>
</SettingsCard>
);
}
@@ -197,10 +192,7 @@ export function HostMetricsChart() {
// instead of an empty plot.
if (rows.length === 0) {
return (
<section
aria-labelledby="admin-host-metrics-heading"
className="bg-card border-border rounded-xl border p-4 md:p-6"
>
<SettingsCard as="section" aria-labelledby="admin-host-metrics-heading">
<div className="mb-3 flex items-center justify-between">
<h2 id="admin-host-metrics-heading" className="text-sm font-semibold">
{t("admin.hostMetrics.title")}
@@ -212,15 +204,12 @@ export function HostMetricsChart() {
<p className="text-muted-foreground text-xs">
{t("admin.hostMetrics.empty")}
</p>
</section>
</SettingsCard>
);
}
return (
<section
aria-labelledby="admin-host-metrics-heading"
className="bg-card border-border rounded-xl border p-4 md:p-6"
>
<SettingsCard as="section" aria-labelledby="admin-host-metrics-heading">
<div className="mb-3 flex items-center justify-between">
<h2 id="admin-host-metrics-heading" className="text-sm font-semibold">
{t("admin.hostMetrics.title")}
@@ -354,6 +343,6 @@ export function HostMetricsChart() {
</LineChart>
</ResponsiveContainer>
</div>
</section>
</SettingsCard>
);
}
@@ -25,6 +25,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { formatDateTime } from "@/lib/format";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -219,7 +220,7 @@ export function LoginOverviewSection() {
}
return (
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
<SettingsCard>
<SettingsCardHeader
icon={ScrollText}
title={t("admin.loginOverview")}
@@ -580,6 +581,6 @@ export function LoginOverviewSection() {
</>
)}
</div>
</div>
</SettingsCard>
);
}
@@ -4,6 +4,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { ToggleLeft } from "lucide-react";
import { toast } from "sonner";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -87,7 +88,7 @@ export function ModuleAvailabilitySection() {
const availability = data?.availability;
return (
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
<SettingsCard>
<SettingsCardHeader
icon={ToggleLeft}
title={t("admin.modules.title")}
@@ -119,6 +120,6 @@ export function ModuleAvailabilitySection() {
);
})}
</div>
</div>
</SettingsCard>
);
}
@@ -20,6 +20,7 @@ import {
} from "lucide-react";
import { EmptyState } from "@/components/ui/empty-state";
import { formatDateTime } from "@/lib/format";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -53,10 +54,7 @@ export function RecentAuditPreview() {
const entries = data?.entries ?? [];
return (
<section
aria-labelledby="admin-overview-audit-heading"
className="bg-card border-border rounded-xl border p-4 sm:p-6"
>
<SettingsCard as="section" aria-labelledby="admin-overview-audit-heading">
<SettingsCardHeader
icon={ScrollText}
titleId="admin-overview-audit-heading"
@@ -145,6 +143,6 @@ export function RecentAuditPreview() {
</ul>
)}
</div>
</section>
</SettingsCard>
);
}
+5 -4
View File
@@ -15,6 +15,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -53,7 +54,7 @@ function NotificationHealthPanel() {
});
return (
<div className="bg-card border-border mt-6 rounded-xl border p-4 sm:p-6">
<SettingsCard className="mt-6">
<SettingsCardHeader
icon={HeartPulse}
title={t("admin.notificationHealth.title")}
@@ -110,7 +111,7 @@ function NotificationHealthPanel() {
</div>
)}
</div>
</div>
</SettingsCard>
);
}
@@ -194,7 +195,7 @@ export function RemindersSection() {
return (
<>
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
<SettingsCard>
{/* v1.4.22 D / F-58 — icon swap so the section header matches
the admin-shell nav tile (Bell). Clock now lives on the
timing chips inline instead of competing as the page icon. */}
@@ -404,7 +405,7 @@ export function RemindersSection() {
</div>
</div>
)}
</div>
</SettingsCard>
<NotificationHealthPanel />
</>
);
+3 -2
View File
@@ -1,6 +1,7 @@
"use client";
import { Bell, Globe, Key, MessageCircle, Smile } from "lucide-react";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { SettingsToggle, useAdminSettings, useUpdateSettings } from "./_shared";
@@ -11,7 +12,7 @@ export function ServicesSection() {
const updateSettings = useUpdateSettings();
return (
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
<SettingsCard>
<SettingsCardHeader
icon={Globe}
title={t("admin.servicesGlobal")}
@@ -69,6 +70,6 @@ export function ServicesSection() {
disabled={updateSettings.isPending}
/>
</div>
</div>
</SettingsCard>
);
}
@@ -13,13 +13,14 @@ import {
Key,
Loader2,
Map,
RotateCw,
RefreshCw,
Server,
Users,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { formatDateTime } from "@/lib/format";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useFormatters, useTranslations } from "@/lib/i18n/context";
import { StatusItem, usePublicVersion, useSystemStatus } from "./_shared";
@@ -51,7 +52,7 @@ export function SystemStatusSection() {
return (
<div className="space-y-6">
<HostMetricsChart />
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
<SettingsCard>
<SettingsCardHeader
icon={Server}
title={t("admin.systemStatus")}
@@ -205,7 +206,7 @@ export function SystemStatusSection() {
{isFetching ? (
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<RotateCw className="h-3.5 w-3.5" />
<RefreshCw className="h-3.5 w-3.5" />
)}
{t("admin.systemStatusRetry")}
</Button>
@@ -218,7 +219,7 @@ export function SystemStatusSection() {
</span>
</div>
)}
</div>
</SettingsCard>
</div>
);
}
@@ -23,6 +23,7 @@ import {
Tag,
} from "lucide-react";
import { formatDateTime } from "@/lib/format";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { StatusItem, usePublicVersion, useSystemStatus } from "./_shared";
@@ -33,9 +34,9 @@ export function SystemStatusSummary() {
const { data: version } = usePublicVersion();
return (
<section
<SettingsCard
as="section"
aria-labelledby="admin-overview-snapshot-heading"
className="bg-card border-border rounded-xl border p-4 sm:p-6"
>
<SettingsCardHeader
icon={Server}
@@ -136,6 +137,6 @@ export function SystemStatusSummary() {
</span>
</div>
)}
</section>
</SettingsCard>
);
}
+3 -2
View File
@@ -4,6 +4,7 @@ import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { Activity, Loader2 } from "lucide-react";
import { toast } from "sonner";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -72,7 +73,7 @@ export function UmamiSection() {
}
return (
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
<SettingsCard>
<SettingsCardHeader
icon={Activity}
title={t("admin.umamiTitle")}
@@ -153,6 +154,6 @@ export function UmamiSection() {
{t("common.save")}
</Button>
</div>
</div>
</SettingsCard>
);
}
@@ -30,7 +30,9 @@ import {
} from "@/components/ui/alert-dialog";
import { useAuth } from "@/hooks/use-auth";
import { formatDate } from "@/lib/format";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { ListRow } from "@/components/ui/list-row";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { type AdminUser, PasswordInput } from "./_shared";
@@ -253,7 +255,7 @@ export function UserManagementSection() {
);
return (
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
<SettingsCard>
<SettingsCardHeader
icon={Users}
title={t("admin.userManagement")}
@@ -385,36 +387,41 @@ export function UserManagementSection() {
data-testid="admin-users-mobile-list"
>
{filteredUsers.map((u) => (
<li
<ListRow
asChild
key={u.id}
className="bg-muted/30 border-border rounded-lg border p-3"
className="bg-muted/30 border-border"
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="truncate font-medium">
{u.username}
</span>
<Badge
variant={u.role === "ADMIN" ? "default" : "secondary"}
className="text-[10px]"
>
{u.role}
</Badge>
<li>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="truncate font-medium">
{u.username}
</span>
<Badge
variant={
u.role === "ADMIN" ? "default" : "secondary"
}
className="text-[10px]"
>
{u.role}
</Badge>
</div>
<p className="text-muted-foreground truncate text-xs">
{u.email || "—"}
</p>
<p className="text-muted-foreground text-[11px]">
{t("admin.userPasskeys")}: {u.passkeyCount} ·{" "}
{formatDate(u.createdAt)}
</p>
</div>
<p className="text-muted-foreground truncate text-xs">
{u.email || "—"}
</p>
<p className="text-muted-foreground text-[11px]">
{t("admin.userPasskeys")}: {u.passkeyCount} ·{" "}
{formatDate(u.createdAt)}
</p>
</div>
</div>
<div className="mt-2 flex flex-wrap items-center gap-1">
{renderUserActions(u)}
</div>
</li>
<div className="mt-2 flex flex-wrap items-center gap-1">
{renderUserActions(u)}
</div>
</li>
</ListRow>
))}
</ul>
</>
@@ -594,6 +601,6 @@ export function UserManagementSection() {
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</SettingsCard>
);
}
@@ -25,6 +25,7 @@ import {
} from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { formatDateTime } from "@/lib/format";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -82,10 +83,7 @@ export function VersionTileSection() {
const sha = shortSha(version?.buildSha ?? null);
return (
<section
aria-labelledby="admin-overview-version-heading"
className="bg-card border-border rounded-xl border p-4 sm:p-6"
>
<SettingsCard as="section" aria-labelledby="admin-overview-version-heading">
<SettingsCardHeader
icon={Tag}
titleId="admin-overview-version-heading"
@@ -133,6 +131,6 @@ export function VersionTileSection() {
value={version?.builtAt ? formatDateTime(version.builtAt) : "—"}
/>
</div>
</section>
</SettingsCard>
);
}
@@ -4,6 +4,7 @@ import { useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { BellRing, KeyRound, Loader2 } from "lucide-react";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { Button } from "@/components/ui/button";
import {
@@ -119,7 +120,7 @@ export function WebPushVapidSection() {
}
return (
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
<SettingsCard>
<SettingsCardHeader
icon={BellRing}
title={t("admin.webPushVapidTitle")}
@@ -242,6 +243,6 @@ export function WebPushVapidSection() {
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</SettingsCard>
);
}