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