diff --git a/libs/shell/common/src/lib/tabs-collabsed.service.ts b/libs/shell/common/src/lib/tabs-collabsed.service.ts index 97c79eead..629b5dbd0 100644 --- a/libs/shell/common/src/lib/tabs-collabsed.service.ts +++ b/libs/shell/common/src/lib/tabs-collabsed.service.ts @@ -1,18 +1,47 @@ import { Injectable, signal } from '@angular/core'; import { logger } from '@isa/core/logging'; +/** + * Service for managing the collapsed/expanded state of the shell tabs bar. + * + * The tabs bar can switch between compact (collapsed) and expanded modes + * based on user proximity. This service provides a centralized state + * that can be accessed by both the tabs component and the shell layout. + * + * @example + * ```typescript + * readonly tabsCollapsed = inject(TabsCollabsedService); + * + * // Read current state + * const isCollapsed = this.tabsCollapsed.get(); + * + * // Set state + * this.tabsCollapsed.set(true); + * + * // Toggle state + * this.tabsCollapsed.toggle(); + * ``` + */ @Injectable({ providedIn: 'root' }) export class TabsCollabsedService { #logger = logger({ service: 'TabsService' }); #state = signal(false); + /** Readonly signal exposing the current collapsed state. */ readonly get = this.#state.asReadonly(); + /** Toggles the collapsed state between true and false. */ toggle(): void { this.#state.update((state) => !state); this.#logger.debug('Tabs toggled', () => ({ isOpen: this.#state() })); } + /** + * Sets the collapsed state to a specific value. + * No-op if the state is already equal to the provided value. + * + * @param state - The new collapsed state (true = collapsed, false = expanded) + */ set(state: boolean): void { if (this.#state() === state) { return; diff --git a/libs/shell/layout/src/lib/shell-layout.component.ts b/libs/shell/layout/src/lib/shell-layout.component.ts index 627ad8d1a..6a55ce6a8 100644 --- a/libs/shell/layout/src/lib/shell-layout.component.ts +++ b/libs/shell/layout/src/lib/shell-layout.component.ts @@ -21,6 +21,23 @@ import { UiElementSizeObserverDirective, } from '@isa/ui/layout'; +/** + * Root layout component for the application shell. + * + * Composes the header, navigation, tabs, and main content area into a + * responsive layout. Handles: + * - Responsive navigation visibility (auto-hide on tablet breakpoint) + * - Click-outside-to-close behavior for mobile navigation + * - Dynamic positioning based on header and navigation sizes + * - Font size scaling for accessibility + * + * @example + * ```html + * + * + * + * ``` + */ @Component({ selector: 'shell-layout', standalone: true, @@ -39,12 +56,19 @@ export class ShellLayoutComponent { #navigationService = inject(NavigationService); #elementRef = inject(ElementRef); + /** Service for managing tabs collapsed state. */ readonly tabsCollabesd = inject(TabsCollabsedService); + /** Service for managing font size scaling. */ readonly fontSizeService = inject(FontSizeService); + /** Signal indicating if the viewport is at or below tablet breakpoint. */ readonly tablet = breakpoint(Breakpoint.Tablet); + /** + * Computed signal determining if navigation should be rendered. + * Always visible on desktop; toggled visibility on tablet/mobile. + */ readonly renderNavigation = computed(() => { const tablet = this.tablet(); @@ -55,6 +79,10 @@ export class ShellLayoutComponent { return this.#navigationService.get(); }); + /** + * Handles clicks outside the navigation to close it on tablet/mobile. + * Ignores clicks on the navigation toggle button. + */ @HostListener('document:click', ['$event']) onDocumentClick(event: MouseEvent): void { if (!this.tablet() || !this.#navigationService.get()) return; @@ -70,6 +98,7 @@ export class ShellLayoutComponent { } } + /** Closes navigation on main content scroll (tablet/mobile only). */ onMainScroll(): void { if (!this.tablet() || !this.#navigationService.get()) return; this.#navigationService.set(false); diff --git a/libs/shell/navigation/src/lib/components/navigation-item/navigation-item.component.ts b/libs/shell/navigation/src/lib/components/navigation-item/navigation-item.component.ts index 8b7b9c04e..2ab58b294 100644 --- a/libs/shell/navigation/src/lib/components/navigation-item/navigation-item.component.ts +++ b/libs/shell/navigation/src/lib/components/navigation-item/navigation-item.component.ts @@ -20,6 +20,25 @@ import { NgTemplateOutlet } from '@angular/common'; import { RouterLink, RouterLinkActive } from '@angular/router'; import { ShellNavigationSubItemComponent } from '../navigation-sub-item/navigation-sub-item.component'; +/** + * Navigation item component for the shell sidebar. + * + * Renders a single navigation item with optional icon, expandable submenu, + * and activity indicator. Supports both direct navigation routes and + * expandable groups with child routes. + * + * Features: + * - Icon display with sanitized SVG content + * - Expandable/collapsible submenu for child routes + * - Activity indicators (red dot) that bubble up from children when collapsed + * - Auto-expansion when a child route becomes active + * - Dynamic route resolution (supports factory functions and signals) + * + * @example + * ```html + * + * ``` + */ @Component({ selector: 'shell-navigation-item', templateUrl: './navigation-item.component.html', @@ -43,12 +62,16 @@ export class ShellNavigationItemComponent { #sanitizer = inject(DomSanitizer); #navigationService = inject(NavigationService); + /** Query for all child sub-item components. */ readonly subItems = viewChildren(ShellNavigationSubItemComponent); + /** Whether the submenu is currently expanded. */ expanded = signal(false); + /** The navigation item configuration to render. */ readonly item = input.required(); + /** Sanitized HTML content for the icon SVG. */ sanitizedHtml = computed(() => { const icon = this.item().icon; @@ -59,14 +82,20 @@ export class ShellNavigationItemComponent { return this.#sanitizer.bypassSecurityTrustHtml(icon); }); + /** Whether this item has child routes. */ hasChildren = computed(() => { return (this.item().childRoutes?.length ?? 0) > 0; }); + /** Whether any child route is currently active. */ hasActiveChild = computed(() => { return this.subItems().some((subItem) => subItem.isActive()); }); + /** + * Resolves the indicator value, handling factory functions and signals. + * Returns the current indicator state (truthy = show red dot). + */ readonly resolvedIndicator = computed(() => { const indicator = this.item().indicator; if (typeof indicator === 'function') { @@ -76,25 +105,33 @@ export class ShellNavigationItemComponent { return indicator; }); + /** Whether any child has an active indicator. */ readonly hasChildIndicator = computed(() => { return this.subItems().some((subItem) => subItem.resolvedIndicator()); }); + /** + * Determines if the indicator dot should be shown. + * Shows if this item has an indicator, or if a child has one and the menu is collapsed. + */ readonly showIndicator = computed(() => { - // Show indicator if: - // 1. This item has its own indicator, OR - // 2. A child has an indicator AND the menu is collapsed (bubble up) return this.resolvedIndicator() || (this.hasChildIndicator() && !this.expanded()); }); + /** Toggles the expanded state of the submenu. */ toggleExpand(): void { this.expanded.update((value) => !value); } + /** Closes the navigation sidebar (used on tablet/mobile after selection). */ closeNavigation(): void { this.#navigationService.set(false); } + /** + * Resolves the first child route for fallback navigation. + * Used when the item has no direct route but has child routes. + */ readonly firstChildRoute = computed(() => { const childRoutes = this.item().childRoutes; if (!childRoutes?.length) return undefined; @@ -107,6 +144,10 @@ export class ShellNavigationItemComponent { return firstChild; }); + /** + * Resolves the navigation route, handling factory functions and signals. + * Falls back to the first child route if no direct route is defined. + */ readonly route = computed(() => { const route = this.item().route; @@ -115,14 +156,12 @@ export class ShellNavigationItemComponent { return isSignal(result) ? result() : result; } - // Fall back to first child route if no direct route return route ?? this.firstChildRoute(); }); constructor() { // Auto-expand when a child route becomes active. - // This is a valid effect use case: one-way sync of UI state based on router state. - // It only expands (never auto-collapses), preserving user's manual toggle preference. + // Valid effect: one-way sync of UI state from router state (only expands, never auto-collapses). effect(() => { const subItems = this.subItems(); if (!subItems.length) return; diff --git a/libs/shell/navigation/src/lib/components/navigation-sub-item/navigation-sub-item.component.ts b/libs/shell/navigation/src/lib/components/navigation-sub-item/navigation-sub-item.component.ts index 8c3f37dd4..e6681690a 100644 --- a/libs/shell/navigation/src/lib/components/navigation-sub-item/navigation-sub-item.component.ts +++ b/libs/shell/navigation/src/lib/components/navigation-sub-item/navigation-sub-item.component.ts @@ -19,6 +19,18 @@ import { NavigationRouteWithLabelFn, } from '../../types'; +/** + * Navigation sub-item component for child routes within a navigation item. + * + * Renders a single child route link within an expanded navigation item submenu. + * Supports dynamic route resolution (factory functions and signals), activity + * indicators, and automatic active state detection. + * + * @example + * ```html + * + * ``` + */ @Component({ selector: 'shell-navigation-sub-item', templateUrl: './navigation-sub-item.component.html', @@ -30,10 +42,14 @@ export class ShellNavigationSubItemComponent { #injector = inject(Injector); #navigationService = inject(NavigationService); + /** Reference to the RouterLinkActive directive for active state detection. */ readonly routerLinkActive = viewChild(RouterLinkActive); + + /** Signal tracking whether this sub-item's route is currently active. */ readonly isActive = signal(false); constructor() { + // Initialize active state after render to capture initial router state afterNextRender(() => { const rla = this.routerLinkActive(); if (rla?.isActive) { @@ -42,10 +58,15 @@ export class ShellNavigationSubItemComponent { }); } + /** The navigation route configuration (static or factory function). */ readonly item = input.required< NavigationRouteWithLabel | NavigationRouteWithLabelFn >(); + /** + * Resolves the route configuration, handling factory functions and signals. + * Returns the current route object with label, path, and query params. + */ readonly route = computed(() => { const item = this.item(); @@ -57,8 +78,13 @@ export class ShellNavigationSubItemComponent { return item; }); + /** Extracts the display label from the resolved route. */ readonly label = computed(() => this.route()?.label ?? ''); + /** + * Resolves the indicator value from the route configuration. + * Handles factory functions and signals for dynamic indicator state. + */ readonly resolvedIndicator = computed((): NavigationItemIndicator => { const route = this.route(); if (!route) return null; @@ -71,6 +97,7 @@ export class ShellNavigationSubItemComponent { return indicator; }); + /** Closes the navigation sidebar (used on tablet/mobile after selection). */ closeNavigation(): void { this.#navigationService.set(false); } diff --git a/libs/shell/navigation/src/lib/navigations.ts b/libs/shell/navigation/src/lib/navigations.ts index 86b22dd06..b7c4e84ce 100644 --- a/libs/shell/navigation/src/lib/navigations.ts +++ b/libs/shell/navigation/src/lib/navigations.ts @@ -26,8 +26,20 @@ import { NavigationGroup, NavigationItem } from './types'; import { startOfDay, subDays } from 'date-fns'; import { computed, inject } from '@angular/core'; +/** Cached start of day timestamp for date-based query params. */ const START_OF_DAY = startOfDay(new Date()); +/** + * Main navigation configuration for the application shell. + * + * Defines the sidebar navigation structure with groups, items, routes, + * and activity indicators. Routes can be: + * - Static routes (e.g., `/dashboard`) + * - Tab-based routes using `injectTabRoute()` or `injectLegacyTabRoute()` + * - Factory functions that return signals for reactive route updates + * + * Indicators show a red dot when items have pending actions (e.g., items in cart). + */ export const navigations: Array = [ // Standalone item with dynamic tabId route {