diff --git a/src/__tests__/empty-state-ratchet.test.ts b/src/__tests__/empty-state-ratchet.test.ts index ec19f87db..028e945b4 100644 --- a/src/__tests__/empty-state-ratchet.test.ts +++ b/src/__tests__/empty-state-ratchet.test.ts @@ -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; diff --git a/src/app/insights/medications/page.tsx b/src/app/insights/medications/page.tsx index 08fccaf84..21864bbef 100644 --- a/src/app/insights/medications/page.tsx +++ b/src/app/insights/medications/page.tsx @@ -394,11 +394,7 @@ function MedicationComplianceCalendar({ } if (!data?.dailyCompliance) { - return ( -
- {t("insights.complianceNoData")} -
- ); + return ; } return ( diff --git a/src/app/insights/workouts/page-client.tsx b/src/app/insights/workouts/page-client.tsx index 0cf9cdd60..c307d65af 100644 --- a/src/app/insights/workouts/page-client.tsx +++ b/src/app/insights/workouts/page-client.tsx @@ -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() { /> ) : ( -

- {t("insights.workouts.filterEmpty")} -

+ /> )} )} diff --git a/src/components/admin/ai-quality-section.tsx b/src/components/admin/ai-quality-section.tsx index a4bc7ee67..97ee96201 100644 --- a/src/components/admin/ai-quality-section.tsx +++ b/src/components/admin/ai-quality-section.tsx @@ -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 ? ( -

- {t("admin.aiQuality.empty")} -

+ ) : (
diff --git a/src/components/admin/coach-feedback-section.tsx b/src/components/admin/coach-feedback-section.tsx index d62a4f8ba..09ac117bc 100644 --- a/src/components/admin/coach-feedback-section.tsx +++ b/src/components/admin/coach-feedback-section.tsx @@ -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 ? ( -

- {t("admin.coachFeedback.empty")} -

+ ) : (
diff --git a/src/components/admin/encryption-section.tsx b/src/components/admin/encryption-section.tsx index 58d8fe7c9..4ba20e22b 100644 --- a/src/components/admin/encryption-section.tsx +++ b/src/components/admin/encryption-section.tsx @@ -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 ? ( -

- {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 diff --git a/src/components/admin/host-metrics-chart.tsx b/src/components/admin/host-metrics-chart.tsx index 20377f6be..702b779fe 100644 --- a/src/components/admin/host-metrics-chart.tsx +++ b/src/components/admin/host-metrics-chart.tsx @@ -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() { } /> -

- {t("admin.hostMetrics.empty")} -

+ ); } diff --git a/src/components/admin/provider-health-section.tsx b/src/components/admin/provider-health-section.tsx index 3032ee8ef..4df470bea 100644 --- a/src/components/admin/provider-health-section.tsx +++ b/src/components/admin/provider-health-section.tsx @@ -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 ? ( -

- {t("admin.providerHealth.empty")} -

+ ) : (
    {providers.map((p) => ( diff --git a/src/components/admin/reminders-section.tsx b/src/components/admin/reminders-section.tsx index 4f39e8361..9856f2e4c 100644 --- a/src/components/admin/reminders-section.tsx +++ b/src/components/admin/reminders-section.tsx @@ -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 && ( -

    - {t("admin.notificationHealth.empty")} -

    + )} {data && data.channels.length > 0 && ( diff --git a/src/components/admin/system-status-section.tsx b/src/components/admin/system-status-section.tsx index 6282cb243..8f520e46c 100644 --- a/src/components/admin/system-status-section.tsx +++ b/src/components/admin/system-status-section.tsx @@ -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 ? ( -

    - {t("admin.failingJobs.none", { hours: failingJobs.windowHours })} -

    + /> ) : (
      {failingJobs.queues.map((queue) => ( diff --git a/src/components/charts/medication-compliance-chart.tsx b/src/components/charts/medication-compliance-chart.tsx index fc10e8a23..2abcb1b26 100644 --- a/src/components/charts/medication-compliance-chart.tsx +++ b/src/components/charts/medication-compliance-chart.tsx @@ -573,9 +573,7 @@ export function MedicationComplianceChart({ } /> ) : !hasData ? ( -
      - {t("charts.noData")} -
      + ) : chartData.length < 3 ? ( // v1.4.16 — sparse-data placeholder consistent with the // BP/weight/pulse/mood charts. diff --git a/src/components/charts/mood-chart.tsx b/src/components/charts/mood-chart.tsx index bbfb9196b..28e9219ed 100644 --- a/src/components/charts/mood-chart.tsx +++ b/src/components/charts/mood-chart.tsx @@ -732,9 +732,7 @@ export function MoodChart({ height={CHART_HEIGHT_PX} /> ) : !chartData?.length ? ( -
      - {t("charts.noData")} -
      + ) : chartData.length < 3 ? ( // v1.4.16 — sparse-data placeholder consistent with the // BP/weight/pulse charts. Height tracks the chart strip's diff --git a/src/components/clinician/identity-sections.tsx b/src/components/clinician/identity-sections.tsx index 934982b55..9dfef4aef 100644 --- a/src/components/clinician/identity-sections.tsx +++ b/src/components/clinician/identity-sections.tsx @@ -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")}

      ) : rows.length === 0 ? ( -

      - {t("clinicianView.sectionEmpty")} -

      + ) : ( rows.map((row) => ( ) : empty ? ( -

      - {t("clinicianView.sectionEmpty")} -

      + ) : ( children )} diff --git a/src/components/custom-metrics/custom-metric-chart.tsx b/src/components/custom-metrics/custom-metric-chart.tsx index fca1346c5..3834a72f1 100644 --- a/src/components/custom-metrics/custom-metric-chart.tsx +++ b/src/components/custom-metrics/custom-metric-chart.tsx @@ -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({ {points.length === 0 ? ( -

      - {t("customMetrics.chart.empty")} -

      + ) : (
      refetch()} /> ) : metrics.length === 0 ? ( - - - -

      - {t("customMetrics.emptyDescription")} -

      + } + title={t("customMetrics.emptyDescription")} + action={ -
      -
      + } + /> ) : ( diff --git a/src/components/custom-metrics/custom-metric-values-list.tsx b/src/components/custom-metrics/custom-metric-values-list.tsx index 2607996b1..46e4bd223 100644 --- a/src/components/custom-metrics/custom-metric-values-list.tsx +++ b/src/components/custom-metrics/custom-metric-values-list.tsx @@ -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 ( -

      - {t("customMetrics.detail.emptyTitle")} -

      + ); } diff --git a/src/components/cycle/cycle-history-chart.tsx b/src/components/cycle/cycle-history-chart.tsx index 1d761e6cb..ad12c6f55 100644 --- a/src/components/cycle/cycle-history-chart.tsx +++ b/src/components/cycle/cycle-history-chart.tsx @@ -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({ {!hasData ? ( -

      - {t("cycle.history.none")} -

      + ) : ( <> {/* Stat chips — avg / variability / regularity classification. */} diff --git a/src/components/cycle/cycle-symptom-patterns.tsx b/src/components/cycle/cycle-symptom-patterns.tsx index a9d0ee94f..9457f60e1 100644 --- a/src/components/cycle/cycle-symptom-patterns.tsx +++ b/src/components/cycle/cycle-symptom-patterns.tsx @@ -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) { {rows.length === 0 ? ( -

      - {t("cycle.insights.symptomPatternsEmpty")} -

      + ) : (
        {rows.map((row) => { diff --git a/src/components/cycle/predictions-panel.tsx b/src/components/cycle/predictions-panel.tsx index b0052dbac..b13ae226e 100644 --- a/src/components/cycle/predictions-panel.tsx +++ b/src/components/cycle/predictions-panel.tsx @@ -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 ? ( -

        - {t("cycle.predictions.none")} -

        + ) : prediction.stillLearning ? ( <> {!stats || stats.avgLengthDays == null ? ( -

        - {t("cycle.history.none")} -

        + ) : (
        ) : isEmpty ? ( -

        - {t("day.empty")} -

        + ) : ( <> diff --git a/src/components/documents/document-ai-panels.tsx b/src/components/documents/document-ai-panels.tsx index daf342284..b2c5e8d66 100644 --- a/src/components/documents/document-ai-panels.tsx +++ b/src/components/documents/document-ai-panels.tsx @@ -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}
      ) : ( -

      - {t("documents.assist.empty")} -

      + )}
      ); diff --git a/src/components/documents/document-link-picker.tsx b/src/components/documents/document-link-picker.tsx index 8e5357132..c80956ae2 100644 --- a/src/components/documents/document-link-picker.tsx +++ b/src/components/documents/document-link-picker.tsx @@ -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 ? ( -

      - {t("documents.linkPicker.empty")} -

      + ) : (
        {documents.map((doc) => { diff --git a/src/components/gamification/recent-achievements-card.tsx b/src/components/gamification/recent-achievements-card.tsx index 2c778df45..e3ba79fd6 100644 --- a/src/components/gamification/recent-achievements-card.tsx +++ b/src/components/gamification/recent-achievements-card.tsx @@ -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 = { Activity, @@ -168,9 +169,11 @@ export function RecentAchievementsCard() { ))}
      ) : recent.length === 0 ? ( -

      - {t("achievements.dashboardCard.empty")} -

      + ) : (
        {recent.map((achievement) => { diff --git a/src/components/illness/illness-day-timeline.tsx b/src/components/illness/illness-day-timeline.tsx index dbf60b7b6..47a636a7f 100644 --- a/src/components/illness/illness-day-timeline.tsx +++ b/src/components/illness/illness-day-timeline.tsx @@ -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 }) { })} ) : ( -

        - {t("illness.timeline.noSymptoms")} -

        + )} {log.functionalImpact !== null ? ( @@ -134,20 +137,18 @@ export function IllnessDayTimeline({ onRetry={() => void refetch()} /> ) : dayLogs.length === 0 ? ( -
        -

        - {t("illness.timeline.empty")} -

        - {canManageIllness && ( - - )} -
        + + {t("illness.logDay")} + + ) : undefined + } + /> ) : (
        {dayLogs.map((log) => ( diff --git a/src/components/illness/illness-insights-card.tsx b/src/components/illness/illness-insights-card.tsx index b15c14335..c74e53a1e 100644 --- a/src/components/illness/illness-insights-card.tsx +++ b/src/components/illness/illness-insights-card.tsx @@ -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 ? ( -

        - {t("illness.insights.none")} -

        + ) : (

        diff --git a/src/components/insights/coach-panel/attachment-picker.tsx b/src/components/insights/coach-panel/attachment-picker.tsx index e230593c4..e733d825f 100644 --- a/src/components/insights/coach-panel/attachment-picker.tsx +++ b/src/components/insights/coach-panel/attachment-picker.tsx @@ -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 ? ( -

        - {t("insights.coach.attach.pickerEmpty")} -

        + ) : (
          {documents.map((doc) => { diff --git a/src/components/insights/coach-panel/history-rail.tsx b/src/components/insights/coach-panel/history-rail.tsx index a1b8f3178..6afb178d9 100644 --- a/src/components/insights/coach-panel/history-rail.tsx +++ b/src/components/insights/coach-panel/history-rail.tsx @@ -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 ? ( -

          - {t("insights.coach.historyEmpty")} -

          + /> ) : ( groups.map((group) => (
          - {t("insights.intradayPulse.empty")} -
        + ) : (
        diff --git a/src/components/insights/medication-efficacy-summary.tsx b/src/components/insights/medication-efficacy-summary.tsx index 5be57af72..3b1d52ff9 100644 --- a/src/components/insights/medication-efficacy-summary.tsx +++ b/src/components/insights/medication-efficacy-summary.tsx @@ -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({ {rows.length === 0 ? ( -

        - {t("medications.efficacy.insights.empty")} -

        + ) : (
          {rows.map(({ med, target }) => { diff --git a/src/components/insights/mood/mood-dimension-trends.tsx b/src/components/insights/mood/mood-dimension-trends.tsx index a628ddb7e..d3f187c72 100644 --- a/src/components/insights/mood/mood-dimension-trends.tsx +++ b/src/components/insights/mood/mood-dimension-trends.tsx @@ -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 ( -

          - {t("insights.mood.dimensions.none")} -

          + ); } diff --git a/src/components/insights/mood/mood-what-stands-out.tsx b/src/components/insights/mood/mood-what-stands-out.tsx index c16adb3a7..77b8124c6 100644 --- a/src/components/insights/mood/mood-what-stands-out.tsx +++ b/src/components/insights/mood/mood-what-stands-out.tsx @@ -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 ? ( -

          - {t("insights.pattern.empty")} -

          + /> ) : null} {hasDiscovered && ( {points.length === 0 ? ( -

          - {t("labs.chart.empty")} -

          + ) : (
          - {t("labs.detail.emptyTitle")} -

          + ); } diff --git a/src/components/measurement-reminders/vorsorge-dashboard-card.tsx b/src/components/measurement-reminders/vorsorge-dashboard-card.tsx index 161f6551e..7fbe3287a 100644 --- a/src/components/measurement-reminders/vorsorge-dashboard-card.tsx +++ b/src/components/measurement-reminders/vorsorge-dashboard-card.tsx @@ -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). refetch()} /> ) : upcoming.length === 0 ? ( -
          -

          - {t("measurementReminders.dashboard.empty")} -

          - -
          + + + {t("measurementReminders.dashboard.openLink")} + + + } + /> ) : (
            {upcoming.map((reminder) => { diff --git a/src/components/measurement-reminders/vorsorge-settings.tsx b/src/components/measurement-reminders/vorsorge-settings.tsx index e6649a806..3fe69268b 100644 --- a/src/components/measurement-reminders/vorsorge-settings.tsx +++ b/src/components/measurement-reminders/vorsorge-settings.tsx @@ -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 ? ( ) : (reminders?.length ?? 0) === 0 ? ( -

            - {t("measurementReminders.manage.empty")} -

            + ) : ( diff --git a/src/components/measurements/measurement-list.tsx b/src/components/measurements/measurement-list.tsx index 15c01cef7..49ca00a81 100644 --- a/src/components/measurements/measurement-list.tsx +++ b/src/components/measurements/measurement-list.tsx @@ -1936,9 +1936,11 @@ function DayDrillDown({ const rows = data?.measurements ?? []; if (rows.length === 0) { return ( -
            - {t("measurements.emptyDescription")} -
            + ); } // v1.11.5 — for SLEEP_DURATION the drill-down rows are stage segments; diff --git a/src/components/medications/detail/efficacy/efficacy-tab.tsx b/src/components/medications/detail/efficacy/efficacy-tab.tsx index 1fd9b33f5..1e7b7e24c 100644 --- a/src/components/medications/detail/efficacy/efficacy-tab.tsx +++ b/src/components/medications/detail/efficacy/efficacy-tab.tsx @@ -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 // `` as a bare 220px `ResponsiveContainer` with no chrome of its own. @@ -299,9 +300,11 @@ function TargetCard({ />
          ) : ( -

          - {t("medications.efficacy.noSeries")} -

          + )} diff --git a/src/components/medications/detail/medication-detail-tabs.tsx b/src/components/medications/detail/medication-detail-tabs.tsx index 62cb75fe6..140e60ab6 100644 --- a/src/components/medications/detail/medication-detail-tabs.tsx +++ b/src/components/medications/detail/medication-detail-tabs.tsx @@ -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} /> ) : ( -

          - {t("medications.detail.uebersicht.complianceEmpty")} -

          + )} )} diff --git a/src/components/medications/dose-history-ledger.tsx b/src/components/medications/dose-history-ledger.tsx index 0ff9aa79a..1df7caeb2 100644 --- a/src/components/medications/dose-history-ledger.tsx +++ b/src/components/medications/dose-history-ledger.tsx @@ -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 && ( -

          - {t("medications.detail.verlauf.empty")} -

          + )} {data && diff --git a/src/components/medications/log-injection-site-dialog.tsx b/src/components/medications/log-injection-site-dialog.tsx index 8e3793936..19b31c5d1 100644 --- a/src/components/medications/log-injection-site-dialog.tsx +++ b/src/components/medications/log-injection-site-dialog.tsx @@ -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 ? ( -

          - {t("medications.logInjectionSiteNoneAvailable")} -

          + ) : ( <> {medications.length === 0 ? ( -

          - {t("medications.logIntake.noMedications")} -

          + ) : (
          diff --git a/src/components/medications/medication-order-editor.tsx b/src/components/medications/medication-order-editor.tsx index 3d2adcdca..3c26dcb28 100644 --- a/src/components/medications/medication-order-editor.tsx +++ b/src/components/medications/medication-order-editor.tsx @@ -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).
          {medications.length === 0 && ( -

          - {t("medications.emptyTitle")} -

          + )} {activeIds.length > 0 && ( = new Set([ @@ -520,9 +521,11 @@ export function CompositionSection({ } > {ingredients.length === 0 ? ( -

          - {t("medications.composition.empty")} -

          + ) : (
            diff --git a/src/components/medications/sections/courses-section.tsx b/src/components/medications/sections/courses-section.tsx index b2edada7f..4f5765c37 100644 --- a/src/components/medications/sections/courses-section.tsx +++ b/src/components/medications/sections/courses-section.tsx @@ -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 = { @@ -262,9 +263,11 @@ export function CoursesRow({
          {courses.length === 0 ? ( -

          - {t("medications.course.none")} -

          + ) : (
            {courses.map((course, index) => ( diff --git a/src/components/medications/side-effects-section.tsx b/src/components/medications/side-effects-section.tsx index b28beff76..365de1d29 100644 --- a/src/components/medications/side-effects-section.tsx +++ b/src/components/medications/side-effects-section.tsx @@ -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 && ( -

            - {t("medications.sideEffects.emptyState")} -

            + )} {!isLoading && !isError && items.length > 0 && ( diff --git a/src/components/medications/titration-timeline.tsx b/src/components/medications/titration-timeline.tsx index 1bc91e710..9ebe31df8 100644 --- a/src/components/medications/titration-timeline.tsx +++ b/src/components/medications/titration-timeline.tsx @@ -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 ? ( -

            - {t("medications.titration.empty")} -

            + /> ) : (
              {steps.map((step, i) => ( diff --git a/src/components/mental-health/assessment-history-chart.tsx b/src/components/mental-health/assessment-history-chart.tsx index 1cdf57adb..bf72551a0 100644 --- a/src/components/mental-health/assessment-history-chart.tsx +++ b/src/components/mental-health/assessment-history-chart.tsx @@ -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 ( -

              - {t("mentalHealth.history.empty")} -

              + ); } diff --git a/src/components/module-list/module-order-editor.tsx b/src/components/module-list/module-order-editor.tsx index a589d7407..f1d20c54c 100644 --- a/src/components/module-list/module-order-editor.tsx +++ b/src/components/module-list/module-order-editor.tsx @@ -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 ( -

              - {t("moduleList.reorder.empty")} -

              + ); } diff --git a/src/components/mood/manage/archived-tags-card.tsx b/src/components/mood/manage/archived-tags-card.tsx index 21bef8aa3..f88ffd2ea 100644 --- a/src/components/mood/manage/archived-tags-card.tsx +++ b/src/components/mood/manage/archived-tags-card.tsx @@ -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) {

              {archived.length === 0 ? ( -

              - {t("mood.manage.archivedEmpty")} -

              + /> ) : (
              {archived.map((tag) => { diff --git a/src/components/mood/manage/tag-groups-card.tsx b/src/components/mood/manage/tag-groups-card.tsx index 9efec6176..ccdfac132 100644 --- a/src/components/mood/manage/tag-groups-card.tsx +++ b/src/components/mood/manage/tag-groups-card.tsx @@ -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) {
              {customGroupCount === 0 && ( -

              - {t("mood.manage.groupEmpty")} -

              + /> )} {visible.categories.length === 0 && ( -

              {t("common.noData")}

              + )} {visible.categories.map((category) => { diff --git a/src/components/records/profile-summary.tsx b/src/components/records/profile-summary.tsx index 226e02eee..d856e58a5 100644 --- a/src/components/records/profile-summary.tsx +++ b/src/components/records/profile-summary.tsx @@ -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 = { @@ -61,9 +62,11 @@ export function ProfileSummary({ ))}
          ) : ( -

          - {t("records.allergies.emptyTitle")} -

          + )} @@ -88,9 +91,11 @@ export function ProfileSummary({ ))}
        ) : ( -

        - {t("records.family.emptyTitle")} -

        + )}
        @@ -112,9 +117,11 @@ export function ProfileSummary({ ))} ) : ( -

        - {t("records.profileFacts.notRecorded")} -

        + )} diff --git a/src/components/settings/environment-section.tsx b/src/components/settings/environment-section.tsx index 1186d58e6..cdfe0bb7b 100644 --- a/src/components/settings/environment-section.tsx +++ b/src/components/settings/environment-section.tsx @@ -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() { )}
        ) : ( -

        - {t("settings.sections.environment.home.none")} -

        + )}
        - {t("settings.sections.environment.noResults")} -

        + )} {homeResults.length > 0 && (
          @@ -467,9 +472,11 @@ export function EnvironmentSection() { ))}
        ) : ( -

        - {t("settings.sections.environment.travel.none")} -

        + )} - {t("settings.sections.environment.noResults")} -

        + )} {travelResults.length > 0 && (
          diff --git a/src/components/settings/share-document-picker.tsx b/src/components/settings/share-document-picker.tsx index a00495378..4778a708f 100644 --- a/src/components/settings/share-document-picker.tsx +++ b/src/components/settings/share-document-picker.tsx @@ -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 ? ( -

          - {t("settings.sharing.pickerEmpty")} -

          + ) : (
            {days.length === 0 ? ( -

            - {t("symptoms.section.recentEmpty")} -

            + ) : (
            {days.map((group) => (