Two different causes, both from the same root — the header was raised to z-[1100] so the Plant Map's Leaflet controls (z-1000) could not scroll over it, which quietly pushed my overlays underneath it. The toast sat at z-60 and the Add Invoice dialog at z-50, both below the header, so a confirmation appeared to vanish behind the nav bar. Rung out against the existing ladder instead of picking a new number: header 1100, nav drawer 1200, dialog 1250, toast 1300. The toast and the combobox list sit at the same layer deliberately — they never open at the same time. The machine dropdown was a separate and worse bug: it was not merely behind anything, it was CLIPPED. It rendered inside the line table's overflow-x-auto container, so the list was cut off by the table edge and the card around it — it could never escape, which is why the search appeared to do nothing. It now renders through a portal into <body>, positioned from the input's measured viewport rect, so no ancestor can clip it. The rect is measured before paint so it never flashes in the wrong place, kept in sync with scroll and resize, and the list flips upward when there is not enough room below — which is the common case on a phone with the keyboard open. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
84 lines
No EOL
3 KiB
TypeScript
84 lines
No EOL
3 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { CheckCircle2, Info, X } from "lucide-react";
|
|
|
|
// A minimal toast for a confirmed import. Deliberately not a dependency: the app needs
|
|
// exactly one, and a full toast library would ship far more surface than that justifies.
|
|
//
|
|
// Both fills come from the existing palette rather than inventing hues: #00798C teal is
|
|
// this app's success colour (the "All booked" badge, the income tiles), and #EDAE49 sunny
|
|
// is its existing attention/amber accent.
|
|
//
|
|
// Announced politely rather than assertively — a confirmation is not urgent, and it
|
|
// should not interrupt whatever a screen reader is currently saying.
|
|
//
|
|
// z-[1300], not a low value: the sticky header is z-[1100] (it has to clear Leaflet's
|
|
// z-1000 control panes) and the mobile nav drawer is z-[1200]. Below those the toast
|
|
// rendered behind the nav bar.
|
|
export function Toast({
|
|
message,
|
|
detail,
|
|
tone = "success",
|
|
onDismiss,
|
|
duration = 6000,
|
|
}: {
|
|
message: string;
|
|
detail?: string;
|
|
// "success" = it was imported. "notice" = nothing was added, but nothing is wrong
|
|
// either — e.g. the file was already in the register.
|
|
tone?: "success" | "notice";
|
|
onDismiss: () => void;
|
|
duration?: number;
|
|
}) {
|
|
const [leaving, setLeaving] = useState(false);
|
|
|
|
const close = () => {
|
|
setLeaving(true);
|
|
setTimeout(onDismiss, 200); // let the transition run before unmounting
|
|
};
|
|
|
|
useEffect(() => {
|
|
const t = setTimeout(close, duration);
|
|
return () => clearTimeout(t);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [message, detail, duration]);
|
|
|
|
return (
|
|
<div
|
|
role="status"
|
|
aria-live="polite"
|
|
className={`pointer-events-none fixed z-[1300] inset-x-3 sm:inset-x-auto sm:right-6 sm:w-auto sm:max-w-sm
|
|
pt-[max(0.75rem,env(safe-area-inset-top))] sm:pt-0 sm:top-6
|
|
transition-all duration-200 ease-out
|
|
${leaving ? "opacity-0 -translate-y-2" : "opacity-100 translate-y-0"}`}
|
|
>
|
|
<div
|
|
className={`pointer-events-auto flex items-start gap-3 px-4 py-3 rounded-xl text-white
|
|
shadow-xl border border-white/15 ${
|
|
tone === "notice"
|
|
? "bg-[#EDAE49] shadow-amber-900/20"
|
|
: "bg-[#00798C] shadow-teal-900/20"
|
|
} ${leaving ? "" : "ring-1 ring-white/25"}`}
|
|
>
|
|
{tone === "notice" ? (
|
|
<Info className="w-5 h-5 text-white shrink-0 mt-0.5" />
|
|
) : (
|
|
<CheckCircle2 className="w-5 h-5 text-white shrink-0 mt-0.5" />
|
|
)}
|
|
<div className="min-w-0 flex-1">
|
|
<div className="text-sm font-semibold text-white">{message}</div>
|
|
{detail && <div className="text-sm text-white/85 mt-0.5 break-words">{detail}</div>}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={close}
|
|
aria-label="Dismiss"
|
|
className="grid place-items-center w-11 h-11 -mr-2 -my-1 rounded-lg text-white/80 hover:bg-white/15 hover:text-white transition shrink-0"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |