mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
Hidden states of the floating coach button now leave the tab order and accessibility tree, its focus ring carries an offset, its icon is dark on the gradient, an unread nudge is announced once via a live region, and it sits correctly above the fold on tablet widths and yields to the onboarding tour. Chart error cards announce themselves, share the exact height variables of the skeleton band, carry per-chart retry labels, and a failed chunk renders inside the regular card shell. The header icon cluster spaces its extended hit areas apart, the tab strip keeps horizontal paint room for focus rings, the streak row reserves its slot in every card state, the selection bar clears the bottom nav on landscape phones, and the version poller keeps an in-memory reload guard for browsers that block session storage. Mood notes render as plain paragraphs with a toggle that appears only when text is actually clamped, the remember control reveals on hover and hands focus to its confirmation, and the hypnogram names its source and paints only the lanes the night contains.
129 lines
4.4 KiB
TypeScript
129 lines
4.4 KiB
TypeScript
"use client";
|
|
|
|
import { Loader2, Trash2, X } from "lucide-react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from "@/components/ui/alert-dialog";
|
|
import { useTranslations } from "@/lib/i18n/context";
|
|
|
|
/**
|
|
* Selection action bar for the data-management lists (measurements +
|
|
* mood). Appears when ≥1 row on the current page is selected; shows
|
|
* "N selected", a destructive "Delete (N)" button gated behind an
|
|
* AlertDialog confirm, and a clear-selection affordance.
|
|
*
|
|
* Pure / presentational: every behaviour is a callback. The component
|
|
* renders nothing when `count === 0`. The bar is sticky to the bottom of
|
|
* the viewport so it stays reachable while scrolling a long page, and the
|
|
* confirm dialog is focus-trapped by the shared `AlertDialog` primitive.
|
|
*
|
|
* The confirm copy is passed in because the title/body differ per surface
|
|
* (measurements vs mood) and the count is interpolated by the caller.
|
|
*/
|
|
export function SelectionActionBar({
|
|
count,
|
|
onClear,
|
|
onConfirmDelete,
|
|
isDeleting,
|
|
confirmTitle,
|
|
confirmBody,
|
|
}: {
|
|
count: number;
|
|
onClear: () => void;
|
|
onConfirmDelete: () => void;
|
|
isDeleting: boolean;
|
|
confirmTitle: string;
|
|
confirmBody: string;
|
|
}) {
|
|
const { t } = useTranslations();
|
|
|
|
if (count <= 0) return null;
|
|
|
|
return (
|
|
<div
|
|
role="region"
|
|
aria-label={t("dataList.selectionBarLabel")}
|
|
// v1.16.8 — the Coach FAB watches for this slot via a CSS
|
|
// `:has()` gate and yields while the bar is mounted, so the
|
|
// floating launcher never sits over the delete action.
|
|
data-slot="selection-action-bar"
|
|
// v1.15.13 HIGH-1 — on mobile the fixed bottom-nav (64px, `md:hidden`,
|
|
// z-50) plus the iOS home-indicator inset sit over the page's lower
|
|
// edge. A `sticky bottom-2` bar lands BEHIND the nav, hiding the
|
|
// destructive "Delete (N)" button. Lift the bar to clear the nav band
|
|
// + safe area using the same offset the Coach FAB uses
|
|
// (`layout-coach-fab.tsx`); the nav only hides from `md:` up
|
|
// (`md:hidden`), so the drop back to `bottom-4` happens at `md:` —
|
|
// an `sm:` drop sat the bar behind the still-visible nav 640-767px.
|
|
// z-30 keeps the bar above page content without needing to beat the
|
|
// (now-cleared) nav's z-50.
|
|
className="bg-card border-border sticky bottom-[calc(env(safe-area-inset-bottom,0px)+5rem)] z-30 mt-2 flex items-center justify-between gap-2 rounded-lg border p-2 shadow-lg md:bottom-4"
|
|
>
|
|
<div className="flex items-center gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="size-11"
|
|
onClick={onClear}
|
|
disabled={isDeleting}
|
|
aria-label={t("dataList.clearSelection")}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
<span className="text-sm font-medium" aria-live="polite">
|
|
{t("dataList.selected", { count: String(count) })}
|
|
</span>
|
|
</div>
|
|
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
className="min-h-11"
|
|
disabled={isDeleting}
|
|
>
|
|
{isDeleting ? (
|
|
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
|
) : (
|
|
<Trash2 className="h-4 w-4" />
|
|
)}
|
|
{t("dataList.deleteN", { count: String(count) })}
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>{confirmTitle}</AlertDialogTitle>
|
|
<AlertDialogDescription>{confirmBody}</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel disabled={isDeleting}>
|
|
{t("common.cancel")}
|
|
</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
variant="destructive"
|
|
onClick={onConfirmDelete}
|
|
disabled={isDeleting}
|
|
>
|
|
{isDeleting ? (
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
|
) : null}
|
|
{t("common.delete")}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
);
|
|
}
|