Invoices: confirm a successful import with a toast
Closing the dialog on success left the OK click completely silent — the user
had to go hunting the table for the new row to find out whether anything had
happened. A toast says so plainly and then gets out of the way.
It carries the useful part: invoice number, line count, billed total, how many
lines booked, and — the bit that actually needs action — the machine codes
that could not be matched, named rather than left for the user to infer from
the badge. Auto-dismisses after six seconds, dismissible by hand, and clears
when the dialog is reopened.
Announced via role=status / aria-live=polite rather than assertively: a
confirmation is not urgent and should not cut into whatever a screen reader
is currently saying.
No toast library — the app needs exactly one, and a dependency would carry
far more surface than that. Sits above the dialog's z-50, full-width on
mobile and bottom-right from sm up, with a 44px dismiss target.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-02 12:46:55 +00:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useEffect, useState } from "react";
|
2026-10-02 12:50:48 +00:00
|
|
|
import { CheckCircle2, X } from "lucide-react";
|
Invoices: confirm a successful import with a toast
Closing the dialog on success left the OK click completely silent — the user
had to go hunting the table for the new row to find out whether anything had
happened. A toast says so plainly and then gets out of the way.
It carries the useful part: invoice number, line count, billed total, how many
lines booked, and — the bit that actually needs action — the machine codes
that could not be matched, named rather than left for the user to infer from
the badge. Auto-dismisses after six seconds, dismissible by hand, and clears
when the dialog is reopened.
Announced via role=status / aria-live=polite rather than assertively: a
confirmation is not urgent and should not cut into whatever a screen reader
is currently saying.
No toast library — the app needs exactly one, and a dependency would carry
far more surface than that. Sits above the dialog's z-50, full-width on
mobile and bottom-right from sm up, with a 44px dismiss target.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-02 12:46:55 +00:00
|
|
|
|
2026-10-02 12:50:48 +00:00
|
|
|
// 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.
|
Invoices: confirm a successful import with a toast
Closing the dialog on success left the OK click completely silent — the user
had to go hunting the table for the new row to find out whether anything had
happened. A toast says so plainly and then gets out of the way.
It carries the useful part: invoice number, line count, billed total, how many
lines booked, and — the bit that actually needs action — the machine codes
that could not be matched, named rather than left for the user to infer from
the badge. Auto-dismisses after six seconds, dismissible by hand, and clears
when the dialog is reopened.
Announced via role=status / aria-live=polite rather than assertively: a
confirmation is not urgent and should not cut into whatever a screen reader
is currently saying.
No toast library — the app needs exactly one, and a dependency would carry
far more surface than that. Sits above the dialog's z-50, full-width on
mobile and bottom-right from sm up, with a 44px dismiss target.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-02 12:46:55 +00:00
|
|
|
//
|
|
|
|
|
// 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,
|
|
|
|
|
onDismiss,
|
|
|
|
|
duration = 6000,
|
|
|
|
|
}: {
|
|
|
|
|
message: string;
|
|
|
|
|
detail?: string;
|
|
|
|
|
onDismiss: () => void;
|
|
|
|
|
duration?: number;
|
|
|
|
|
}) {
|
|
|
|
|
const [leaving, setLeaving] = useState(false);
|
|
|
|
|
|
|
|
|
|
const close = () => {
|
|
|
|
|
setLeaving(true);
|
2026-10-02 12:50:48 +00:00
|
|
|
setTimeout(onDismiss, 200); // let the transition run before unmounting
|
Invoices: confirm a successful import with a toast
Closing the dialog on success left the OK click completely silent — the user
had to go hunting the table for the new row to find out whether anything had
happened. A toast says so plainly and then gets out of the way.
It carries the useful part: invoice number, line count, billed total, how many
lines booked, and — the bit that actually needs action — the machine codes
that could not be matched, named rather than left for the user to infer from
the badge. Auto-dismisses after six seconds, dismissible by hand, and clears
when the dialog is reopened.
Announced via role=status / aria-live=polite rather than assertively: a
confirmation is not urgent and should not cut into whatever a screen reader
is currently saying.
No toast library — the app needs exactly one, and a dependency would carry
far more surface than that. Sits above the dialog's z-50, full-width on
mobile and bottom-right from sm up, with a 44px dismiss target.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-02 12:46:55 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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"
|
2026-10-02 12:50:48 +00:00
|
|
|
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
|
Invoices: confirm a successful import with a toast
Closing the dialog on success left the OK click completely silent — the user
had to go hunting the table for the new row to find out whether anything had
happened. A toast says so plainly and then gets out of the way.
It carries the useful part: invoice number, line count, billed total, how many
lines booked, and — the bit that actually needs action — the machine codes
that could not be matched, named rather than left for the user to infer from
the badge. Auto-dismisses after six seconds, dismissible by hand, and clears
when the dialog is reopened.
Announced via role=status / aria-live=polite rather than assertively: a
confirmation is not urgent and should not cut into whatever a screen reader
is currently saying.
No toast library — the app needs exactly one, and a dependency would carry
far more surface than that. Sits above the dialog's z-50, full-width on
mobile and bottom-right from sm up, with a 44px dismiss target.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-02 12:46:55 +00:00
|
|
|
transition-all duration-200 ease-out
|
2026-10-02 12:50:48 +00:00
|
|
|
${leaving ? "opacity-0 -translate-y-2" : "opacity-100 translate-y-0"}`}
|
Invoices: confirm a successful import with a toast
Closing the dialog on success left the OK click completely silent — the user
had to go hunting the table for the new row to find out whether anything had
happened. A toast says so plainly and then gets out of the way.
It carries the useful part: invoice number, line count, billed total, how many
lines booked, and — the bit that actually needs action — the machine codes
that could not be matched, named rather than left for the user to infer from
the badge. Auto-dismisses after six seconds, dismissible by hand, and clears
when the dialog is reopened.
Announced via role=status / aria-live=polite rather than assertively: a
confirmation is not urgent and should not cut into whatever a screen reader
is currently saying.
No toast library — the app needs exactly one, and a dependency would carry
far more surface than that. Sits above the dialog's z-50, full-width on
mobile and bottom-right from sm up, with a 44px dismiss target.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-02 12:46:55 +00:00
|
|
|
>
|
|
|
|
|
<div
|
2026-10-02 12:50:48 +00:00
|
|
|
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"}`}
|
Invoices: confirm a successful import with a toast
Closing the dialog on success left the OK click completely silent — the user
had to go hunting the table for the new row to find out whether anything had
happened. A toast says so plainly and then gets out of the way.
It carries the useful part: invoice number, line count, billed total, how many
lines booked, and — the bit that actually needs action — the machine codes
that could not be matched, named rather than left for the user to infer from
the badge. Auto-dismisses after six seconds, dismissible by hand, and clears
when the dialog is reopened.
Announced via role=status / aria-live=polite rather than assertively: a
confirmation is not urgent and should not cut into whatever a screen reader
is currently saying.
No toast library — the app needs exactly one, and a dependency would carry
far more surface than that. Sits above the dialog's z-50, full-width on
mobile and bottom-right from sm up, with a 44px dismiss target.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-02 12:46:55 +00:00
|
|
|
>
|
2026-10-02 12:50:48 +00:00
|
|
|
<CheckCircle2 className="w-5 h-5 text-white shrink-0 mt-0.5" />
|
Invoices: confirm a successful import with a toast
Closing the dialog on success left the OK click completely silent — the user
had to go hunting the table for the new row to find out whether anything had
happened. A toast says so plainly and then gets out of the way.
It carries the useful part: invoice number, line count, billed total, how many
lines booked, and — the bit that actually needs action — the machine codes
that could not be matched, named rather than left for the user to infer from
the badge. Auto-dismisses after six seconds, dismissible by hand, and clears
when the dialog is reopened.
Announced via role=status / aria-live=polite rather than assertively: a
confirmation is not urgent and should not cut into whatever a screen reader
is currently saying.
No toast library — the app needs exactly one, and a dependency would carry
far more surface than that. Sits above the dialog's z-50, full-width on
mobile and bottom-right from sm up, with a 44px dismiss target.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-02 12:46:55 +00:00
|
|
|
<div className="min-w-0 flex-1">
|
2026-10-02 12:50:48 +00:00
|
|
|
<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>}
|
Invoices: confirm a successful import with a toast
Closing the dialog on success left the OK click completely silent — the user
had to go hunting the table for the new row to find out whether anything had
happened. A toast says so plainly and then gets out of the way.
It carries the useful part: invoice number, line count, billed total, how many
lines booked, and — the bit that actually needs action — the machine codes
that could not be matched, named rather than left for the user to infer from
the badge. Auto-dismisses after six seconds, dismissible by hand, and clears
when the dialog is reopened.
Announced via role=status / aria-live=polite rather than assertively: a
confirmation is not urgent and should not cut into whatever a screen reader
is currently saying.
No toast library — the app needs exactly one, and a dependency would carry
far more surface than that. Sits above the dialog's z-50, full-width on
mobile and bottom-right from sm up, with a 44px dismiss target.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-02 12:46:55 +00:00
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={close}
|
|
|
|
|
aria-label="Dismiss"
|
2026-10-02 12:50:48 +00:00
|
|
|
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"
|
Invoices: confirm a successful import with a toast
Closing the dialog on success left the OK click completely silent — the user
had to go hunting the table for the new row to find out whether anything had
happened. A toast says so plainly and then gets out of the way.
It carries the useful part: invoice number, line count, billed total, how many
lines booked, and — the bit that actually needs action — the machine codes
that could not be matched, named rather than left for the user to infer from
the badge. Auto-dismisses after six seconds, dismissible by hand, and clears
when the dialog is reopened.
Announced via role=status / aria-live=polite rather than assertively: a
confirmation is not urgent and should not cut into whatever a screen reader
is currently saying.
No toast library — the app needs exactly one, and a dependency would carry
far more surface than that. Sits above the dialog's z-50, full-width on
mobile and bottom-right from sm up, with a 44px dismiss target.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-02 12:46:55 +00:00
|
|
|
>
|
|
|
|
|
<X className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|