mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
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:
1 parent
fe021c0ffd
commit
e7ee416e19
56 files changed
+353
-224
No files matched your search
@@ -31,7 +31,7 @@ const SRC = join(__dirname, "..");
|
||||
* The hand-written empty states that remain. Lower it when a site moves to
|
||||
* `EmptyState`; never raise it.
|
||||
*/
|
||||
const CEILING = 75;
|
||||
const CEILING = 13;
|
||||
|
||||
const ELEMENT_RE =
|
||||
/<(p|div|span)\b([^>]*?)>\s*\{t\(\s*"([a-zA-Z0-9_.]+)"[^)]*\)\}\s*<\/\1>/g;
|
||||
|
||||
@@ -394,11 +394,7 @@ function MedicationComplianceCalendar({
|
||||
}
|
||||
|
||||
if (!data?.dailyCompliance) {
|
||||
return (
|
||||
<div className="text-muted-foreground rounded-lg border border-dashed px-3 py-4 text-xs">
|
||||
{t("insights.complianceNoData")}
|
||||
</div>
|
||||
);
|
||||
return <EmptyState size="compact" title={t("insights.complianceNoData")} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
|
||||
import { canLogWorkout } from "@/lib/workouts/manual-entry";
|
||||
import { QueryErrorCard } from "@/components/ui/query-error-card";
|
||||
import { translateOrNull } from "@/lib/i18n/translate-code";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
/**
|
||||
* v1.4.32 — `/insights/workouts`.
|
||||
@@ -187,12 +188,13 @@ export default function InsightsWorkoutsPageClient() {
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<p
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("insights.workouts.filterEmpty")}
|
||||
className="px-4 md:px-6"
|
||||
data-slot="workout-list-filter-empty"
|
||||
className="text-muted-foreground px-4 text-sm md:px-6"
|
||||
>
|
||||
{t("insights.workouts.filterEmpty")}
|
||||
</p>
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
import { helpfulRateColour } from "./_shared";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
interface FeedbackBucket {
|
||||
severity: string;
|
||||
@@ -110,9 +111,11 @@ export function AiQualitySection() {
|
||||
onRetry={() => void query.refetch()}
|
||||
/>
|
||||
) : !hasData ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("admin.aiQuality.empty")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("admin.aiQuality.empty")}
|
||||
/>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm" data-slot="ai-quality-table">
|
||||
|
||||
@@ -28,6 +28,7 @@ import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
import { helpfulRateColour } from "./_shared";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
interface CoachFeedbackBucket {
|
||||
promptVersion: string;
|
||||
@@ -105,9 +106,11 @@ export function CoachFeedbackSection() {
|
||||
onRetry={() => void query.refetch()}
|
||||
/>
|
||||
) : !hasData ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("admin.coachFeedback.empty")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("admin.coachFeedback.empty")}
|
||||
/>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm" data-slot="coach-feedback-table">
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
import { KeyBackupCard } from "./key-backup-card";
|
||||
import { useKeyBackupStatus } from "./use-key-backup-status";
|
||||
import { QueryErrorRow } from "@/components/ui/query-error-row";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
interface ColumnScan {
|
||||
model: string;
|
||||
@@ -446,9 +447,11 @@ export function ColumnCoverage({
|
||||
{open ? (
|
||||
<>
|
||||
{shown.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("admin.section.encryption.columnsNoneWithRows")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("admin.section.encryption.columnsNoneWithRows")}
|
||||
/>
|
||||
) : (
|
||||
/* Focusable, named scroll region: on a phone the table scrolls
|
||||
sideways, and a keyboard user has to be able to reach it
|
||||
|
||||
@@ -48,6 +48,7 @@ import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
import { readStoredTimezone } from "@/lib/timezone-mirror";
|
||||
import { resolveIntlLocale } from "@/lib/format-locale";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
interface HostMetricsApiSample {
|
||||
capturedAt: string;
|
||||
@@ -227,9 +228,11 @@ export function HostMetricsChart() {
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("admin.hostMetrics.empty")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("admin.hostMetrics.empty")}
|
||||
/>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { queryKeys } from "@/lib/query-keys";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { QueryErrorRow } from "@/components/ui/query-error-row";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
/**
|
||||
* v1.37.31 — the delivery-health readout the `provider_health` ledger never
|
||||
@@ -143,9 +144,11 @@ export function ProviderHealthSection() {
|
||||
onRetry={() => void refetch()}
|
||||
/>
|
||||
) : providers.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("admin.providerHealth.empty")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("admin.providerHealth.empty")}
|
||||
/>
|
||||
) : (
|
||||
<ul className="flex flex-col gap-3" data-testid="provider-health-list">
|
||||
{providers.map((p) => (
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
describeChannelTestFailures,
|
||||
type ChannelTestResult,
|
||||
} from "./notification-test-failures";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
interface NotificationHealth {
|
||||
windowHours: number;
|
||||
@@ -73,9 +74,11 @@ function NotificationHealthPanel() {
|
||||
)}
|
||||
|
||||
{data && data.channels.length === 0 && (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("admin.notificationHealth.empty")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("admin.notificationHealth.empty")}
|
||||
/>
|
||||
)}
|
||||
|
||||
{data && data.channels.length > 0 && (
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
useSystemStatus,
|
||||
type FailingQueue,
|
||||
} from "./_shared";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
// v1.4.16: Recharts is ~108 KiB Brotli — defer-load the host
|
||||
// metrics chart so the system-status page only ships it when an admin
|
||||
@@ -89,12 +90,14 @@ export function FailingJobsCard({
|
||||
description={t("admin.failingJobs.description")}
|
||||
/>
|
||||
{failingJobs.queues.length === 0 ? (
|
||||
<p
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("admin.failingJobs.none", {
|
||||
hours: failingJobs.windowHours,
|
||||
})}
|
||||
data-testid="failing-jobs-none"
|
||||
className="text-muted-foreground text-sm"
|
||||
>
|
||||
{t("admin.failingJobs.none", { hours: failingJobs.windowHours })}
|
||||
</p>
|
||||
/>
|
||||
) : (
|
||||
<ul data-testid="failing-jobs-list" className="space-y-3">
|
||||
{failingJobs.queues.map((queue) => (
|
||||
|
||||
@@ -573,9 +573,7 @@ export function MedicationComplianceChart({
|
||||
}
|
||||
/>
|
||||
) : !hasData ? (
|
||||
<div className="text-muted-foreground flex h-48 items-center justify-center text-sm">
|
||||
{t("charts.noData")}
|
||||
</div>
|
||||
<ChartEmptyState title={t("charts.noData")} height={192} />
|
||||
) : chartData.length < 3 ? (
|
||||
// v1.4.16 — sparse-data placeholder consistent with the
|
||||
// BP/weight/pulse/mood charts.
|
||||
|
||||
@@ -732,9 +732,7 @@ export function MoodChart({
|
||||
height={CHART_HEIGHT_PX}
|
||||
/>
|
||||
) : !chartData?.length ? (
|
||||
<div className="text-muted-foreground flex h-48 items-center justify-center text-sm">
|
||||
{t("charts.noData")}
|
||||
</div>
|
||||
<ChartEmptyState title={t("charts.noData")} height={192} />
|
||||
) : chartData.length < 3 ? (
|
||||
// v1.4.16 — sparse-data placeholder consistent with the
|
||||
// BP/weight/pulse charts. Height tracks the chart strip's
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
type LeafScope,
|
||||
type Translate,
|
||||
} from "./report-sections";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
/**
|
||||
* Only the three stored gender values get a label. A row on a clinical
|
||||
@@ -240,9 +241,11 @@ export function EmergencySection({
|
||||
{t("clinicianView.sectionUnavailable")}
|
||||
</p>
|
||||
) : rows.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("clinicianView.sectionEmpty")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("clinicianView.sectionEmpty")}
|
||||
/>
|
||||
) : (
|
||||
rows.map((row) => (
|
||||
<StatRow
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
type ReportLeafId,
|
||||
} from "@/lib/report-selection/catalogue";
|
||||
import type { ReportSelection } from "@/lib/report-selection/selection";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
export type Translate = (
|
||||
key: string,
|
||||
@@ -185,9 +186,11 @@ export function LeafSection({
|
||||
{t("clinicianView.sectionUnavailable")}
|
||||
</p>
|
||||
) : empty ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("clinicianView.sectionEmpty")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("clinicianView.sectionEmpty")}
|
||||
/>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
|
||||
@@ -32,6 +32,7 @@ import { dateKeyOfInstant } from "../day/day-url";
|
||||
import { formatMetricValue } from "./format-value";
|
||||
import type { CustomMetricEntryDto } from "./types";
|
||||
import { Segmented } from "@/components/ui/segmented";
|
||||
import { ChartEmptyState } from "@/components/charts/chart-empty-state";
|
||||
|
||||
/**
|
||||
* v1.25.5 — per-custom-metric trend chart, in the dashboard's visual language.
|
||||
@@ -151,9 +152,7 @@ export function CustomMetricChart({
|
||||
</div>
|
||||
|
||||
{points.length === 0 ? (
|
||||
<p className="text-muted-foreground py-10 text-center text-sm">
|
||||
{t("customMetrics.chart.empty")}
|
||||
</p>
|
||||
<ChartEmptyState title={t("customMetrics.chart.empty")} height={160} />
|
||||
) : (
|
||||
<div>
|
||||
<div
|
||||
|
||||
@@ -21,6 +21,7 @@ import { queryKeys } from "@/lib/query-keys";
|
||||
import { CustomMetricForm } from "./custom-metric-form";
|
||||
import { formatMetricValue } from "./format-value";
|
||||
import type { CustomMetricDto, CustomMetricListResponse } from "./types";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
/**
|
||||
* v1.25.5 — inline list of the user's custom metrics, mounted ON the
|
||||
@@ -99,12 +100,10 @@ export function CustomMetricList() {
|
||||
onRetry={() => refetch()}
|
||||
/>
|
||||
) : metrics.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center gap-3 py-6 text-center">
|
||||
<Gauge className="text-muted-foreground size-6" />
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("customMetrics.emptyDescription")}
|
||||
</p>
|
||||
<EmptyState
|
||||
icon={<Gauge className="size-6" />}
|
||||
title={t("customMetrics.emptyDescription")}
|
||||
action={
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setAddOpen(true)}
|
||||
@@ -112,8 +111,8 @@ export function CustomMetricList() {
|
||||
>
|
||||
{t("customMetrics.add")}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="divide-border divide-y p-0">
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
CustomMetricEntryDto,
|
||||
CustomMetricEntryListResponse,
|
||||
} from "./types";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
const ENTRIES_PAGE_SIZE = 200;
|
||||
|
||||
@@ -90,9 +91,11 @@ export function CustomMetricValuesList({
|
||||
|
||||
if (entries.length === 0) {
|
||||
return (
|
||||
<p className="text-muted-foreground py-8 text-center text-sm">
|
||||
{t("customMetrics.detail.emptyTitle")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("customMetrics.detail.emptyTitle")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
HISTORY_REST_OPACITY,
|
||||
HISTORY_PERIOD_OPACITY,
|
||||
} from "./phase-tokens";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
/**
|
||||
* v1.15.1 — the cycle-length history chart.
|
||||
@@ -136,9 +137,11 @@ export function CycleHistoryChart({
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{!hasData ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("cycle.history.none")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("cycle.history.none")}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* Stat chips — avg / variability / regularity classification. */}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { CYCLE_SYMPTOM_CATALOG } from "./symptom-catalog";
|
||||
import { PHASE_HUE } from "./phase-tokens";
|
||||
import type { CyclePhase } from "./types";
|
||||
import type { SymptomPhaseRow } from "./use-cycle";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
/**
|
||||
* v1.15.0 — symptom patterns by phase (the cycle-NATIVE insight).
|
||||
@@ -64,9 +65,11 @@ export function CycleSymptomPatterns({ rows }: CycleSymptomPatternsProps) {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{rows.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("cycle.insights.symptomPatternsEmpty")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("cycle.insights.symptomPatternsEmpty")}
|
||||
/>
|
||||
) : (
|
||||
<ul className="space-y-3" data-slot="cycle-symptom-pattern-list">
|
||||
{rows.map((row) => {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import type { Formatters } from "@/lib/format-locale";
|
||||
import { FERTILE_HUE, FLOW_HUE, OVULATION_HUE } from "./phase-tokens";
|
||||
import type { CyclePrediction, CycleHistoryResponse } from "./types";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
/**
|
||||
* v1.15.0 — the predictions panel.
|
||||
@@ -81,9 +82,11 @@ export function PredictionsPanel({
|
||||
body={t("cycle.predictions.rawModeBody")}
|
||||
/>
|
||||
) : prediction == null ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("cycle.predictions.none")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("cycle.predictions.none")}
|
||||
/>
|
||||
) : prediction.stillLearning ? (
|
||||
<>
|
||||
<Learning
|
||||
@@ -215,9 +218,11 @@ function HistoryCard({
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{!stats || stats.avgLengthDays == null ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("cycle.history.none")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("cycle.history.none")}
|
||||
/>
|
||||
) : (
|
||||
<dl className="grid grid-cols-2 gap-4 sm:grid-cols-3">
|
||||
<Stat
|
||||
|
||||
@@ -44,6 +44,7 @@ import { tileKeyOf } from "./day-values-model";
|
||||
import { shiftDateKey } from "./day-url";
|
||||
import { useDay, usePrefetchDay } from "./use-day";
|
||||
import { useDayValueFormat } from "./use-day-value-format";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
/**
|
||||
* One day, as the layer shows it in every shell: a header with the date and
|
||||
@@ -282,9 +283,12 @@ export function DayView({
|
||||
) : data === undefined ? (
|
||||
<DaySkeleton />
|
||||
) : isEmpty ? (
|
||||
<p className="text-muted-foreground text-sm" data-slot="day-empty">
|
||||
{t("day.empty")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("day.empty")}
|
||||
data-slot="day-empty"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<DayRunning items={data.running} date={date} />
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from "@/lib/validations/inbound-documents";
|
||||
|
||||
import type { DocumentDescribeResult } from "./use-document-assist";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
/**
|
||||
* The line under a document AI action while its read runs in the background,
|
||||
@@ -220,9 +221,11 @@ export function AssistSuggestionReview({
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("documents.assist.empty")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("documents.assist.empty")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -31,6 +31,7 @@ import type {
|
||||
InboundDocumentDto,
|
||||
} from "@/lib/validations/inbound-documents";
|
||||
import { DOCUMENT_KIND_ICONS } from "./document-kind-meta";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
interface ListPage {
|
||||
documents: InboundDocumentDto[];
|
||||
@@ -140,9 +141,11 @@ export function DocumentLinkPicker({
|
||||
onRetry={() => list.refetch()}
|
||||
/>
|
||||
) : documents.length === 0 ? (
|
||||
<p className="text-muted-foreground py-4 text-center text-sm">
|
||||
{t("documents.linkPicker.empty")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("documents.linkPicker.empty")}
|
||||
/>
|
||||
) : (
|
||||
<ul className="max-h-[50vh] space-y-1.5 overflow-y-auto overscroll-contain">
|
||||
{documents.map((doc) => {
|
||||
|
||||
@@ -27,6 +27,7 @@ import { useAchievementsQuery } from "@/lib/queries/use-achievements-query";
|
||||
import type { AchievementProgress } from "@/lib/gamification/achievements";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
const iconMap: Record<string, LucideIcon> = {
|
||||
Activity,
|
||||
@@ -168,9 +169,11 @@ export function RecentAchievementsCard() {
|
||||
))}
|
||||
</ul>
|
||||
) : recent.length === 0 ? (
|
||||
<p className="text-muted-foreground py-2 text-sm">
|
||||
{t("achievements.dashboardCard.empty")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("achievements.dashboardCard.empty")}
|
||||
/>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{recent.map((achievement) => {
|
||||
|
||||
@@ -26,6 +26,7 @@ import { ILLNESS_SYMPTOM_CATALOG } from "./symptom-catalog";
|
||||
import { useIllnessDayLogList } from "./use-illness";
|
||||
import type { IllnessDayLogDTO } from "./types";
|
||||
import { QueryErrorRow } from "@/components/ui/query-error-row";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
function symptomLabelKey(key: string): string | null {
|
||||
return ILLNESS_SYMPTOM_CATALOG.find((s) => s.key === key)?.labelKey ?? null;
|
||||
@@ -64,9 +65,11 @@ function DayLogRow({ log }: { log: IllnessDayLogDTO }) {
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("illness.timeline.noSymptoms")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("illness.timeline.noSymptoms")}
|
||||
/>
|
||||
)}
|
||||
|
||||
{log.functionalImpact !== null ? (
|
||||
@@ -134,20 +137,18 @@ export function IllnessDayTimeline({
|
||||
onRetry={() => void refetch()}
|
||||
/>
|
||||
) : dayLogs.length === 0 ? (
|
||||
<div className="space-y-3">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("illness.timeline.empty")}
|
||||
</p>
|
||||
{canManageIllness && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
onClick={onLogDay}
|
||||
>
|
||||
{t("illness.logDay")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("illness.timeline.empty")}
|
||||
action={
|
||||
canManageIllness ? (
|
||||
<Button variant="outline" onClick={onLogDay}>
|
||||
{t("illness.logDay")}
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{dayLogs.map((log) => (
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
import { useIllnessInsights } from "./use-illness";
|
||||
import { QueryErrorRow } from "@/components/ui/query-error-row";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
export function IllnessInsightsCard({
|
||||
windowDays = 365,
|
||||
@@ -64,9 +65,11 @@ export function IllnessInsightsCard({
|
||||
onRetry={() => void counts.refetch()}
|
||||
/>
|
||||
) : !hasEpisodes ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("illness.insights.none")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("illness.insights.none")}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-4 text-sm">
|
||||
<p className="text-foreground">
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { InboundDocumentDto } from "@/lib/validations/inbound-documents";
|
||||
|
||||
import { DOCUMENT_KIND_ICONS } from "@/components/documents/document-kind-meta";
|
||||
import { QueryErrorRow } from "@/components/ui/query-error-row";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
/**
|
||||
* v1.29.x (S7) — the "Choose from documents" picker for attaching stored vault
|
||||
@@ -190,9 +191,11 @@ export function AttachmentPicker({
|
||||
onRetry={() => void list.refetch()}
|
||||
/>
|
||||
) : documents.length === 0 ? (
|
||||
<p className="text-muted-foreground py-6 text-center text-sm">
|
||||
{t("insights.coach.attach.pickerEmpty")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("insights.coach.attach.pickerEmpty")}
|
||||
/>
|
||||
) : (
|
||||
<ul className="max-h-[50vh] space-y-1.5 overflow-y-auto overscroll-contain">
|
||||
{documents.map((doc) => {
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
useDeleteCoachConversationWithUndo,
|
||||
} from "./use-coach";
|
||||
import { QueryErrorRow } from "@/components/ui/query-error-row";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
/**
|
||||
* The Coach's conversation list: the full history grouped by recency
|
||||
@@ -169,12 +170,13 @@ export function HistoryRail({
|
||||
className="px-2 py-3"
|
||||
/>
|
||||
) : groups.length === 0 ? (
|
||||
<p
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("insights.coach.historyEmpty")}
|
||||
className="px-2"
|
||||
data-slot="coach-history-empty"
|
||||
className="text-muted-foreground px-2 py-3 text-sm leading-relaxed"
|
||||
>
|
||||
{t("insights.coach.historyEmpty")}
|
||||
</p>
|
||||
/>
|
||||
) : (
|
||||
groups.map((group) => (
|
||||
<section
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
type IntradayResolution,
|
||||
type TensionWindow,
|
||||
} from "@/lib/analytics/intraday-pulse";
|
||||
import { ChartEmptyState } from "@/components/charts/chart-empty-state";
|
||||
|
||||
/** Slim DTO the intraday route returns (type-only — no server code bundled). */
|
||||
interface IntradayPulseDto {
|
||||
@@ -264,9 +265,10 @@ export function IntradayPulseChart({
|
||||
// A lone reading or two on a 24-hour axis drew a dot in an empty
|
||||
// field and called it the course of the day. Below a handful of
|
||||
// buckets the card says there is not enough for a course.
|
||||
<div className="text-muted-foreground flex h-44 items-center justify-center rounded-lg border border-dashed text-sm">
|
||||
{t("insights.intradayPulse.empty")}
|
||||
</div>
|
||||
<ChartEmptyState
|
||||
title={t("insights.intradayPulse.empty")}
|
||||
height={176}
|
||||
/>
|
||||
) : (
|
||||
<div className="touch-pan-y">
|
||||
<ResponsiveContainer width="100%" height={180}>
|
||||
|
||||
@@ -25,6 +25,7 @@ import type {
|
||||
EfficacyTargetView,
|
||||
} from "@/lib/medications/efficacy/build-efficacy";
|
||||
import { InlineAction } from "@/components/ui/inline-action";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
function primaryWithDelta(
|
||||
dto: MedicationEfficacyDTO,
|
||||
@@ -79,9 +80,11 @@ export function MedicationEfficacySummary({
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{rows.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("medications.efficacy.insights.empty")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("medications.efficacy.insights.empty")}
|
||||
/>
|
||||
) : (
|
||||
<ul className="divide-border/60 divide-y">
|
||||
{rows.map(({ med, target }) => {
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from "@/lib/mood/dimensions";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Segmented } from "@/components/ui/segmented";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
/**
|
||||
* v1.37 — the five level-A dimensions over time.
|
||||
@@ -129,9 +130,11 @@ export function MoodDimensionTrends({
|
||||
|
||||
if (present.length === 0) {
|
||||
return (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("insights.mood.dimensions.none")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("insights.mood.dimensions.none")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
moodPairsOf,
|
||||
type CorrelationDiscoveryResponse,
|
||||
} from "./mood-discovered-relations";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
/**
|
||||
* v1.12.7 — the single "What stands out" card.
|
||||
@@ -96,12 +97,12 @@ export function MoodWhatStandsOut({
|
||||
/>
|
||||
) : null}
|
||||
{!isLoading && !isError && data && !hasDiscovered ? (
|
||||
<p
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("insights.pattern.empty")}
|
||||
data-slot="mood-discovery-empty"
|
||||
className="text-muted-foreground text-sm"
|
||||
>
|
||||
{t("insights.pattern.empty")}
|
||||
</p>
|
||||
/>
|
||||
) : null}
|
||||
{hasDiscovered && (
|
||||
<MoodDiscoveredRelations
|
||||
|
||||
@@ -34,6 +34,7 @@ import { dateKeyOfInstant } from "../day/day-url";
|
||||
import type { LabResultDto } from "./types";
|
||||
import { useLabNumber } from "./use-lab-format";
|
||||
import { Segmented } from "@/components/ui/segmented";
|
||||
import { ChartEmptyState } from "@/components/charts/chart-empty-state";
|
||||
|
||||
/**
|
||||
* v1.18.1 — per-biomarker trend chart, in the dashboard's visual language.
|
||||
@@ -265,9 +266,7 @@ export function LabBiomarkerChart({
|
||||
</div>
|
||||
|
||||
{points.length === 0 ? (
|
||||
<p className="text-muted-foreground py-10 text-center text-sm">
|
||||
{t("labs.chart.empty")}
|
||||
</p>
|
||||
<ChartEmptyState title={t("labs.chart.empty")} height={160} />
|
||||
) : (
|
||||
<div>
|
||||
<div
|
||||
|
||||
@@ -13,6 +13,7 @@ import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
import { LabHistoryList } from "./lab-history-list";
|
||||
import type { LabResultDto, LabResultListResponse } from "./types";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
// Page size for the offset-paginated reading feed. The server caps a single
|
||||
// read at 500 (`listLabResultsSchema`); this stays well under that and keeps
|
||||
@@ -77,9 +78,11 @@ export function LabValuesList({ biomarkerId }: { biomarkerId: string }) {
|
||||
|
||||
if (readings.length === 0) {
|
||||
return (
|
||||
<p className="text-muted-foreground py-8 text-center text-sm">
|
||||
{t("labs.detail.emptyTitle")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("labs.detail.emptyTitle")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
type MeasurementReminder,
|
||||
} from "@/hooks/use-measurement-reminders";
|
||||
import { translateOrNull } from "@/lib/i18n/translate-code";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
const SUMMARY_LIMIT = 3;
|
||||
|
||||
@@ -168,16 +169,18 @@ export function VorsorgeDashboardCard() {
|
||||
// checkup is the harm behind that fall-through (§6).
|
||||
<QueryErrorRow inset error={error} onRetry={() => refetch()} />
|
||||
) : upcoming.length === 0 ? (
|
||||
<div className="space-y-2">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("measurementReminders.dashboard.empty")}
|
||||
</p>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/checkups">
|
||||
{t("measurementReminders.dashboard.openLink")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("measurementReminders.dashboard.empty")}
|
||||
action={
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/checkups">
|
||||
{t("measurementReminders.dashboard.openLink")}
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{upcoming.map((reminder) => {
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
type MeasurementReminder,
|
||||
} from "@/hooks/use-measurement-reminders";
|
||||
import { translateOrNull } from "@/lib/i18n/translate-code";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
function resolveLabel(
|
||||
reminder: MeasurementReminder,
|
||||
@@ -104,9 +105,11 @@ export function VorsorgeSettings() {
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-24 w-full" />
|
||||
) : (reminders?.length ?? 0) === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("measurementReminders.manage.empty")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("measurementReminders.manage.empty")}
|
||||
/>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="divide-border divide-y p-0">
|
||||
|
||||
@@ -1936,9 +1936,11 @@ function DayDrillDown({
|
||||
const rows = data?.measurements ?? [];
|
||||
if (rows.length === 0) {
|
||||
return (
|
||||
<div className="text-muted-foreground py-2 text-xs">
|
||||
{t("measurements.emptyDescription")}
|
||||
</div>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("measurements.emptyDescription")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// v1.11.5 — for SLEEP_DURATION the drill-down rows are stage segments;
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -49,6 +49,7 @@ import { dateKeyOfInstant } from "../day/day-url";
|
||||
import { INSTRUMENTS } from "@/lib/mental-health/instruments";
|
||||
|
||||
import type { AssessmentRow, InstrumentId } from "./types";
|
||||
import { ChartEmptyState } from "@/components/charts/chart-empty-state";
|
||||
|
||||
interface ChartPoint {
|
||||
timestamp: number;
|
||||
@@ -106,9 +107,7 @@ export function AssessmentHistoryChart({
|
||||
|
||||
if (points.length === 0) {
|
||||
return (
|
||||
<p className="text-muted-foreground py-10 text-center text-sm">
|
||||
{t("mentalHealth.history.empty")}
|
||||
</p>
|
||||
<ChartEmptyState title={t("mentalHealth.history.empty")} height={160} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { reorderById } from "@/lib/insights-layout-reorder";
|
||||
import { prefersReducedMotion } from "@/lib/charts/reduced-motion";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
/**
|
||||
* v1.18.6 — generic, controlled reorder list shared by the
|
||||
@@ -97,9 +98,11 @@ export function ModuleOrderEditor({ items, onChange }: ModuleOrderEditorProps) {
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("moduleList.reorder.empty")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("moduleList.reorder.empty")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
type ManageCatalog,
|
||||
type ManageTag,
|
||||
} from "./use-mood-tag-manage";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
/**
|
||||
* v1.17 — archived custom tags for `/settings/mood`. Archiving keeps
|
||||
@@ -109,12 +110,12 @@ export function ArchivedTagsCard({ catalog }: ArchivedTagsCardProps) {
|
||||
</p>
|
||||
|
||||
{archived.length === 0 ? (
|
||||
<p
|
||||
className="text-muted-foreground text-sm"
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("mood.manage.archivedEmpty")}
|
||||
data-slot="mood-archived-empty"
|
||||
>
|
||||
{t("mood.manage.archivedEmpty")}
|
||||
</p>
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{archived.map((tag) => {
|
||||
|
||||
@@ -81,6 +81,7 @@ import {
|
||||
type ManageCatalog,
|
||||
type MoodTagLayoutResolved,
|
||||
} from "./use-mood-tag-manage";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
/**
|
||||
* v1.17 — group management card for `/settings/mood`. Lists every
|
||||
@@ -245,12 +246,12 @@ export function TagGroupsCard({ catalog }: TagGroupsCardProps) {
|
||||
</div>
|
||||
|
||||
{customGroupCount === 0 && (
|
||||
<p
|
||||
className="text-muted-foreground text-sm"
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("mood.manage.groupEmpty")}
|
||||
data-slot="mood-groups-empty"
|
||||
>
|
||||
{t("mood.manage.groupEmpty")}
|
||||
</p>
|
||||
/>
|
||||
)}
|
||||
|
||||
<DndContext
|
||||
|
||||
@@ -74,6 +74,7 @@ import {
|
||||
type ManageTag,
|
||||
type MoodTagLayoutResolved,
|
||||
} from "./use-mood-tag-manage";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
/**
|
||||
* v1.17 — per-group tag management for `/settings/mood`. Mirrors the
|
||||
@@ -317,7 +318,7 @@ export function TagManagerCard({ catalog }: TagManagerCardProps) {
|
||||
</div>
|
||||
|
||||
{visible.categories.length === 0 && (
|
||||
<p className="text-muted-foreground text-sm">{t("common.noData")}</p>
|
||||
<EmptyState variant="plain" size="compact" title={t("common.noData")} />
|
||||
)}
|
||||
|
||||
{visible.categories.map((category) => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { apiGet } from "@/lib/api/api-fetch";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import type { ProfileSummaryPayload } from "@/lib/records/profile-summary";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
function profileFactLabel(label: string, t: (key: string) => string): string {
|
||||
const keys: Record<string, string> = {
|
||||
@@ -61,9 +62,11 @@ export function ProfileSummary({
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("records.allergies.emptyTitle")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("records.allergies.emptyTitle")}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -88,9 +91,11 @@ export function ProfileSummary({
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("records.family.emptyTitle")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("records.family.emptyTitle")}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -112,9 +117,11 @@ export function ProfileSummary({
|
||||
))}
|
||||
</dl>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("records.profileFacts.notRecorded")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("records.profileFacts.notRecorded")}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
userDayKey,
|
||||
validTimezoneOr,
|
||||
} from "@/lib/tz/format";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
interface GeocodeResult {
|
||||
lat: number;
|
||||
@@ -343,9 +344,11 @@ export function EnvironmentSection() {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("settings.sections.environment.home.none")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("settings.sections.environment.home.none")}
|
||||
/>
|
||||
)}
|
||||
<form
|
||||
className="flex gap-2"
|
||||
@@ -378,9 +381,11 @@ export function EnvironmentSection() {
|
||||
{homeSearched &&
|
||||
!searchHome.isPending &&
|
||||
homeResults.length === 0 && (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("settings.sections.environment.noResults")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("settings.sections.environment.noResults")}
|
||||
/>
|
||||
)}
|
||||
{homeResults.length > 0 && (
|
||||
<ul className="divide-border divide-y rounded-md border">
|
||||
@@ -467,9 +472,11 @@ export function EnvironmentSection() {
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("settings.sections.environment.travel.none")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("settings.sections.environment.travel.none")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<form
|
||||
@@ -505,9 +512,11 @@ export function EnvironmentSection() {
|
||||
{travelSearched &&
|
||||
!searchTravel.isPending &&
|
||||
travelResults.length === 0 && (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("settings.sections.environment.noResults")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("settings.sections.environment.noResults")}
|
||||
/>
|
||||
)}
|
||||
{travelResults.length > 0 && (
|
||||
<ul className="divide-border divide-y rounded-md border">
|
||||
|
||||
@@ -29,6 +29,7 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { InboundDocumentDto } from "@/lib/validations/inbound-documents";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
/** A picked document, carried by id + a display title (for the owner chips). */
|
||||
export interface PickedDocument {
|
||||
@@ -159,9 +160,11 @@ export function ShareDocumentPicker({
|
||||
onRetry={() => list.refetch()}
|
||||
/>
|
||||
) : documents.length === 0 ? (
|
||||
<p className="text-muted-foreground py-4 text-center text-sm">
|
||||
{t("settings.sharing.pickerEmpty")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("settings.sharing.pickerEmpty")}
|
||||
/>
|
||||
) : (
|
||||
<ul
|
||||
className="max-h-[50vh] space-y-1.5 overflow-y-auto overscroll-contain"
|
||||
|
||||
@@ -68,6 +68,7 @@ import {
|
||||
useSymptomEvents,
|
||||
useUpdateSymptomDefinition,
|
||||
} from "./use-symptoms";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
@@ -489,9 +490,11 @@ export function SymptomsSection() {
|
||||
{t("symptoms.section.recent")}
|
||||
</p>
|
||||
{days.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("symptoms.section.recentEmpty")}
|
||||
</p>
|
||||
<EmptyState
|
||||
variant="plain"
|
||||
size="compact"
|
||||
title={t("symptoms.section.recentEmpty")}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{days.map((group) => (
|
||||
|
||||
Reference in new issue
Block a user