fix(ui): close remaining consistency-audit findings

Sweeps the banned gap-5/space-y-5/pl-5 spacing residue the audit found
scattered across coach pages, settings coach cards, the labs/records/
custom-metrics form family, onboarding, and a handful of one-off
surfaces (glucose panel, clinician view, illness correlation card,
invite-tokens, the mental-health check-in wizard including its stray
sm:p-6 shell steps) down onto the 4/6 scale.

Two medication chart colours (dose-strength-curve, drug-level-chart)
painted raw --dracula-purple with no light-theme override; both now
read --chart-1, the same alias the mood-chart fix already established.

Raises ~40 sub-text-xs muted-prose sites below the UI-STANDARDS §3/§5
floor (settings API/MCP/sharing/coach-prefs cards, insights workout
detail, suggested prompts, tab-strip group popovers, sources rail,
metric-stat-strip, the health-score provisional pill) to text-xs,
leaving genuine tabular-nums Badges untouched — the one exemption the
standard carries.

Routes the 10 hand-rolled settings/insights "no data yet" paragraphs
(coach memory, coach reminders, security sessions/activity/keys, API
tokens, sharing, insights pill order, per-metric overrides, nutrient
intake) through the shared EmptyState primitive instead of a bare
muted <p>, so they read the same, centered, icon-ready shape every
other empty list in the app already uses. The notification-status
card's empty copy stays on SettingsCardHeader's description slot —
converting it would mean restructuring the card, not routing it.
This commit is contained in:
Marc-André Bombeck committed 2026-07-18 03:52:32 +02:00
1 parent a2f926576e
commit 3a45fcc3bd
40 files changed
+135 -110

No files matched your search

+1 -1
View File
@@ -177,7 +177,7 @@ function CoachConversationsBody() {
<div
data-slot="coach-conversations-list"
className={cn(
"-mx-1 flex min-h-0 flex-1 flex-col gap-5 overflow-y-auto px-1",
"-mx-1 flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-1",
COACH_SCROLLBAR,
)}
>
+1 -1
View File
@@ -230,7 +230,7 @@ function CoachPlansBody() {
<div
data-slot="coach-plans-list"
className={cn(
"-mx-1 flex min-h-0 flex-1 flex-col gap-5 overflow-y-auto px-1",
"-mx-1 flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-1",
COACH_SCROLLBAR,
)}
>
+1 -1
View File
@@ -99,7 +99,7 @@ export default async function OnboardingStepPage({ params }: PageProps) {
{completed ? (
<section
aria-labelledby="onboarding-welcomeback-title"
className="space-y-5"
className="space-y-4"
>
<h1
id="onboarding-welcomeback-title"
@@ -562,7 +562,7 @@ export function InviteTokensSection() {
}}
className="space-y-4"
>
<div className="space-y-5">
<div className="space-y-4">
<fieldset>
<legend className="text-sm font-medium">
{t("admin.invites.ttlLegend")}
+1 -1
View File
@@ -262,7 +262,7 @@ export function ClinicianView({
</p>
</header>
<div className="space-y-5">
<div className="space-y-4">
{/* ── Clinical vitals ─────────────────────────────────────── */}
{measurementEntries.length > 0 ? (
<Section title={t("clinicianView.vitals")}>
@@ -131,7 +131,7 @@ export function CustomMetricEntryForm({
);
return (
<form id={formId} onSubmit={handleSubmit} className="space-y-5">
<form id={formId} onSubmit={handleSubmit} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="custom-metric-value">
@@ -149,7 +149,7 @@ export function CustomMetricForm({
);
return (
<form id={formId} onSubmit={handleSubmit} className="space-y-5">
<form id={formId} onSubmit={handleSubmit} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-1.5 sm:col-span-2">
<Label htmlFor="custom-metric-name">
@@ -266,7 +266,7 @@ export function CustomMetricHistoryList({
title={t("customMetrics.entry.editTitle")}
description={t("customMetrics.entry.editDescription")}
>
<form onSubmit={submitEdit} className="space-y-5">
<form onSubmit={submitEdit} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="custom-metric-edit-value">
@@ -77,7 +77,7 @@ function CorrelationBody({ value }: { value: IllnessCorrelationValue }) {
}
return (
<div className="space-y-5">
<div className="space-y-4">
{value.redFlags.length > 0 ? (
<div className="border-border bg-muted/40 rounded-lg border p-3">
<p className="text-foreground text-sm font-medium">
@@ -157,7 +157,7 @@ export function SourcesRail({ className, activeScopeLabel }: SourcesRailProps) {
<div data-slot="coach-sources-window" className="flex flex-col gap-1">
<label
htmlFor="coach-sources-window-select"
className="text-muted-foreground text-[11px] font-medium tracking-wide uppercase"
className="text-muted-foreground text-xs font-medium tracking-wide uppercase"
>
{t("insights.coach.windowLabel")}
</label>
@@ -215,7 +215,7 @@ export function SourcesRail({ className, activeScopeLabel }: SourcesRailProps) {
>
{t(`insights.coach.cluster.${cluster}.label`)}
</label>
<span className="text-muted-foreground text-[11px] leading-snug">
<span className="text-muted-foreground text-xs leading-snug">
{t("insights.coach.sourcesMemberCount", {
count: memberCount,
})}
@@ -147,7 +147,7 @@ export function GlucoseClinicalPanel() {
</span>
</div>
</CardHeader>
<CardContent className="space-y-5">
<CardContent className="space-y-4">
{/* Time in range */}
{dist ? (
<section className="space-y-2" data-slot="glucose-tir-section">
@@ -343,7 +343,7 @@ export function HealthScoreCard({
{isProvisional && (
<span
data-slot="health-score-card-provisional-badge"
className="border-muted-foreground/30 text-muted-foreground inline-flex items-center rounded-full border px-1.5 py-0.5 text-[9px] font-medium tracking-wide uppercase"
className="border-muted-foreground/30 text-muted-foreground inline-flex items-center rounded-full border px-1.5 py-0.5 text-xs font-medium tracking-wide uppercase"
title={t("insights.healthScore.provenance.provisional", {
count: presentCount,
total: totalCount,
@@ -834,7 +834,7 @@ function InsightsTabStripImpl({
data-slot="insights-tab-strip-group-popover"
className="w-56 p-2 text-sm"
>
<p className="text-muted-foreground px-2 py-1 text-[11px] font-semibold tracking-wide uppercase">
<p className="text-muted-foreground px-2 py-1 text-xs font-semibold tracking-wide uppercase">
{t(tab.headerKey)}
</p>
<ul className="space-y-1" role="list">
@@ -219,7 +219,7 @@ function SeriesBlock({
data-stat={cell.key}
className="space-y-0.5"
>
<p className="text-muted-foreground text-[10px] tracking-wide uppercase">
<p className="text-muted-foreground text-xs tracking-wide uppercase">
{cell.label}
</p>
<p className="text-base font-semibold tabular-nums">
@@ -75,7 +75,7 @@ export function SuggestedPrompts({
>
<span
data-slot="insights-suggested-prompts-label"
className="text-muted-foreground mr-1 text-[11px] font-medium tracking-wide uppercase"
className="text-muted-foreground mr-1 text-xs font-medium tracking-wide uppercase"
>
{t("insights.suggestedPrompts.label")}
</span>
+1 -1
View File
@@ -163,7 +163,7 @@ export function BiomarkerForm({
);
return (
<form id={formId} onSubmit={handleSubmit} className="space-y-5">
<form id={formId} onSubmit={handleSubmit} className="space-y-4">
{!existing ? (
<div className="space-y-1.5">
<Label htmlFor="biomarker-seed">
+1 -1
View File
@@ -282,7 +282,7 @@ export function LabForm({
return (
<>
<form id={formId} onSubmit={handleSubmit} className="space-y-5">
<form id={formId} onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-1.5">
<Label htmlFor="lab-biomarker">{t("labs.form.biomarker")}</Label>
<Select
+1 -1
View File
@@ -356,7 +356,7 @@ export function LabHistoryList({ readings }: { readings: LabResultDto[] }) {
title={t("labs.editTitle")}
description={t("labs.editDescription")}
>
<form onSubmit={submitEdit} className="space-y-5">
<form onSubmit={submitEdit} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="lab-edit-value">
@@ -48,7 +48,10 @@ import { apiGet } from "@/lib/api/api-fetch";
import { prefersReducedMotion } from "@/lib/charts/reduced-motion";
import { MedicationDetailSection } from "@/components/medications/medication-detail-section";
const CURVE_COLOR = "var(--dracula-purple)";
// --chart-1 is the light/dark-aware alias for this hue (dracula-purple in
// dark mode); raw --dracula-purple has no light-theme override and mis-
// renders on the light card.
const CURVE_COLOR = "var(--chart-1)";
interface DoseChange {
id: string;
@@ -62,7 +62,10 @@ import { parseDoseMg } from "@/lib/medications/dose-string";
import { prefersReducedMotion } from "@/lib/charts/reduced-motion";
import { MedicationDetailSection } from "@/components/medications/medication-detail-section";
const CHART_COLOR = "var(--dracula-purple)";
// --chart-1 is the light/dark-aware alias for this hue (dracula-purple in
// dark mode); raw --dracula-purple has no light-theme override and mis-
// renders on the light card.
const CHART_COLOR = "var(--chart-1)";
const CHART_FILL_OPACITY = 0.18;
const HOURS_PER_DAY = 24;
/** 21 days back, 0 days forward — three weekly cycles of history with
@@ -360,7 +360,7 @@ function TimelineRow({
children: React.ReactNode;
}) {
return (
<li className="relative pb-4 pl-5 last:pb-0" data-slot={dataSlot}>
<li className="relative pb-4 pl-6 last:pb-0" data-slot={dataSlot}>
<span
aria-hidden="true"
className={cn(
@@ -123,7 +123,7 @@ export function CheckInWizard({
return (
<section
className="flex flex-col gap-5"
className="flex flex-col gap-4"
data-slot="mental-health-check-in"
aria-busy={isPending || undefined}
>
@@ -161,7 +161,7 @@ export function CheckInWizard({
</header>
<Card className="gap-0 p-0">
<CardContent className="flex flex-col gap-5 p-4 sm:p-6">
<CardContent className="flex flex-col gap-4 p-4 md:p-6">
<div className="flex flex-col gap-4">
<p className="text-muted-foreground text-xs">
{t("mentalHealth.progress", {
@@ -224,7 +224,7 @@ export function CheckInWizard({
</div>
</CardContent>
<div className="border-border/70 flex items-center justify-between gap-2 border-t p-4 sm:p-6">
<div className="border-border/70 flex items-center justify-between gap-2 border-t p-4 md:p-6">
<Button
type="button"
variant="ghost"
+1 -1
View File
@@ -141,7 +141,7 @@ export function AllergyForm({
);
return (
<form id={formId} onSubmit={handleSubmit} className="space-y-5">
<form id={formId} onSubmit={handleSubmit} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-1.5 sm:col-span-2">
<Label htmlFor="allergy-substance">
@@ -131,7 +131,7 @@ export function FamilyHistoryForm({
);
return (
<form id={formId} onSubmit={handleSubmit} className="space-y-5">
<form id={formId} onSubmit={handleSubmit} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="family-relationship">
+1 -1
View File
@@ -165,7 +165,7 @@ export function AboutMeSection({
as="section"
aria-labelledby="settings-ai-about-me-title"
data-testid="settings-about-me-card"
className="space-y-5"
className="space-y-4"
>
<div className="flex flex-wrap items-start justify-between gap-3">
<SettingsCardHeader
+14 -13
View File
@@ -19,6 +19,7 @@ import {
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { EmptyState } from "@/components/ui/empty-state";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useAuth } from "@/hooks/use-auth";
@@ -124,13 +125,13 @@ function ApiEndpointsCard() {
<code className="font-mono break-all">{endpoint.path}</code>
</div>
<div>
<p className="text-muted-foreground text-[10px] tracking-wide uppercase">
<p className="text-muted-foreground text-xs tracking-wide uppercase">
{t("settings.apiEndpointAuth")}
</p>
<code className="font-mono break-all">{endpoint.auth}</code>
</div>
<div>
<p className="text-muted-foreground text-[10px] tracking-wide uppercase">
<p className="text-muted-foreground text-xs tracking-wide uppercase">
{t("settings.apiEndpointExample")}
</p>
<code className="block font-mono break-all">
@@ -432,12 +433,12 @@ function ApiTokensCard() {
narrow viewport. The revoke action stays full-width to
clear the 44pt tap-target floor. */}
{activeTokens.length === 0 ? (
<p
className="text-muted-foreground rounded-lg border border-dashed px-3 py-4 text-center text-sm md:hidden"
<EmptyState
className="md:hidden"
size="compact"
data-testid="settings-api-tokens-active-empty"
>
{t("settings.noActiveTokens")}
</p>
title={t("settings.noActiveTokens")}
/>
) : (
<ul
className="space-y-2 md:hidden"
@@ -465,19 +466,19 @@ function ApiTokensCard() {
</Badge>
)}
</div>
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.tokenTablePermissions")}:
</span>{" "}
{tok.permissions.join(", ")}
</p>
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.tokenTableCreated")}:
</span>{" "}
{formatDate(tok.createdAt)}
</p>
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.tokenTableLastUsed")}:
</span>{" "}
@@ -599,19 +600,19 @@ function ApiTokensCard() {
<p className="text-sm font-medium break-all">
{tok.name}
</p>
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.tokenTablePermissions")}:
</span>{" "}
{tok.permissions.join(", ")}
</p>
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.tokenTableCreated")}:
</span>{" "}
{formatDate(tok.createdAt)}
</p>
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.tokenTableLastUsed")}:
</span>{" "}
@@ -34,6 +34,7 @@ import {
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import { formatDateOrRelative } from "@/lib/format";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
@@ -211,14 +212,14 @@ export function CoachMemorySection({
)}
{!query.isError && facts.length === 0 ? (
<p
<EmptyState
data-testid="settings-coach-memory-empty"
className="text-muted-foreground text-sm"
>
{t("settings.ai.coachMemory.empty")}
</p>
variant="plain"
size="compact"
title={t("settings.ai.coachMemory.empty")}
/>
) : (
<div className="space-y-5">
<div className="space-y-4">
{grouped.map((group) => (
<div key={group.category} className="space-y-2">
<h3
@@ -116,7 +116,7 @@ export function CoachPrefsSection({ isAuthenticated }: CoachPrefsSectionProps) {
{isLoading ? (
<div
data-slot="coach-prefs-skeleton"
className="mt-4 flex flex-col gap-5 pl-7"
className="mt-4 flex flex-col gap-4 pl-7"
>
<div className="flex flex-col gap-2">
<Skeleton className="h-3 w-20" />
@@ -133,8 +133,8 @@ export function CoachPrefsSection({ isAuthenticated }: CoachPrefsSectionProps) {
</div>
</div>
) : (
<div className="mt-4 flex flex-col gap-5 pl-7">
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
<div className="mt-4 flex flex-col gap-4 pl-7">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{/* Tone */}
<div className="flex flex-col gap-2">
<Label htmlFor="coach-prefs-tone" className="text-xs font-medium">
@@ -212,7 +212,7 @@ export function CoachPrefsSection({ isAuthenticated }: CoachPrefsSectionProps) {
them. */}
<p
data-slot="coach-prefs-sources-pointer"
className="text-muted-foreground border-border/60 rounded-md border border-dashed px-3 py-2 text-[11px] leading-relaxed"
className="text-muted-foreground border-border/60 rounded-md border border-dashed px-3 py-2 text-xs leading-relaxed"
>
{t("insights.coach.settingsSourcesPointer")}{" "}
<Link
@@ -228,7 +228,7 @@ export function CoachPrefsSection({ isAuthenticated }: CoachPrefsSectionProps) {
<Label className="text-xs font-medium">
{t("insights.coach.settingsExcludeLabel")}
</Label>
<p className="text-muted-foreground text-[11px] leading-relaxed">
<p className="text-muted-foreground text-xs leading-relaxed">
{t("insights.coach.settingsExcludeHint")}
</p>
<ul
@@ -265,7 +265,7 @@ export function CoachPrefsSection({ isAuthenticated }: CoachPrefsSectionProps) {
<Label className="text-xs font-medium">
{t("insights.coach.settingsContextLabel")}
</Label>
<p className="text-muted-foreground text-[11px] leading-relaxed">
<p className="text-muted-foreground text-xs leading-relaxed">
{t("insights.coach.settingsContextHint")}
</p>
<ul
@@ -307,7 +307,7 @@ export function CoachPrefsSection({ isAuthenticated }: CoachPrefsSectionProps) {
>
{t("coach.settings.reminderSuggestionsLabel")}
</Label>
<p className="text-muted-foreground text-[11px] leading-relaxed">
<p className="text-muted-foreground text-xs leading-relaxed">
{t("coach.settings.reminderSuggestionsDescription")}
</p>
</div>
@@ -21,6 +21,7 @@ import { useMemo } from "react";
import { BellRing, Check, Loader2, Trash2, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import { formatDateOrRelative } from "@/lib/format";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
@@ -177,14 +178,14 @@ export function CoachRemindersSection({
)}
{!query.isError && reminders.length === 0 ? (
<p
<EmptyState
data-testid="settings-coach-reminders-empty"
className="text-muted-foreground text-sm"
>
{t("settings.ai.coachReminders.empty")}
</p>
variant="plain"
size="compact"
title={t("settings.ai.coachReminders.empty")}
/>
) : (
<div className="space-y-5">
<div className="space-y-4">
{due.length > 0 && (
<div className="space-y-2">
<h3 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
@@ -716,7 +716,7 @@ export function DashboardLayoutSection({ id }: { id: string }) {
{/* v1.4.47 W4 — column header spacer additionally reserves the
width of the new drag-handle icon (w-7) so Tile / Chart
alignment with the row switches below stays pixel-perfect. */}
<div className="text-muted-foreground flex items-center gap-2 px-3 pb-1 text-[10px] font-medium tracking-wide uppercase">
<div className="text-muted-foreground flex items-center gap-2 px-3 pb-1 text-xs font-medium tracking-wide uppercase">
<span className="w-7" aria-hidden="true" />
<span className="flex-1" aria-hidden="true" />
<span className="w-12 text-center">
+1 -1
View File
@@ -147,7 +147,7 @@ function ExportCardShell({
title={title}
description={description}
status={
<span className="border-border text-muted-foreground rounded-full border px-2 py-0.5 text-[11px] font-medium tracking-wide uppercase">
<span className="border-border text-muted-foreground rounded-full border px-2 py-0.5 text-xs font-medium tracking-wide uppercase">
{format}
</span>
}
@@ -43,6 +43,7 @@ import {
} from "@/lib/insights/sub-page-metric";
import { SUB_PAGE_TABS } from "@/components/insights/insights-tab-strip";
import { apiPut } from "@/lib/api/api-fetch";
import { EmptyState } from "@/components/ui/empty-state";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
@@ -240,12 +241,12 @@ export function InsightsPillOrderSection({ id }: { id?: string }) {
<Loader2 className="mr-2 inline h-3.5 w-3.5 animate-spin align-text-bottom motion-reduce:animate-none" />
</p>
) : tilesByGroup.length === 0 ? (
<p
className="text-muted-foreground text-sm"
<EmptyState
variant="plain"
size="compact"
data-slot="insights-pill-order-empty"
>
{t("insights.pillOrder.empty")}
</p>
title={t("insights.pillOrder.empty")}
/>
) : (
<div className="max-h-[60vh] space-y-3 overflow-y-auto overscroll-contain">
{tilesByGroup.map(({ group, rows }) => (
@@ -254,7 +255,7 @@ export function InsightsPillOrderSection({ id }: { id?: string }) {
data-slot="insights-pill-order-group"
data-group={group}
>
<p className="text-muted-foreground px-1 py-1 text-[11px] font-semibold tracking-wide uppercase">
<p className="text-muted-foreground px-1 py-1 text-xs font-semibold tracking-wide uppercase">
{t(MANAGER_GROUP_HEADER_KEYS[group])}
</p>
<DndContext
+6 -6
View File
@@ -128,13 +128,13 @@ function McpConnectionsCard() {
{t("settings.mcp.connectionConnected")}
</Badge>
</div>
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.tokenTablePermissions")}:
</span>{" "}
{conn.scope}
</p>
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.mcp.connectionLastUsed")}:
</span>{" "}
@@ -353,7 +353,7 @@ function McpTokensCard() {
aria-label={t("settings.mcp.writeScopeToggleLabel")}
/>
</div>
<p className="text-muted-foreground text-[11px] leading-relaxed">
<p className="text-muted-foreground text-xs leading-relaxed">
{t("settings.mcp.writeScopeNote")}
</p>
</div>
@@ -447,19 +447,19 @@ function McpTokensCard() {
</Badge>
)}
</div>
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.tokenTablePermissions")}:
</span>{" "}
{tok.permissions.join(", ")}
</p>
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.tokenTableCreated")}:
</span>{" "}
{formatDate(tok.createdAt)}
</p>
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.tokenTableLastUsed")}:
</span>{" "}
@@ -21,6 +21,7 @@ import { Leaf } from "lucide-react";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { EmptyState } from "@/components/ui/empty-state";
import { Skeleton } from "@/components/ui/skeleton";
import { useAuth } from "@/hooks/use-auth";
import { useTranslations } from "@/lib/i18n/context";
@@ -85,9 +86,11 @@ export function NutrientIntakeCard() {
<Skeleton className="h-9 w-full rounded-md" />
</div>
) : rows.length === 0 ? (
<p className="text-muted-foreground pl-7 text-sm">
{t("settings.sections.sources.nutrients.empty")}
</p>
<EmptyState
variant="plain"
size="compact"
title={t("settings.sections.sources.nutrients.empty")}
/>
) : (
<ul className="divide-border divide-y rounded-md border" role="list">
{rows.map((row) => (
@@ -13,6 +13,7 @@ import { useQuery } from "@tanstack/react-query";
import { ChevronDown, Loader2, ShieldCheck } from "lucide-react";
import { SettingsCard } from "@/components/settings/settings-card";
import { EmptyState } from "@/components/ui/empty-state";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { apiGet } from "@/lib/api/api-fetch";
@@ -112,9 +113,11 @@ export function SecurityActivityCard({
)}
{!isLoading && !isError && events.length === 0 && (
<p className="text-muted-foreground text-sm">
{t("settings.security.activityEmpty")}
</p>
<EmptyState
variant="plain"
size="compact"
title={t("settings.security.activityEmpty")}
/>
)}
{events.length > 0 && (
@@ -25,6 +25,7 @@ import {
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { EmptyState } from "@/components/ui/empty-state";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations } from "@/lib/i18n/context";
@@ -138,9 +139,11 @@ export function SecurityKeysCard({ keys }: { keys: WebauthnKeyInfo[] }) {
<div className="mt-4">
{keys.length === 0 ? (
<p className="text-muted-foreground text-sm">
{t("settings.security.keys.empty")}
</p>
<EmptyState
variant="plain"
size="compact"
title={t("settings.security.keys.empty")}
/>
) : (
<ul className="space-y-2" data-testid="security-keys-list">
{keys.map((key) => (
@@ -15,6 +15,7 @@ import { ChevronDown, Loader2, MonitorSmartphone } from "lucide-react";
import { Button } from "@/components/ui/button";
import { SettingsCard } from "@/components/settings/settings-card";
import { EmptyState } from "@/components/ui/empty-state";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { apiGet, apiDelete } from "@/lib/api/api-fetch";
@@ -126,9 +127,11 @@ export function SecuritySessionsCard({
)}
{!isLoading && !isError && sessions.length === 0 && (
<p className="text-muted-foreground text-sm">
{t("settings.security.sessionsEmpty")}
</p>
<EmptyState
variant="plain"
size="compact"
title={t("settings.security.sessionsEmpty")}
/>
)}
{sessions.length > 0 && (
@@ -320,7 +320,7 @@ export function ShareLinkCreateForm({
setRangeDays(Math.max(1, Number(e.target.value) || 1))
}
/>
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
{t("settings.sharing.rangeHint")}
</p>
</div>
@@ -338,7 +338,7 @@ export function ShareLinkCreateForm({
}
aria-invalid={expiryDays < 1 || expiryDays > MAX_DAYS}
/>
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
{t("settings.sharing.expiryHint", { max: MAX_DAYS })}
</p>
</div>
@@ -353,7 +353,7 @@ export function ShareLinkCreateForm({
<Label htmlFor="share-fhir" className="text-sm font-medium">
{t("settings.sharing.fhirApi")}
</Label>
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
{t("settings.sharing.fhirApiHint")}
</p>
</div>
@@ -396,7 +396,7 @@ export function ShareLinkCreateForm({
<Label className="text-sm font-medium">
{t("settings.sharing.attachTitle")}
</Label>
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
{t("settings.sharing.attachCount", {
count: selectedDocs.length,
max: MAX_DOCUMENTS,
@@ -445,11 +445,11 @@ export function ShareLinkCreateForm({
</ul>
) : null}
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
{t("settings.sharing.attachFrozen")}
</p>
{selectedDocs.length > 0 ? (
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
{t("settings.sharing.exifNote")}
</p>
) : null}
@@ -499,7 +499,7 @@ export function ShareLinkCreateForm({
})}
</p>
)}
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
{t("settings.sharing.shownOnce")}
</p>
@@ -537,7 +537,7 @@ export function ShareLinkCreateForm({
{/* The link (no passphrase). On its own it cannot open the record. */}
<div className="space-y-1">
<p className="text-foreground text-[11px] font-medium">
<p className="text-foreground text-xs font-medium">
{t("settings.sharing.linkLabel")}
</p>
<div className="flex items-center gap-2">
@@ -556,7 +556,7 @@ export function ShareLinkCreateForm({
</Button>
</div>
{copied === "link" && (
<p className="text-success text-[11px]">
<p className="text-success text-xs">
{t("settings.sharing.copied")}
</p>
)}
@@ -565,7 +565,7 @@ export function ShareLinkCreateForm({
{/* The passphrase — the second factor. Shown once; only its hash is
stored, so it cannot be recovered. */}
<div className="space-y-1">
<p className="text-foreground text-[11px] font-medium">
<p className="text-foreground text-xs font-medium">
{t("settings.sharing.passphraseLabel")}
</p>
<div className="flex items-center gap-2">
@@ -587,11 +587,11 @@ export function ShareLinkCreateForm({
</Button>
</div>
{copied === "passphrase" && (
<p className="text-success text-[11px]">
<p className="text-success text-xs">
{t("settings.sharing.copied")}
</p>
)}
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
{t("settings.sharing.passphraseHint")}
</p>
</div>
+9 -9
View File
@@ -38,6 +38,7 @@ import {
} from "@/components/ui/alert-dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import {
@@ -107,12 +108,11 @@ function ShareLinksCard() {
{t("settings.sharing.activeTitle")}
</h3>
{activeLinks.length === 0 ? (
<p
className="text-muted-foreground rounded-lg border border-dashed px-3 py-4 text-center text-sm"
<EmptyState
size="compact"
data-testid="share-active-empty"
>
{t("settings.sharing.noActive")}
</p>
title={t("settings.sharing.noActive")}
/>
) : (
<ul className="space-y-2" data-testid="share-active-list">
{activeLinks.map((link) => (
@@ -158,19 +158,19 @@ function ShareLinksCard() {
)}
</div>
</div>
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.sharing.created")}:
</span>{" "}
{formatDate(link.createdAt)}
</p>
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.sharing.expires")}:
</span>{" "}
{formatDateTime(link.expiresAt)}
</p>
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.sharing.accessCount")}:
</span>{" "}
@@ -246,7 +246,7 @@ function ShareLinksCard() {
: t("settings.sharing.statusExpired")}
</Badge>
</div>
<p className="text-muted-foreground text-[11px]">
<p className="text-muted-foreground text-xs">
<span className="font-medium">
{t("settings.sharing.accessCount")}:
</span>{" "}
+6 -3
View File
@@ -42,6 +42,7 @@ import {
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { EmptyState } from "@/components/ui/empty-state";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { NutrientIntakeCard } from "@/components/settings/nutrient-intake-card";
@@ -412,9 +413,11 @@ export function SourcesSection() {
{t("settings.sections.sources.perMetricHelp")}
</p>
{overriddenMetrics.length === 0 ? (
<p className="text-muted-foreground text-xs italic">
{t("settings.sections.sources.perMetricEmpty")}
</p>
<EmptyState
variant="plain"
size="compact"
title={t("settings.sections.sources.perMetricEmpty")}
/>
) : (
<ul className="space-y-2">
{overriddenMetrics.map((metric) => (