diff --git a/messages/de.json b/messages/de.json index c97fb115e..e7def3d80 100644 --- a/messages/de.json +++ b/messages/de.json @@ -7129,14 +7129,7 @@ "unhiddenToast": "Biomarker wiederhergestellt", "hideError": "Biomarker konnte nicht aktualisiert werden", "allHidden": "Alle Biomarker sind ausgeblendet.", - "hiddenSection": "Ausgeblendet", - "targetRange": { - "title": "Zielbereich anpassen", - "description": "Lege die untere und obere Referenz für diesen Marker fest. Lass ein Feld leer für eine offene Grenze.", - "save": "Bereich speichern", - "savedToast": "Zielbereich aktualisiert", - "saveError": "Zielbereich konnte nicht aktualisiert werden" - } + "hiddenSection": "Ausgeblendet" }, "catalog": { "total-cholesterol": "Gesamtcholesterin", diff --git a/messages/en.json b/messages/en.json index 3f2844f28..bd894e35d 100644 --- a/messages/en.json +++ b/messages/en.json @@ -7129,14 +7129,7 @@ "unhiddenToast": "Biomarker restored", "hideError": "Could not update the biomarker", "allHidden": "Every biomarker is hidden.", - "hiddenSection": "Hidden", - "targetRange": { - "title": "Adjust target range", - "description": "Set the reference low and high for this marker. Leave a field empty for an open-ended bound.", - "save": "Save range", - "savedToast": "Target range updated", - "saveError": "Could not update the target range" - } + "hiddenSection": "Hidden" }, "catalog": { "total-cholesterol": "Total cholesterol", diff --git a/messages/es.json b/messages/es.json index 98aee4b3d..503038b73 100644 --- a/messages/es.json +++ b/messages/es.json @@ -7129,14 +7129,7 @@ "unhiddenToast": "Biomarcador restaurado", "hideError": "No se pudo actualizar el biomarcador", "allHidden": "Todos los biomarcadores están ocultos.", - "hiddenSection": "Ocultos", - "targetRange": { - "title": "Ajustar el rango objetivo", - "description": "Define la referencia mínima y máxima de este marcador. Deja un campo vacío para un límite abierto.", - "save": "Guardar rango", - "savedToast": "Rango objetivo actualizado", - "saveError": "No se pudo actualizar el rango objetivo" - } + "hiddenSection": "Ocultos" }, "catalog": { "total-cholesterol": "Colesterol total", diff --git a/messages/fr.json b/messages/fr.json index 80c0be5fe..905061214 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -7129,14 +7129,7 @@ "unhiddenToast": "Biomarqueur restauré", "hideError": "Impossible de mettre à jour le biomarqueur", "allHidden": "Tous les biomarqueurs sont masqués.", - "hiddenSection": "Masqués", - "targetRange": { - "title": "Ajuster la plage cible", - "description": "Définis la référence basse et haute de ce marqueur. Laisse un champ vide pour une limite ouverte.", - "save": "Enregistrer la plage", - "savedToast": "Plage cible mise à jour", - "saveError": "Impossible de mettre à jour la plage cible" - } + "hiddenSection": "Masqués" }, "catalog": { "total-cholesterol": "Cholestérol total", diff --git a/messages/it.json b/messages/it.json index 03a3fa8ed..e8f815b74 100644 --- a/messages/it.json +++ b/messages/it.json @@ -7129,14 +7129,7 @@ "unhiddenToast": "Biomarcatore ripristinato", "hideError": "Impossibile aggiornare il biomarcatore", "allHidden": "Tutti i biomarcatori sono nascosti.", - "hiddenSection": "Nascosti", - "targetRange": { - "title": "Regola l'intervallo obiettivo", - "description": "Imposta il riferimento minimo e massimo per questo marcatore. Lascia un campo vuoto per un limite aperto.", - "save": "Salva intervallo", - "savedToast": "Intervallo obiettivo aggiornato", - "saveError": "Impossibile aggiornare l'intervallo obiettivo" - } + "hiddenSection": "Nascosti" }, "catalog": { "total-cholesterol": "Colesterolo totale", diff --git a/messages/pl.json b/messages/pl.json index 4b2ba446c..25388622c 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -7129,14 +7129,7 @@ "unhiddenToast": "Biomarker przywrócony", "hideError": "Nie udało się zaktualizować biomarkera", "allHidden": "Wszystkie biomarkery są ukryte.", - "hiddenSection": "Ukryte", - "targetRange": { - "title": "Dostosuj zakres docelowy", - "description": "Ustaw dolną i górną wartość referencyjną dla tego markera. Pozostaw pole puste, aby granica była otwarta.", - "save": "Zapisz zakres", - "savedToast": "Zaktualizowano zakres docelowy", - "saveError": "Nie udało się zaktualizować zakresu docelowego" - } + "hiddenSection": "Ukryte" }, "catalog": { "total-cholesterol": "Cholesterol całkowity", diff --git a/src/components/data-list/delete-button.tsx b/src/components/data-list/delete-button.tsx index 47ee7d5dd..c963e0026 100644 --- a/src/components/data-list/delete-button.tsx +++ b/src/components/data-list/delete-button.tsx @@ -32,6 +32,7 @@ export function DeleteButton({ description, cancelLabel, confirmLabel, + triggerTitle, className = "", iconClassName = "h-3.5 w-3.5", }: { @@ -40,6 +41,8 @@ export function DeleteButton({ description: string; cancelLabel?: string; confirmLabel?: string; + /** Native hover tooltip on the trigger; defaults to the accessible name. */ + triggerTitle?: string; className?: string; iconClassName?: string; }) { @@ -52,6 +55,7 @@ export function DeleteButton({ size="icon-lg" className={`text-destructive ${className}`} aria-label={confirmLabel ?? t("common.delete")} + title={triggerTitle ?? confirmLabel ?? t("common.delete")} > diff --git a/src/components/labs/__tests__/lab-biomarker-detail.test.tsx b/src/components/labs/__tests__/lab-biomarker-detail.test.tsx index 7da3247d5..bec4b97b6 100644 --- a/src/components/labs/__tests__/lab-biomarker-detail.test.tsx +++ b/src/components/labs/__tests__/lab-biomarker-detail.test.tsx @@ -136,4 +136,22 @@ describe("", () => { // skeleton geometry — proof the assessment card mounted on the page. expect(html).toContain('data-testid="insight-status-card-loading"'); }); + + it("renders the rich catalog description for a catalogued marker", () => { + // "LDL Cholesterol" resolves to the `ldl` catalog slug (case-insensitive), + // so the page shows the per-biomarker explainer, not the generic fallback. + expect(html).toContain("low-density lipoprotein"); + expect(html).not.toContain( + "A lab biomarker you track. Add readings to follow", + ); + }); + + it("offers the edit + delete controls with hover tooltips", () => { + expect(html).toContain('title="Edit"'); + expect(html).toContain('title="Delete"'); + }); + + it("drops the standalone adjust-target-range control", () => { + expect(html).not.toContain("Adjust target range"); + }); }); diff --git a/src/components/labs/lab-biomarker-detail.tsx b/src/components/labs/lab-biomarker-detail.tsx index 166586105..8f1c7ca47 100644 --- a/src/components/labs/lab-biomarker-detail.tsx +++ b/src/components/labs/lab-biomarker-detail.tsx @@ -9,32 +9,25 @@ import { useQuery, useQueryClient, } from "@tanstack/react-query"; -import { - FlaskConical, - ListOrdered, - Pencil, - Plus, - SlidersHorizontal, -} from "lucide-react"; +import { FlaskConical, ListOrdered, Pencil, Plus } from "lucide-react"; import { toast } from "sonner"; import { DeleteButton } from "@/components/data-list"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { EmptyState } from "@/components/ui/empty-state"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { ResponsiveSheet } from "@/components/ui/responsive-sheet"; import { Skeleton } from "@/components/ui/skeleton"; import { InsightStatusCard } from "@/components/insights/insight-status-card"; import { MetricStatStrip } from "@/components/insights/metric-stat-strip"; import { useInsightBiomarkerAssessment } from "@/hooks/use-insight-status"; import { useMounted } from "@/hooks/use-mounted"; -import { apiDelete, apiGet, apiPut } from "@/lib/api/api-fetch"; +import { apiDelete, apiGet } from "@/lib/api/api-fetch"; import { summarize, type DataSummary } from "@/lib/analytics/trends"; import { BIOMARKER_CATALOG } from "@/lib/labs/biomarker-catalog"; import { useTranslations } from "@/lib/i18n/context"; -import { cn } from "@/lib/utils"; +import { fallbackMessages } from "@/lib/i18n/load-locale"; +import { resolveKey } from "@/lib/i18n/resolve-key"; import { queryKeys } from "@/lib/query-keys"; import dynamic from "next/dynamic"; @@ -89,13 +82,29 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) { const queryClient = useQueryClient(); // v1.24 — the per-marker description used to live on the labs overview rows. // It belongs on the detail page beneath the heading (mirroring the metric - // pages' explainer caption). Resolve the catalog slug from the marker name - // exactly as the overview did, then fall back to the user's own `context`. + // pages' explainer caption). Resolve the catalog slug from the marker name, + // then fall back to the user's own `context`. + // + // v1.25.3 — key the lookup on BOTH the current-locale display name and the + // English canonical name. A marker stores the catalog name as it read at + // creation time (`biomarker-form` writes `t("labs.catalog.")`), so a + // marker minted under the English default and then viewed in another locale + // never matched a map built only from the current locale's names — the rich + // description silently fell through to the generic line. The English bundle + // is always in the client chunk (`fallbackMessages`), so adding its names + // makes resolution independent of the locale the marker was created in. const slugByName = useMemo(() => { const map = new Map(); for (const seed of BIOMARKER_CATALOG) { - const norm = t(`labs.catalog.${seed.slug}`).trim().toLowerCase(); - if (norm) map.set(norm, seed.slug); + const localized = t(`labs.catalog.${seed.slug}`).trim().toLowerCase(); + if (localized) map.set(localized, seed.slug); + const canonical = resolveKey( + fallbackMessages, + `labs.catalog.${seed.slug}`, + ) + ?.trim() + .toLowerCase(); + if (canonical && !map.has(canonical)) map.set(canonical, seed.slug); } return map; }, [t]); @@ -104,12 +113,6 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) { const [addFooterEl, setAddFooterEl] = useState(null); const [editOpen, setEditOpen] = useState(false); const [editFooterEl, setEditFooterEl] = useState(null); - // v1.24 — focused "adjust target range" sheet. Edits only the reference - // bounds (the full marker editor lives behind the pencil); seeded from the - // marker each time it opens. - const [rangeOpen, setRangeOpen] = useState(false); - const [lowerInput, setLowerInput] = useState(""); - const [upperInput, setUpperInput] = useState(""); const { data: marker, @@ -217,33 +220,6 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) { onError: () => toast.error(t("labs.biomarker.deleteError")), }); - function openRange() { - setLowerInput(marker?.lowerBound != null ? String(marker.lowerBound) : ""); - setUpperInput(marker?.upperBound != null ? String(marker.upperBound) : ""); - setRangeOpen(true); - } - - const saveRange = useMutation({ - mutationFn: () => { - const lower = lowerInput.trim() === "" ? null : Number(lowerInput); - const upper = upperInput.trim() === "" ? null : Number(upperInput); - return apiPut(`/api/biomarkers/${biomarkerId}`, { - lowerBound: lower, - upperBound: upper, - }); - }, - onSuccess: () => { - toast.success(t("labs.biomarker.targetRange.savedToast")); - setRangeOpen(false); - queryClient.invalidateQueries({ - queryKey: queryKeys.biomarkerDetail(biomarkerId), - }); - queryClient.invalidateQueries({ queryKey: queryKeys.biomarkers() }); - queryClient.invalidateQueries({ queryKey: queryKeys.labResults() }); - }, - onError: () => toast.error(t("labs.biomarker.targetRange.saveError")), - }); - // v1.25.1 — the per-marker description leads the page, mirroring the metric // sub-pages' explainer caption. Resolve the catalog slug from the marker name // and fall back to the user's own `context`, then to a generic explainer so a @@ -306,45 +282,17 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) { ) : null} - {/* v1.25.1 — "show all readings" rides the header cluster as an icon - button, mirroring the metric sub-pages' `` control. - The full reading feed lives on `/labs/[biomarkerId]/values`; the - detail page keeps the numbers-first spine. */} - {readings.length > 0 ? ( - - - - - - ) : null} - setEditOpen(true)} - disabled={!marker} - aria-label={t("labs.biomarker.edit")} - > - - + {/* v1.25.3 — header control cluster, left → right: + Delete · Edit · Show-all-values · Add. The icon controls share one + ghost-icon treatment (the target-range editing lives inside the + full marker editor behind the pencil, so the separate range + control was dropped). */} deleteMarker.mutate()} title={t("labs.biomarker.deleteConfirmTitle")} description={t("labs.biomarker.deleteConfirmDescription")} confirmLabel={t("labs.biomarker.delete")} + triggerTitle={t("labs.biomarker.delete")} className="min-h-11 min-w-11 sm:min-h-9 sm:min-w-9" iconClassName="h-4 w-4" /> @@ -352,12 +300,34 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) { variant="ghost" size="icon" className="min-h-11 min-w-11 sm:min-h-9 sm:min-w-9" - onClick={openRange} + onClick={() => setEditOpen(true)} disabled={!marker} - aria-label={t("labs.biomarker.targetRange.title")} + aria-label={t("labs.biomarker.edit")} + title={t("labs.biomarker.edit")} > - + + {/* "Show all readings" mirrors the metric sub-pages' `` + control. The full reading feed lives on + `/labs/[biomarkerId]/values`; the detail page keeps the + numbers-first spine. */} + {readings.length > 0 ? ( + + + + + + ) : null} setAddOpen(true)} // v1.18.10 (W10) — on the narrowest phones the h1 + Edit + Delete + @@ -496,62 +466,6 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) { /> ) : null} - - - setRangeOpen(false)} - disabled={saveRange.isPending} - > - {t("common.cancel")} - - saveRange.mutate()} - disabled={saveRange.isPending} - > - {t("labs.biomarker.targetRange.save")} - - - } - > - - - - {t("labs.biomarker.form.lowerBound")} - - setLowerInput(e.target.value)} - /> - - - - {t("labs.biomarker.form.upperBound")} - - setUpperInput(e.target.value)} - /> - - - {marker?.unit ? ( - - {t("labs.biomarker.form.rangeHint")} - - ) : null} - ); }
- {t("labs.biomarker.form.rangeHint")} -