import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
computed,
input,
output,
} from '@angular/core';
import { ButtonComponent } from '../button/button.component';
/**
* Compact error pill for non-blocking failures while stale data stays visible —
* distinct from {@link ErrorStateComponent} (full-section blocking).
*
* Parent owns placement — typically a flex row above the content
* (`class="flex justify-end"` with `max-w-[min(100%,20rem)]` on the indicator).
* Renders nothing when {@link error} is null, undefined, or whitespace-only.
*
* @example
* ```html
*
* ```
*/
@Component({
selector: 'africanies-error-indicator',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ButtonComponent],
host: {
class: 'africanies-error-indicator block',
},
styles: [
`
@keyframes africanies-error-indicator-enter {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes africanies-error-indicator-dot-pulse {
0%,
100% {
transform: scale(1);
opacity: 0.7;
}
50% {
transform: scale(1.3);
opacity: 1;
}
}
@keyframes africanies-error-indicator-dot-ping {
0% {
transform: scale(1);
opacity: 0.75;
}
70%,
100% {
transform: scale(2);
opacity: 0;
}
}
.africanies-error-indicator-pill {
animation: africanies-error-indicator-enter 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.africanies-error-indicator-dot-ping {
animation: africanies-error-indicator-dot-ping 1.5s cubic-bezier(0, 0, 0.2, 1)
infinite;
}
.africanies-error-indicator-dot-core {
animation: africanies-error-indicator-dot-pulse 1.5s ease-in-out infinite
alternate;
}
.africanies-error-indicator-retry:not(:disabled) {
transition: transform 0.15s ease;
}
.africanies-error-indicator-retry:not(:disabled):hover {
transform: scale(1.05);
}
.africanies-error-indicator-retry:not(:disabled):active {
transform: scale(0.95);
}
@media (prefers-reduced-motion: reduce) {
.africanies-error-indicator-pill,
.africanies-error-indicator-dot-ping,
.africanies-error-indicator-dot-core,
.africanies-error-indicator-retry:not(:disabled) {
animation: none;
transition: none;
}
.africanies-error-indicator-retry:not(:disabled):hover,
.africanies-error-indicator-retry:not(:disabled):active {
transform: none;
}
}
`,
],
template: `
@if (visible()) {
{{ message() }}
{{ actionLabel() }}
}
`,
})
/**
* Compact error pill for non-blocking failures while stale data stays visible — distinct from {@link ErrorStateComponent} (full-section blocking).
*/
export class ErrorIndicatorComponent {
/**
* Error copy. When empty, the indicator is not shown.
*/
readonly error = input(undefined);
/**
* Retry button label when not {@link refreshing}.
*/
readonly retryText = input('Retry');
/**
* When true, the action label becomes a loading phrase and the button stays
* enabled unless {@link disabled} is set.
*/
readonly refreshing = input(false, { transform: booleanAttribute });
/**
* Loading label while {@link refreshing} is true.
*/
readonly refreshingText = input('Loading...');
/**
* Disables the retry control and dims the label.
*/
readonly disabled = input(false, { transform: booleanAttribute });
/** Emitted when the user activates the retry control. */
readonly retry = output();
protected readonly message = computed(() => {
const text = this.error()?.trim();
return text ? text : null;
});
protected readonly visible = computed(() => this.message() !== null);
protected readonly actionLabel = computed(() =>
this.refreshing() ? this.refreshingText() : this.retryText(),
);
protected onRetry(): void {
if (this.disabled()) {
return;
}
this.retry.emit();
}
}