From 14b3dab88ba17f6e07554c822e2ce3d073a0fc0d Mon Sep 17 00:00:00 2001 From: "cubecraft-agents[bot]" <3458173+cubecraft-agents[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 13:09:18 +0000 Subject: [PATCH] CUB-44: add task-progress-bar component with determinate mode and elapsed time --- .../app/components/task-progress-bar/index.ts | 6 + .../task-progress-bar.component.html | 18 +++ .../task-progress-bar.component.scss | 77 +++++++++++++ .../task-progress-bar.component.ts | 109 ++++++++++++++++++ 4 files changed, 210 insertions(+) create mode 100644 frontend/src/app/components/task-progress-bar/index.ts create mode 100644 frontend/src/app/components/task-progress-bar/task-progress-bar.component.html create mode 100644 frontend/src/app/components/task-progress-bar/task-progress-bar.component.scss create mode 100644 frontend/src/app/components/task-progress-bar/task-progress-bar.component.ts diff --git a/frontend/src/app/components/task-progress-bar/index.ts b/frontend/src/app/components/task-progress-bar/index.ts new file mode 100644 index 0000000..67414db --- /dev/null +++ b/frontend/src/app/components/task-progress-bar/index.ts @@ -0,0 +1,6 @@ +// ============================================================================ +// Task Progress Bar — Barrel Export +// CUB-44 +// ============================================================================ + +export { TaskProgressBarComponent } from './task-progress-bar.component'; \ No newline at end of file diff --git a/frontend/src/app/components/task-progress-bar/task-progress-bar.component.html b/frontend/src/app/components/task-progress-bar/task-progress-bar.component.html new file mode 100644 index 0000000..f8d8a7d --- /dev/null +++ b/frontend/src/app/components/task-progress-bar/task-progress-bar.component.html @@ -0,0 +1,18 @@ + +
+ +
+ {{ clampedProgress }}% + + {{ elapsedText }} + +
+ + + +
\ No newline at end of file diff --git a/frontend/src/app/components/task-progress-bar/task-progress-bar.component.scss b/frontend/src/app/components/task-progress-bar/task-progress-bar.component.scss new file mode 100644 index 0000000..bb467a2 --- /dev/null +++ b/frontend/src/app/components/task-progress-bar/task-progress-bar.component.scss @@ -0,0 +1,77 @@ +// ============================================================================ +// Task Progress Bar — Tactical Dark Theme Styling +// Per CUB-44: Uses --color-primary for bar fill and --color-surface-light +// for track background, mapped to the Control Center's M3 dark tokens. +// ============================================================================ + +// --------------------------------------------------------------------------- +// Container +// --------------------------------------------------------------------------- +.task-progress-bar { + display: flex; + flex-direction: column; + gap: 6px; + width: 100%; +} + +// --------------------------------------------------------------------------- +// Info row: percentage label + elapsed time +// --------------------------------------------------------------------------- +.task-progress-bar__info { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 8px; +} + +.task-progress-bar__percent { + font-family: var(--cc-font-mono, 'Roboto Mono', monospace); + font-size: 14px; + font-weight: 600; + color: var(--cc-on-surface, #E2E8F0); + letter-spacing: 0.02em; +} + +.task-progress-bar__elapsed { + font-family: var(--cc-font-mono, 'Roboto Mono', monospace); + font-size: 12px; + font-weight: 400; + color: var(--cc-on-surface-variant, #8A9BB0); + letter-spacing: 0.01em; +} + +// --------------------------------------------------------------------------- +// Material Progress Bar Overrides +// --------------------------------------------------------------------------- +// Map the spec's --color-primary and --color-surface-light to the Control +// Center's actual theme tokens. This ensures the bar uses the tactical dark +// palette while respecting the spec's variable naming. +// --------------------------------------------------------------------------- + +.task-progress-bar__bar { + // Override the track (background) to use the surface container + --mat-progress-bar-track-height: 6px; + --mat-progress-bar-active-indicator-height: 6px; + + // Bar fill color: primary (cyan/sky blue per tactical dark theme) + --mat-progress-bar-active-indicator-color: var(--color-primary, var(--mat-sys-primary, #38BDF8)); + + // Track background: surface container (dark slate) + --mat-progress-bar-track-color: var(--color-surface-light, var(--cc-surface-container, #1C2027)); + + // Border radius for a softer bar + border-radius: 3px; + + // Smooth transition on value changes + transition: none; +} + +// Rounded ends on the progress bar fill +:host ::ng-deep .mdc-linear-progress__bar-inner { + border-radius: 3px; +} + +// Rounded track background +:host ::ng-deep .mdc-linear-progress__track { + border-radius: 3px; +} \ No newline at end of file diff --git a/frontend/src/app/components/task-progress-bar/task-progress-bar.component.ts b/frontend/src/app/components/task-progress-bar/task-progress-bar.component.ts new file mode 100644 index 0000000..a380566 --- /dev/null +++ b/frontend/src/app/components/task-progress-bar/task-progress-bar.component.ts @@ -0,0 +1,109 @@ +// ============================================================================ +// Task Progress Bar Component +// Per CUB-44: Determinate progress bar with optional elapsed time display. +// Uses Angular Material mat-progress-bar in determinate mode with tactical +// dark theme styling via CSS custom properties. +// ============================================================================ + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Input, + OnDestroy, + OnInit, +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; + +/** + * Displays a determinate progress bar with an optional elapsed time indicator. + * + * Usage: + * ```html + * + * + * ``` + */ +@Component({ + selector: 'app-task-progress-bar', + standalone: true, + imports: [CommonModule, MatProgressBarModule], + templateUrl: './task-progress-bar.component.html', + styleUrl: './task-progress-bar.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TaskProgressBarComponent implements OnInit, OnDestroy { + // --------------------------------------------------------------------------- + // Inputs + // --------------------------------------------------------------------------- + + /** Current progress percentage (0–100). Required. */ + @Input({ required: true }) + progress!: number; + + /** Whether to show elapsed time next to the percentage. Defaults to false. */ + @Input() + showElapsed = false; + + // --------------------------------------------------------------------------- + // Internal state + // --------------------------------------------------------------------------- + + /** Timestamp when the component initialized — used for elapsed calculation. */ + startTime = Date.now(); + + /** Formatted elapsed time string, e.g. "2m 15s ago". */ + elapsedText = ''; + + /** Interval timer for updating the elapsed display. */ + private timer: ReturnType | null = null; + + constructor(private cdr: ChangeDetectorRef) {} + + // --------------------------------------------------------------------------- + // Lifecycle + // --------------------------------------------------------------------------- + + ngOnInit(): void { + this.updateElapsed(); + + if (this.showElapsed) { + // Update elapsed time every second + this.timer = setInterval(() => { + this.updateElapsed(); + this.cdr.markForCheck(); + }, 1000); + } + } + + ngOnDestroy(): void { + if (this.timer) { + clearInterval(this.timer); + this.timer = null; + } + } + + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + /** Clamp progress to 0–100 for safety. */ + get clampedProgress(): number { + return Math.max(0, Math.min(100, this.progress ?? 0)); + } + + /** Recalculate the elapsed time string. */ + private updateElapsed(): void { + const elapsedMs = Date.now() - this.startTime; + const totalSeconds = Math.floor(elapsedMs / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + + if (minutes > 0) { + this.elapsedText = `${minutes}m ${seconds}s ago`; + } else { + this.elapsedText = `${seconds}s ago`; + } + } +} \ No newline at end of file