Merge branch 'dev' into agent/rex/CUB-44-task-progress-bar
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
<!-- ========================================================================== -->
|
||||
<!-- AgentCard — per spec Section 7.3 -->
|
||||
<!-- Integrates: Status Badge · Task Progress Bar · Quick‑Jump Button -->
|
||||
<!-- Left‑border accent matches status color. role="article" + aria‑labels. -->
|
||||
<!-- ========================================================================== -->
|
||||
<article
|
||||
class="agent-card"
|
||||
role="article"
|
||||
[attr.aria-label]="displayName + ' — ' + statusLabel()"
|
||||
[style.border-left-color]="statusBorderColor()"
|
||||
>
|
||||
|
||||
<!-- ── Header: status badge + agent info ── -->
|
||||
<div class="agent-card__header">
|
||||
<div class="agent-card__badge" [attr.aria-label]="'Status: ' + statusLabel()">
|
||||
<span
|
||||
class="status-dot"
|
||||
[ngClass]="[statusDotClass()]"
|
||||
></span>
|
||||
<span class="agent-card__status-label">{{ statusLabel() }}</span>
|
||||
</div>
|
||||
|
||||
<div class="agent-card__identity">
|
||||
<span class="agent-card__name">{{ displayName || agentId }}</span>
|
||||
<span class="agent-card__role">{{ role }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Body: current task ── -->
|
||||
<div class="agent-card__body" *ngIf="task || status === 'error'">
|
||||
<p
|
||||
class="agent-card__task"
|
||||
[attr.aria-label]="'Current task: ' + (status === 'error' ? errorMessage : task)"
|
||||
>
|
||||
{{ status === 'error' ? errorMessage || task : task }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- ── Task Progress Bar ── -->
|
||||
<div
|
||||
class="agent-card__progress"
|
||||
*ngIf="progress > 0 && status !== 'error'"
|
||||
[attr.aria-label]="'Task progress: ' + progress + '%'"
|
||||
>
|
||||
<mat-progress-bar
|
||||
mode="determinate"
|
||||
[value]="progress"
|
||||
[aria-label]="'Progress ' + progress + '% complete'"
|
||||
></mat-progress-bar>
|
||||
<span class="agent-card__progress-label text-mono">{{ progress }}%</span>
|
||||
</div>
|
||||
|
||||
<!-- ── Footer: channel + last activity + quick‑jump ── -->
|
||||
<div class="agent-card__footer">
|
||||
<div class="agent-card__meta">
|
||||
<span
|
||||
class="agent-card__channel text-mono"
|
||||
[attr.aria-label]="'Channel: ' + channel"
|
||||
>
|
||||
<mat-icon aria-hidden="true">{{ channelIcon() }}</mat-icon>
|
||||
{{ channel }}
|
||||
</span>
|
||||
<span
|
||||
class="agent-card__last-activity text-mono"
|
||||
[attr.aria-label]="'Last activity: ' + lastActivityLabel()"
|
||||
>
|
||||
{{ lastActivityLabel() }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Quick‑Jump Button -->
|
||||
<a
|
||||
class="agent-card__jump"
|
||||
mat-button
|
||||
[routerLink]="jumpRoute()"
|
||||
[attr.aria-label]="'Jump to session for ' + (displayName || agentId)"
|
||||
matTooltip="Jump to session"
|
||||
>
|
||||
<mat-icon aria-hidden="true">arrow_forward</mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,234 @@
|
||||
// ============================================================================
|
||||
// AgentCard — M3 tactical dark styling
|
||||
// Per spec Section 7.3: left‑border accent, status‑aware coloring,
|
||||
// responsive card layout with 320px min‑width.
|
||||
// ============================================================================
|
||||
|
||||
.agent-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: var(--cc-card-min-width);
|
||||
padding: var(--cc-card-padding);
|
||||
background-color: var(--cc-surface-container);
|
||||
border-radius: var(--cc-card-border-radius);
|
||||
border-left: 4px solid var(--status-offline); // default; overridden by [style]
|
||||
border-top: 1px solid var(--cc-outline);
|
||||
border-right: 1px solid var(--cc-outline);
|
||||
border-bottom: 1px solid var(--cc-outline);
|
||||
gap: 16px;
|
||||
transition: border-left-color 0.3s ease, box-shadow 0.2s ease;
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
outline: 2px solid var(--status-active);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Header ──
|
||||
.agent-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.agent-card__badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
background-color: var(--status-active-bg); // overridden per status below
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
color: var(--cc-on-surface);
|
||||
|
||||
// Per‑status background tints
|
||||
.status-dot--active + & {
|
||||
background-color: var(--status-active-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.agent-card__status-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--cc-on-surface-variant);
|
||||
}
|
||||
|
||||
.agent-card__identity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.agent-card__name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--cc-on-surface);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.agent-card__role {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--cc-on-surface-variant);
|
||||
}
|
||||
|
||||
// ── Body ──
|
||||
.agent-card__body {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.agent-card__task {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--cc-on-surface);
|
||||
line-height: 1.4;
|
||||
|
||||
// Error messages get distinct styling
|
||||
.agent-card--error & {
|
||||
color: var(--status-error);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Progress Bar ──
|
||||
.agent-card__progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.agent-card__progress-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--cc-on-surface-variant);
|
||||
white-space: nowrap;
|
||||
min-width: 36px;
|
||||
}
|
||||
|
||||
// Override mat-progress-bar to match tactical dark theme
|
||||
.agent-card__progress ::ng-deep .mat-mdc-progress-bar {
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
|
||||
.mdc-linear-progress__bar-inner {
|
||||
background-color: var(--status-active);
|
||||
}
|
||||
|
||||
.mdc-linear-progress__track {
|
||||
background-color: var(--cc-outline);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Footer ──
|
||||
.agent-card__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-top: auto; // push footer to bottom
|
||||
}
|
||||
|
||||
.agent-card__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.agent-card__channel {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--cc-on-surface-variant);
|
||||
}
|
||||
|
||||
.agent-card__channel-icon,
|
||||
.agent-card__channel .mat-icon {
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.agent-card__last-activity {
|
||||
font-size: 12px;
|
||||
color: var(--cc-on-surface-variant);
|
||||
}
|
||||
|
||||
// ── Quick‑Jump Button ──
|
||||
.agent-card__jump {
|
||||
flex-shrink: 0;
|
||||
|
||||
// Match M3 text button sizing
|
||||
.mat-mdc-button {
|
||||
min-width: 36px;
|
||||
padding: 0 8px;
|
||||
color: var(--status-active);
|
||||
}
|
||||
|
||||
.mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Status‑specific background tints for badge ──
|
||||
// We use the global status-dot classes from styles.scss and pair them
|
||||
// with contextual background-color overrides here.
|
||||
|
||||
.agent-card[data-status="active"] .agent-card__badge,
|
||||
.agent-card .status-dot--active ~ .agent-card__badge {
|
||||
background-color: var(--status-active-bg);
|
||||
}
|
||||
|
||||
.agent-card[data-status="idle"] .agent-card__badge {
|
||||
background-color: var(--status-idle-bg);
|
||||
}
|
||||
|
||||
.agent-card[data-status="thinking"] .agent-card__badge {
|
||||
background-color: var(--status-thinking-bg);
|
||||
}
|
||||
|
||||
.agent-card[data-status="error"] .agent-card__badge {
|
||||
background-color: var(--status-error-bg);
|
||||
}
|
||||
|
||||
// ── Responsive ──
|
||||
@media (max-width: 599px) {
|
||||
.agent-card {
|
||||
min-width: unset;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.agent-card__header {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.agent-card__footer {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.agent-card__meta {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Accessibility: reduced motion ──
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.agent-card {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
computed,
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { AgentStatus } from '../../../models/agent.model';
|
||||
|
||||
// ============================================================================
|
||||
// AgentCard Component
|
||||
// Per spec Section 7.3: Composes Agent Status Badge, Task Progress Bar,
|
||||
// and Quick‑Jump Button into a card with left‑border status accent.
|
||||
// ============================================================================
|
||||
|
||||
@Component({
|
||||
selector: 'app-agent-card',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatProgressBarModule,
|
||||
MatTooltipModule,
|
||||
],
|
||||
templateUrl: './agent-card.component.html',
|
||||
styleUrl: './agent-card.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AgentCardComponent {
|
||||
// --- Six required inputs per spec ---
|
||||
|
||||
/** Agent status — drives badge color and left‑border accent */
|
||||
@Input({ required: true }) status!: AgentStatus;
|
||||
|
||||
/** Current task description, e.g. "Reviewing PR #42" */
|
||||
@Input() task = '';
|
||||
|
||||
/** Task progress percentage 0–100 */
|
||||
@Input() progress = 0;
|
||||
|
||||
/** Full session key for quick‑jump navigation */
|
||||
@Input({ required: true }) sessionKey = '';
|
||||
|
||||
/** Communication channel, e.g. "telegram" */
|
||||
@Input({ required: true }) channel = '';
|
||||
|
||||
/** Timestamp of last agent activity */
|
||||
@Input({ required: true }) lastActivity!: Date;
|
||||
|
||||
// --- Additional display inputs ---
|
||||
|
||||
/** Short agent ID, e.g. "otto" */
|
||||
@Input() agentId = '';
|
||||
|
||||
/** Display name, e.g. "Otto" */
|
||||
@Input() displayName = '';
|
||||
|
||||
/** Role description, e.g. "Orchestrator Agent" */
|
||||
@Input() role = '';
|
||||
|
||||
/** Error message (shown only when status is 'error') */
|
||||
@Input() errorMessage = '';
|
||||
|
||||
// --- Computed values ---
|
||||
|
||||
/** Map status → CSS custom property for the left‑border accent */
|
||||
readonly statusBorderColor = computed(() => {
|
||||
const map: Record<AgentStatus, string> = {
|
||||
active: 'var(--status-active)',
|
||||
idle: 'var(--status-idle)',
|
||||
thinking: 'var(--status-thinking)',
|
||||
error: 'var(--status-error)',
|
||||
offline: 'var(--status-offline)',
|
||||
};
|
||||
return map[this.status] ?? 'var(--status-offline)';
|
||||
});
|
||||
|
||||
/** Human‑readable status label */
|
||||
readonly statusLabel = computed(() => {
|
||||
const labels: Record<AgentStatus, string> = {
|
||||
active: 'Active',
|
||||
idle: 'Idle',
|
||||
thinking: 'Thinking…',
|
||||
error: 'Error',
|
||||
offline: 'Offline',
|
||||
};
|
||||
return labels[this.status] ?? this.status;
|
||||
});
|
||||
|
||||
/** CSS class suffix for the status badge dot */
|
||||
readonly statusDotClass = computed(() => `status-dot--${this.status}`);
|
||||
|
||||
/** Material icon name for the channel */
|
||||
readonly channelIcon = computed(() => {
|
||||
const icons: Record<string, string> = {
|
||||
telegram: 'telegram', // falls back to font icon if no SVG registered
|
||||
slack: 'chat',
|
||||
discord: 'forum',
|
||||
whatsapp: 'chat',
|
||||
webchat: 'language',
|
||||
email: 'email',
|
||||
};
|
||||
return icons[this.channel] ?? 'chat';
|
||||
});
|
||||
|
||||
/** Relative time string for lastActivity */
|
||||
readonly lastActivityLabel = computed(() => {
|
||||
if (!this.lastActivity) return '';
|
||||
const now = Date.now();
|
||||
const then = this.lastActivity.getTime();
|
||||
const diffSec = Math.max(0, Math.floor((now - then) / 1000));
|
||||
if (diffSec < 60) return 'just now';
|
||||
if (diffSec < 3600) return `${Math.floor(diffSec / 60)}m ago`;
|
||||
if (diffSec < 86400) return `${Math.floor(diffSec / 3600)}h ago`;
|
||||
return `${Math.floor(diffSec / 86400)}d ago`;
|
||||
});
|
||||
|
||||
/** Quick‑jump route derived from sessionKey */
|
||||
readonly jumpRoute = computed(() => `/sessions/${this.sessionKey}`);
|
||||
}
|
||||
1
frontend/src/app/command-hub/components/index.ts
Normal file
1
frontend/src/app/command-hub/components/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './agent-card/agent-card.component';
|
||||
@@ -0,0 +1,8 @@
|
||||
<span class="badge"
|
||||
[class]="statusClass"
|
||||
[class.badge--pulse]="hasPulse"
|
||||
[attr.aria-label]="'Agent status: ' + displayLabel"
|
||||
role="status">
|
||||
<span class="badge__dot"></span>
|
||||
<span class="badge__label">{{ displayLabel }}</span>
|
||||
</span>
|
||||
@@ -0,0 +1,146 @@
|
||||
// ============================================================================
|
||||
// Agent Status Badge — per spec Section 7.3
|
||||
// Colored pill with dot indicator and optional pulse animation.
|
||||
// ============================================================================
|
||||
|
||||
$badge-height: 24px;
|
||||
$dot-size: 8px;
|
||||
$border-radius: 12px;
|
||||
$font-size: 12px;
|
||||
$font-weight: 500;
|
||||
$padding-x: 8px;
|
||||
$gap: 6px;
|
||||
|
||||
@use 'sass:color';
|
||||
|
||||
// Status color palette
|
||||
$color-active: #22c55e; // green-500
|
||||
$color-idle: #9ca3af; // gray-400
|
||||
$color-thinking: #3b82f6; // blue-500
|
||||
$color-error: #ef4444; // red-500
|
||||
$color-offline: #9ca3af; // gray-400
|
||||
|
||||
// Background tints (12% opacity for soft pill background)
|
||||
$bg-active: rgba($color-active, 0.12);
|
||||
$bg-idle: rgba($color-idle, 0.12);
|
||||
$bg-thinking: rgba($color-thinking, 0.12);
|
||||
$bg-error: rgba($color-error, 0.12);
|
||||
$bg-offline: rgba($color-offline, 0.12);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Base pill
|
||||
// ---------------------------------------------------------------------------
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: $badge-height;
|
||||
padding: 0 $padding-x;
|
||||
border-radius: $border-radius;
|
||||
gap: $gap;
|
||||
font-size: $font-size;
|
||||
font-weight: $font-weight;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dot indicator
|
||||
// ---------------------------------------------------------------------------
|
||||
.badge__dot {
|
||||
width: $dot-size;
|
||||
height: $dot-size;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Label text
|
||||
// ---------------------------------------------------------------------------
|
||||
.badge__label {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Status color variants
|
||||
// ---------------------------------------------------------------------------
|
||||
.badge--active {
|
||||
background: $bg-active;
|
||||
color: color.adjust($color-active, $lightness: -10%);
|
||||
|
||||
.badge__dot {
|
||||
background: $color-active;
|
||||
}
|
||||
}
|
||||
|
||||
.badge--idle {
|
||||
background: $bg-idle;
|
||||
color: color.adjust($color-idle, $lightness: -15%);
|
||||
|
||||
.badge__dot {
|
||||
background: $color-idle;
|
||||
}
|
||||
}
|
||||
|
||||
.badge--thinking {
|
||||
background: $bg-thinking;
|
||||
color: color.adjust($color-thinking, $lightness: -10%);
|
||||
|
||||
.badge__dot {
|
||||
background: $color-thinking;
|
||||
}
|
||||
}
|
||||
|
||||
.badge--error {
|
||||
background: $bg-error;
|
||||
color: color.adjust($color-error, $lightness: -10%);
|
||||
|
||||
.badge__dot {
|
||||
background: $color-error;
|
||||
}
|
||||
}
|
||||
|
||||
.badge--offline {
|
||||
background: $bg-offline;
|
||||
color: color.adjust($color-offline, $lightness: -15%);
|
||||
|
||||
.badge__dot {
|
||||
background: $color-offline;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pulse animation — applied when status is active, thinking, or error
|
||||
// ---------------------------------------------------------------------------
|
||||
.badge--pulse {
|
||||
.badge__dot {
|
||||
animation: pulse-dot 2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
// Active: 2s pulse
|
||||
.badge--active.badge--pulse .badge__dot {
|
||||
animation-duration: 2s;
|
||||
}
|
||||
|
||||
// Thinking: 3s pulse
|
||||
.badge--thinking.badge--pulse .badge__dot {
|
||||
animation-duration: 3s;
|
||||
}
|
||||
|
||||
// Error: 0.8s pulse (fast, urgent)
|
||||
.badge--error.badge--pulse .badge__dot {
|
||||
animation-duration: 0.8s;
|
||||
}
|
||||
|
||||
@keyframes pulse-dot {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
import { AgentStatus } from '../../models/agent.model';
|
||||
|
||||
/**
|
||||
* Agent Status Badge component.
|
||||
* Displays a colored pill with a pulse animation indicating the agent's current status.
|
||||
* Per spec Section 7.3: Agent Card Component Interface — status indicator.
|
||||
*
|
||||
* Color mapping:
|
||||
* - Active → green
|
||||
* - Idle → gray
|
||||
* - Thinking → blue
|
||||
* - Error → red
|
||||
* - Offline → gray (no pulse)
|
||||
*
|
||||
* Pulse animations:
|
||||
* - Active → 2s
|
||||
* - Error → 0.8s
|
||||
* - Thinking → 3s
|
||||
* - Idle / Offline → no pulse
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-agent-status-badge',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
templateUrl: './agent-status-badge.component.html',
|
||||
styleUrl: './agent-status-badge.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AgentStatusBadgeComponent {
|
||||
/** Current agent status — binds to the AgentStatus type from the model. */
|
||||
readonly status = input.required<AgentStatus>();
|
||||
|
||||
/** Label text shown inside the badge. Defaults to title-cased status. */
|
||||
readonly label = input<string>();
|
||||
|
||||
get displayLabel(): string {
|
||||
return this.label() ?? this.titleCase(this.status());
|
||||
}
|
||||
|
||||
/** CSS class driven by the current status value. */
|
||||
get statusClass(): string {
|
||||
return `badge--${this.status()}`;
|
||||
}
|
||||
|
||||
/** Whether the pulse animation should be active for the current status. */
|
||||
get hasPulse(): boolean {
|
||||
return this.status() === 'active' || this.status() === 'thinking' || this.status() === 'error';
|
||||
}
|
||||
|
||||
private titleCase(value: string): string {
|
||||
return value.charAt(0).toUpperCase() + value.slice(1);
|
||||
}
|
||||
}
|
||||
1
frontend/src/app/components/agent-status-badge/index.ts
Normal file
1
frontend/src/app/components/agent-status-badge/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { AgentStatusBadgeComponent } from './agent-status-badge.component';
|
||||
1
frontend/src/app/components/index.ts
Normal file
1
frontend/src/app/components/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { AgentStatusBadgeComponent } from './agent-status-badge/agent-status-badge.component';
|
||||
28
frontend/src/app/pages/hub/hub-page.component.scss
Normal file
28
frontend/src/app/pages/hub/hub-page.component.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
// ============================================================================
|
||||
// Hub Page — Responsive AgentCard Grid
|
||||
// Desktop (≥1024px): 2×2 grid
|
||||
// Mobile (<1024px): single-column stack
|
||||
// ============================================================================
|
||||
|
||||
.hub-page {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
padding: var(--cc-section-padding, 16px);
|
||||
min-height: 400px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.hub-page__placeholder {
|
||||
color: var(--cc-on-surface-variant);
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
// Desktop / kiosk breakpoint — 2-column grid
|
||||
@media (min-width: 1024px) {
|
||||
.hub-page {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@@ -9,18 +9,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
<p class="hub-page__placeholder">Command Hub — Fleet status grid will render here</p>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.hub-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
.hub-page__placeholder {
|
||||
color: var(--cc-on-surface-variant);
|
||||
font-size: 16px;
|
||||
}
|
||||
`],
|
||||
styleUrl: './hub-page.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class HubPageComponent {}
|
||||
@@ -48,6 +48,17 @@ html {
|
||||
// These are NOT part of the M3 tonal palette; they are semantic overrides.
|
||||
// ---------------------------------------------------------------------------
|
||||
:root {
|
||||
// --- Tactical Dark Mode color palette (CUB-47) ---
|
||||
--color-surface: #0F172A;
|
||||
--color-surface-light: #1E293B;
|
||||
--color-primary: #38BDF8;
|
||||
--color-secondary: #2DD4BF;
|
||||
--color-accent: #A78BFA;
|
||||
--color-danger: #F87171;
|
||||
--color-text-primary: #FFFFFF;
|
||||
--color-text-secondary: #94A3B8;
|
||||
--color-border: #334155;
|
||||
|
||||
// --- Status colors ---
|
||||
--status-active: #38BDF8;
|
||||
--status-idle: #2DD4BF;
|
||||
@@ -90,7 +101,7 @@ html {
|
||||
// Global Body Styles
|
||||
// ---------------------------------------------------------------------------
|
||||
body {
|
||||
background-color: var(--cc-background);
|
||||
background-color: var(--color-surface);
|
||||
color: var(--cc-on-surface);
|
||||
font-family: 'Inter', 'Roboto', sans-serif;
|
||||
margin: 0;
|
||||
|
||||
Reference in New Issue
Block a user