💡 docs(shell): add JSDoc documentation for shell components and services

Add comprehensive JSDoc comments to shell layout, navigation, and tabs
components to improve code documentation and IDE support.

- Document TabsCollabsedService with usage examples
- Document ShellLayoutComponent with feature descriptions
- Document ShellNavigationItemComponent with signal and method descriptions
- Document ShellNavigationSubItemComponent with route resolution details
- Document navigations configuration with route type explanations
This commit is contained in:
Lorenz Hilpert
2025-12-10 20:07:54 +01:00
parent 7a86fcf507
commit 16b9761573
5 changed files with 142 additions and 6 deletions

View File

@@ -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<boolean>(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;

View File

@@ -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
* <shell-layout>
* <router-outlet />
* </shell-layout>
* ```
*/
@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);

View File

@@ -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
* <shell-navigation-item [item]="navigationItem" />
* ```
*/
@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<NavigationItem>();
/** 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;

View File

@@ -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
* <shell-navigation-sub-item [item]="childRoute" />
* ```
*/
@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);
}

View File

@@ -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<NavigationGroup | NavigationItem> = [
// Standalone item with dynamic tabId route
{