From ed1ee886db219331607fc650812c9e358b74c560 Mon Sep 17 00:00:00 2001 From: rex-bot Date: Mon, 27 Apr 2026 14:08:57 +0000 Subject: [PATCH] CUB-49: [Control Center] Global Action Modal Component --- .../global-action-modal.component.html | 31 +++ .../global-action-modal.component.scss | 198 ++++++++++++++++++ .../global-action-modal.component.ts | 87 ++++++++ 3 files changed, 316 insertions(+) create mode 100644 frontend/src/app/components/global-action-modal/global-action-modal.component.html create mode 100644 frontend/src/app/components/global-action-modal/global-action-modal.component.scss create mode 100644 frontend/src/app/components/global-action-modal/global-action-modal.component.ts diff --git a/frontend/src/app/components/global-action-modal/global-action-modal.component.html b/frontend/src/app/components/global-action-modal/global-action-modal.component.html new file mode 100644 index 0000000..47eb934 --- /dev/null +++ b/frontend/src/app/components/global-action-modal/global-action-modal.component.html @@ -0,0 +1,31 @@ + +
+ + + \ No newline at end of file diff --git a/frontend/src/app/components/global-action-modal/global-action-modal.component.scss b/frontend/src/app/components/global-action-modal/global-action-modal.component.scss new file mode 100644 index 0000000..fce6731 --- /dev/null +++ b/frontend/src/app/components/global-action-modal/global-action-modal.component.scss @@ -0,0 +1,198 @@ +// ============================================================================ +// Global Action Modal — Tactical Dark Mode Styling +// Uses Control Center design tokens from styles.scss +// ============================================================================ + +// --------------------------------------------------------------------------- +// Backdrop +// --------------------------------------------------------------------------- +:host { + display: block; + position: fixed; + inset: 0; + z-index: 1000; +} + +.global-action-modal__backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4px); +} + +// --------------------------------------------------------------------------- +// Modal Panel +// --------------------------------------------------------------------------- +.global-action-modal__panel { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: min(560px, calc(100vw - 48px)); + background: var(--cc-surface-container); + border: 1px solid var(--cc-outline); + border-radius: var(--cc-card-border-radius); + box-shadow: 0 24px 48px rgba(0, 0, 0, 0.5); + overflow: hidden; +} + +// --------------------------------------------------------------------------- +// Header +// --------------------------------------------------------------------------- +.global-action-modal__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px 12px; +} + +.global-action-modal__title { + margin: 0; + font-size: 20px; + font-weight: 600; + color: var(--cc-on-surface); + letter-spacing: 0.01em; +} + +.global-action-modal__close { + --mat-icon-button-state-layer-color: transparent; + color: var(--cc-on-surface-variant); + + &:hover { + color: var(--cc-on-surface); + } +} + +// --------------------------------------------------------------------------- +// Action Grid +// --------------------------------------------------------------------------- +.global-action-modal__actions { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + padding: 12px 24px 24px; +} + +// --------------------------------------------------------------------------- +// Action Button +// --------------------------------------------------------------------------- +.global-action-modal__action-btn { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 20px 16px; + border: 1px solid var(--cc-outline); + border-radius: 12px; + background: var(--cc-surface); + color: var(--cc-on-surface); + cursor: pointer; + transition: background 150ms ease, border-color 150ms ease, transform 100ms ease; + font-family: inherit; + text-align: center; + + &:hover { + background: var(--cc-surface-container-high); + border-color: var(--cc-on-surface-variant); + transform: translateY(-1px); + } + + &:active { + transform: translateY(0); + } + + &:focus-visible { + outline: 2px solid var(--mat-sys-primary, #38BDF8); + outline-offset: 2px; + } +} + +// Action icon wrapper +.global-action-modal__action-icon { + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 12px; + + .mat-icon { + width: 28px; + height: 28px; + font-size: 28px; + } +} + +// Action label +.global-action-modal__action-label { + font-size: 15px; + font-weight: 600; + letter-spacing: 0.01em; +} + +// Action description +.global-action-modal__action-desc { + font-size: 12px; + color: var(--cc-on-surface-variant); + line-height: 1.4; +} + +// --------------------------------------------------------------------------- +// Color Variants — per-action accent colors +// --------------------------------------------------------------------------- +.global-action-modal__action-btn--deploy { + .global-action-modal__action-icon { + background: var(--status-active-bg); + color: var(--status-active); + } + + &:hover { + border-color: var(--status-active); + } +} + +.global-action-modal__action-btn--pause { + .global-action-modal__action-icon { + background: var(--status-idle-bg); + color: var(--status-idle); + } + + &:hover { + border-color: var(--status-idle); + } +} + +.global-action-modal__action-btn--emergency { + .global-action-modal__action-icon { + background: var(--status-error-bg); + color: var(--status-error); + } + + &:hover { + border-color: var(--status-error); + } + + .global-action-modal__action-label { + color: var(--status-error); + } +} + +.global-action-modal__action-btn--add { + .global-action-modal__action-icon { + background: var(--status-thinking-bg); + color: var(--status-thinking); + } + + &:hover { + border-color: var(--status-thinking); + } +} + +// --------------------------------------------------------------------------- +// Responsive — stack single column on narrow viewports +// --------------------------------------------------------------------------- +@media (max-width: 400px) { + .global-action-modal__actions { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/global-action-modal/global-action-modal.component.ts b/frontend/src/app/components/global-action-modal/global-action-modal.component.ts new file mode 100644 index 0000000..dc36fa2 --- /dev/null +++ b/frontend/src/app/components/global-action-modal/global-action-modal.component.ts @@ -0,0 +1,87 @@ +import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Output, ViewChild } from '@angular/core'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; + +/** + * Global Action Modal — overlay for fleet-wide commands. + * + * Four main actions: Deploy All, Pause All, Emergency Stop, Add Agent. + * Tactical Dark Mode styling using Control Center design tokens. + * Dismisses on backdrop click or close button. + */ +@Component({ + selector: 'app-global-action-modal', + standalone: true, + imports: [MatIconModule, MatButtonModule], + templateUrl: './global-action-modal.component.html', + styleUrl: './global-action-modal.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class GlobalActionModalComponent { + /** Emitted when any action button is clicked. Payload is the action key. */ + @Output() readonly actionSelected = new EventEmitter(); + + /** Emitted when the modal is dismissed (backdrop click or close button). */ + @Output() readonly dismissed = new EventEmitter(); + + @ViewChild('backdrop') backdropEl!: ElementRef; + + /** All available global actions. */ + readonly actions: GlobalActionDef[] = [ + { + key: 'deploy-all', + label: 'Deploy All', + description: 'Deploy all agents in the fleet', + icon: 'rocket_launch', + color: 'deploy', + }, + { + key: 'pause-all', + label: 'Pause All', + description: 'Pause all running agents', + icon: 'pause_circle', + color: 'pause', + }, + { + key: 'emergency-stop', + label: 'Emergency Stop', + description: 'Immediately halt all agents', + icon: 'emergency', + color: 'emergency', + }, + { + key: 'add-agent', + label: 'Add Agent', + description: 'Register a new agent to the fleet', + icon: 'person_add', + color: 'add', + }, + ]; + + onBackdropClick(): void { + this.dismissed.emit(); + } + + onModalClick(event: Event): void { + // Prevent clicks inside the modal panel from closing it + event.stopPropagation(); + } + + onClose(): void { + this.dismissed.emit(); + } + + onAction(action: GlobalActionDef): void { + this.actionSelected.emit(action.key); + } +} + +export type GlobalAction = 'deploy-all' | 'pause-all' | 'emergency-stop' | 'add-agent'; + +export interface GlobalActionDef { + key: GlobalAction; + label: string; + description: string; + icon: string; + color: 'deploy' | 'pause' | 'emergency' | 'add'; +} \ No newline at end of file -- 2.53.0