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..73b6a4e
--- /dev/null
+++ b/frontend/src/app/pages/hub/hub-page.component.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
Command Hub
+
+
+
+ @for (filter of filters; track filter.value) {
+
+ }
+
+
+
+
+ @for (agent of filteredAgents(); track agent.id) {
+
+ } @empty {
+
No agents online
+ }
+
+
+
+
+
\ 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 1fb6338..1355b88 100644
--- a/frontend/src/app/pages/hub/hub-page.component.scss
+++ b/frontend/src/app/pages/hub/hub-page.component.scss
@@ -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 (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;
}
@@ -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 (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 aff9f56..c52b02e 100644
--- a/frontend/src/app/pages/hub/hub-page.component.ts
+++ b/frontend/src/app/pages/hub/hub-page.component.ts
@@ -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: `
-
-
Command Hub
-
- @for (agent of agents(); track agent.id) {
-
- } @empty {
-
No agents online
- }
-
-
-
-
-
- `,
+ 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('all');
+
/** Stub agent data (TODO: wire to AgentStatusService / SignalR). */
readonly agents = signal([
{
@@ -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);
diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss
index 5a06940..2035f40 100644
--- a/frontend/src/styles.scss
+++ b/frontend/src/styles.scss
@@ -54,6 +54,67 @@ html {
// ---------------------------------------------------------------------------
@include css-props.emit-custom-properties;
+// ---------------------------------------------------------------------------
+// Per spec Section 5.1 "Status Colors (Semantic — outside M3 tonal system)"
+// 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;
+ --status-thinking: #A78BFA;
+ --status-error: #F87171;
+ --status-offline: #64748B;
+
+ // --- Status background tints (12% opacity) ---
+ --status-active-bg: rgba(56, 189, 248, 0.12);
+ --status-idle-bg: rgba(45, 212, 191, 0.12);
+ --status-thinking-bg: rgba(167, 139, 250, 0.12);
+ --status-error-bg: rgba(248, 113, 113, 0.12);
+
+ // --- Surface overrides (tactical dark palette) ---
+ --cc-background: #0D0F12;
+ --cc-surface: #13161A;
+ --cc-surface-container: #1C2027;
+ --cc-surface-container-high: #252B33;
+ --cc-on-surface: #E2E8F0;
+ --cc-on-surface-variant: #8A9BB0;
+ --cc-outline: #2D3748;
+
+ // --- Mono font stack ---
+ --cc-font-mono: 'Roboto Mono', 'Cascadia Code', 'Fira Code', monospace;
+
+ // --- Layout constants ---
+ --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: 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
+}
+
// ---------------------------------------------------------------------------
// Global Body Styles
// ---------------------------------------------------------------------------