CUB-27: Responsive layout and adaptive navigation
All checks were successful
Dev Build / build-test (pull_request) Successful in 2m46s

This commit is contained in:
2026-04-28 08:43:57 -04:00
committed by Rex
parent 048101e85c
commit 999f6614ce
13 changed files with 558 additions and 447 deletions

View File

@@ -0,0 +1,57 @@
<!-- ========================================================================== -->
<!-- Hub Page — Responsive Agent Card Grid with Filter Chips -->
<!-- Per CUB-27 spec breakpoints: -->
<!-- Compact (0599px): Single-column cards, horizontal-scroll filter chips -->
<!-- Medium (6001023px): 2-column grid -->
<!-- Expanded (≥1024px): 3+ column auto-fill grid -->
<!-- CUB-26: Integrates AgentCard click/long-press with session drawer. -->
<!-- ========================================================================== -->
<div class="hub-page">
<h1 class="hub-page__title">Command Hub</h1>
<!-- Filter Chip Group — horizontal scroll on mobile -->
<div class="hub-page__filters" role="tablist" aria-label="Filter agents by status">
@for (filter of filters; track filter.value) {
<button
class="hub-page__filter-chip"
[class.hub-page__filter-chip--active]="activeFilter() === filter.value"
role="tab"
[attr.aria-selected]="activeFilter() === filter.value"
(click)="selectFilter(filter.value)"
>
{{ filter.label }}
</button>
}
</div>
<!-- Agent Card Grid -->
<div class="hub-page__grid">
@for (agent of filteredAgents(); track agent.id) {
<app-agent-card
[status]="agent.status"
[task]="agent.currentTask ?? ''"
[progress]="agent.taskProgress ?? 0"
[sessionKey]="agent.sessionKey"
[channel]="agent.channel"
[lastActivity]="agent.lastActivity"
[agentId]="agent.id"
[displayName]="agent.displayName"
[role]="agent.role"
[errorMessage]="agent.errorMessage ?? ''"
(cardClick)="onCardClick($event)"
(cardLongPress)="onCardLongPress($event)"
/>
} @empty {
<p class="hub-page__empty">No agents online</p>
}
</div>
</div>
<!-- Agent Session Drawer (CUB-26) -->
<app-agent-session-drawer
[isMobile]="isMobile()"
(openSession)="onOpenSession($event)"
(pinToDashboard)="onPinToDashboard($event)"
(drawerClose)="onDrawerClose()"
/>

View File

@@ -1,14 +1,15 @@
// ============================================================================
// Hub Page — Responsive AgentCard Grid
// Desktop (≥1024px): 2×2 grid
// Mobile (<1024px): single-column stack
// Hub Page — Responsive AgentCard Grid with Filter Chips
// Per CUB-27 spec breakpoints:
// Compact (0599px): Single-column cards, horizontal-scroll filter chips
// Medium (6001023px): 2-column grid
// Expanded (≥1024px): 3+ column auto-fill grid
// ============================================================================
.hub-page {
display: grid;
grid-template-columns: 1fr;
display: flex;
flex-direction: column;
gap: 16px;
padding: var(--cc-section-padding, 16px);
min-height: 400px;
overflow-x: hidden;
}
@@ -21,17 +22,119 @@
margin: 0 0 8px;
}
// ---------------------------------------------------------------------------
// Filter Chip Group
// ---------------------------------------------------------------------------
.hub-page__filters {
display: flex;
gap: 8px;
padding: 4px 0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none; // Firefox
&::-webkit-scrollbar {
display: none; // Chrome/Safari
}
}
.hub-page__filter-chip {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 36px;
min-width: 48px; // Touch target
padding: 6px 16px;
border: 1px solid var(--cc-outline);
border-radius: 20px;
background-color: transparent;
color: var(--cc-on-surface-variant);
font-size: 13px;
font-weight: 500;
letter-spacing: 0.02em;
cursor: pointer;
white-space: nowrap;
transition: background-color 150ms ease, color 150ms ease, border-color 150ms ease;
flex-shrink: 0; // Prevent shrinking in scroll container
&:hover {
background-color: rgba(255, 255, 255, 0.06);
color: var(--cc-on-surface);
}
&:focus-visible {
outline: 2px solid var(--status-active);
outline-offset: 2px;
}
&--active {
background-color: var(--status-active-bg);
color: var(--status-active);
border-color: var(--status-active);
}
}
// ---------------------------------------------------------------------------
// Agent Card Grid
// ---------------------------------------------------------------------------
.hub-page__grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--cc-card-gap);
}
// ---------------------------------------------------------------------------
// Empty State
// ---------------------------------------------------------------------------
.hub-page__placeholder,
.hub-page__empty {
display: flex;
align-items: center;
justify-content: center;
padding: 48px 24px;
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) {
// ---------------------------------------------------------------------------
// Compact (0599px): Single-column cards
// ---------------------------------------------------------------------------
@media (max-width: 599px) {
.hub-page {
padding: 0;
}
.hub-page__grid {
grid-template-columns: 1fr;
gap: 12px;
}
.hub-page__filters {
padding: 4px 0 8px;
// Ensure horizontal scroll on mobile
margin: 0 -8px;
padding-left: 8px;
padding-right: 8px;
}
}
// ---------------------------------------------------------------------------
// Medium (6001023px): 2-column grid
// ---------------------------------------------------------------------------
@media (min-width: 600px) and (max-width: 1023px) {
.hub-page__grid {
grid-template-columns: repeat(2, 1fr);
gap: var(--cc-card-gap);
}
}
// ---------------------------------------------------------------------------
// Expanded (≥1024px): 3+ column auto-fill grid
// ---------------------------------------------------------------------------
@media (min-width: 1024px) {
.hub-page__grid {
grid-template-columns: repeat(auto-fill, minmax(var(--cc-card-min-width), 1fr));
gap: var(--cc-card-gap);
}
}

View File

@@ -1,51 +1,22 @@
import { ChangeDetectionStrategy, Component, signal, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, Component, signal, computed, ViewChild } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatChipsModule } from '@angular/material/chips';
import { AgentCardComponent } from '../../command-hub/components/agent-card/agent-card.component';
import { AgentSessionDrawerComponent } from '../../components/agent-session-drawer/index';
import { AgentCardData } from '../../models/agent.model';
import { AgentStatus } from '../../models/agent.model';
// ============================================================================
// Hub Page — Fleet status grid
// CUB-26: Integrates AgentCard click/long-press with session drawer.
// ============================================================================
/**
* Filter options for the hub page agent card grid.
* Per CUB-27: "Filter chip group (All, Active, Error, etc.) with horizontal scroll on mobile"
*/
export type AgentFilter = 'all' | AgentStatus;
@Component({
selector: 'app-hub-page',
standalone: true,
imports: [CommonModule, AgentCardComponent, AgentSessionDrawerComponent],
template: `
<div class="hub-page">
<h1 class="hub-page__title">Command Hub</h1>
<div class="hub-page__grid">
@for (agent of agents(); track agent.id) {
<app-agent-card
[status]="agent.status"
[task]="agent.currentTask ?? ''"
[progress]="agent.taskProgress ?? 0"
[sessionKey]="agent.sessionKey"
[channel]="agent.channel"
[lastActivity]="agent.lastActivity"
[agentId]="agent.id"
[displayName]="agent.displayName"
[role]="agent.role"
[errorMessage]="agent.errorMessage ?? ''"
(cardClick)="onCardClick($event)"
(cardLongPress)="onCardLongPress($event)"
/>
} @empty {
<p class="hub-page__empty">No agents online</p>
}
</div>
</div>
<!-- Agent Session Drawer -->
<app-agent-session-drawer
[isMobile]="isMobile()"
(openSession)="onOpenSession($event)"
(pinToDashboard)="onPinToDashboard($event)"
(drawerClose)="onDrawerClose()"
/>
`,
imports: [CommonModule, MatChipsModule, AgentCardComponent, AgentSessionDrawerComponent],
templateUrl: './hub-page.component.html',
styleUrl: './hub-page.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
@@ -54,6 +25,17 @@ export class HubPageComponent {
readonly isMobile = signal(false);
protected readonly filters: { label: string; value: AgentFilter }[] = [
{ label: 'All', value: 'all' },
{ label: 'Active', value: 'active' },
{ label: 'Idle', value: 'idle' },
{ label: 'Thinking', value: 'thinking' },
{ label: 'Error', value: 'error' },
{ label: 'Offline', value: 'offline' },
];
protected readonly activeFilter = signal<AgentFilter>('all');
/** Stub agent data (TODO: wire to AgentStatusService / SignalR). */
readonly agents = signal<AgentCardData[]>([
{
@@ -73,10 +55,10 @@ export class HubPageComponent {
displayName: 'Rex',
role: 'Frontend Agent',
status: 'thinking',
currentTask: 'Building agent session drawer',
currentTask: 'Building responsive layout',
taskProgress: 40,
taskElapsed: '02m 30s',
sessionKey: 'agent:rex:telegram:CUB-26:def456',
sessionKey: 'agent:rex:telegram:CUB-27:def456',
channel: 'telegram',
lastActivity: new Date(Date.now() - 30000),
},
@@ -119,6 +101,12 @@ export class HubPageComponent {
},
]);
protected readonly filteredAgents = computed(() => {
const filter = this.activeFilter();
if (filter === 'all') return this.agents();
return this.agents().filter(a => a.status === filter);
});
constructor() {
// Detect mobile viewport
if (typeof window !== 'undefined') {
@@ -128,6 +116,10 @@ export class HubPageComponent {
}
}
protected selectFilter(filter: AgentFilter): void {
this.activeFilter.set(filter);
}
/** Card click → open session drawer with agent details. */
onCardClick(sessionKey: string): void {
const agent = this.agents().find((a) => a.sessionKey === sessionKey);