mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
✨ feat(shell): add logging and JSDoc documentation across shell components
- Add @isa/core/logging to services and components with actions - Add comprehensive JSDoc documentation with @example blocks - Fix typo: TabsCollabsedService → TabsCollapsedService - Add error handling with try/catch for async operations - Add tap operator for logging in NetworkStatusService
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import {
|
||||
map,
|
||||
Observable,
|
||||
@@ -7,22 +8,56 @@ import {
|
||||
merge,
|
||||
startWith,
|
||||
shareReplay,
|
||||
tap,
|
||||
} from 'rxjs';
|
||||
|
||||
/** Network connectivity status values. */
|
||||
export type NetworkStatus = 'online' | 'offline';
|
||||
|
||||
/**
|
||||
* Service for monitoring browser network connectivity status.
|
||||
*
|
||||
* Listens to browser online/offline events and provides a reactive
|
||||
* observable that emits the current network status. The observable
|
||||
* is shared and replays the latest value to new subscribers.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* readonly networkService = inject(NetworkStatusService);
|
||||
*
|
||||
* // Subscribe to status changes
|
||||
* this.networkService.status$.subscribe(status => {
|
||||
* console.log('Network status:', status);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class NetworkStatusService {
|
||||
#logger = logger({ service: 'NetworkStatusService' });
|
||||
|
||||
/**
|
||||
* Observable that emits current network status ('online' | 'offline').
|
||||
* Emits immediately on subscription with current state.
|
||||
*/
|
||||
readonly status$: Observable<NetworkStatus> = merge(
|
||||
fromEvent(window, 'online'),
|
||||
fromEvent(window, 'offline'),
|
||||
).pipe(
|
||||
startWith(null), // emit immediately
|
||||
map((): NetworkStatus => (navigator.onLine ? 'online' : 'offline')),
|
||||
tap((status) => this.#logger.debug('Network status changed', () => ({ status }))),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection function to get the network status observable.
|
||||
* @returns Observable of network status
|
||||
*/
|
||||
export const injectNetworkStatus$ = () => inject(NetworkStatusService).status$;
|
||||
|
||||
/**
|
||||
* Injection function to get network status as a signal.
|
||||
* @returns Signal of network status (undefined initially until first emission)
|
||||
*/
|
||||
export const injectNetworkStatus = () => toSignal(injectNetworkStatus$());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './lib/navigation.service';
|
||||
export * from './lib/font-size.service';
|
||||
export * from './lib/notifications.service';
|
||||
export * from './lib/tabs-collabsed.service';
|
||||
export * from './lib/tabs-collapsed.service';
|
||||
|
||||
@@ -9,14 +9,40 @@ import {
|
||||
} from '@angular/core';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
/** Available font size options for the application. */
|
||||
export type FontSize = 'small' | 'medium' | 'large';
|
||||
|
||||
/** Mapping of font size names to pixel values. */
|
||||
const FONT_SIZE_PX_MAP: Record<FontSize, number> = {
|
||||
small: 14,
|
||||
medium: 16,
|
||||
large: 18,
|
||||
};
|
||||
|
||||
/**
|
||||
* Service for managing application-wide font size.
|
||||
*
|
||||
* Provides reactive font size state with automatic DOM updates.
|
||||
* The font size is applied to the document root element, enabling
|
||||
* rem-based scaling throughout the application.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* readonly fontSizeService = inject(FontSizeService);
|
||||
*
|
||||
* // Get current font size
|
||||
* const size = this.fontSizeService.get(); // 'small' | 'medium' | 'large'
|
||||
*
|
||||
* // Get pixel value
|
||||
* const px = this.fontSizeService.getPx(); // 14 | 16 | 18
|
||||
*
|
||||
* // Set font size
|
||||
* this.fontSizeService.set('large');
|
||||
*
|
||||
* // Convert rem to pixels
|
||||
* const pixels = this.fontSizeService.remToPx(2); // 32 (when medium)
|
||||
* ```
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FontSizeService {
|
||||
#logger = logger({ service: 'FontSizeService' });
|
||||
@@ -26,17 +52,29 @@ export class FontSizeService {
|
||||
|
||||
#renderer = inject(RendererFactory2).createRenderer(this.#document, null);
|
||||
|
||||
/** Readonly signal exposing the current font size name. */
|
||||
readonly get = this.#state.asReadonly();
|
||||
|
||||
/** Computed signal returning the current font size in pixels. */
|
||||
readonly getPx = computed(() => FONT_SIZE_PX_MAP[this.#state()]);
|
||||
|
||||
/**
|
||||
* Sets the application font size.
|
||||
* @param size - The font size to apply ('small', 'medium', or 'large')
|
||||
*/
|
||||
set(size: FontSize): void {
|
||||
this.#logger.debug('Font size changed', () => ({ size }));
|
||||
this.#state.set(size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts rem units to pixels based on current font size.
|
||||
* @param rem - The value in rem units
|
||||
* @returns The equivalent value in pixels
|
||||
*/
|
||||
readonly remToPx = (rem: number) => rem * this.getPx();
|
||||
|
||||
/** Effect that applies the font size to the document root element. */
|
||||
readonly fontSizeEffect = effect(() => {
|
||||
const fontSize = this.#state();
|
||||
this.#renderer.setStyle(
|
||||
|
||||
@@ -1,18 +1,45 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
/**
|
||||
* Service for managing the navigation sidebar visibility state.
|
||||
*
|
||||
* Controls whether the sidebar navigation panel is open or closed,
|
||||
* primarily used for responsive mobile/tablet layouts where the
|
||||
* navigation can be toggled via a hamburger menu.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* readonly navigationService = inject(NavigationService);
|
||||
*
|
||||
* // Check if navigation is open
|
||||
* const isOpen = this.navigationService.get();
|
||||
*
|
||||
* // Toggle navigation
|
||||
* this.navigationService.toggle();
|
||||
*
|
||||
* // Explicitly set state
|
||||
* this.navigationService.set(false); // Close navigation
|
||||
* ```
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class NavigationService {
|
||||
#logger = logger({ service: 'NavigationService' });
|
||||
#state = signal<boolean>(false);
|
||||
|
||||
/** Readonly signal exposing the current navigation open/closed state. */
|
||||
readonly get = this.#state.asReadonly();
|
||||
|
||||
/** Toggles the navigation state between open and closed. */
|
||||
toggle(): void {
|
||||
this.#state.update((state) => !state);
|
||||
this.#logger.debug('Navigation toggled', () => ({ isOpen: this.#state() }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the navigation visibility state.
|
||||
* @param state - True to open navigation, false to close
|
||||
*/
|
||||
set(state: boolean): void {
|
||||
this.#logger.debug('Navigation state set', () => ({ state }));
|
||||
this.#state.set(state);
|
||||
|
||||
@@ -1,47 +1,103 @@
|
||||
import { computed, Injectable, signal } from '@angular/core';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
/** Unix timestamp in milliseconds. */
|
||||
type Timestamp = number;
|
||||
|
||||
/** Unique identifier for a notification. */
|
||||
type NotificationId = string | number;
|
||||
|
||||
/**
|
||||
* Represents an in-app notification.
|
||||
*
|
||||
* Notifications are displayed in the notifications panel and can be
|
||||
* grouped, marked as read, and trigger actions when clicked.
|
||||
*/
|
||||
export type Notification = {
|
||||
/** Unique identifier for this notification. */
|
||||
id: NotificationId;
|
||||
/** Group name for categorizing notifications (e.g., 'orders', 'system'). */
|
||||
group: string;
|
||||
/** Short title displayed prominently. */
|
||||
title: string;
|
||||
/** Detailed message content. */
|
||||
message: string;
|
||||
/** Action to perform when notification is clicked. */
|
||||
action: NotificationAction;
|
||||
/** Timestamp when marked as read (undefined if unread). */
|
||||
markedAsRead?: Timestamp;
|
||||
/** Creation timestamp for sorting. */
|
||||
timestamp: Timestamp;
|
||||
};
|
||||
|
||||
/** Base properties for notification actions. */
|
||||
export type NotificationActionBase = {
|
||||
/** Button label text. */
|
||||
label: string;
|
||||
/** Action type discriminator. */
|
||||
type: 'navigate' | 'callback';
|
||||
};
|
||||
|
||||
/** Navigation action that routes to an internal or external URL. */
|
||||
export type NotificationActionNavigate = NotificationActionBase & {
|
||||
type: 'navigate';
|
||||
/** Whether the route is internal (Angular router) or external (new tab). */
|
||||
target: 'internal' | 'external';
|
||||
/** The route path or URL to navigate to. */
|
||||
route: string;
|
||||
};
|
||||
|
||||
/** Callback action that executes a function. */
|
||||
export type NotificationActionCallback = NotificationActionBase & {
|
||||
type: 'callback';
|
||||
/** Function to execute when action is triggered. */
|
||||
callback: () => void;
|
||||
};
|
||||
|
||||
/** Union type for all notification action types. */
|
||||
export type NotificationAction =
|
||||
| NotificationActionNavigate
|
||||
| NotificationActionCallback;
|
||||
|
||||
/**
|
||||
* Service for managing in-app notifications.
|
||||
*
|
||||
* Provides a centralized store for notifications with support for
|
||||
* adding, removing, grouping, and tracking read/unread state.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* readonly notifications = inject(NotificationsService);
|
||||
*
|
||||
* // Add a notification
|
||||
* this.notifications.add({
|
||||
* id: 'order-123',
|
||||
* group: 'orders',
|
||||
* title: 'New Order',
|
||||
* message: 'Order #123 has been placed',
|
||||
* timestamp: Date.now(),
|
||||
* action: { type: 'navigate', target: 'internal', route: '/orders/123', label: 'View' }
|
||||
* });
|
||||
*
|
||||
* // Get unread count
|
||||
* const unread = this.notifications.unreadCount();
|
||||
*
|
||||
* // Mark as read
|
||||
* this.notifications.markAsRead('order-123');
|
||||
* ```
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class NotificationsService {
|
||||
#logger = logger({ service: 'NotificationsService' });
|
||||
#state = signal<Notification[]>([]);
|
||||
|
||||
/** Readonly signal exposing all current notifications. */
|
||||
readonly get = this.#state.asReadonly();
|
||||
|
||||
/**
|
||||
* Adds a new notification to the store.
|
||||
* @param notification - The notification to add
|
||||
*/
|
||||
add(notification: Notification): void {
|
||||
this.#logger.debug('Notification added', () => ({
|
||||
id: notification.id,
|
||||
@@ -50,6 +106,10 @@ export class NotificationsService {
|
||||
this.#state.update((notifications) => [...notifications, notification]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a notification by its ID.
|
||||
* @param id - The notification ID to remove
|
||||
*/
|
||||
remove(id: NotificationId): void {
|
||||
this.#logger.debug('Notification removed', () => ({ id }));
|
||||
this.#state.update((notifications) =>
|
||||
@@ -57,11 +117,16 @@ export class NotificationsService {
|
||||
);
|
||||
}
|
||||
|
||||
/** Removes all notifications from the store. */
|
||||
clear(): void {
|
||||
this.#logger.debug('All notifications cleared');
|
||||
this.#state.set([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a specific notification as read.
|
||||
* @param id - The notification ID to mark as read
|
||||
*/
|
||||
markAsRead(id: NotificationId): void {
|
||||
this.#logger.debug('Notification marked as read', () => ({ id }));
|
||||
this.#state.update((notifications) =>
|
||||
@@ -73,6 +138,7 @@ export class NotificationsService {
|
||||
);
|
||||
}
|
||||
|
||||
/** Marks all notifications as read. */
|
||||
markAllAsRead(): void {
|
||||
this.#logger.debug('All notifications marked as read');
|
||||
this.#state.update((notifications) =>
|
||||
@@ -83,6 +149,7 @@ export class NotificationsService {
|
||||
);
|
||||
}
|
||||
|
||||
/** Computed signal returning the count of unread notifications. */
|
||||
readonly unreadCount = computed(() =>
|
||||
this.#state().filter((notification) => !notification.markedAsRead).length,
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { logger } from '@isa/core/logging';
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* readonly tabsCollapsed = inject(TabsCollabsedService);
|
||||
* readonly tabsCollapsed = inject(TabsCollapsedService);
|
||||
*
|
||||
* // Read current state
|
||||
* const isCollapsed = this.tabsCollapsed.get();
|
||||
@@ -23,7 +23,7 @@ import { logger } from '@isa/core/logging';
|
||||
* ```
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TabsCollabsedService {
|
||||
export class TabsCollapsedService {
|
||||
#logger = logger({ service: 'TabsService' });
|
||||
#state = signal<boolean>(false);
|
||||
|
||||
@@ -10,12 +10,28 @@ import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { isaNavigationFontsize } from '@isa/icons';
|
||||
import { FontSize, FontSizeService } from '@isa/shell/common';
|
||||
|
||||
/** Configuration for a font size option in the selector. */
|
||||
interface FontSizeOption {
|
||||
/** The font size value ('small', 'medium', 'large'). */
|
||||
value: FontSize;
|
||||
/** Accessible label for screen readers. */
|
||||
label: string;
|
||||
/** Icon size for visual representation. */
|
||||
iconSize: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Font size selector component for accessibility settings.
|
||||
*
|
||||
* Displays three font size options (small, medium, large) with visual
|
||||
* icons indicating the relative size. Includes an animated indicator
|
||||
* that slides to show the currently selected option.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <shell-font-size-selector />
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'shell-font-size-selector',
|
||||
templateUrl: './font-size-selector.component.html',
|
||||
@@ -31,24 +47,32 @@ interface FontSizeOption {
|
||||
export class ShellFontSizeSelectorComponent {
|
||||
#logger = logger({ component: 'ShellFontSizeSelectorComponent' });
|
||||
|
||||
/** Font size service for reading and updating the current size. */
|
||||
readonly fontSizeService = inject(FontSizeService);
|
||||
|
||||
/** Available font size options with labels and icon sizes. */
|
||||
readonly fontSizeOptions: FontSizeOption[] = [
|
||||
{ value: 'small', label: 'Kleine Schriftgröße', iconSize: '0.63rem' },
|
||||
{ value: 'medium', label: 'Mittlere Schriftgröße', iconSize: '1rem' },
|
||||
{ value: 'large', label: 'Große Schriftgröße', iconSize: '1.3rem' },
|
||||
];
|
||||
|
||||
/** Pixel offsets for the sliding indicator animation. */
|
||||
readonly #offsetMap: Record<FontSize, number> = {
|
||||
small: 0,
|
||||
medium: 3,
|
||||
large: 6,
|
||||
};
|
||||
|
||||
/** Computed offset position for the sliding indicator. */
|
||||
readonly indicatorOffset = computed(
|
||||
() => this.#offsetMap[this.fontSizeService.get()],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles font size selection changes.
|
||||
* @param size - The newly selected font size
|
||||
*/
|
||||
onFontSizeChange(size: FontSize): void {
|
||||
this.#logger.debug('Font size changed', () => ({ size }));
|
||||
this.fontSizeService.set(size);
|
||||
|
||||
@@ -5,6 +5,17 @@ import { AuthService } from '@isa/core/auth';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { isaNavigationLogout } from '@isa/icons';
|
||||
|
||||
/**
|
||||
* Logout button component for the header.
|
||||
*
|
||||
* Displays a logout icon button that triggers user logout when clicked.
|
||||
* Includes "NEU" label to indicate the new shell interface.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <shell-logout-button />
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'shell-logout-button',
|
||||
template: `<ui-info-button
|
||||
@@ -24,6 +35,7 @@ export class ShellLogoutButtonComponent {
|
||||
#logger = logger({ component: 'ShellLogoutButtonComponent' });
|
||||
#authService = inject(AuthService);
|
||||
|
||||
/** Logs out the current user via the AuthService. */
|
||||
logout(): void {
|
||||
this.#logger.info('User logging out');
|
||||
this.#authService.logout();
|
||||
|
||||
@@ -14,6 +14,17 @@ import {
|
||||
} from '@isa/ui/buttons';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
|
||||
/**
|
||||
* Navigation toggle button (hamburger menu) for tablet/mobile layouts.
|
||||
*
|
||||
* Toggles the sidebar navigation visibility. The icon changes between
|
||||
* a hamburger menu icon (closed) and close icon (open) based on state.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <shell-navigation-toggle />
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'shell-navigation-toggle',
|
||||
template: `<ui-icon-button
|
||||
@@ -33,20 +44,24 @@ import { provideIcons } from '@ng-icons/core';
|
||||
export class ShellNavigationToggleComponent {
|
||||
#logger = logger({ component: 'ShellNavigationToggleComponent' });
|
||||
|
||||
/** Navigation service for controlling sidebar visibility. */
|
||||
readonly navigationService = inject(NavigationService);
|
||||
|
||||
readonly IconButtonColor = IconButtonColor;
|
||||
readonly IconButtonSize = IconButtonSize;
|
||||
|
||||
/** Computed icon name based on navigation state. */
|
||||
iconName = computed(() => {
|
||||
const open = this.navigationService.get();
|
||||
return open ? 'isaActionClose' : 'isaNavigationSidemenu';
|
||||
});
|
||||
|
||||
/** Computed ARIA label for accessibility. */
|
||||
ariaLabel = computed(() =>
|
||||
this.navigationService.get() ? 'Menü schließen' : 'Menü öffnen',
|
||||
);
|
||||
|
||||
/** Toggles the navigation sidebar visibility. */
|
||||
toggle(): void {
|
||||
this.navigationService.toggle();
|
||||
this.#logger.debug('Navigation toggled', () => ({
|
||||
|
||||
@@ -17,6 +17,18 @@ import {
|
||||
import { NotificationsService } from '@isa/shell/common';
|
||||
import { ShellNotificationsComponent } from '@isa/shell/notifications';
|
||||
|
||||
/**
|
||||
* Notifications toggle button with dropdown panel.
|
||||
*
|
||||
* Displays a bell icon that shows unread indicator when there are
|
||||
* unread notifications. Clicking opens an overlay panel with the
|
||||
* full notifications list.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <shell-notifications-toggle />
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'shell-notifications-toggle',
|
||||
templateUrl: './notifications-toggle.component.html',
|
||||
@@ -33,10 +45,13 @@ export class ShellNotificationsToggleComponent {
|
||||
readonly IconButtonColor = IconButtonColor;
|
||||
readonly IconButtonSize = IconButtonSize;
|
||||
|
||||
/** Notifications service for accessing notification state. */
|
||||
readonly notificationsService = inject(NotificationsService);
|
||||
|
||||
/** Whether the notifications panel overlay is currently open. */
|
||||
isOpen = signal(false);
|
||||
|
||||
/** CDK overlay positions for the notifications panel. */
|
||||
readonly positions: ConnectedPosition[] = [
|
||||
// Bottom right
|
||||
{ originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top' },
|
||||
@@ -48,6 +63,7 @@ export class ShellNotificationsToggleComponent {
|
||||
{ originX: 'start', originY: 'bottom', overlayX: 'end', overlayY: 'bottom' },
|
||||
];
|
||||
|
||||
/** Toggles the notifications panel visibility. */
|
||||
toggle(): void {
|
||||
this.isOpen.update((open) => !open);
|
||||
this.#logger.debug('Notifications panel toggled', () => ({
|
||||
@@ -55,23 +71,28 @@ export class ShellNotificationsToggleComponent {
|
||||
}));
|
||||
}
|
||||
|
||||
/** Closes the notifications panel. */
|
||||
close(): void {
|
||||
this.isOpen.set(false);
|
||||
this.#logger.debug('Notifications panel closed');
|
||||
}
|
||||
|
||||
/** Whether there are any notifications. */
|
||||
hasNotifications = computed(() => this.notificationsService.get().length > 0);
|
||||
|
||||
/** Whether there are unread notifications. */
|
||||
unreadNotifications = computed(
|
||||
() => this.notificationsService.unreadCount() > 0,
|
||||
);
|
||||
|
||||
/** Icon name based on unread notification state. */
|
||||
icon = computed(() =>
|
||||
this.unreadNotifications()
|
||||
? 'isaNavigationMessageUnread'
|
||||
: 'isaNavigationMessage',
|
||||
);
|
||||
|
||||
/** ARIA label for accessibility based on current state. */
|
||||
ariaLabel = computed(() => {
|
||||
if (!this.hasNotifications()) return 'Keine Benachrichtigungen';
|
||||
return this.isOpen() ? 'Benachrichtigungen schließen' : 'Benachrichtigungen öffnen';
|
||||
|
||||
@@ -5,6 +5,22 @@ import { ShellFontSizeSelectorComponent } from './components/font-size-selector/
|
||||
import { ShellLogoutButtonComponent } from './components/logout-button.component';
|
||||
import { ShellNotificationsToggleComponent } from './components/notifications-toggle/notifications-toggle.component';
|
||||
|
||||
/**
|
||||
* Application header component for the shell layout.
|
||||
*
|
||||
* Displays the main header bar with:
|
||||
* - Navigation toggle (hamburger menu on tablet/mobile)
|
||||
* - Font size selector for accessibility
|
||||
* - Notifications toggle with unread indicator
|
||||
* - Logout button
|
||||
*
|
||||
* The navigation toggle is only visible on tablet breakpoint and below.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <shell-header />
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'shell-header',
|
||||
templateUrl: './shell-header.component.html',
|
||||
@@ -18,5 +34,6 @@ import { ShellNotificationsToggleComponent } from './components/notifications-to
|
||||
],
|
||||
})
|
||||
export class ShellHeaderComponent {
|
||||
/** Signal indicating if the viewport is at or below tablet breakpoint. */
|
||||
readonly isTablet = breakpoint(Breakpoint.Tablet);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,12 @@ import {
|
||||
HostListener,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { NetworkStatusBannerComponent } from './components/network-status-banner.component';
|
||||
import {
|
||||
NavigationService,
|
||||
FontSizeService,
|
||||
TabsCollabsedService,
|
||||
TabsCollapsedService,
|
||||
} from '@isa/shell/common';
|
||||
import { ShellHeaderComponent } from '@isa/shell/header';
|
||||
import { ShellNavigationComponent } from '@isa/shell/navigation';
|
||||
@@ -53,11 +54,12 @@ import {
|
||||
],
|
||||
})
|
||||
export class ShellLayoutComponent {
|
||||
#logger = logger({ component: 'ShellLayoutComponent' });
|
||||
#navigationService = inject(NavigationService);
|
||||
#elementRef = inject(ElementRef);
|
||||
|
||||
/** Service for managing tabs collapsed state. */
|
||||
readonly tabsCollabesd = inject(TabsCollabsedService);
|
||||
readonly tabsCollapsed = inject(TabsCollapsedService);
|
||||
|
||||
/** Service for managing font size scaling. */
|
||||
readonly fontSizeService = inject(FontSizeService);
|
||||
@@ -101,6 +103,7 @@ export class ShellLayoutComponent {
|
||||
/** Closes navigation on main content scroll (tablet/mobile only). */
|
||||
onMainScroll(): void {
|
||||
if (!this.tablet() || !this.#navigationService.get()) return;
|
||||
this.#logger.debug('Closing navigation on scroll');
|
||||
this.#navigationService.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,17 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
import { NavigationGroup } from '../../types';
|
||||
import { ShellNavigationItemComponent } from '../navigation-item/navigation-item.component';
|
||||
|
||||
/**
|
||||
* Navigation group component that renders a labeled section with navigation items.
|
||||
*
|
||||
* Groups navigation items under a common label (e.g., "Kunden", "Filiale").
|
||||
* Each group displays its label as a header followed by its child items.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <shell-navigation-group [group]="navigationGroup" />
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'shell-navigation-group',
|
||||
templateUrl: './navigation-group.component.html',
|
||||
@@ -10,5 +21,6 @@ import { ShellNavigationItemComponent } from '../navigation-item/navigation-item
|
||||
imports: [ShellNavigationItemComponent],
|
||||
})
|
||||
export class ShellNavigationGroupComponent {
|
||||
/** The navigation group configuration with label and items. */
|
||||
readonly group = input.required<NavigationGroup>();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,18 @@ import { navigations } from './navigations';
|
||||
import { ShellNavigationGroupComponent } from './components/navigation-group/navigation-group.component';
|
||||
import { ShellNavigationItemComponent } from './components/navigation-item/navigation-item.component';
|
||||
|
||||
/**
|
||||
* Sidebar navigation component for the shell layout.
|
||||
*
|
||||
* Renders the main navigation sidebar with groups and items.
|
||||
* The navigation structure is defined in the `navigations` configuration,
|
||||
* which supports both standalone items and grouped items.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <shell-navigation />
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'shell-navigation',
|
||||
imports: [ShellNavigationGroupComponent, ShellNavigationItemComponent],
|
||||
@@ -10,5 +22,6 @@ import { ShellNavigationItemComponent } from './components/navigation-item/navig
|
||||
styleUrl: './shell-navigation.component.css',
|
||||
})
|
||||
export class ShellNavigationComponent {
|
||||
/** The navigation configuration array containing groups and items. */
|
||||
readonly navigations = navigations;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,20 @@ import { isaActionCheck } from '@isa/icons';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { Notification } from '@isa/shell/common';
|
||||
|
||||
/**
|
||||
* Single notification component displaying a notification item.
|
||||
*
|
||||
* Shows the notification title, message, relative time, and action button.
|
||||
* Handles navigation (internal/external) and callback actions when clicked.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <shell-notification
|
||||
* [notification]="notification"
|
||||
* (actionTriggered)="onNotificationAction($event)"
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'shell-notification',
|
||||
templateUrl: './notification.component.html',
|
||||
@@ -26,13 +40,21 @@ export class ShellNotificationComponent {
|
||||
readonly #logger = logger({ component: 'ShellNotificationComponent' });
|
||||
readonly #router = inject(Router);
|
||||
|
||||
/** The notification data to display. */
|
||||
notification = input.required<Notification>();
|
||||
|
||||
/** Emits when the notification action is triggered. */
|
||||
actionTriggered = output<Notification>();
|
||||
|
||||
/** Human-readable relative time (e.g., "2 hours ago"). */
|
||||
relativeTime = computed(() =>
|
||||
formatDistanceToNow(this.notification().timestamp, { addSuffix: true }),
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles the notification action button click.
|
||||
* Navigates to route or executes callback based on action type.
|
||||
*/
|
||||
onAction(): void {
|
||||
const notification = this.notification();
|
||||
const action = notification.action;
|
||||
|
||||
@@ -9,6 +9,18 @@ import { ShellNotificationComponent } from './components/notification/notificati
|
||||
import { Notification } from '@isa/shell/common';
|
||||
import { KeyValuePipe } from '@angular/common';
|
||||
|
||||
/**
|
||||
* Notifications panel component that displays grouped notifications.
|
||||
*
|
||||
* Shows all notifications organized by group (e.g., "orders", "system").
|
||||
* Within each group, notifications are sorted with unread items first,
|
||||
* then by timestamp (newest first).
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <shell-notifications />
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'shell-notifications',
|
||||
templateUrl: './shell-notifications.component.html',
|
||||
@@ -17,8 +29,14 @@ import { KeyValuePipe } from '@angular/common';
|
||||
imports: [ShellNotificationComponent, KeyValuePipe],
|
||||
})
|
||||
export class ShellNotificationsComponent {
|
||||
|
||||
/** Notifications service for accessing the notification store. */
|
||||
readonly notificationsService = inject(NotificationsService);
|
||||
|
||||
/**
|
||||
* Computed signal that groups and sorts notifications.
|
||||
* Groups by notification.group, sorts unread first then by timestamp.
|
||||
*/
|
||||
groupedNotifications = computed(() => {
|
||||
const notifications = this.notificationsService.get();
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
inject,
|
||||
computed,
|
||||
} from '@angular/core';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { Tab } from '@isa/core/tabs';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { isaActionClose } from '@isa/icons';
|
||||
@@ -38,6 +39,7 @@ import { Router } from '@angular/router';
|
||||
},
|
||||
})
|
||||
export class ShellTabItemComponent {
|
||||
#logger = logger({ component: 'ShellTabItemComponent' });
|
||||
#router = inject(Router);
|
||||
#tabService = inject(TabService);
|
||||
|
||||
@@ -71,20 +73,27 @@ export class ShellTabItemComponent {
|
||||
* Closes this tab and navigates to the previously active tab.
|
||||
* If no previous tab exists, navigates to the root route.
|
||||
*/
|
||||
async close(event: MouseEvent) {
|
||||
async close(event: MouseEvent): Promise<void> {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
const previousTab = this.#tabService.removeTab(this.tab().id);
|
||||
const tabId = this.tab().id;
|
||||
this.#logger.debug('Closing tab', () => ({ tabId }));
|
||||
|
||||
if (previousTab) {
|
||||
const location = this.#tabService.getCurrentLocation(previousTab.id);
|
||||
if (location?.url) {
|
||||
await this.#router.navigateByUrl(location.url);
|
||||
return;
|
||||
try {
|
||||
const previousTab = this.#tabService.removeTab(tabId);
|
||||
|
||||
if (previousTab) {
|
||||
const location = this.#tabService.getCurrentLocation(previousTab.id);
|
||||
if (location?.url) {
|
||||
await this.#router.navigateByUrl(location.url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.#router.navigateByUrl('/');
|
||||
await this.#router.navigateByUrl('/');
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to close tab', error as Error, () => ({ tabId }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Component, inject, computed, signal, ElementRef } from '@angular/core';
|
||||
import { Component, inject, ElementRef } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { ShellTabItemComponent } from './components/shell-tab-item.component';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { CarouselComponent } from '@isa/ui/carousel';
|
||||
import { IconButtonComponent, TextButtonComponent } from '@isa/ui/buttons';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import { isaActionPlus } from '@isa/icons';
|
||||
import { TabsCollabsedService } from '@isa/shell/common';
|
||||
import { TabsCollapsedService } from '@isa/shell/common';
|
||||
import { Breakpoint, breakpoint } from '@isa/ui/layout';
|
||||
|
||||
/**
|
||||
@@ -49,10 +50,11 @@ const PROXIMITY_THRESHOLD_PX = 50;
|
||||
},
|
||||
})
|
||||
export class ShellTabsComponent {
|
||||
#logger = logger({ component: 'ShellTabsComponent' });
|
||||
#tablet = breakpoint(Breakpoint.Tablet);
|
||||
|
||||
#tabService = inject(TabService);
|
||||
#tabsCollabsedService = inject(TabsCollabsedService);
|
||||
#tabsCollapsedService = inject(TabsCollapsedService);
|
||||
#elementRef = inject(ElementRef);
|
||||
#router = inject(Router);
|
||||
|
||||
@@ -60,7 +62,7 @@ export class ShellTabsComponent {
|
||||
readonly tabs = this.#tabService.entities;
|
||||
|
||||
/** Whether tabs should display in compact mode (mouse is far from component). */
|
||||
compact = this.#tabsCollabsedService.get;
|
||||
compact = this.#tabsCollapsedService.get;
|
||||
|
||||
/**
|
||||
* Handles mouse movement to detect proximity to the tabs bar.
|
||||
@@ -84,19 +86,30 @@ export class ShellTabsComponent {
|
||||
: 0;
|
||||
|
||||
const isNear = isWithinX && distanceY <= PROXIMITY_THRESHOLD_PX;
|
||||
this.#tabsCollabsedService.set(!isNear);
|
||||
this.#tabsCollapsedService.set(!isNear);
|
||||
}
|
||||
|
||||
/** Closes all open tabs and navigates to the root route. */
|
||||
async closeAll(): Promise<void> {
|
||||
this.#tabService.removeAllTabs();
|
||||
await this.#router.navigateByUrl('/');
|
||||
this.#logger.info('Closing all tabs');
|
||||
try {
|
||||
this.#tabService.removeAllTabs();
|
||||
await this.#router.navigateByUrl('/');
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to close all tabs', error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates a new tab and navigates to the product search view. */
|
||||
async addTab(): Promise<void> {
|
||||
await this.#router.navigateByUrl(
|
||||
`/kunde/${Date.now()}/product/(filter//side:search)`,
|
||||
);
|
||||
const tabId = Date.now();
|
||||
this.#logger.debug('Creating new tab', () => ({ tabId }));
|
||||
try {
|
||||
await this.#router.navigateByUrl(
|
||||
`/kunde/${tabId}/product/(filter//side:search)`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to create new tab', error as Error, () => ({ tabId }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,25 @@ import {
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
|
||||
/**
|
||||
* Directive that observes and reports element size changes.
|
||||
*
|
||||
* Uses ResizeObserver to track the host element's dimensions and
|
||||
* provides both a signal and output event for reactive updates.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <!-- As output event -->
|
||||
* <div uiElementSizeObserver (uiElementSizeObserver)="onSizeChange($event)">
|
||||
* Content
|
||||
* </div>
|
||||
*
|
||||
* <!-- As template reference -->
|
||||
* <div uiElementSizeObserver #sizeRef="uiElementSizeObserver">
|
||||
* Width: {{ sizeRef.size().width }}px
|
||||
* </div>
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[uiElementSizeObserver]',
|
||||
exportAs: 'uiElementSizeObserver',
|
||||
@@ -32,6 +51,7 @@ export class UiElementSizeObserverDirective
|
||||
this.#resizeObserver?.disconnect();
|
||||
}
|
||||
|
||||
/** Emits the current element size to both signal and output. */
|
||||
#emitSize() {
|
||||
const size = this.#element.getBoundingClientRect();
|
||||
this.#size.set(size);
|
||||
@@ -40,8 +60,10 @@ export class UiElementSizeObserverDirective
|
||||
|
||||
#size = signal<{ width: number; height: number }>({ height: 0, width: 0 });
|
||||
|
||||
/** Readonly signal exposing the current element dimensions. */
|
||||
readonly size: Signal<{ width: number; height: number }> =
|
||||
this.#size.asReadonly();
|
||||
|
||||
/** Output event emitting size changes. */
|
||||
readonly uiElementSizeObserver = output<{ width: number; height: number }>();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user