Give every empty list one empty state and one add action

EmptyState now has a single action shape: centred at its own width on
the 44 px tap floor, never stretched edge to edge on some pages and
compact on others; the per-call-site size switch is gone, and the
dashed card takes the card radius. While a list is empty its empty
state carries the add action, so the header drops its own add button
there (measurements, mood, medications, labs, vaccinations, documents,
illness, check-ups, visits, procedures). The dashboard medication tile
of an account without medications offers 'Add a medication' instead of
'no data in the chosen range'.
This commit is contained in:
Marc-André Bombeck committed 2026-10-08 18:53:24 +02:00
1 parent 793c19e179
commit 005cb32ea8
30 files changed
+158 -65

No files matched your search

-1
View File
@@ -86,7 +86,6 @@ export default function InsightsBmiPage() {
icon={<Ruler className="size-6" />}
title={t("insights.bmiEmptyTitle")}
description={t("insights.bmiEmptyDescription")}
ctaSize="lg"
action={
<Button size="sm" asChild>
<Link href="/settings/account">
-1
View File
@@ -199,7 +199,6 @@ export default function InsightsMedikamentePage() {
icon={<Pill className="size-6" />}
title={t("insights.emptyState.medication.title")}
description={t("insights.emptyState.medication.description")}
ctaSize="lg"
action={
<Button size="sm" asChild>
<Link href="/medications">
-2
View File
@@ -408,7 +408,6 @@ export default function InsightsPageClient() {
icon={<TrendingUp className="size-6" />}
title={t("insights.emptyTitle")}
description={t("insights.emptyDescription")}
ctaSize="lg"
action={
<Button size="sm" asChild>
<Link href="/measurements">
@@ -624,7 +623,6 @@ export default function InsightsPageClient() {
icon={<SlidersHorizontal className="size-6" />}
title={t("insights.editMode.emptyTitle")}
description={t("insights.editMode.emptyDescription")}
ctaSize="lg"
action={
<Button size="sm" asChild>
<Link href="/settings/layout/insights">
+8 -2
View File
@@ -58,6 +58,7 @@ export default function LabsPage() {
const queryClient = useQueryClient();
const { t } = useTranslations();
const [dialogOpen, setDialogOpen] = useState(false);
const [listEmpty, setListEmpty] = useState(false);
// Sticky-footer slot element for the add-result sheet (filled by the
// ResponsiveSheet `footer` ref so the form portals its action row there).
const [addFooterEl, setAddFooterEl] = useState<HTMLDivElement | null>(null);
@@ -185,7 +186,9 @@ export default function LabsPage() {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : canAddLab ? (
) : canAddLab && !listEmpty ? (
// Hidden while the list is empty: its empty state carries the
// manual add then. The scan choice above has no such twin.
<Button
onClick={() => setDialogOpen(true)}
className="min-h-11 sm:min-h-9"
@@ -263,7 +266,10 @@ export default function LabsPage() {
/>
) : null}
<LabList onAddFirst={() => setDialogOpen(true)} />
<LabList
onAddFirst={() => setDialogOpen(true)}
onEmptyChange={setListEmpty}
/>
</div>
);
}
+5 -1
View File
@@ -67,6 +67,9 @@ export default function MeasurementsPage() {
? `${addParam}\u0000${returnToParam ?? ""}`
: null;
const [dialogOpen, setDialogOpen] = useState(() => initialAdd != null);
// The list's empty state carries the add action while there is nothing
// to list, so the header drops its own then.
const [listEmpty, setListEmpty] = useState(false);
const [defaultType, setDefaultType] = useState<string | undefined>(
() => initialAdd ?? undefined,
);
@@ -151,7 +154,7 @@ export default function MeasurementsPage() {
// admits, so this is the header action a delegate keeps. A read-only
// delegate loses it entirely rather than meeting a disabled one.
actions={
canAddMeasurement ? (
canAddMeasurement && !listEmpty ? (
<Button
data-slot="measurement-add"
className="min-h-11 sm:min-h-9"
@@ -212,6 +215,7 @@ export default function MeasurementsPage() {
fallback={<Skeleton className="h-[28rem] w-full rounded-xl" />}
>
<MeasurementList
onEmptyChange={setListEmpty}
onAddFirst={() => {
setReturnTo(null);
setDialogOpen(true);
+5 -1
View File
@@ -521,6 +521,8 @@ export default function MedicationsPageClient() {
),
});
const listEmpty = !isLoading && !isError && !medications?.length;
return (
<div className="space-y-6">
<PullToRefreshIndicator {...pull} />
@@ -575,7 +577,9 @@ export default function MedicationsPageClient() {
button's responsive tap-target floor (`min-h-11 sm:min-h-9`) so
both primary "add" entry points clear the WCAG 2.5.5 44px mobile
minimum identically. */}
{canAddMedication && (
{/* While the list is empty its empty state carries the one
add action; a second one in the header said it twice. */}
{canAddMedication && !listEmpty && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="min-h-11 sm:min-h-9">
+8 -2
View File
@@ -28,6 +28,9 @@ export default function MoodPageClient() {
const router = useRouter();
const queryClient = useQueryClient();
const [dialogOpen, setDialogOpen] = useState(false);
// The list's empty state carries the add action while there is nothing
// to list, so the header drops its own then.
const [listEmpty, setListEmpty] = useState(false);
// v1.4.27 R4 RC2 — DOM-ref handle the form portals its action row
// into. The ref lives on the `<ResponsiveSheet>` footer slot so the
// Sheet branch can sticky-pin Save / Cancel above the keyboard.
@@ -109,7 +112,7 @@ export default function MoodPageClient() {
)}
{/* A mood entry is a MANAGE create: a WRITE delegate gets no add
path here, a guardian or a MANAGE delegate does. */}
{canAddMood && (
{canAddMood && !listEmpty && (
<Button
onClick={() => setDialogOpen(true)}
className="min-h-11 sm:min-h-9"
@@ -136,7 +139,10 @@ export default function MoodPageClient() {
/>
</ResponsiveSheet>
<MoodList onAddFirst={() => setDialogOpen(true)} />
<MoodList
onAddFirst={() => setDialogOpen(true)}
onEmptyChange={setListEmpty}
/>
{/* 2026-07-17 UX/IA audit M9 — mood tracking, the mental-wellbeing
screeners, and the mood insights page form one mental-health