storebound/apps/desktop/src/ui/Toasts.tsx

46 lines
2.3 KiB
TypeScript

import { t } from '../i18n';
import { bytes, num } from '../lib/format';
import { useData } from '../store';
import { Icon, type IconName } from './icons';
const ICON: Record<string, [IconName, string]> = { job: ['refresh', 'tone-bg-accent'], ok: ['check', 'tone-bg-ok'], bad: ['alertCircle', 'tone-bg-bad'], info: ['info', 'tone-bg-info'] };
/** Background jobs and results, bottom right. Jobs show progress and a Cancel button; results close by themselves. */
export function Toasts() {
const d = useData();
return (
<div className="toasts" aria-live="polite" aria-relevant="additions text">
{d.toasts.map(x => {
const [icon, cls] = ICON[x.kind]!;
const p = x.progress;
const big = p && p.total > 10_000;
return (
<div key={x.id} className="toast" role={x.kind === 'bad' ? 'alert' : 'status'}>
<div className="toast-top">
<span className={`ti ${cls}`}><Icon name={icon} className={x.kind === 'job' ? 'spin' : undefined} /></span>
<div className="tt"><b>{x.title}</b>{x.detail && <span>{x.detail}</span>}</div>
<button className="btn btn-ghost btn-sm btn-icon" onClick={() => d.dismiss(x.id)} aria-label={t('common.close')}><Icon name="x" /></button>
</div>
{x.kind === 'job' && (
<>
{p ? (
<div className="prog">
<div className="bar"><span style={{ width: `${Math.min(100, (p.done / Math.max(1, p.total)) * 100)}%` }} /></div>
<div className="c"><span>{big ? `${bytes(p.done)} / ${bytes(p.total)}` : `${num(p.done)} / ${num(p.total)}`}</span><span>{Math.round((p.done / Math.max(1, p.total)) * 100)}%</span></div>
</div>
) : <div className="bar indet"><span /></div>}
{x.lines && x.lines.length > 1 && (
<details className="toast-log">
<summary className="small muted">{t('toast.log', { count: x.lines.length })}</summary>
<pre className="log">{x.lines.join('\n')}</pre>
</details>
)}
{x.job && <div className="toast-actions"><button className="btn btn-sm" onClick={() => d.cancelJob(x.job!)}>{t('common.cancel')}</button></div>}
</>
)}
</div>
);
})}
</div>
);
}