diff --git a/frontend/src/app/components/adaptive-navigation/adaptive-navigation.component.html b/frontend/src/app/components/adaptive-navigation/adaptive-navigation.component.html new file mode 100644 index 0000000..c51fffa --- /dev/null +++ b/frontend/src/app/components/adaptive-navigation/adaptive-navigation.component.html @@ -0,0 +1,112 @@ + + + + + + + + + + + + + +@if (mobileMenuOpen()) { + + +} \ No newline at end of file diff --git a/frontend/src/app/components/adaptive-navigation/adaptive-navigation.component.scss b/frontend/src/app/components/adaptive-navigation/adaptive-navigation.component.scss new file mode 100644 index 0000000..27fbd5b --- /dev/null +++ b/frontend/src/app/components/adaptive-navigation/adaptive-navigation.component.scss @@ -0,0 +1,316 @@ +// ============================================================================ +// Adaptive Navigation — Desktop sidebar / Mobile header +// Desktop (≥768px): 72px sidebar with full navigation items +// Mobile (<768px): 56px compact header with hamburger menu +// ============================================================================ + +// --------------------------------------------------------------------------- +// Desktop Sidebar (visible ≥768px) +// --------------------------------------------------------------------------- +.adaptive-nav__sidebar { + display: flex; + flex-direction: column; + width: var(--cc-nav-rail-collapsed-width, 72px); + min-height: 100vh; + background-color: var(--cc-surface-container-high); + border-right: 1px solid var(--cc-outline); + z-index: 10; +} + +.adaptive-nav__sidebar-header { + display: flex; + align-items: center; + justify-content: center; + height: 64px; + border-bottom: 1px solid var(--cc-outline); +} + +.adaptive-nav__brand { + font-size: 18px; + font-weight: 700; + color: var(--status-active); + letter-spacing: 0.04em; +} + +.adaptive-nav__sidebar-nav { + flex: 1; + padding-top: 8px; +} + +.adaptive-nav__sidebar-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + min-height: 56px; + padding: 8px 0; + margin: 2px 8px; + border-radius: 28px; + color: var(--cc-on-surface-variant); + text-decoration: none; + transition: background-color 150ms ease, color 150ms ease; + + &:hover { + background-color: rgba(255, 255, 255, 0.08); + color: var(--cc-on-surface); + } + + &--active { + background-color: var(--status-active-bg); + color: var(--status-active); + + .adaptive-nav__sidebar-label { + font-weight: 500; + } + } +} + +.adaptive-nav__sidebar-label { + font-size: 11px; + font-weight: 400; + letter-spacing: 0.02em; + white-space: nowrap; +} + +// --------------------------------------------------------------------------- +// Sidebar Footer — LIVE indicator + action buttons +// --------------------------------------------------------------------------- +.adaptive-nav__sidebar-footer { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 12px 0 20px; + border-top: 1px solid var(--cc-outline); +} + +.adaptive-nav__sidebar-actions { + display: flex; + gap: 4px; + + .mat-mdc-icon-button { + color: var(--cc-on-surface-variant) !important; + --mdc-icon-button-icon-size: 20px; + + &:hover { + color: var(--cc-on-surface) !important; + } + } +} + +// --------------------------------------------------------------------------- +// LIVE Status Indicator +// --------------------------------------------------------------------------- +.adaptive-nav__live { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + border-radius: 16px; + transition: background-color 200ms ease; + + &--connected { + background-color: var(--status-active-bg); + } +} + +.adaptive-nav__live-dot { + display: inline-block; + width: 8px; + height: 8px; + min-width: 8px; + border-radius: 50%; + background-color: var(--status-error); + transition: background-color 200ms ease; + + &--connected { + background-color: var(--status-active); + animation: pulse-active 2s ease-in-out infinite; + } +} + +.adaptive-nav__live-chip { + font-size: 11px !important; + font-weight: 600 !important; + letter-spacing: 0.06em; + height: 24px !important; + min-height: 24px !important; + padding: 0 8px !important; + color: var(--status-active) !important; + --mdc-chip-elevated-container-color: transparent; + background: transparent !important; + border: none !important; + box-shadow: none !important; +} + +.adaptive-nav__live-text { + font-size: 12px; + font-weight: 600; + letter-spacing: 0.06em; + color: var(--status-active); +} + +// --------------------------------------------------------------------------- +// Mobile Header (visible <768px) +// --------------------------------------------------------------------------- +.adaptive-nav__mobile-header { + display: none; // Hidden on desktop, shown on mobile via media query + align-items: center; + height: 56px; + padding: 0 12px; + background-color: var(--cc-surface-container-high); + border-bottom: 1px solid var(--cc-outline); + z-index: 20; + gap: 8px; +} + +.adaptive-nav__hamburger { + color: var(--cc-on-surface-variant) !important; + + &:hover { + color: var(--cc-on-surface) !important; + } +} + +.adaptive-nav__mobile-title { + flex: 1; + font-size: 20px; + font-weight: 500; + color: var(--cc-on-surface); + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.adaptive-nav__live--mobile { + padding: 4px 10px; + border-radius: 16px; + + .adaptive-nav__live-text { + font-size: 11px; + font-weight: 700; + } +} + +.adaptive-nav__mobile-action { + color: var(--cc-on-surface-variant) !important; + + &:hover { + color: var(--cc-on-surface) !important; + } +} + +// --------------------------------------------------------------------------- +// Mobile Drawer +// --------------------------------------------------------------------------- +.adaptive-nav__overlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 40; +} + +.adaptive-nav__mobile-drawer { + position: fixed; + top: 56px; // Below header + left: 0; + bottom: 0; + width: 280px; + max-width: 80vw; + background-color: var(--cc-surface-container); + border-right: 1px solid var(--cc-outline); + z-index: 50; + padding: 12px 0; + overflow-y: auto; + animation: slide-in-left 200ms cubic-bezier(0.4, 0, 0.2, 1); +} + +.adaptive-nav__drawer-item { + display: flex; + align-items: center; + gap: 16px; + min-height: 48px; + padding: 0 20px; + color: var(--cc-on-surface-variant); + text-decoration: none; + transition: background-color 150ms ease, color 150ms ease; + + &:hover { + background-color: rgba(255, 255, 255, 0.08); + color: var(--cc-on-surface); + } + + &--active { + background-color: var(--status-active-bg); + color: var(--status-active); + + .adaptive-nav__drawer-label { + font-weight: 500; + } + } +} + +.adaptive-nav__drawer-label { + font-size: 14px; + font-weight: 400; + white-space: nowrap; +} + +// --------------------------------------------------------------------------- +// Drawer slide-in animation +// --------------------------------------------------------------------------- +@keyframes slide-in-left { + from { + transform: translateX(-100%); + } + to { + transform: translateX(0); + } +} + +// --------------------------------------------------------------------------- +// Media Queries — Layout Switch +// --------------------------------------------------------------------------- +// Desktop (≥768px): Show sidebar, hide mobile header +// Mobile (<768px): Hide sidebar, show compact header +// --------------------------------------------------------------------------- +@media (min-width: 768px) { + .adaptive-nav__sidebar { + display: flex; + } + + .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) { + .adaptive-nav__sidebar { + display: none; + } + + .adaptive-nav__mobile-header { + display: flex; + } +} + +// --------------------------------------------------------------------------- +// Accessibility: Reduced Motion +// --------------------------------------------------------------------------- +@media (prefers-reduced-motion: reduce) { + .adaptive-nav__live-dot--connected { + animation: none; + } + + .adaptive-nav__mobile-drawer { + animation: 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 new file mode 100644 index 0000000..77adc96 --- /dev/null +++ b/frontend/src/app/components/adaptive-navigation/adaptive-navigation.component.ts @@ -0,0 +1,53 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; +import { RouterLink, RouterLinkActive } from '@angular/router'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatBadgeModule } from '@angular/material/badge'; +import { NAV_DESTINATIONS } from '../../models/nav.model'; + +/** + * Adaptive Navigation Component — switches between desktop sidebar + * and mobile header layouts using CSS media queries. + * + * Desktop (≥768px): 72px sidebar with full navigation items. + * Mobile (<768px): 56px compact header with hamburger menu. + * + * The LIVE status indicator is visible in both layouts. + * Per spec Section 3.1 (kiosk) and 3.2 (mobile). + */ +@Component({ + selector: 'app-adaptive-navigation', + standalone: true, + imports: [ + RouterLink, + RouterLinkActive, + MatIconModule, + MatButtonModule, + MatChipsModule, + MatBadgeModule, + ], + templateUrl: './adaptive-navigation.component.html', + styleUrl: './adaptive-navigation.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AdaptiveNavigationComponent { + /** Navigation destinations shared with other nav components */ + protected readonly destinations = NAV_DESTINATIONS; + + /** Whether the mobile drawer is open */ + protected readonly mobileMenuOpen = signal(false); + + /** Live connection status */ + protected readonly isConnected = signal(true); + + /** Toggle mobile menu */ + toggleMobileMenu(): void { + this.mobileMenuOpen.update((v) => !v); + } + + /** Close mobile menu (e.g. on nav) */ + closeMobileMenu(): void { + this.mobileMenuOpen.set(false); + } +} \ No newline at end of file diff --git a/frontend/src/app/components/adaptive-navigation/index.ts b/frontend/src/app/components/adaptive-navigation/index.ts new file mode 100644 index 0000000..c154a5e --- /dev/null +++ b/frontend/src/app/components/adaptive-navigation/index.ts @@ -0,0 +1 @@ +export * from './adaptive-navigation.component'; \ No newline at end of file