refactor(ui): move 62 hand-written empty states onto EmptyState

The admin sections, the cycle, illness, medication, mood, records and
settings cards, the pickers and dialogs said nothing-here-yet in a
hand-written muted paragraph in five sizes and three alignments. List
and section states now render through EmptyState (plain, compact; the
three with a next step carry it as the action), and the six chart
bodies through ChartEmptyState at their former height. The thirteen
that stay are a field without a value or a hint inside an editor, not
a list state. The ratchet ceiling drops from 75 to 13.
This commit is contained in:
Marc-André Bombeck committed 2026-10-20 13:31:02 +02:00
1 parent fe021c0ffd
commit e7ee416e19
56 files changed
+353 -224

No files matched your search

@@ -42,6 +42,7 @@ import type {
MedicationEfficacyDTO,
EfficacyTargetView,
} from "@/lib/medications/efficacy/build-efficacy";
import { EmptyState } from "@/components/ui/empty-state";
// v1.30 UI audit (M8) — `EfficacyChart` mounts inside `TargetCard`'s own
// `<Card>` as a bare 220px `ResponsiveContainer` with no chrome of its own.
@@ -299,9 +300,11 @@ function TargetCard({
/>
</div>
) : (
<p className="text-muted-foreground text-sm">
{t("medications.efficacy.noSeries")}
</p>
<EmptyState
variant="plain"
size="compact"
title={t("medications.efficacy.noSeries")}
/>
)}
<BeforeAfterCard target={target} minWeeks={dto.minWeeksPerSide} />
@@ -105,6 +105,7 @@ import { IntakeTrackingBody } from "@/components/medications/sections/intake-tra
import { MedicationRelatedSection } from "@/components/medications/detail/medication-related-section";
import type { RelatedLink } from "@/lib/links/related";
import { InlineAction } from "@/components/ui/inline-action";
import { EmptyState } from "@/components/ui/empty-state";
// v1.15.20 — Recharts stays out of the detail page's initial bundle:
// both chart bodies load through `next/dynamic` (client-only) with the
@@ -677,9 +678,11 @@ export function MedicationDetailTabs({
longDays={longDays}
/>
) : (
<p className="text-muted-foreground text-sm">
{t("medications.detail.uebersicht.complianceEmpty")}
</p>
<EmptyState
variant="plain"
size="compact"
title={t("medications.detail.uebersicht.complianceEmpty")}
/>
)}
</MedicationDetailSection>
)}
@@ -115,6 +115,7 @@ import {
import { apiDelete, apiGet, apiPost, apiPut } from "@/lib/api/api-fetch";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
import { QueryErrorRow } from "@/components/ui/query-error-row";
import { EmptyState } from "@/components/ui/empty-state";
const DAY_MS = 24 * 60 * 60 * 1000;
@@ -477,9 +478,11 @@ export function DoseHistoryLedger({
)}
{data && !isLoading && groups.length === 0 && (
<p className="text-muted-foreground py-6 text-sm">
{t("medications.detail.verlauf.empty")}
</p>
<EmptyState
variant="plain"
size="compact"
title={t("medications.detail.verlauf.empty")}
/>
)}
{data &&
@@ -12,6 +12,7 @@ import {
type InjectionSiteKey,
} from "@/lib/medications/injection-sites";
import { useTranslations } from "@/lib/i18n/context";
import { EmptyState } from "@/components/ui/empty-state";
/**
* v1.8.5 — post-dose injection-site capture.
@@ -111,9 +112,11 @@ export function LogInjectionSiteDialog({
}
>
{allowed.length === 0 ? (
<p className="text-muted-foreground py-4 text-center text-sm">
{t("medications.logInjectionSiteNoneAvailable")}
</p>
<EmptyState
variant="plain"
size="compact"
title={t("medications.logInjectionSiteNoneAvailable")}
/>
) : (
<>
<InjectionSitePicker
@@ -46,6 +46,7 @@ import {
} from "@/components/medications/use-medication-intake";
import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
import { useMedicationDueThresholds } from "@/lib/queries/use-reminder-thresholds";
import { EmptyState } from "@/components/ui/empty-state";
interface LogIntakeSchedule extends ScheduleWindowInput {
label: string | null;
@@ -276,9 +277,11 @@ export function LogIntakeDialog({
className="space-y-4"
>
{medications.length === 0 ? (
<p className="text-muted-foreground py-4 text-sm">
{t("medications.logIntake.noMedications")}
</p>
<EmptyState
variant="plain"
size="compact"
title={t("medications.logIntake.noMedications")}
/>
) : (
<div className="space-y-3">
<div className="space-y-2">
@@ -26,6 +26,7 @@ import { prefersReducedMotion } from "@/lib/charts/reduced-motion";
import { useTranslations } from "@/lib/i18n/context";
import { formatDose } from "@/lib/medications/format-dose";
import { runSaveMedicationListOrder } from "@/lib/queries/use-medication-list-layout";
import { EmptyState } from "@/components/ui/empty-state";
/**
* v1.16.10 — manual medication ordering, shared by BOTH /medications
@@ -153,9 +154,11 @@ export function MedicationOrderEditor({
// itself scrollable (second scrollbar — UI-STANDARDS §9 one-scroll-floor).
<div className="relative space-y-4" data-slot="medication-order-editor">
{medications.length === 0 && (
<p className="text-muted-foreground text-sm">
{t("medications.emptyTitle")}
</p>
<EmptyState
variant="plain"
size="compact"
title={t("medications.emptyTitle")}
/>
)}
{activeIds.length > 0 && (
<ReorderSection
@@ -40,6 +40,7 @@ import {
type NutrientCatalogEntryDto,
type SupplementLabelProposal,
} from "./use-composition";
import { EmptyState } from "@/components/ui/empty-state";
/** The categories the composition is offered for without being asked. */
export const COMPOSITION_CATEGORIES: ReadonlySet<string> = new Set([
@@ -520,9 +521,11 @@ export function CompositionSection({
}
>
{ingredients.length === 0 ? (
<p className="text-muted-foreground">
{t("medications.composition.empty")}
</p>
<EmptyState
variant="plain"
size="compact"
title={t("medications.composition.empty")}
/>
) : (
<div className="space-y-3">
<ul className="divide-border/60 divide-y" role="list">
@@ -37,6 +37,7 @@ import { ApiError, apiDelete, apiPatch, apiPost } from "@/lib/api/api-fetch";
import { formatDate } from "@/lib/date-format";
import { useDateFormatPreference, useTranslations } from "@/lib/i18n/context";
import { invalidateMedicationReads } from "@/lib/query-keys";
import { EmptyState } from "@/components/ui/empty-state";
/** The refusal codes the course routes answer, mapped to their copy. */
const REFUSAL_KEYS: Record<string, string> = {
@@ -262,9 +263,11 @@ export function CoursesRow({
</div>
{courses.length === 0 ? (
<p className="text-muted-foreground text-xs">
{t("medications.course.none")}
</p>
<EmptyState
variant="plain"
size="compact"
title={t("medications.course.none")}
/>
) : (
<ul className="space-y-2">
{courses.map((course, index) => (
@@ -50,6 +50,7 @@ import {
import { MedicationDetailSection } from "@/components/medications/medication-detail-section";
import { apiDelete, apiGetList, apiPost } from "@/lib/api/api-fetch";
import { queryKeys } from "@/lib/query-keys";
import { EmptyState } from "@/components/ui/empty-state";
/**
* v1.4.25 — GLP-1 side-effect section for the medication detail
@@ -339,9 +340,11 @@ export function SideEffectsSection({ medicationId }: SideEffectsSectionProps) {
)}
{!isLoading && !isError && items.length === 0 && (
<p className="text-muted-foreground py-1">
{t("medications.sideEffects.emptyState")}
</p>
<EmptyState
variant="plain"
size="compact"
title={t("medications.sideEffects.emptyState")}
/>
)}
{!isLoading && !isError && items.length > 0 && (
@@ -31,6 +31,7 @@ import { queryKeys } from "@/lib/query-keys";
import { apiGet } from "@/lib/api/api-fetch";
import { cn } from "@/lib/utils";
import { MedicationDetailSection } from "@/components/medications/medication-detail-section";
import { EmptyState } from "@/components/ui/empty-state";
interface DoseChange {
id: string;
@@ -141,12 +142,12 @@ export function TitrationTimeline({
dataSlot="titration-timeline"
>
{!hasPlan ? (
<p
className="text-muted-foreground bg-muted/40 rounded-md p-4 text-sm"
<EmptyState
variant="plain"
size="compact"
title={t("medications.titration.empty")}
data-slot="titration-timeline-empty"
>
{t("medications.titration.empty")}
</p>
/>
) : (
<ol className="relative space-y-0" data-slot="titration-timeline-list">
{steps.map((step, i) => (