mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
Document vault: card list drops a stray glyph and gives the real filename its own line (was truncating on the meta row); the detail sheet's close button aligns correctly — ResponsiveSheet reserved the right gutter for a close button it wasn't rendering, now conditional on showCloseButton. The open-in-coach action reads 'Coach fragen'. Bulk share: a multi-selection in the overview shares several documents in ONE link (documents-only privacy preserved, owner-scoped, up to 50 per link; >50 refuses with a toast rather than silently truncating). Reminders can link to WAIST_CIRCUMFERENCE (additive MeasurementReminderType enum value, OpenAPI regenerated). The WHO-5 and SCI well-being reminder labels were absent in all six locales and rendered their raw dotted keys (the shared 'measurementReminders.types' prefix is what showed as the stray 'Measurement Reminders Types'); added in every language. A new type-labels-i18n test asserts every dynamic reminder-type key resolves non-empty in all six bundles — the static coverage guard cannot see the dynamic key that let this through.
156 lines
5.1 KiB
TypeScript
156 lines
5.1 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* Floating bulk-action bar for the vault's multi-select mode. Appears once
|
|
* at least one document is selected, pinned above the bottom nav on phones
|
|
* and near the bottom edge on desktop. Carries the selected count and the
|
|
* bulk verbs — set type, link condition, share (one link for the whole
|
|
* selection), delete (undo-able), clear — all driving the page's handlers.
|
|
*
|
|
* A `role="toolbar"` with proper labels; the destructive verb sits last
|
|
* before Clear so a stray tap sequence never ends on Delete. Share sits just
|
|
* before Delete (the safe actions lead). The bar is a deliberate hand-rolled
|
|
* shell (a floating toolbar is not a Card surface): dense-tile padding `p-3`
|
|
* per the standards.
|
|
*/
|
|
import { FolderPlus, Share2, Tag, Trash2, X } from "lucide-react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { useTranslations } from "@/lib/i18n/context";
|
|
import { cn } from "@/lib/utils";
|
|
import type { InboundDocumentKindValue } from "@/lib/validations/inbound-documents";
|
|
import { DOCUMENT_KIND_ICONS, DOCUMENT_KIND_ORDER } from "./document-kind-meta";
|
|
|
|
export interface BulkEpisodeOption {
|
|
id: string;
|
|
label: string;
|
|
}
|
|
|
|
export function DocumentBulkBar({
|
|
selectedCount,
|
|
episodes,
|
|
busy,
|
|
onSetKind,
|
|
onLinkEpisode,
|
|
onShare,
|
|
onDelete,
|
|
onClear,
|
|
}: {
|
|
selectedCount: number;
|
|
/** The caller's live illness episodes — the link-condition targets. */
|
|
episodes: BulkEpisodeOption[];
|
|
/** A bulk call is in flight — the verbs disable, Clear stays live. */
|
|
busy: boolean;
|
|
onSetKind: (kind: InboundDocumentKindValue) => void;
|
|
onLinkEpisode: (episodeId: string) => void;
|
|
/**
|
|
* Share the whole selection as ONE documents-only link. The page caps the
|
|
* selection at `SHARE_LINK_MAX_DOCUMENTS` and surfaces the over-cap hint —
|
|
* this handler just opens the share sheet seeded with the selection.
|
|
*/
|
|
onShare: () => void;
|
|
onDelete: () => void;
|
|
onClear: () => void;
|
|
}) {
|
|
const { t } = useTranslations();
|
|
|
|
return (
|
|
<div
|
|
data-slot="document-bulk-bar"
|
|
role="toolbar"
|
|
aria-label={t("documents.bulk.barLabel")}
|
|
className={cn(
|
|
"bg-card border-border fixed bottom-20 left-1/2 z-40 -translate-x-1/2 md:bottom-6",
|
|
"flex w-[calc(100%-2rem)] max-w-2xl flex-wrap items-center gap-2 rounded-xl border p-3 shadow-lg",
|
|
)}
|
|
>
|
|
<p className="px-1 text-sm font-medium whitespace-nowrap" role="status">
|
|
{t("documents.selection.count", { count: selectedCount })}
|
|
</p>
|
|
|
|
<div className="ml-auto flex flex-wrap items-center justify-end gap-1.5">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline" size="sm" disabled={busy}>
|
|
<Tag className="size-4" aria-hidden />
|
|
{t("documents.bulk.setKind")}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
{DOCUMENT_KIND_ORDER.map((kind) => {
|
|
const Icon = DOCUMENT_KIND_ICONS[kind];
|
|
return (
|
|
<DropdownMenuItem key={kind} onSelect={() => onSetKind(kind)}>
|
|
<Icon className="size-4" aria-hidden />
|
|
{t(`documents.kind.${kind}`)}
|
|
</DropdownMenuItem>
|
|
);
|
|
})}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
{episodes.length > 0 ? (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline" size="sm" disabled={busy}>
|
|
<FolderPlus className="size-4" aria-hidden />
|
|
{t("documents.bulk.linkCondition")}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
{episodes.map((episode) => (
|
|
<DropdownMenuItem
|
|
key={episode.id}
|
|
onSelect={() => onLinkEpisode(episode.id)}
|
|
>
|
|
<span className="max-w-56 truncate">{episode.label}</span>
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
) : null}
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={busy}
|
|
onClick={onShare}
|
|
data-slot="document-bulk-share"
|
|
>
|
|
<Share2 className="size-4" aria-hidden />
|
|
{t("documents.bulk.share")}
|
|
</Button>
|
|
|
|
{/* Solid destructive (matching the detail sheet's Delete) — the
|
|
outline variant's destructive text on the card surface fails the
|
|
WCAG contrast gate. */}
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
disabled={busy}
|
|
onClick={onDelete}
|
|
>
|
|
<Trash2 className="size-4" aria-hidden />
|
|
{t("documents.bulk.delete")}
|
|
</Button>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="text-muted-foreground"
|
|
onClick={onClear}
|
|
>
|
|
<X className="size-3.5" aria-hidden />
|
|
{t("documents.selection.clear")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|