Files
HealthLog/src/components/ui/info-popover.tsx
T
Marc-André Bombeck a977df24a0 fix(ui): catalog loading/error truth, info-popover dedupe, touch sweep
Metric catalog page painted every metric as "Not tracked yet" (with
Connect CTAs) while its four availability probes were still in flight,
and stayed there forever on a failed batch — a load failure indistinguishable
from an honest absence. Gate rows on the probes' aggregate loading/error
state: row skeletons while loading, a QueryErrorCard with retry on failure.

Repoint ComplianceInfoTip onto InfoPopover instead of carrying a verbatim
structural copy (adds icon/align/data-slot override props to the shared
primitive); migrate the cycle log-sheet's FieldInfo off a hover Tooltip
onto the same click-opened InfoPopover, since a hover trigger never opens
reliably on the sheet's thumb-first touch surface.

Lift ViewToggle/SegmentedToggle off their stale 40px tap floor onto the
44px/36px floor the sibling intake and history-rail controls already
carry, and correct the docstrings to match. Restore the dropped
safe-area-inset-top term in /coach/conversations' height calc so it
matches /coach on notched devices. Bring the coach history rail's
off-scale spacing and sub-floor timestamp text onto the standard scale.
2026-07-18 06:47:32 +02:00

114 lines
3.7 KiB
TypeScript

"use client";
import { useState } from "react";
import { Info, type LucideIcon } from "lucide-react";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { useTranslations } from "@/lib/i18n/context";
import { cn } from "@/lib/utils";
/** A plain external citation link rendered under the popover body. */
export interface InfoPopoverLink {
/** Display name of the cited source (e.g. "WHO BMI classification"). */
name: string;
/** Absolute URL, rendered as a plain `<a>`, never HTML. */
url: string;
}
/**
* The app's one (i) info-affordance. Replaces the bare `title=` attribute
* (which never surfaces on touch) and, since v1.28.17, the earlier
* two-primitive split: a click-opened `Popover` — not a hover `Tooltip` —
* because most of this app's use is on touch, where a hover-only tooltip
* never opens reliably on tap. `v1.29.2`'s `VitalsInfoTip` made exactly
* this argument for the vitals grid; the same reasoning applies to every
* other (i) trigger in the tree, so the two primitives merge into this one.
*
* The trigger is a real button with an accessible name, so keyboard and
* screen-reader users reach the same content a pointer user gets on tap.
*/
export interface InfoPopoverProps {
/** Body content — plain text or a short mix of text nodes. */
content: React.ReactNode;
/** Optional cited source, rendered as a plain external link. */
link?: InfoPopoverLink;
/**
* Accessible name of the trigger button. Defaults to the generic
* "More information" — pass a more specific label (e.g. "How is this
* calculated?") where the caller wants that framing.
*/
label?: string;
className?: string;
/**
* Trigger glyph. Defaults to `Info` (h-3.5 w-3.5); the compliance-tip
* wrapper passes `HelpCircle` at `h-3 w-3` to keep its pre-merge look.
*/
icon?: LucideIcon;
iconClassName?: string;
/** Popover alignment relative to the trigger. Defaults to `"end"`. */
align?: "start" | "center" | "end";
/** `data-slot` override for the trigger button. */
triggerDataSlot?: string;
/** `data-slot` override for the popover body. */
bodyDataSlot?: string;
}
export function InfoPopover({
content,
link,
label,
className,
icon: Icon = Info,
iconClassName = "h-3.5 w-3.5",
align = "end",
triggerDataSlot = "info-popover-trigger",
bodyDataSlot = "info-popover-body",
}: InfoPopoverProps) {
const { t } = useTranslations();
const [open, setOpen] = useState(false);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
data-slot={triggerDataSlot}
aria-label={label ?? t("common.moreInfo")}
aria-expanded={open}
className={cn(
"text-muted-foreground hover:text-foreground focus-visible:ring-ring/50",
"inline-flex min-h-11 min-w-11 shrink-0 items-center justify-center rounded-full",
"-mx-2 -my-3",
"transition-colors focus-visible:ring-2 focus-visible:outline-none",
className,
)}
>
<Icon className={iconClassName} aria-hidden="true" />
</button>
</PopoverTrigger>
<PopoverContent
data-slot={bodyDataSlot}
align={align}
sideOffset={6}
className="max-w-xs space-y-1.5"
>
<p className="text-muted-foreground text-xs leading-snug">{content}</p>
{link ? (
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="text-primary block text-xs leading-snug hover:underline"
>
{link.name}
</a>
) : null}
</PopoverContent>
</Popover>
);
}