From ff9f6fec83f3155517436c126f786c2121f0d610 Mon Sep 17 00:00:00 2001 From: rex-bot Date: Tue, 28 Apr 2026 08:43:57 -0400 Subject: [PATCH] CUB-27: Responsive layout and adaptive navigation --- frontend/src/app/app.html | 345 +----------------- frontend/src/app/app.ts | 2 +- .../adaptive-navigation.component.scss | 81 +++- .../adaptive-navigation.component.ts | 45 ++- .../bottom-nav/bottom-nav.component.scss | 24 +- .../header-bar/header-bar.component.scss | 76 +++- .../layout-shell/layout-shell.component.scss | 24 +- .../layout/nav-rail/nav-rail.component.scss | 54 ++- .../app/layout/nav-rail/nav-rail.component.ts | 41 ++- .../src/app/pages/hub/hub-page.component.html | 49 +++ .../src/app/pages/hub/hub-page.component.scss | 123 ++++++- .../src/app/pages/hub/hub-page.component.ts | 114 +++++- frontend/src/styles.scss | 9 +- 13 files changed, 571 insertions(+), 416 deletions(-) create mode 100644 frontend/src/app/pages/hub/hub-page.component.html diff --git a/frontend/src/app/app.html b/frontend/src/app/app.html index a1c4296..12b5d24 100644 --- a/frontend/src/app/app.html +++ b/frontend/src/app/app.html @@ -1,344 +1 @@ - - - - - - - - - - - -
-
-
- -

Hello, {{ title() }}

-

Congratulations! Your app is running. 🎉

-
- -
-
- @for (item of [ - { title: 'Explore the Docs', link: 'https://angular.dev' }, - { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' }, - { title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'}, - { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' }, - { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' }, - { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' }, - ]; track item.title) { - - {{ item.title }} - - - - - } -
- -
-
-
- - - - - - - - - - - + \ No newline at end of file diff --git a/frontend/src/app/app.ts b/frontend/src/app/app.ts index 425baa8..d514aa5 100644 --- a/frontend/src/app/app.ts +++ b/frontend/src/app/app.ts @@ -5,7 +5,7 @@ import { RouterOutlet } from '@angular/router'; selector: 'app-root', standalone: true, imports: [RouterOutlet], - template: ``, + template: '', styles: [` :host { display: block; diff --git a/frontend/src/app/components/adaptive-navigation/adaptive-navigation.component.scss b/frontend/src/app/components/adaptive-navigation/adaptive-navigation.component.scss index 27fbd5b..6428d77 100644 --- a/frontend/src/app/components/adaptive-navigation/adaptive-navigation.component.scss +++ b/frontend/src/app/components/adaptive-navigation/adaptive-navigation.component.scss @@ -1,14 +1,16 @@ // ============================================================================ // Adaptive Navigation — Desktop sidebar / Mobile header -// Desktop (≥768px): 72px sidebar with full navigation items -// Mobile (<768px): 56px compact header with hamburger menu +// Per CUB-27 spec breakpoints: +// Compact (0–599px): Mobile header + hamburger + drawer +// Medium (600–1023px): Collapsed sidebar (icon-only, 72px) +// Expanded (≥1024px): Full sidebar with labels (72px collapsed, 256px expanded) // ============================================================================ // --------------------------------------------------------------------------- -// Desktop Sidebar (visible ≥768px) +// Desktop Sidebar (visible ≥600px) // --------------------------------------------------------------------------- .adaptive-nav__sidebar { - display: flex; + display: none; // Hidden by default (mobile-first) flex-direction: column; width: var(--cc-nav-rail-collapsed-width, 72px); min-height: 100vh; @@ -152,12 +154,12 @@ } // --------------------------------------------------------------------------- -// Mobile Header (visible <768px) +// Mobile Header (visible <600px only) // --------------------------------------------------------------------------- .adaptive-nav__mobile-header { display: none; // Hidden on desktop, shown on mobile via media query align-items: center; - height: 56px; + height: var(--cc-header-height-compact, 56px); padding: 0 12px; background-color: var(--cc-surface-container-high); border-bottom: 1px solid var(--cc-outline); @@ -167,6 +169,8 @@ .adaptive-nav__hamburger { color: var(--cc-on-surface-variant) !important; + min-width: 48px; + min-height: 48px; &:hover { color: var(--cc-on-surface) !important; @@ -196,6 +200,8 @@ .adaptive-nav__mobile-action { color: var(--cc-on-surface-variant) !important; + min-width: 48px; + min-height: 48px; &:hover { color: var(--cc-on-surface) !important; @@ -214,7 +220,7 @@ .adaptive-nav__mobile-drawer { position: fixed; - top: 56px; // Below header + top: var(--cc-header-height-compact, 56px); // Below header left: 0; bottom: 0; width: 280px; @@ -271,34 +277,73 @@ } // --------------------------------------------------------------------------- -// Media Queries — Layout Switch +// Media Queries — Layout Switch (CUB-27 breakpoints) // --------------------------------------------------------------------------- -// Desktop (≥768px): Show sidebar, hide mobile header -// Mobile (<768px): Hide sidebar, show compact header -// --------------------------------------------------------------------------- -@media (min-width: 768px) { + +// Compact (0–599px): Show mobile header, hide sidebar +@media (max-width: 599px) { + .adaptive-nav__sidebar { + display: none; + } + + .adaptive-nav__mobile-header { + display: flex; + } + + // Hide mobile drawer and overlay on desktop + .adaptive-nav__overlay, + .adaptive-nav__mobile-drawer { + // These are conditional via @if in template, no display:none needed + } +} + +// Medium (600–1023px): Show collapsed sidebar (icon-only), hide mobile +@media (min-width: 600px) and (max-width: 1023px) { .adaptive-nav__sidebar { display: flex; + width: var(--cc-nav-rail-collapsed-width, 72px); + } + + // Hide labels on medium (collapsed) + .adaptive-nav__sidebar-label, + .adaptive-nav__brand { + display: none; + } + + .adaptive-nav__sidebar-header { + justify-content: center; + } + + .adaptive-nav__sidebar-item { + flex-direction: column; + justify-content: center; } .adaptive-nav__mobile-header { display: none; } - // Hide mobile drawer and overlay on desktop .adaptive-nav__overlay, .adaptive-nav__mobile-drawer { display: none; } } -@media (max-width: 767px) { +// Expanded (≥1024px): Show sidebar with labels +@media (min-width: 1024px) { .adaptive-nav__sidebar { - display: none; + display: flex; + width: var(--cc-nav-rail-collapsed-width, 72px); + transition: width 200ms cubic-bezier(0.4, 0, 0.2, 1); } .adaptive-nav__mobile-header { - display: flex; + display: none; + } + + .adaptive-nav__overlay, + .adaptive-nav__mobile-drawer { + display: none; } } @@ -313,4 +358,8 @@ .adaptive-nav__mobile-drawer { animation: none; } + + .adaptive-nav__sidebar { + transition: none; + } } \ No newline at end of file diff --git a/frontend/src/app/components/adaptive-navigation/adaptive-navigation.component.ts b/frontend/src/app/components/adaptive-navigation/adaptive-navigation.component.ts index 77adc96..8163821 100644 --- a/frontend/src/app/components/adaptive-navigation/adaptive-navigation.component.ts +++ b/frontend/src/app/components/adaptive-navigation/adaptive-navigation.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, signal, HostListener, OnDestroy, OnInit } from '@angular/core'; import { RouterLink, RouterLinkActive } from '@angular/router'; import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; @@ -8,13 +8,14 @@ import { NAV_DESTINATIONS } from '../../models/nav.model'; /** * Adaptive Navigation Component — switches between desktop sidebar - * and mobile header layouts using CSS media queries. + * and mobile header layouts using CSS media queries + JS breakpoint sync. * - * Desktop (≥768px): 72px sidebar with full navigation items. - * Mobile (<768px): 56px compact header with hamburger menu. + * Per CUB-27 spec breakpoints: + * Compact (0–599px): Mobile header + hamburger + bottom nav + * Medium (600–1023px): Collapsed sidebar (icon-only) + * Expanded (≥1024px): Expandable sidebar (hover/click) * - * The LIVE status indicator is visible in both layouts. - * Per spec Section 3.1 (kiosk) and 3.2 (mobile). + * The LIVE status indicator is visible in all layouts. */ @Component({ selector: 'app-adaptive-navigation', @@ -31,7 +32,7 @@ import { NAV_DESTINATIONS } from '../../models/nav.model'; styleUrl: './adaptive-navigation.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class AdaptiveNavigationComponent { +export class AdaptiveNavigationComponent implements OnInit, OnDestroy { /** Navigation destinations shared with other nav components */ protected readonly destinations = NAV_DESTINATIONS; @@ -41,6 +42,22 @@ export class AdaptiveNavigationComponent { /** Live connection status */ protected readonly isConnected = signal(true); + /** Responsive breakpoint state */ + protected readonly isMedium = signal(false); + protected readonly isExpanded = signal(false); + + private readonly COMPACT_MAX = 599; + private readonly MEDIUM_MAX = 1023; + + ngOnInit(): void { + this.updateBreakpoint(); + } + + @HostListener('window:resize') + onResize(): void { + this.updateBreakpoint(); + } + /** Toggle mobile menu */ toggleMobileMenu(): void { this.mobileMenuOpen.update((v) => !v); @@ -50,4 +67,18 @@ export class AdaptiveNavigationComponent { closeMobileMenu(): void { this.mobileMenuOpen.set(false); } + + private updateBreakpoint(): void { + const w = window.innerWidth; + this.isMedium.set(w >= this.COMPACT_MAX + 1 && w <= this.MEDIUM_MAX); + this.isExpanded.set(w > this.MEDIUM_MAX); + // Close mobile menu when switching to desktop + if (w > this.COMPACT_MAX) { + this.mobileMenuOpen.set(false); + } + } + + ngOnDestroy(): void { + // HostListener auto-unsubscribes + } } \ No newline at end of file diff --git a/frontend/src/app/layout/bottom-nav/bottom-nav.component.scss b/frontend/src/app/layout/bottom-nav/bottom-nav.component.scss index ffcac74..3194735 100644 --- a/frontend/src/app/layout/bottom-nav/bottom-nav.component.scss +++ b/frontend/src/app/layout/bottom-nav/bottom-nav.component.scss @@ -1,7 +1,8 @@ // ============================================================================ // Bottom Navigation Bar — Mobile Navigation -// Per spec Section 3.2: M3 NavigationBar pattern -// Visible only on compact breakpoint (< 600px) +// Per CUB-27 spec breakpoints: +// Compact (0–599px): Visible — M3 NavigationBar pattern +// Medium+ (≥600px): Hidden — nav rail takes over // ============================================================================ .bottom-nav { @@ -17,6 +18,8 @@ align-items: center; justify-content: space-around; padding: 0 8px; + // Safe area inset for notched devices + padding-bottom: env(safe-area-inset-bottom, 0px); } .bottom-nav__item { @@ -68,9 +71,24 @@ white-space: nowrap; } -// Show bottom nav only on compact breakpoint +// --------------------------------------------------------------------------- +// Compact (0–599px): Show bottom nav +// --------------------------------------------------------------------------- @media (max-width: 599px) { .bottom-nav { display: flex; } +} + +// --------------------------------------------------------------------------- +// Medium+ (≥600px): Hidden — nav rail takes over +// --------------------------------------------------------------------------- + +// --------------------------------------------------------------------------- +// Accessibility: Reduced Motion +// --------------------------------------------------------------------------- +@media (prefers-reduced-motion: reduce) { + .bottom-nav__item { + transition: none; + } } \ No newline at end of file diff --git a/frontend/src/app/layout/header-bar/header-bar.component.scss b/frontend/src/app/layout/header-bar/header-bar.component.scss index e982485..e7086ba 100644 --- a/frontend/src/app/layout/header-bar/header-bar.component.scss +++ b/frontend/src/app/layout/header-bar/header-bar.component.scss @@ -1,36 +1,43 @@ // ============================================================================ // Header Bar — Top App Bar -// Per spec Section 3.1: 64px tall, M3 MediumTopAppBar on expanded -// Section 3.2: SmallTopAppBar on mobile +// Per CUB-27 spec breakpoints: +// Compact (0–599px): SmallTopAppBar — 56px height, compact title, hidden labels +// Medium (600–1023px): Medium top bar — 64px height +// Expanded (≥1024px): MediumTopAppBar — 64px height, full actions // ============================================================================ .header-bar { display: flex; align-items: center; justify-content: space-between; - height: var(--cc-header-height); - padding: 0 var(--cc-section-padding); + height: var(--cc-header-height-compact); // Compact by default (mobile-first) + padding: 0 var(--cc-section-padding-compact); background-color: var(--cc-surface-container-high); border-bottom: 1px solid var(--cc-outline); z-index: 20; } .header-bar__title { - font-size: 28px; - font-weight: 400; + font-size: 20px; + font-weight: 500; color: var(--cc-on-surface); margin: 0; letter-spacing: -0.01em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .header-bar__actions { display: flex; align-items: center; - gap: 8px; + gap: 4px; } .header-bar__action-btn { color: var(--cc-on-surface-variant) !important; + min-width: 48px; + min-height: 48px; &:hover { color: var(--cc-on-surface) !important; @@ -59,18 +66,61 @@ vertical-align: middle; } -// Mobile: smaller title +// --------------------------------------------------------------------------- +// Compact (0–599px): SmallTopAppBar — hide live label, tighter spacing +// --------------------------------------------------------------------------- @media (max-width: 599px) { + .header-bar__live-label { + display: none; // Space saving on compact — dot alone is enough + } + + .header-bar__actions { + gap: 0; + } +} + +// --------------------------------------------------------------------------- +// Medium (600–1023px): Medium top bar +// --------------------------------------------------------------------------- +@media (min-width: 600px) and (max-width: 1023px) { .header-bar { - padding: 0 16px; + height: var(--cc-header-height); + padding: 0 var(--cc-section-padding); } .header-bar__title { - font-size: 22px; - font-weight: 500; + font-size: 24px; } - .header-bar__live-label { - display: none; // Space saving on mobile — dot alone is enough + .header-bar__actions { + gap: 4px; + } +} + +// --------------------------------------------------------------------------- +// Expanded (≥1024px): Full top bar +// --------------------------------------------------------------------------- +@media (min-width: 1024px) { + .header-bar { + height: var(--cc-header-height); + padding: 0 var(--cc-section-padding); + } + + .header-bar__title { + font-size: 28px; + font-weight: 400; + } + + .header-bar__actions { + gap: 8px; + } +} + +// --------------------------------------------------------------------------- +// Accessibility: Reduced Motion +// --------------------------------------------------------------------------- +@media (prefers-reduced-motion: reduce) { + .header-bar__live-dot--connected { + animation: none; } } \ No newline at end of file diff --git a/frontend/src/app/layout/layout-shell/layout-shell.component.scss b/frontend/src/app/layout/layout-shell/layout-shell.component.scss index 4ab8538..2905da4 100644 --- a/frontend/src/app/layout/layout-shell/layout-shell.component.scss +++ b/frontend/src/app/layout/layout-shell/layout-shell.component.scss @@ -1,7 +1,9 @@ // ============================================================================ // Layout Shell — Adaptive layout container -// Desktop: Nav Rail (left) + Main Content (right) -// Mobile: Header + Content + Bottom Nav (stacked) +// Per CUB-27 spec breakpoints: +// Compact (0–599px): Header + Content + Bottom Nav (stacked) +// Medium (600–1023px): Collapsed Nav Rail + Header + Content +// Expanded (≥1024px): Expandable Nav Rail + Header + Content // ============================================================================ .layout-shell { @@ -37,21 +39,35 @@ flex-shrink: 0; } -// Mobile: Stack layout vertically, add bottom padding for bottom nav +// --------------------------------------------------------------------------- +// Compact (0–599px): Stack layout vertically, bottom nav visible +// --------------------------------------------------------------------------- @media (max-width: 599px) { .layout-shell { flex-direction: column; } .layout-shell__content { + padding: var(--cc-section-padding-compact); // Account for bottom nav bar height padding-bottom: calc(var(--cc-bottom-nav-height) + 16px); } } -// Tablet: Ensure content padding accommodates collapsed nav rail +// --------------------------------------------------------------------------- +// Medium (600–1023px): Sidebar + content, collapsed nav rail +// --------------------------------------------------------------------------- @media (min-width: 600px) and (max-width: 1023px) { .layout-shell__content { padding: 20px; } +} + +// --------------------------------------------------------------------------- +// Expanded (≥1024px): Full nav rail with expandable behavior +// --------------------------------------------------------------------------- +@media (min-width: 1024px) { + .layout-shell__content { + padding: var(--cc-section-padding); + } } \ No newline at end of file diff --git a/frontend/src/app/layout/nav-rail/nav-rail.component.scss b/frontend/src/app/layout/nav-rail/nav-rail.component.scss index daf10b5..0f31592 100644 --- a/frontend/src/app/layout/nav-rail/nav-rail.component.scss +++ b/frontend/src/app/layout/nav-rail/nav-rail.component.scss @@ -1,11 +1,14 @@ // ============================================================================ // Nav Rail — Desktop/Kiosk Navigation -// Per spec Section 3.1: 72px collapsed / 256px expanded +// Per CUB-27 spec breakpoints: +// Compact (0–599px): Hidden — bottom nav takes over +// Medium (600–1023px): Collapsed (72px), icon-only +// Expanded (≥1024px): Expandable (72px collapsed / 256px expanded on hover) // Section 5.4: Spacing & Grid // ============================================================================ .nav-rail { - display: flex; + display: none; // Hidden by default (mobile-first) flex-direction: column; width: var(--cc-nav-rail-collapsed-width); min-height: 100vh; @@ -104,9 +107,52 @@ text-overflow: ellipsis; } -// Responsive: Hide nav rail on mobile (bottom nav takes over) -@media (max-width: 599px) { +// --------------------------------------------------------------------------- +// Medium (600–1023px): Show collapsed nav rail (icon-only) +// --------------------------------------------------------------------------- +@media (min-width: 600px) and (max-width: 1023px) { .nav-rail { + display: flex; + width: var(--cc-nav-rail-collapsed-width); + } + + // Always collapsed on medium — hide labels + .nav-rail__brand, + .nav-rail__label { display: none; } + + .nav-rail__header { + justify-content: center; + padding: 16px 0; + } + + .nav-rail__item { + justify-content: center; + padding: 0; + margin: 2px 8px; + } + + // Disable expand on medium + .nav-rail--expanded { + width: var(--cc-nav-rail-collapsed-width); + } +} + +// --------------------------------------------------------------------------- +// Expanded (≥1024px): Full expandable nav rail +// --------------------------------------------------------------------------- +@media (min-width: 1024px) { + .nav-rail { + display: flex; + } +} + +// --------------------------------------------------------------------------- +// Accessibility: Reduced Motion +// --------------------------------------------------------------------------- +@media (prefers-reduced-motion: reduce) { + .nav-rail { + transition: none; + } } \ No newline at end of file diff --git a/frontend/src/app/layout/nav-rail/nav-rail.component.ts b/frontend/src/app/layout/nav-rail/nav-rail.component.ts index c12240d..78258e2 100644 --- a/frontend/src/app/layout/nav-rail/nav-rail.component.ts +++ b/frontend/src/app/layout/nav-rail/nav-rail.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, signal, HostListener } from '@angular/core'; +import { ChangeDetectionStrategy, Component, HostListener, signal, OnDestroy, OnInit } from '@angular/core'; import { RouterLink, RouterLinkActive } from '@angular/router'; import { MatIconModule } from '@angular/material/icon'; import { MatBadgeModule } from '@angular/material/badge'; @@ -12,21 +12,52 @@ import { NAV_DESTINATIONS } from '../../models/nav.model'; styleUrl: './nav-rail.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class NavRailComponent { +export class NavRailComponent implements OnInit, OnDestroy { protected readonly destinations = NAV_DESTINATIONS; protected readonly expanded = signal(false); + protected readonly isExpandedBreakpoint = signal(false); + + private readonly EXPANDED_BP = 1024; + + ngOnInit(): void { + this.updateBreakpoint(); + } + + @HostListener('window:resize') + onResize(): void { + this.updateBreakpoint(); + } @HostListener('mouseenter') onHoverIn(): void { - this.expanded.set(true); + if (this.isExpandedBreakpoint()) { + this.expanded.set(true); + } } @HostListener('mouseleave') onHoverOut(): void { - this.expanded.set(false); + if (this.isExpandedBreakpoint()) { + this.expanded.set(false); + } } toggleExpand(): void { - this.expanded.update(v => !v); + if (this.isExpandedBreakpoint()) { + this.expanded.update(v => !v); + } + } + + private updateBreakpoint(): void { + const isExpanded = window.innerWidth >= this.EXPANDED_BP; + this.isExpandedBreakpoint.set(isExpanded); + // Collapse when leaving expanded breakpoint + if (!isExpanded) { + this.expanded.set(false); + } + } + + ngOnDestroy(): void { + // Cleanup is handled by HostListener auto-unsubscribe } } \ No newline at end of file diff --git a/frontend/src/app/pages/hub/hub-page.component.html b/frontend/src/app/pages/hub/hub-page.component.html new file mode 100644 index 0000000..83ac324 --- /dev/null +++ b/frontend/src/app/pages/hub/hub-page.component.html @@ -0,0 +1,49 @@ + + + + + + + + +
+ +
+ @for (filter of filters; track filter.value) { + + } +
+ + +
+ @for (agent of filteredAgents(); track agent.agentId) { + + } +
+ + + @if (filteredAgents().length === 0) { +
+

No agents match the selected filter.

+
+ } +
\ No newline at end of file diff --git a/frontend/src/app/pages/hub/hub-page.component.scss b/frontend/src/app/pages/hub/hub-page.component.scss index 1b2c65f..0ea0e67 100644 --- a/frontend/src/app/pages/hub/hub-page.component.scss +++ b/frontend/src/app/pages/hub/hub-page.component.scss @@ -1,28 +1,131 @@ // ============================================================================ -// 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 (0–599px): Single-column cards, horizontal-scroll filter chips +// Medium (600–1023px): 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; } -.hub-page__placeholder { +// --------------------------------------------------------------------------- +// 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__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 (0–599px): 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 (600–1023px): 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); } } \ No newline at end of file diff --git a/frontend/src/app/pages/hub/hub-page.component.ts b/frontend/src/app/pages/hub/hub-page.component.ts index 1749b94..1c98fb4 100644 --- a/frontend/src/app/pages/hub/hub-page.component.ts +++ b/frontend/src/app/pages/hub/hub-page.component.ts @@ -1,15 +1,113 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, signal, computed } from '@angular/core'; +import { MatChipsModule } from '@angular/material/chips'; +import { CommonModule } from '@angular/common'; +import { AgentCardComponent } from '../../command-hub/components/agent-card/agent-card.component'; +import { AgentStatus } from '../../models/agent.model'; + +/** + * 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: [], - template: ` -
-

Command Hub — Fleet status grid will render here

-
- `, + imports: [CommonModule, MatChipsModule, AgentCardComponent], + templateUrl: './hub-page.component.html', styleUrl: './hub-page.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class HubPageComponent {} \ No newline at end of file +export class HubPageComponent { + 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('all'); + + /** Stub agent data for demonstrating the responsive grid layout */ + protected readonly agents = signal([ + { + status: 'active' as AgentStatus, + displayName: 'Otto', + agentId: 'otto', + role: 'Orchestrator', + task: 'Reviewing PR #42', + progress: 65, + sessionKey: 'otto-main', + channel: 'slack', + lastActivity: new Date(), + }, + { + status: 'thinking' as AgentStatus, + displayName: 'Rex', + agentId: 'rex', + role: 'Frontend Engineer', + task: 'Building responsive layout', + progress: 40, + sessionKey: 'rex-task-27', + channel: 'webchat', + lastActivity: new Date(Date.now() - 30000), + }, + { + status: 'idle' as AgentStatus, + displayName: 'Dex', + agentId: 'dex', + role: 'Backend Engineer', + task: '', + progress: 0, + sessionKey: 'dex-standby', + channel: 'slack', + lastActivity: new Date(Date.now() - 300000), + }, + { + status: 'error' as AgentStatus, + displayName: 'Hex', + agentId: 'hex', + role: 'Database Engineer', + task: 'Migration failed', + errorMessage: 'Connection timeout to primary DB', + progress: 0, + sessionKey: 'hex-err-db1', + channel: 'telegram', + lastActivity: new Date(Date.now() - 120000), + }, + { + status: 'offline' as AgentStatus, + displayName: 'Pip', + agentId: 'pip', + role: 'Pi Engineer', + task: '', + progress: 0, + sessionKey: 'pip-offline', + channel: 'webchat', + lastActivity: new Date(Date.now() - 3600000), + }, + { + status: 'active' as AgentStatus, + displayName: 'Nano', + agentId: 'nano', + role: 'Firmware Engineer', + task: 'Flashing ESP32 build #18', + progress: 80, + sessionKey: 'nano-flash-18', + channel: 'webchat', + lastActivity: new Date(), + }, + ]); + + protected readonly filteredAgents = computed(() => { + const filter = this.activeFilter(); + if (filter === 'all') return this.agents(); + return this.agents().filter(a => a.status === filter); + }); + + protected selectFilter(filter: AgentFilter): void { + this.activeFilter.set(filter); + } +} \ No newline at end of file diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index a581a28..8f84c68 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -88,13 +88,20 @@ html { --cc-nav-rail-collapsed-width: 72px; --cc-nav-rail-expanded-width: 256px; --cc-header-height: 64px; + --cc-header-height-compact: 56px; --cc-bottom-nav-height: 80px; --cc-card-border-radius: 16px; - --cc-card-min-width: 320px; + --cc-card-min-width: 280px; --cc-card-gap: 16px; --cc-card-padding: 20px; --cc-section-padding: 24px; + --cc-section-padding-compact: 16px; --cc-spacing-unit: 8px; + + // --- Responsive breakpoints (CUB-27) --- + --cc-breakpoint-compact: 599px; // 0-599px: phone / compact + --cc-breakpoint-medium: 600px; // 600-1023px: tablet / medium + --cc-breakpoint-expanded: 1024px; // ≥1024px: desktop/kiosk / expanded } // ---------------------------------------------------------------------------