CUB-48: Add Agent Status Badge component with pulse animations
- Colored pill badge for Active, Idle, Thinking, Error, Offline statuses - Color mapping uses CSS custom properties (--status-active, etc.) - Pulse animations: Active 2s, Thinking 3s, Error 0.8s, Idle/Offline static - Respects prefers-reduced-motion for accessibility - Standalone component with OnPush change detection - Barrel export via index.ts
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<!-- Agent Status Badge: colored pill with pulse animation -->
|
||||
<span
|
||||
class="status-badge"
|
||||
[class]="statusClass"
|
||||
[attr.aria-label]="statusLabels[status] + ' status'"
|
||||
[attr.role]="'status'"
|
||||
>
|
||||
<span class="status-badge__dot"></span>
|
||||
<span *ngIf="showLabel" class="status-badge__label">
|
||||
{{ statusLabels[status] }}
|
||||
</span>
|
||||
</span>
|
||||
@@ -0,0 +1,143 @@
|
||||
// ============================================================================
|
||||
// Agent Status Badge — Pill Style with Pulse Animation
|
||||
// Per CUB-48: Color mapping + animation durations:
|
||||
// Active → --color-primary (#38BDF8) 2s pulse
|
||||
// Idle → --color-secondary (#2DD4BF) no animation
|
||||
// Thinking → --color-accent (#A78BFA) 3s pulse
|
||||
// Error → --color-danger (#F87171) 0.8s pulse
|
||||
// ============================================================================
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Badge Container
|
||||
// ---------------------------------------------------------------------------
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
|
||||
// --- Status-specific backgrounds & text ---
|
||||
&--active {
|
||||
background-color: var(--status-active-bg);
|
||||
color: var(--status-active);
|
||||
}
|
||||
|
||||
&--idle {
|
||||
background-color: var(--status-idle-bg);
|
||||
color: var(--status-idle);
|
||||
}
|
||||
|
||||
&--thinking {
|
||||
background-color: var(--status-thinking-bg);
|
||||
color: var(--status-thinking);
|
||||
}
|
||||
|
||||
&--error {
|
||||
background-color: var(--status-error-bg);
|
||||
color: var(--status-error);
|
||||
}
|
||||
|
||||
&--offline {
|
||||
background-color: rgba(100, 116, 139, 0.12);
|
||||
color: var(--status-offline);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Status Dot (inner indicator circle)
|
||||
// ---------------------------------------------------------------------------
|
||||
.status-badge__dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
|
||||
.status-badge--active & {
|
||||
background-color: var(--status-active);
|
||||
animation: badge-pulse-active 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-badge--idle & {
|
||||
background-color: var(--status-idle);
|
||||
// Idle: no animation — steady dot
|
||||
}
|
||||
|
||||
.status-badge--thinking & {
|
||||
background-color: var(--status-thinking);
|
||||
animation: badge-pulse-thinking 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-badge--error & {
|
||||
background-color: var(--status-error);
|
||||
animation: badge-pulse-error 0.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-badge--offline & {
|
||||
background-color: var(--status-offline);
|
||||
// Offline: no animation
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Label Text
|
||||
// ---------------------------------------------------------------------------
|
||||
.status-badge__label {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Badge Pulse Keyframes
|
||||
// ---------------------------------------------------------------------------
|
||||
// These are scoped to the badge component rather than reusing the global
|
||||
// .status-dot animations, because the badge pulse is subtler (scale + opacity
|
||||
// blend for a pill context vs. standalone dot context).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@keyframes badge-pulse-active {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1.3);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes badge-pulse-thinking {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes badge-pulse-error {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Accessibility: Reduced Motion
|
||||
// ---------------------------------------------------------------------------
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.status-badge__dot {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// ============================================================================
|
||||
// Agent Status Badge Component
|
||||
// Per CUB-48: Colored pill badge with pulse animation for agent statuses.
|
||||
// Displays Active, Idle, Thinking, or Error with correct color mapping.
|
||||
// ============================================================================
|
||||
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { AgentStatus } from '../../models';
|
||||
|
||||
/**
|
||||
* Reusable status badge that renders a colored pill with label text
|
||||
* and a subtle pulse animation for Active, Thinking, and Error states.
|
||||
*
|
||||
* Usage:
|
||||
* ```html
|
||||
* <app-agent-status-badge status="active" />
|
||||
* <app-agent-status-badge status="error" />
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-agent-status-badge',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './agent-status-badge.component.html',
|
||||
styleUrl: './agent-status-badge.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AgentStatusBadgeComponent {
|
||||
/** Agent status to display. Maps to a color and animation. */
|
||||
@Input({ required: true })
|
||||
status!: AgentStatus;
|
||||
|
||||
/** Whether to show the status label text alongside the dot. Defaults to true. */
|
||||
@Input()
|
||||
showLabel = true;
|
||||
|
||||
/** Mapping from status to human-readable label. */
|
||||
readonly statusLabels: Record<AgentStatus, string> = {
|
||||
active: 'Active',
|
||||
idle: 'Idle',
|
||||
thinking: 'Thinking',
|
||||
error: 'Error',
|
||||
offline: 'Offline',
|
||||
};
|
||||
|
||||
/** CSS class string for the badge container based on current status. */
|
||||
get statusClass(): string {
|
||||
return `status-badge--${this.status}`;
|
||||
}
|
||||
}
|
||||
6
frontend/src/app/components/agent-status-badge/index.ts
Normal file
6
frontend/src/app/components/agent-status-badge/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// ============================================================================
|
||||
// Agent Status Badge — Barrel Export
|
||||
// CUB-48
|
||||
// ============================================================================
|
||||
|
||||
export { AgentStatusBadgeComponent } from './agent-status-badge.component';
|
||||
Reference in New Issue
Block a user