diff --git a/frontend/src/app/components/agent-status-badge/agent-status-badge.component.html b/frontend/src/app/components/agent-status-badge/agent-status-badge.component.html new file mode 100644 index 0000000..91d4544 --- /dev/null +++ b/frontend/src/app/components/agent-status-badge/agent-status-badge.component.html @@ -0,0 +1,12 @@ + + + + + {{ statusLabels[status] }} + + \ No newline at end of file diff --git a/frontend/src/app/components/agent-status-badge/agent-status-badge.component.scss b/frontend/src/app/components/agent-status-badge/agent-status-badge.component.scss new file mode 100644 index 0000000..d8e8935 --- /dev/null +++ b/frontend/src/app/components/agent-status-badge/agent-status-badge.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/agent-status-badge/agent-status-badge.component.ts b/frontend/src/app/components/agent-status-badge/agent-status-badge.component.ts new file mode 100644 index 0000000..0a7519d --- /dev/null +++ b/frontend/src/app/components/agent-status-badge/agent-status-badge.component.ts @@ -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 + * + * + * ``` + */ +@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 = { + 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}`; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/agent-status-badge/index.ts b/frontend/src/app/components/agent-status-badge/index.ts new file mode 100644 index 0000000..bd3aec6 --- /dev/null +++ b/frontend/src/app/components/agent-status-badge/index.ts @@ -0,0 +1,6 @@ +// ============================================================================ +// Agent Status Badge — Barrel Export +// CUB-48 +// ============================================================================ + +export { AgentStatusBadgeComponent } from './agent-status-badge.component'; \ No newline at end of file