import { ChangeDetectionStrategy, Component, computed, inject, input, output, signal, } from '@angular/core'; import { AfricaniesIconComponent } from '@africanies/africanies-icons'; import { ButtonComponent } from '../button/button.component'; import { splitMessageLines } from '../feedback/message-lines'; import { AfricaniesUiI18n } from '../i18n/africanies-ui-i18n'; import { TOAST_ICONS, type ToastItem, type ToastVariant, } from './toast.types'; /** * Single toast card — dense, token-based, with optional countdown rail. * Stacked identical toasts show a count; host Expand/Collapse all and Close all manage the stack; × peels one copy. */ @Component({ selector: 'africanies-toast-item', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [AfricaniesIconComponent, ButtonComponent], host: { class: 'pointer-events-auto block w-full', '[class.is-paused]': 'isPaused()', '(mouseenter)': 'onPause()', '(mouseleave)': 'onResume()', '(focusin)': 'onPause()', '(focusout)': 'onResume()', }, template: `
@if (item().expanded && item().count > 1) { @for (copy of copies(); track copy; let i = $index) {
@if (item().title; as heading) {

{{ heading }}

} @if (messageLines().length > 1) {
    @for (line of messageLines(); track $index) {
  1. {{ line }}
  2. }
} @else {

{{ item().message }}

}
} } @else {
@if (item().title; as heading) {

{{ heading }}

} @if (messageLines().length > 1) {
    @for (line of messageLines(); track $index) {
  1. {{ line }}
  2. }
} @else {

{{ item().message }}

}
@if (item().count > 1) { ×{{ item().count }} }
@if (item().durationMs) { }
}
`, styles: ` @keyframes africanies-toast-in { from { opacity: 0; transform: translateX(0.75rem); } to { opacity: 1; transform: translateX(0); } } @keyframes africanies-toast-rail { from { transform: scaleX(1); } to { transform: scaleX(0); } } .africanies-toast-enter { animation: africanies-toast-in 220ms ease-out both; } .africanies-toast-rail { animation-name: africanies-toast-rail; animation-timing-function: linear; animation-fill-mode: forwards; } :host.is-paused .africanies-toast-rail { animation-play-state: paused; } `, }) /** * Single toast card — dense, token-based, with optional countdown rail. Stacked identical toasts show a count; host Expand/Collapse all and Close all manage the stack; × peels one copy. */ export class ToastItemComponent { private readonly i18n = inject(AfricaniesUiI18n); /** Stack entry to render. */ readonly item = input.required(); /** Peel one copy (outermost) from the stack. */ readonly dismissOne = output(); /** Hover / focus — pause timer. */ readonly paused = output(); /** Leave — resume timer. */ readonly resumed = output(); protected readonly isPaused = signal(false); protected readonly dismissLabel = computed(() => this.i18n.t('africaniesUi.toast.dismiss', 'Dismiss'), ); protected readonly dismissAriaLabel = computed(() => this.item().count > 1 ? this.i18n.t( 'africaniesUi.toast.dismissOutermost', 'Dismiss outermost', ) : this.dismissLabel(), ); protected readonly copies = computed(() => Array.from({ length: Math.max(1, this.item().count) }, (_, i) => i), ); protected readonly messageLines = computed(() => splitMessageLines(this.item().message), ); protected readonly defaultIcon = computed( () => TOAST_ICONS[this.item().variant], ); protected readonly a11yRole = computed(() => { const v = this.item().variant; return v === 'warning' || v === 'danger' ? 'alert' : 'status'; }); protected readonly a11yLive = computed(() => { const v = this.item().variant; return v === 'warning' || v === 'danger' ? 'assertive' : 'polite'; }); protected readonly shellClass = computed( () => 'africanies-toast-enter relative flex w-full overflow-hidden rounded-panel border border-border ' + 'bg-surface/95 text-ink shadow-floating backdrop-blur-md ' + 'dark:border-white/15 dark:bg-ink-950/95 dark:text-white', ); protected readonly accentClass = computed( () => `w-1 shrink-0 self-stretch ${accentFill(this.item().variant)}`, ); protected readonly iconClass = computed(() => { switch (this.item().variant) { case 'success': return 'text-export-strong dark:text-export-light'; case 'warning': return 'text-warning-dark dark:text-warning'; case 'danger': return 'text-danger-dark dark:text-danger'; default: return 'text-ink-blue dark:text-neutral-300'; } }); protected readonly titleClass = computed(() => { const base = 'm-0 break-words text-body-sm font-semibold'; switch (this.item().variant) { case 'success': return `${base} text-export-strong dark:text-export-light`; case 'warning': return `${base} text-warning-dark dark:text-warning`; case 'danger': return `${base} text-danger-dark dark:text-danger`; default: return `${base} text-ink dark:text-white`; } }); protected readonly railFillClass = computed(() => railFill(this.item().variant), ); protected onPause(): void { this.isPaused.set(true); this.paused.emit(); } protected onResume(): void { this.isPaused.set(false); this.resumed.emit(); } } /** * @param variant - Toast tone. * @returns Accent bar fill. */ function accentFill(variant: ToastVariant): string { switch (variant) { case 'success': return 'bg-export'; case 'warning': return 'bg-warning'; case 'danger': return 'bg-danger'; default: return 'bg-neutral-400'; } } /** * @param variant - Toast tone. * @returns Progress rail fill class. */ function railFill(variant: ToastVariant): string { return accentFill(variant); }