Invoices: toast to the top, solid teal

The confirmation now arrives at the top of the screen instead of the bottom,
and is a solid teal panel rather than a white card with a green icon — at a
glance it reads as a system success message rather than a quiet note.

Teal rather than a literal green: #00798C is already this app's success
colour (the "All booked" badge, the income tiles, the import button), so a new
hue would have been off-palette. Solid fill with white text reads unambiguously
as confirmation, where the previous white card could be missed.

Slides down from above rather than up from below. Honours the notch on mobile
via env(safe-area-inset-top) and keeps a 44px dismiss target on the new fill.

The unused tone prop went with it — failures stay in the dialog, so the toast
is only ever a success.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
David 2026-10-02 14:50:48 +02:00
parent fe4b1fc3dd
commit b0b31ad830

View file

@ -1,23 +1,25 @@
"use client";
import { useEffect, useState } from "react";
import { CheckCircle2, X, AlertTriangle } from "lucide-react";
import { CheckCircle2, X } from "lucide-react";
// A minimal toast. Deliberately not a dependency: the app needs exactly one, and a
// full toast library would ship far more surface than that justifies.
// 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.
//
// Solid teal rather than a new green: #00798C is already this app's success colour (the
// "All booked" badge, the income tiles, the import button), so it reads as success without
// introducing a hue outside the palette.
//
// Announced politely rather than assertively — a confirmation is not urgent, and it
// should not interrupt whatever a screen reader is currently saying.
export function Toast({
message,
detail,
tone = "success",
onDismiss,
duration = 6000,
}: {
message: string;
detail?: string;
tone?: "success" | "error";
onDismiss: () => void;
duration?: number;
}) {
@ -25,7 +27,7 @@ export function Toast({
const close = () => {
setLeaving(true);
setTimeout(onDismiss, 200); // let the fade-out run before unmounting
setTimeout(onDismiss, 200); // let the transition run before unmounting
};
useEffect(() => {
@ -38,39 +40,26 @@ export function Toast({
<div
role="status"
aria-live="polite"
className={`fixed z-[60] inset-x-4 sm:inset-x-auto sm:right-6 sm:bottom-6 sm:w-auto sm:max-w-sm
className={`pointer-events-none fixed z-[60] 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"}`}
${leaving ? "opacity-0 -translate-y-2" : "opacity-100 translate-y-0"}`}
>
<div
className={`flex items-start gap-3 px-4 py-3 rounded-xl shadow-xl border backdrop-blur ${
tone === "success"
? "bg-white dark:bg-[#131a21] border-[#00798C]/30"
: "bg-white dark:bg-[#131a21] border-[#D1495B]/30"
}`}
className={`pointer-events-auto flex items-start gap-3 px-4 py-3 rounded-xl
bg-[#00798C] text-white shadow-xl shadow-teal-900/20 border border-white/15
${leaving ? "" : "ring-1 ring-[#00798C]/30"}`}
>
{tone === "success" ? (
<CheckCircle2 className="w-5 h-5 text-[#00798C] shrink-0 mt-0.5" />
) : (
<AlertTriangle className="w-5 h-5 text-[#D1495B] 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 ${
tone === "success" ? "text-[#00798C]" : "text-[#D1495B]"
}`}
>
{message}
</div>
{detail && (
<div className="text-sm text-gray-600 dark:text-gray-300 mt-0.5 break-words">{detail}</div>
)}
<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-gray-500 hover:bg-gray-100 dark:hover:bg-white/5 transition shrink-0"
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>