import { booleanAttribute, ChangeDetectionStrategy, Component, computed, input, output, } from '@angular/core'; import type { IconName } from '@africanies/africanies-icons'; import { AfricaniesIconComponent } from '@africanies/africanies-icons'; import { ButtonComponent } from '../button/button.component'; import { splitMessageLines } from '../feedback/message-lines'; /** * Visual tone for {@link AlertComponent}. * * - `info` — neutral guidance * - `success` — positive confirmation (export-green subtle) * - `warning` — caution that needs attention * - `danger` — blocking / destructive messaging */ export type AlertVariant = 'info' | 'success' | 'warning' | 'danger'; const DEFAULT_ICONS: Record = { info: 'info-circle', success: 'check-circle', warning: 'warning', danger: 'warning', }; /** * Inline page/section banner — distinct from {@link ErrorStateComponent} * (full-section async failure + Retry). * * Parent owns visibility (`@if` / signal). When the user dismisses, emit * {@link dismissed} and unmount the host — this component does not hide itself. * * @example * ```html * @if (showBanner()) { * * } * ``` */ @Component({ selector: 'africanies-alert', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [AfricaniesIconComponent, ButtonComponent], template: `
@if (title(); as heading) {

{{ heading }}

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

{{ message() }}

}
@if (dismissible()) { }
`, }) /** * Inline page/section banner — distinct from {@link ErrorStateComponent} (full-section async failure + Retry). */ export class AlertComponent { /** Semantic tone. Defaults to `info`. */ readonly variant = input('info'); /** Optional short heading above the message. */ readonly title = input(undefined); /** Body copy — required so the banner is never an empty colored box. */ readonly message = input.required(); /** * When true (default), shows a dismiss control that emits {@link dismissed}. */ readonly dismissible = input(true, { transform: booleanAttribute }); /** Override the default variant icon. */ readonly icon = input(undefined); /** Emitted when the user activates Dismiss — parent should unmount/hide. */ readonly dismissed = output(); protected readonly resolvedIcon = computed( () => this.icon() ?? DEFAULT_ICONS[this.variant()], ); protected readonly a11yRole = computed(() => { const v = this.variant(); return v === 'warning' || v === 'danger' ? 'alert' : 'status'; }); protected readonly a11yLive = computed(() => { const v = this.variant(); return v === 'warning' || v === 'danger' ? 'assertive' : 'polite'; }); protected readonly shellClass = computed(() => { const base = 'flex items-start gap-3 rounded-lg border px-4 py-3 text-body ' + 'text-ink dark:text-white'; switch (this.variant()) { case 'success': return `${base} border-export/30 bg-export-subtle dark:border-export/40 dark:bg-export/15`; case 'warning': return `${base} border-warning/40 bg-warning-subtle dark:border-warning/50 dark:bg-warning/15`; case 'danger': return `${base} border-danger/30 bg-danger-subtle dark:border-danger/40 dark:bg-danger/15`; default: return `${base} border-border bg-background-welcome dark:border-white/15 dark:bg-ink-950`; } }); protected readonly iconClass = computed(() => { switch (this.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 text-body font-medium'; switch (this.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 messageClass = computed( () => 'm-0 text-body-sm text-neutral-600 dark:text-neutral-300', ); protected readonly listClass = computed( () => 'm-0 list-decimal space-y-1 pl-5 text-body-sm text-neutral-600 dark:text-neutral-300', ); protected readonly messageLines = computed(() => splitMessageLines(this.message()), ); protected onDismiss(): void { this.dismissed.emit(); } }