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 { inject, Injectable } from '@angular/core';
|
||||||
import { toSignal } from '@angular/core/rxjs-interop';
|
import { toSignal } from '@angular/core/rxjs-interop';
|
||||||
|
import { logger } from '@isa/core/logging';
|
||||||
import {
|
import {
|
||||||
map,
|
map,
|
||||||
Observable,
|
Observable,
|
||||||
@@ -7,22 +8,56 @@ import {
|
|||||||
merge,
|
merge,
|
||||||
startWith,
|
startWith,
|
||||||
shareReplay,
|
shareReplay,
|
||||||
|
tap,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
|
|
||||||
|
/** Network connectivity status values. */
|
||||||
export type NetworkStatus = 'online' | 'offline';
|
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' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class NetworkStatusService {
|
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(
|
readonly status$: Observable<NetworkStatus> = merge(
|
||||||
fromEvent(window, 'online'),
|
fromEvent(window, 'online'),
|
||||||
fromEvent(window, 'offline'),
|
fromEvent(window, 'offline'),
|
||||||
).pipe(
|
).pipe(
|
||||||
startWith(null), // emit immediately
|
startWith(null), // emit immediately
|
||||||
map((): NetworkStatus => (navigator.onLine ? 'online' : 'offline')),
|
map((): NetworkStatus => (navigator.onLine ? 'online' : 'offline')),
|
||||||
|
tap((status) => this.#logger.debug('Network status changed', () => ({ status }))),
|
||||||
shareReplay({ bufferSize: 1, refCount: true }),
|
shareReplay({ bufferSize: 1, refCount: true }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection function to get the network status observable.
|
||||||
|
* @returns Observable of network status
|
||||||
|
*/
|
||||||
export const injectNetworkStatus$ = () => inject(NetworkStatusService).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$());
|
export const injectNetworkStatus = () => toSignal(injectNetworkStatus$());
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export * from './lib/navigation.service';
|
export * from './lib/navigation.service';
|
||||||
export * from './lib/font-size.service';
|
export * from './lib/font-size.service';
|
||||||
export * from './lib/notifications.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';
|
} from '@angular/core';
|
||||||
import { logger } from '@isa/core/logging';
|
import { logger } from '@isa/core/logging';
|
||||||
|
|
||||||
|
/** Available font size options for the application. */
|
||||||
export type FontSize = 'small' | 'medium' | 'large';
|
export type FontSize = 'small' | 'medium' | 'large';
|
||||||
|
|
||||||
|
/** Mapping of font size names to pixel values. */
|
||||||
const FONT_SIZE_PX_MAP: Record<FontSize, number> = {
|
const FONT_SIZE_PX_MAP: Record<FontSize, number> = {
|
||||||
small: 14,
|
small: 14,
|
||||||
medium: 16,
|
medium: 16,
|
||||||
large: 18,
|
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' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class FontSizeService {
|
export class FontSizeService {
|
||||||
#logger = logger({ service: 'FontSizeService' });
|
#logger = logger({ service: 'FontSizeService' });
|
||||||
@@ -26,17 +52,29 @@ export class FontSizeService {
|
|||||||
|
|
||||||
#renderer = inject(RendererFactory2).createRenderer(this.#document, null);
|
#renderer = inject(RendererFactory2).createRenderer(this.#document, null);
|
||||||
|
|
||||||
|
/** Readonly signal exposing the current font size name. */
|
||||||
readonly get = this.#state.asReadonly();
|
readonly get = this.#state.asReadonly();
|
||||||
|
|
||||||
|
/** Computed signal returning the current font size in pixels. */
|
||||||
readonly getPx = computed(() => FONT_SIZE_PX_MAP[this.#state()]);
|
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 {
|
set(size: FontSize): void {
|
||||||
this.#logger.debug('Font size changed', () => ({ size }));
|
this.#logger.debug('Font size changed', () => ({ size }));
|
||||||
this.#state.set(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();
|
readonly remToPx = (rem: number) => rem * this.getPx();
|
||||||
|
|
||||||
|
/** Effect that applies the font size to the document root element. */
|
||||||
readonly fontSizeEffect = effect(() => {
|
readonly fontSizeEffect = effect(() => {
|
||||||
const fontSize = this.#state();
|
const fontSize = this.#state();
|
||||||
this.#renderer.setStyle(
|
this.#renderer.setStyle(
|
||||||
|
|||||||
@@ -1,18 +1,45 @@
|
|||||||
import { Injectable, signal } from '@angular/core';
|
import { Injectable, signal } from '@angular/core';
|
||||||
import { logger } from '@isa/core/logging';
|
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' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class NavigationService {
|
export class NavigationService {
|
||||||
#logger = logger({ service: 'NavigationService' });
|
#logger = logger({ service: 'NavigationService' });
|
||||||
#state = signal<boolean>(false);
|
#state = signal<boolean>(false);
|
||||||
|
|
||||||
|
/** Readonly signal exposing the current navigation open/closed state. */
|
||||||
readonly get = this.#state.asReadonly();
|
readonly get = this.#state.asReadonly();
|
||||||
|
|
||||||
|
/** Toggles the navigation state between open and closed. */
|
||||||
toggle(): void {
|
toggle(): void {
|
||||||
this.#state.update((state) => !state);
|
this.#state.update((state) => !state);
|
||||||
this.#logger.debug('Navigation toggled', () => ({ isOpen: this.#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 {
|
set(state: boolean): void {
|
||||||
this.#logger.debug('Navigation state set', () => ({ state }));
|
this.#logger.debug('Navigation state set', () => ({ state }));
|
||||||
this.#state.set(state);
|
this.#state.set(state);
|
||||||
|
|||||||
@@ -1,47 +1,103 @@
|
|||||||
import { computed, Injectable, signal } from '@angular/core';
|
import { computed, Injectable, signal } from '@angular/core';
|
||||||
import { logger } from '@isa/core/logging';
|
import { logger } from '@isa/core/logging';
|
||||||
|
|
||||||
|
/** Unix timestamp in milliseconds. */
|
||||||
type Timestamp = number;
|
type Timestamp = number;
|
||||||
|
|
||||||
|
/** Unique identifier for a notification. */
|
||||||
type NotificationId = string | number;
|
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 = {
|
export type Notification = {
|
||||||
|
/** Unique identifier for this notification. */
|
||||||
id: NotificationId;
|
id: NotificationId;
|
||||||
|
/** Group name for categorizing notifications (e.g., 'orders', 'system'). */
|
||||||
group: string;
|
group: string;
|
||||||
|
/** Short title displayed prominently. */
|
||||||
title: string;
|
title: string;
|
||||||
|
/** Detailed message content. */
|
||||||
message: string;
|
message: string;
|
||||||
|
/** Action to perform when notification is clicked. */
|
||||||
action: NotificationAction;
|
action: NotificationAction;
|
||||||
|
/** Timestamp when marked as read (undefined if unread). */
|
||||||
markedAsRead?: Timestamp;
|
markedAsRead?: Timestamp;
|
||||||
|
/** Creation timestamp for sorting. */
|
||||||
timestamp: Timestamp;
|
timestamp: Timestamp;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Base properties for notification actions. */
|
||||||
export type NotificationActionBase = {
|
export type NotificationActionBase = {
|
||||||
|
/** Button label text. */
|
||||||
label: string;
|
label: string;
|
||||||
|
/** Action type discriminator. */
|
||||||
type: 'navigate' | 'callback';
|
type: 'navigate' | 'callback';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Navigation action that routes to an internal or external URL. */
|
||||||
export type NotificationActionNavigate = NotificationActionBase & {
|
export type NotificationActionNavigate = NotificationActionBase & {
|
||||||
type: 'navigate';
|
type: 'navigate';
|
||||||
|
/** Whether the route is internal (Angular router) or external (new tab). */
|
||||||
target: 'internal' | 'external';
|
target: 'internal' | 'external';
|
||||||
|
/** The route path or URL to navigate to. */
|
||||||
route: string;
|
route: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Callback action that executes a function. */
|
||||||
export type NotificationActionCallback = NotificationActionBase & {
|
export type NotificationActionCallback = NotificationActionBase & {
|
||||||
type: 'callback';
|
type: 'callback';
|
||||||
|
/** Function to execute when action is triggered. */
|
||||||
callback: () => void;
|
callback: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Union type for all notification action types. */
|
||||||
export type NotificationAction =
|
export type NotificationAction =
|
||||||
| NotificationActionNavigate
|
| NotificationActionNavigate
|
||||||
| NotificationActionCallback;
|
| 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' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class NotificationsService {
|
export class NotificationsService {
|
||||||
#logger = logger({ service: 'NotificationsService' });
|
#logger = logger({ service: 'NotificationsService' });
|
||||||
#state = signal<Notification[]>([]);
|
#state = signal<Notification[]>([]);
|
||||||
|
|
||||||
|
/** Readonly signal exposing all current notifications. */
|
||||||
readonly get = this.#state.asReadonly();
|
readonly get = this.#state.asReadonly();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new notification to the store.
|
||||||
|
* @param notification - The notification to add
|
||||||
|
*/
|
||||||
add(notification: Notification): void {
|
add(notification: Notification): void {
|
||||||
this.#logger.debug('Notification added', () => ({
|
this.#logger.debug('Notification added', () => ({
|
||||||
id: notification.id,
|
id: notification.id,
|
||||||
@@ -50,6 +106,10 @@ export class NotificationsService {
|
|||||||
this.#state.update((notifications) => [...notifications, notification]);
|
this.#state.update((notifications) => [...notifications, notification]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a notification by its ID.
|
||||||
|
* @param id - The notification ID to remove
|
||||||
|
*/
|
||||||
remove(id: NotificationId): void {
|
remove(id: NotificationId): void {
|
||||||
this.#logger.debug('Notification removed', () => ({ id }));
|
this.#logger.debug('Notification removed', () => ({ id }));
|
||||||
this.#state.update((notifications) =>
|
this.#state.update((notifications) =>
|
||||||
@@ -57,11 +117,16 @@ export class NotificationsService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Removes all notifications from the store. */
|
||||||
clear(): void {
|
clear(): void {
|
||||||
this.#logger.debug('All notifications cleared');
|
this.#logger.debug('All notifications cleared');
|
||||||
this.#state.set([]);
|
this.#state.set([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks a specific notification as read.
|
||||||
|
* @param id - The notification ID to mark as read
|
||||||
|
*/
|
||||||
markAsRead(id: NotificationId): void {
|
markAsRead(id: NotificationId): void {
|
||||||
this.#logger.debug('Notification marked as read', () => ({ id }));
|
this.#logger.debug('Notification marked as read', () => ({ id }));
|
||||||
this.#state.update((notifications) =>
|
this.#state.update((notifications) =>
|
||||||
@@ -73,6 +138,7 @@ export class NotificationsService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Marks all notifications as read. */
|
||||||
markAllAsRead(): void {
|
markAllAsRead(): void {
|
||||||
this.#logger.debug('All notifications marked as read');
|
this.#logger.debug('All notifications marked as read');
|
||||||
this.#state.update((notifications) =>
|
this.#state.update((notifications) =>
|
||||||
@@ -83,6 +149,7 @@ export class NotificationsService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Computed signal returning the count of unread notifications. */
|
||||||
readonly unreadCount = computed(() =>
|
readonly unreadCount = computed(() =>
|
||||||
this.#state().filter((notification) => !notification.markedAsRead).length,
|
this.#state().filter((notification) => !notification.markedAsRead).length,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { logger } from '@isa/core/logging';
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* readonly tabsCollapsed = inject(TabsCollabsedService);
|
* readonly tabsCollapsed = inject(TabsCollapsedService);
|
||||||
*
|
*
|
||||||
* // Read current state
|
* // Read current state
|
||||||
* const isCollapsed = this.tabsCollapsed.get();
|
* const isCollapsed = this.tabsCollapsed.get();
|
||||||
@@ -23,7 +23,7 @@ import { logger } from '@isa/core/logging';
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class TabsCollabsedService {
|
export class TabsCollapsedService {
|
||||||
#logger = logger({ service: 'TabsService' });
|
#logger = logger({ service: 'TabsService' });
|
||||||
#state = signal<boolean>(false);
|
#state = signal<boolean>(false);
|
||||||
|
|
||||||
@@ -10,12 +10,28 @@ import { NgIcon, provideIcons } from '@ng-icons/core';
|
|||||||
import { isaNavigationFontsize } from '@isa/icons';
|
import { isaNavigationFontsize } from '@isa/icons';
|
||||||
import { FontSize, FontSizeService } from '@isa/shell/common';
|
import { FontSize, FontSizeService } from '@isa/shell/common';
|
||||||
|
|
||||||
|
/** Configuration for a font size option in the selector. */
|
||||||
interface FontSizeOption {
|
interface FontSizeOption {
|
||||||
|
/** The font size value ('small', 'medium', 'large'). */
|
||||||
value: FontSize;
|
value: FontSize;
|
||||||
|
/** Accessible label for screen readers. */
|
||||||
label: string;
|
label: string;
|
||||||
|
/** Icon size for visual representation. */
|
||||||
iconSize: string;
|
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({
|
@Component({
|
||||||
selector: 'shell-font-size-selector',
|
selector: 'shell-font-size-selector',
|
||||||
templateUrl: './font-size-selector.component.html',
|
templateUrl: './font-size-selector.component.html',
|
||||||
@@ -31,24 +47,32 @@ interface FontSizeOption {
|
|||||||
export class ShellFontSizeSelectorComponent {
|
export class ShellFontSizeSelectorComponent {
|
||||||
#logger = logger({ component: 'ShellFontSizeSelectorComponent' });
|
#logger = logger({ component: 'ShellFontSizeSelectorComponent' });
|
||||||
|
|
||||||
|
/** Font size service for reading and updating the current size. */
|
||||||
readonly fontSizeService = inject(FontSizeService);
|
readonly fontSizeService = inject(FontSizeService);
|
||||||
|
|
||||||
|
/** Available font size options with labels and icon sizes. */
|
||||||
readonly fontSizeOptions: FontSizeOption[] = [
|
readonly fontSizeOptions: FontSizeOption[] = [
|
||||||
{ value: 'small', label: 'Kleine Schriftgröße', iconSize: '0.63rem' },
|
{ value: 'small', label: 'Kleine Schriftgröße', iconSize: '0.63rem' },
|
||||||
{ value: 'medium', label: 'Mittlere Schriftgröße', iconSize: '1rem' },
|
{ value: 'medium', label: 'Mittlere Schriftgröße', iconSize: '1rem' },
|
||||||
{ value: 'large', label: 'Große Schriftgröße', iconSize: '1.3rem' },
|
{ value: 'large', label: 'Große Schriftgröße', iconSize: '1.3rem' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** Pixel offsets for the sliding indicator animation. */
|
||||||
readonly #offsetMap: Record<FontSize, number> = {
|
readonly #offsetMap: Record<FontSize, number> = {
|
||||||
small: 0,
|
small: 0,
|
||||||
medium: 3,
|
medium: 3,
|
||||||
large: 6,
|
large: 6,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Computed offset position for the sliding indicator. */
|
||||||
readonly indicatorOffset = computed(
|
readonly indicatorOffset = computed(
|
||||||
() => this.#offsetMap[this.fontSizeService.get()],
|
() => this.#offsetMap[this.fontSizeService.get()],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles font size selection changes.
|
||||||
|
* @param size - The newly selected font size
|
||||||
|
*/
|
||||||
onFontSizeChange(size: FontSize): void {
|
onFontSizeChange(size: FontSize): void {
|
||||||
this.#logger.debug('Font size changed', () => ({ size }));
|
this.#logger.debug('Font size changed', () => ({ size }));
|
||||||
this.fontSizeService.set(size);
|
this.fontSizeService.set(size);
|
||||||
|
|||||||
@@ -5,6 +5,17 @@ import { AuthService } from '@isa/core/auth';
|
|||||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||||
import { isaNavigationLogout } from '@isa/icons';
|
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({
|
@Component({
|
||||||
selector: 'shell-logout-button',
|
selector: 'shell-logout-button',
|
||||||
template: `<ui-info-button
|
template: `<ui-info-button
|
||||||
@@ -24,6 +35,7 @@ export class ShellLogoutButtonComponent {
|
|||||||
#logger = logger({ component: 'ShellLogoutButtonComponent' });
|
#logger = logger({ component: 'ShellLogoutButtonComponent' });
|
||||||
#authService = inject(AuthService);
|
#authService = inject(AuthService);
|
||||||
|
|
||||||
|
/** Logs out the current user via the AuthService. */
|
||||||
logout(): void {
|
logout(): void {
|
||||||
this.#logger.info('User logging out');
|
this.#logger.info('User logging out');
|
||||||
this.#authService.logout();
|
this.#authService.logout();
|
||||||
|
|||||||
@@ -14,6 +14,17 @@ import {
|
|||||||
} from '@isa/ui/buttons';
|
} from '@isa/ui/buttons';
|
||||||
import { provideIcons } from '@ng-icons/core';
|
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({
|
@Component({
|
||||||
selector: 'shell-navigation-toggle',
|
selector: 'shell-navigation-toggle',
|
||||||
template: `<ui-icon-button
|
template: `<ui-icon-button
|
||||||
@@ -33,20 +44,24 @@ import { provideIcons } from '@ng-icons/core';
|
|||||||
export class ShellNavigationToggleComponent {
|
export class ShellNavigationToggleComponent {
|
||||||
#logger = logger({ component: 'ShellNavigationToggleComponent' });
|
#logger = logger({ component: 'ShellNavigationToggleComponent' });
|
||||||
|
|
||||||
|
/** Navigation service for controlling sidebar visibility. */
|
||||||
readonly navigationService = inject(NavigationService);
|
readonly navigationService = inject(NavigationService);
|
||||||
|
|
||||||
readonly IconButtonColor = IconButtonColor;
|
readonly IconButtonColor = IconButtonColor;
|
||||||
readonly IconButtonSize = IconButtonSize;
|
readonly IconButtonSize = IconButtonSize;
|
||||||
|
|
||||||
|
/** Computed icon name based on navigation state. */
|
||||||
iconName = computed(() => {
|
iconName = computed(() => {
|
||||||
const open = this.navigationService.get();
|
const open = this.navigationService.get();
|
||||||
return open ? 'isaActionClose' : 'isaNavigationSidemenu';
|
return open ? 'isaActionClose' : 'isaNavigationSidemenu';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Computed ARIA label for accessibility. */
|
||||||
ariaLabel = computed(() =>
|
ariaLabel = computed(() =>
|
||||||
this.navigationService.get() ? 'Menü schließen' : 'Menü öffnen',
|
this.navigationService.get() ? 'Menü schließen' : 'Menü öffnen',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Toggles the navigation sidebar visibility. */
|
||||||
toggle(): void {
|
toggle(): void {
|
||||||
this.navigationService.toggle();
|
this.navigationService.toggle();
|
||||||
this.#logger.debug('Navigation toggled', () => ({
|
this.#logger.debug('Navigation toggled', () => ({
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ import {
|
|||||||
import { NotificationsService } from '@isa/shell/common';
|
import { NotificationsService } from '@isa/shell/common';
|
||||||
import { ShellNotificationsComponent } from '@isa/shell/notifications';
|
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({
|
@Component({
|
||||||
selector: 'shell-notifications-toggle',
|
selector: 'shell-notifications-toggle',
|
||||||
templateUrl: './notifications-toggle.component.html',
|
templateUrl: './notifications-toggle.component.html',
|
||||||
@@ -33,10 +45,13 @@ export class ShellNotificationsToggleComponent {
|
|||||||
readonly IconButtonColor = IconButtonColor;
|
readonly IconButtonColor = IconButtonColor;
|
||||||
readonly IconButtonSize = IconButtonSize;
|
readonly IconButtonSize = IconButtonSize;
|
||||||
|
|
||||||
|
/** Notifications service for accessing notification state. */
|
||||||
readonly notificationsService = inject(NotificationsService);
|
readonly notificationsService = inject(NotificationsService);
|
||||||
|
|
||||||
|
/** Whether the notifications panel overlay is currently open. */
|
||||||
isOpen = signal(false);
|
isOpen = signal(false);
|
||||||
|
|
||||||
|
/** CDK overlay positions for the notifications panel. */
|
||||||
readonly positions: ConnectedPosition[] = [
|
readonly positions: ConnectedPosition[] = [
|
||||||
// Bottom right
|
// Bottom right
|
||||||
{ originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top' },
|
{ originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top' },
|
||||||
@@ -48,6 +63,7 @@ export class ShellNotificationsToggleComponent {
|
|||||||
{ originX: 'start', originY: 'bottom', overlayX: 'end', overlayY: 'bottom' },
|
{ originX: 'start', originY: 'bottom', overlayX: 'end', overlayY: 'bottom' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** Toggles the notifications panel visibility. */
|
||||||
toggle(): void {
|
toggle(): void {
|
||||||
this.isOpen.update((open) => !open);
|
this.isOpen.update((open) => !open);
|
||||||
this.#logger.debug('Notifications panel toggled', () => ({
|
this.#logger.debug('Notifications panel toggled', () => ({
|
||||||
@@ -55,23 +71,28 @@ export class ShellNotificationsToggleComponent {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Closes the notifications panel. */
|
||||||
close(): void {
|
close(): void {
|
||||||
this.isOpen.set(false);
|
this.isOpen.set(false);
|
||||||
this.#logger.debug('Notifications panel closed');
|
this.#logger.debug('Notifications panel closed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether there are any notifications. */
|
||||||
hasNotifications = computed(() => this.notificationsService.get().length > 0);
|
hasNotifications = computed(() => this.notificationsService.get().length > 0);
|
||||||
|
|
||||||
|
/** Whether there are unread notifications. */
|
||||||
unreadNotifications = computed(
|
unreadNotifications = computed(
|
||||||
() => this.notificationsService.unreadCount() > 0,
|
() => this.notificationsService.unreadCount() > 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Icon name based on unread notification state. */
|
||||||
icon = computed(() =>
|
icon = computed(() =>
|
||||||
this.unreadNotifications()
|
this.unreadNotifications()
|
||||||
? 'isaNavigationMessageUnread'
|
? 'isaNavigationMessageUnread'
|
||||||
: 'isaNavigationMessage',
|
: 'isaNavigationMessage',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** ARIA label for accessibility based on current state. */
|
||||||
ariaLabel = computed(() => {
|
ariaLabel = computed(() => {
|
||||||
if (!this.hasNotifications()) return 'Keine Benachrichtigungen';
|
if (!this.hasNotifications()) return 'Keine Benachrichtigungen';
|
||||||
return this.isOpen() ? 'Benachrichtigungen schließen' : 'Benachrichtigungen öffnen';
|
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 { ShellLogoutButtonComponent } from './components/logout-button.component';
|
||||||
import { ShellNotificationsToggleComponent } from './components/notifications-toggle/notifications-toggle.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({
|
@Component({
|
||||||
selector: 'shell-header',
|
selector: 'shell-header',
|
||||||
templateUrl: './shell-header.component.html',
|
templateUrl: './shell-header.component.html',
|
||||||
@@ -18,5 +34,6 @@ import { ShellNotificationsToggleComponent } from './components/notifications-to
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ShellHeaderComponent {
|
export class ShellHeaderComponent {
|
||||||
|
/** Signal indicating if the viewport is at or below tablet breakpoint. */
|
||||||
readonly isTablet = breakpoint(Breakpoint.Tablet);
|
readonly isTablet = breakpoint(Breakpoint.Tablet);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import {
|
|||||||
HostListener,
|
HostListener,
|
||||||
inject,
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { logger } from '@isa/core/logging';
|
||||||
import { NetworkStatusBannerComponent } from './components/network-status-banner.component';
|
import { NetworkStatusBannerComponent } from './components/network-status-banner.component';
|
||||||
import {
|
import {
|
||||||
NavigationService,
|
NavigationService,
|
||||||
FontSizeService,
|
FontSizeService,
|
||||||
TabsCollabsedService,
|
TabsCollapsedService,
|
||||||
} from '@isa/shell/common';
|
} from '@isa/shell/common';
|
||||||
import { ShellHeaderComponent } from '@isa/shell/header';
|
import { ShellHeaderComponent } from '@isa/shell/header';
|
||||||
import { ShellNavigationComponent } from '@isa/shell/navigation';
|
import { ShellNavigationComponent } from '@isa/shell/navigation';
|
||||||
@@ -53,11 +54,12 @@ import {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ShellLayoutComponent {
|
export class ShellLayoutComponent {
|
||||||
|
#logger = logger({ component: 'ShellLayoutComponent' });
|
||||||
#navigationService = inject(NavigationService);
|
#navigationService = inject(NavigationService);
|
||||||
#elementRef = inject(ElementRef);
|
#elementRef = inject(ElementRef);
|
||||||
|
|
||||||
/** Service for managing tabs collapsed state. */
|
/** Service for managing tabs collapsed state. */
|
||||||
readonly tabsCollabesd = inject(TabsCollabsedService);
|
readonly tabsCollapsed = inject(TabsCollapsedService);
|
||||||
|
|
||||||
/** Service for managing font size scaling. */
|
/** Service for managing font size scaling. */
|
||||||
readonly fontSizeService = inject(FontSizeService);
|
readonly fontSizeService = inject(FontSizeService);
|
||||||
@@ -101,6 +103,7 @@ export class ShellLayoutComponent {
|
|||||||
/** Closes navigation on main content scroll (tablet/mobile only). */
|
/** Closes navigation on main content scroll (tablet/mobile only). */
|
||||||
onMainScroll(): void {
|
onMainScroll(): void {
|
||||||
if (!this.tablet() || !this.#navigationService.get()) return;
|
if (!this.tablet() || !this.#navigationService.get()) return;
|
||||||
|
this.#logger.debug('Closing navigation on scroll');
|
||||||
this.#navigationService.set(false);
|
this.#navigationService.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,17 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
|||||||
import { NavigationGroup } from '../../types';
|
import { NavigationGroup } from '../../types';
|
||||||
import { ShellNavigationItemComponent } from '../navigation-item/navigation-item.component';
|
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({
|
@Component({
|
||||||
selector: 'shell-navigation-group',
|
selector: 'shell-navigation-group',
|
||||||
templateUrl: './navigation-group.component.html',
|
templateUrl: './navigation-group.component.html',
|
||||||
@@ -10,5 +21,6 @@ import { ShellNavigationItemComponent } from '../navigation-item/navigation-item
|
|||||||
imports: [ShellNavigationItemComponent],
|
imports: [ShellNavigationItemComponent],
|
||||||
})
|
})
|
||||||
export class ShellNavigationGroupComponent {
|
export class ShellNavigationGroupComponent {
|
||||||
|
/** The navigation group configuration with label and items. */
|
||||||
readonly group = input.required<NavigationGroup>();
|
readonly group = input.required<NavigationGroup>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,18 @@ import { navigations } from './navigations';
|
|||||||
import { ShellNavigationGroupComponent } from './components/navigation-group/navigation-group.component';
|
import { ShellNavigationGroupComponent } from './components/navigation-group/navigation-group.component';
|
||||||
import { ShellNavigationItemComponent } from './components/navigation-item/navigation-item.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({
|
@Component({
|
||||||
selector: 'shell-navigation',
|
selector: 'shell-navigation',
|
||||||
imports: [ShellNavigationGroupComponent, ShellNavigationItemComponent],
|
imports: [ShellNavigationGroupComponent, ShellNavigationItemComponent],
|
||||||
@@ -10,5 +22,6 @@ import { ShellNavigationItemComponent } from './components/navigation-item/navig
|
|||||||
styleUrl: './shell-navigation.component.css',
|
styleUrl: './shell-navigation.component.css',
|
||||||
})
|
})
|
||||||
export class ShellNavigationComponent {
|
export class ShellNavigationComponent {
|
||||||
|
/** The navigation configuration array containing groups and items. */
|
||||||
readonly navigations = navigations;
|
readonly navigations = navigations;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,20 @@ import { isaActionCheck } from '@isa/icons';
|
|||||||
import { logger } from '@isa/core/logging';
|
import { logger } from '@isa/core/logging';
|
||||||
import { Notification } from '@isa/shell/common';
|
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({
|
@Component({
|
||||||
selector: 'shell-notification',
|
selector: 'shell-notification',
|
||||||
templateUrl: './notification.component.html',
|
templateUrl: './notification.component.html',
|
||||||
@@ -26,13 +40,21 @@ export class ShellNotificationComponent {
|
|||||||
readonly #logger = logger({ component: 'ShellNotificationComponent' });
|
readonly #logger = logger({ component: 'ShellNotificationComponent' });
|
||||||
readonly #router = inject(Router);
|
readonly #router = inject(Router);
|
||||||
|
|
||||||
|
/** The notification data to display. */
|
||||||
notification = input.required<Notification>();
|
notification = input.required<Notification>();
|
||||||
|
|
||||||
|
/** Emits when the notification action is triggered. */
|
||||||
actionTriggered = output<Notification>();
|
actionTriggered = output<Notification>();
|
||||||
|
|
||||||
|
/** Human-readable relative time (e.g., "2 hours ago"). */
|
||||||
relativeTime = computed(() =>
|
relativeTime = computed(() =>
|
||||||
formatDistanceToNow(this.notification().timestamp, { addSuffix: true }),
|
formatDistanceToNow(this.notification().timestamp, { addSuffix: true }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the notification action button click.
|
||||||
|
* Navigates to route or executes callback based on action type.
|
||||||
|
*/
|
||||||
onAction(): void {
|
onAction(): void {
|
||||||
const notification = this.notification();
|
const notification = this.notification();
|
||||||
const action = notification.action;
|
const action = notification.action;
|
||||||
|
|||||||
@@ -9,6 +9,18 @@ import { ShellNotificationComponent } from './components/notification/notificati
|
|||||||
import { Notification } from '@isa/shell/common';
|
import { Notification } from '@isa/shell/common';
|
||||||
import { KeyValuePipe } from '@angular/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({
|
@Component({
|
||||||
selector: 'shell-notifications',
|
selector: 'shell-notifications',
|
||||||
templateUrl: './shell-notifications.component.html',
|
templateUrl: './shell-notifications.component.html',
|
||||||
@@ -17,8 +29,14 @@ import { KeyValuePipe } from '@angular/common';
|
|||||||
imports: [ShellNotificationComponent, KeyValuePipe],
|
imports: [ShellNotificationComponent, KeyValuePipe],
|
||||||
})
|
})
|
||||||
export class ShellNotificationsComponent {
|
export class ShellNotificationsComponent {
|
||||||
|
|
||||||
|
/** Notifications service for accessing the notification store. */
|
||||||
readonly notificationsService = inject(NotificationsService);
|
readonly notificationsService = inject(NotificationsService);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computed signal that groups and sorts notifications.
|
||||||
|
* Groups by notification.group, sorts unread first then by timestamp.
|
||||||
|
*/
|
||||||
groupedNotifications = computed(() => {
|
groupedNotifications = computed(() => {
|
||||||
const notifications = this.notificationsService.get();
|
const notifications = this.notificationsService.get();
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
computed,
|
computed,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { logger } from '@isa/core/logging';
|
||||||
import { Tab } from '@isa/core/tabs';
|
import { Tab } from '@isa/core/tabs';
|
||||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||||
import { isaActionClose } from '@isa/icons';
|
import { isaActionClose } from '@isa/icons';
|
||||||
@@ -38,6 +39,7 @@ import { Router } from '@angular/router';
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
export class ShellTabItemComponent {
|
export class ShellTabItemComponent {
|
||||||
|
#logger = logger({ component: 'ShellTabItemComponent' });
|
||||||
#router = inject(Router);
|
#router = inject(Router);
|
||||||
#tabService = inject(TabService);
|
#tabService = inject(TabService);
|
||||||
|
|
||||||
@@ -71,20 +73,27 @@ export class ShellTabItemComponent {
|
|||||||
* Closes this tab and navigates to the previously active tab.
|
* Closes this tab and navigates to the previously active tab.
|
||||||
* If no previous tab exists, navigates to the root route.
|
* If no previous tab exists, navigates to the root route.
|
||||||
*/
|
*/
|
||||||
async close(event: MouseEvent) {
|
async close(event: MouseEvent): Promise<void> {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const previousTab = this.#tabService.removeTab(this.tab().id);
|
const tabId = this.tab().id;
|
||||||
|
this.#logger.debug('Closing tab', () => ({ tabId }));
|
||||||
|
|
||||||
if (previousTab) {
|
try {
|
||||||
const location = this.#tabService.getCurrentLocation(previousTab.id);
|
const previousTab = this.#tabService.removeTab(tabId);
|
||||||
if (location?.url) {
|
|
||||||
await this.#router.navigateByUrl(location.url);
|
if (previousTab) {
|
||||||
return;
|
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 { Router } from '@angular/router';
|
||||||
|
import { logger } from '@isa/core/logging';
|
||||||
import { ShellTabItemComponent } from './components/shell-tab-item.component';
|
import { ShellTabItemComponent } from './components/shell-tab-item.component';
|
||||||
import { TabService } from '@isa/core/tabs';
|
import { TabService } from '@isa/core/tabs';
|
||||||
import { CarouselComponent } from '@isa/ui/carousel';
|
import { CarouselComponent } from '@isa/ui/carousel';
|
||||||
import { IconButtonComponent, TextButtonComponent } from '@isa/ui/buttons';
|
import { IconButtonComponent, TextButtonComponent } from '@isa/ui/buttons';
|
||||||
import { provideIcons } from '@ng-icons/core';
|
import { provideIcons } from '@ng-icons/core';
|
||||||
import { isaActionPlus } from '@isa/icons';
|
import { isaActionPlus } from '@isa/icons';
|
||||||
import { TabsCollabsedService } from '@isa/shell/common';
|
import { TabsCollapsedService } from '@isa/shell/common';
|
||||||
import { Breakpoint, breakpoint } from '@isa/ui/layout';
|
import { Breakpoint, breakpoint } from '@isa/ui/layout';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,10 +50,11 @@ const PROXIMITY_THRESHOLD_PX = 50;
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
export class ShellTabsComponent {
|
export class ShellTabsComponent {
|
||||||
|
#logger = logger({ component: 'ShellTabsComponent' });
|
||||||
#tablet = breakpoint(Breakpoint.Tablet);
|
#tablet = breakpoint(Breakpoint.Tablet);
|
||||||
|
|
||||||
#tabService = inject(TabService);
|
#tabService = inject(TabService);
|
||||||
#tabsCollabsedService = inject(TabsCollabsedService);
|
#tabsCollapsedService = inject(TabsCollapsedService);
|
||||||
#elementRef = inject(ElementRef);
|
#elementRef = inject(ElementRef);
|
||||||
#router = inject(Router);
|
#router = inject(Router);
|
||||||
|
|
||||||
@@ -60,7 +62,7 @@ export class ShellTabsComponent {
|
|||||||
readonly tabs = this.#tabService.entities;
|
readonly tabs = this.#tabService.entities;
|
||||||
|
|
||||||
/** Whether tabs should display in compact mode (mouse is far from component). */
|
/** 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.
|
* Handles mouse movement to detect proximity to the tabs bar.
|
||||||
@@ -84,19 +86,30 @@ export class ShellTabsComponent {
|
|||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const isNear = isWithinX && distanceY <= PROXIMITY_THRESHOLD_PX;
|
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. */
|
/** Closes all open tabs and navigates to the root route. */
|
||||||
async closeAll(): Promise<void> {
|
async closeAll(): Promise<void> {
|
||||||
this.#tabService.removeAllTabs();
|
this.#logger.info('Closing all tabs');
|
||||||
await this.#router.navigateByUrl('/');
|
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. */
|
/** Creates a new tab and navigates to the product search view. */
|
||||||
async addTab(): Promise<void> {
|
async addTab(): Promise<void> {
|
||||||
await this.#router.navigateByUrl(
|
const tabId = Date.now();
|
||||||
`/kunde/${Date.now()}/product/(filter//side:search)`,
|
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,
|
signal,
|
||||||
} from '@angular/core';
|
} 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({
|
@Directive({
|
||||||
selector: '[uiElementSizeObserver]',
|
selector: '[uiElementSizeObserver]',
|
||||||
exportAs: 'uiElementSizeObserver',
|
exportAs: 'uiElementSizeObserver',
|
||||||
@@ -32,6 +51,7 @@ export class UiElementSizeObserverDirective
|
|||||||
this.#resizeObserver?.disconnect();
|
this.#resizeObserver?.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Emits the current element size to both signal and output. */
|
||||||
#emitSize() {
|
#emitSize() {
|
||||||
const size = this.#element.getBoundingClientRect();
|
const size = this.#element.getBoundingClientRect();
|
||||||
this.#size.set(size);
|
this.#size.set(size);
|
||||||
@@ -40,8 +60,10 @@ export class UiElementSizeObserverDirective
|
|||||||
|
|
||||||
#size = signal<{ width: number; height: number }>({ height: 0, width: 0 });
|
#size = signal<{ width: number; height: number }>({ height: 0, width: 0 });
|
||||||
|
|
||||||
|
/** Readonly signal exposing the current element dimensions. */
|
||||||
readonly size: Signal<{ width: number; height: number }> =
|
readonly size: Signal<{ width: number; height: number }> =
|
||||||
this.#size.asReadonly();
|
this.#size.asReadonly();
|
||||||
|
|
||||||
|
/** Output event emitting size changes. */
|
||||||
readonly uiElementSizeObserver = output<{ width: number; height: number }>();
|
readonly uiElementSizeObserver = output<{ width: number; height: number }>();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user