From c6cbd5442f70d90126d67cf298aeec2c6a738d97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 29 Jun 2026 14:55:05 +0200 Subject: [PATCH] fix(labs): polish the biomarker-detail header and description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve the catalog slug from both the current-locale display name and the English canonical name. A marker stores the catalog name as it read at creation time, so one minted under the English default and then viewed in another locale never matched a map built only from the current locale's names — its rich per-biomarker description silently fell back to the generic line. The English bundle is always in the client chunk, so adding its names makes the lookup independent of the locale the marker was created in. Tidy the header control cluster: - Drop the standalone "adjust target range" control and its sheet; the reference bounds are already editable inside the full marker editor behind the pencil. Remove the now-dead handler, mutation, and `targetRange.*` strings across all locales. - Give the show-all-values, edit, and delete controls one shared ghost-icon treatment instead of the show-all control's separate muted variant. - Add hover tooltips to edit and delete, matching the other icon controls. - Order the controls left → right: delete, edit, show all values, add value. --- messages/de.json | 9 +- messages/en.json | 9 +- messages/es.json | 9 +- messages/fr.json | 9 +- messages/it.json | 9 +- messages/pl.json | 9 +- src/components/data-list/delete-button.tsx | 4 + .../__tests__/lab-biomarker-detail.test.tsx | 18 ++ src/components/labs/lab-biomarker-detail.tsx | 196 +++++------------- 9 files changed, 83 insertions(+), 189 deletions(-) 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} - + {/* 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} - -
- } - > -
-
- - setLowerInput(e.target.value)} - /> -
-
- - setUpperInput(e.target.value)} - /> -
-
- {marker?.unit ? ( -

- {t("labs.biomarker.form.rangeHint")} -

- ) : null} - ); }