mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Compare commits
7 Commits
86b0493591
...
5fe85282e7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fe85282e7 | ||
|
|
9a8eac3f9a | ||
|
|
93752efb9d | ||
|
|
0c546802fa | ||
|
|
3ed3d0b466 | ||
|
|
daf79d55a5 | ||
|
|
062a8044f2 |
@@ -1,11 +1,11 @@
|
||||
# Library Reference Guide
|
||||
|
||||
> **Last Updated:** 2025-11-28
|
||||
> **Last Updated:** 2025-12-03
|
||||
> **Angular Version:** 20.3.6
|
||||
> **Nx Version:** 21.3.2
|
||||
> **Total Libraries:** 75
|
||||
> **Total Libraries:** 81
|
||||
|
||||
All 75 libraries in the monorepo have comprehensive README.md documentation located at `libs/[domain]/[layer]/[feature]/README.md`.
|
||||
All 81 libraries in the monorepo have comprehensive README.md documentation located at `libs/[domain]/[layer]/[feature]/README.md`.
|
||||
|
||||
**IMPORTANT: Always use the `docs-researcher` subagent** to retrieve and analyze library documentation. This keeps the main context clean and prevents pollution.
|
||||
|
||||
@@ -66,7 +66,7 @@ A comprehensive loyalty rewards catalog feature for Angular applications support
|
||||
|
||||
---
|
||||
|
||||
## Common Libraries (3 libraries)
|
||||
## Common Libraries (4 libraries)
|
||||
|
||||
### `@isa/common/data-access`
|
||||
A foundational data access library providing core utilities, error handling, RxJS operators, response models, and advanced batching infrastructure for Angular applications.
|
||||
@@ -83,6 +83,11 @@ A comprehensive print management library for Angular applications providing prin
|
||||
|
||||
**Location:** `libs/common/print/`
|
||||
|
||||
### `@isa/common/title-management`
|
||||
Reusable title management patterns for Angular applications with reactive updates and tab integration.
|
||||
|
||||
**Location:** `libs/common/title-management/`
|
||||
|
||||
---
|
||||
|
||||
## Core Libraries (7 libraries)
|
||||
@@ -92,16 +97,16 @@ Type-safe role-based authorization utilities with Angular signals integration fo
|
||||
|
||||
**Location:** `libs/core/auth/`
|
||||
|
||||
### `@isa/core/connectivity`
|
||||
Network connectivity status service providing reactive online/offline observables for monitoring network state across the application.
|
||||
|
||||
**Location:** `libs/core/connectivity/`
|
||||
|
||||
### `@isa/core/config`
|
||||
A lightweight, type-safe configuration management system for Angular applications with runtime validation and nested object access.
|
||||
|
||||
**Location:** `libs/core/config/`
|
||||
|
||||
### `@isa/core/connectivity`
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
**Location:** `libs/core/connectivity/`
|
||||
|
||||
### `@isa/core/logging`
|
||||
A structured, high-performance logging library for Angular applications with hierarchical context support and flexible sink architecture.
|
||||
|
||||
@@ -430,6 +435,35 @@ A lightweight Zod utility library for safe parsing with automatic fallback to or
|
||||
|
||||
---
|
||||
|
||||
## Shell Domain (5 libraries)
|
||||
|
||||
### `@isa/shell/common`
|
||||
**Type:** Util Library
|
||||
|
||||
**Location:** `libs/shell/common/`
|
||||
|
||||
### `@isa/shell/header`
|
||||
**Type:** Feature Library
|
||||
|
||||
**Location:** `libs/shell/header/`
|
||||
|
||||
### `@isa/shell/layout`
|
||||
**Type:** Feature Library
|
||||
|
||||
**Location:** `libs/shell/layout/`
|
||||
|
||||
### `@isa/shell/navigation`
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
**Location:** `libs/shell/navigation/`
|
||||
|
||||
### `@isa/shell/notifications`
|
||||
**Type:** Feature Library
|
||||
|
||||
**Location:** `libs/shell/notifications/`
|
||||
|
||||
---
|
||||
|
||||
## How to Use This Guide
|
||||
|
||||
1. **Quick Lookup**: Use this guide to find the purpose of any library in the monorepo
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* Core Auth Library
|
||||
*
|
||||
* Provides role-based authorization utilities for the ISA Frontend application.
|
||||
*/
|
||||
|
||||
export { RoleService } from './lib/role.service';
|
||||
export { IfRoleDirective } from './lib/if-role.directive';
|
||||
export { TokenProvider, TOKEN_PROVIDER, parseJwt } from './lib/token-provider';
|
||||
export { Role } from './lib/role';
|
||||
/**
|
||||
* Core Auth Library
|
||||
*
|
||||
* Provides role-based authorization utilities for the ISA Frontend application.
|
||||
*/
|
||||
|
||||
export { AuthService } from './lib/auth.service';
|
||||
export { RoleService } from './lib/role.service';
|
||||
export { IfRoleDirective } from './lib/if-role.directive';
|
||||
export { TokenProvider, TOKEN_PROVIDER, parseJwt } from './lib/token-provider';
|
||||
export { Role } from './lib/role';
|
||||
|
||||
14
libs/core/auth/src/lib/auth.service.ts
Normal file
14
libs/core/auth/src/lib/auth.service.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { OAuthService } from 'angular-oauth2-oidc';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AuthService {
|
||||
#logger = logger({ service: 'AuthService' });
|
||||
#oAuthService = inject(OAuthService);
|
||||
|
||||
logout(): void {
|
||||
this.#logger.info('User logging out');
|
||||
this.#oAuthService.logOut();
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,8 @@ import { Role } from './role';
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class RoleService {
|
||||
private readonly _log = logger({ service: 'RoleService' });
|
||||
private readonly _tokenProvider = inject(TOKEN_PROVIDER);
|
||||
#logger = logger({ service: 'RoleService' });
|
||||
#tokenProvider = inject(TOKEN_PROVIDER);
|
||||
|
||||
/**
|
||||
* Check if the authenticated user has specific role(s)
|
||||
@@ -45,10 +45,10 @@ export class RoleService {
|
||||
const roles = coerceArray(role);
|
||||
|
||||
try {
|
||||
const userRoles = this._tokenProvider.getClaimByKey('role');
|
||||
const userRoles = this.#tokenProvider.getClaimByKey('role');
|
||||
|
||||
if (!userRoles) {
|
||||
this._log.debug('No roles found in token claims');
|
||||
this.#logger.debug('No roles found in token claims');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -57,14 +57,14 @@ export class RoleService {
|
||||
|
||||
const hasAllRoles = roles.every((r) => userRolesArray.includes(r));
|
||||
|
||||
this._log.debug(`Role check: ${roles.join(', ')} => ${hasAllRoles}`, () => ({
|
||||
this.#logger.debug(`Role check: ${roles.join(', ')} => ${hasAllRoles}`, () => ({
|
||||
requiredRoles: roles,
|
||||
userRoles: userRolesArray,
|
||||
}));
|
||||
|
||||
return hasAllRoles;
|
||||
} catch (error) {
|
||||
this._log.error('Error checking roles', error as Error, () => ({ requiredRoles: roles }));
|
||||
this.#logger.error('Error checking roles', error as Error, () => ({ requiredRoles: roles }));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
201
libs/shell/common/README.md
Normal file
201
libs/shell/common/README.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# shell-common
|
||||
|
||||
> **Type:** Util Library
|
||||
> **Domain:** Shell
|
||||
> **Path:** `libs/shell/common`
|
||||
|
||||
## Overview
|
||||
|
||||
Shared services and types for shell-domain components. Provides state management for navigation, font size, and notifications.
|
||||
|
||||
## Services
|
||||
|
||||
### NavigationService
|
||||
|
||||
Controls the navigation drawer open/closed state.
|
||||
|
||||
```typescript
|
||||
import { NavigationService } from '@isa/shell/common';
|
||||
|
||||
@Component({...})
|
||||
export class MyComponent {
|
||||
navigationService = inject(NavigationService);
|
||||
|
||||
// Read state (readonly signal)
|
||||
isOpen = this.navigationService.get;
|
||||
|
||||
// Toggle navigation
|
||||
toggleNav() {
|
||||
this.navigationService.toggle();
|
||||
}
|
||||
|
||||
// Set specific state
|
||||
closeNav() {
|
||||
this.navigationService.set(false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**API:**
|
||||
- `get` - Readonly signal of navigation state (`boolean`)
|
||||
- `toggle()` - Toggles navigation open/closed
|
||||
- `set(state: boolean)` - Sets navigation state
|
||||
|
||||
### FontSizeService
|
||||
|
||||
Manages application-wide font size for accessibility.
|
||||
|
||||
```typescript
|
||||
import { FontSizeService, FontSize } from '@isa/shell/common';
|
||||
|
||||
@Component({...})
|
||||
export class MyComponent {
|
||||
fontSizeService = inject(FontSizeService);
|
||||
|
||||
// Read current size
|
||||
currentSize = this.fontSizeService.get;
|
||||
|
||||
// Get size in pixels
|
||||
currentPx = this.fontSizeService.getPx;
|
||||
|
||||
// Change font size
|
||||
setLarge() {
|
||||
this.fontSizeService.set('large');
|
||||
}
|
||||
|
||||
// Convert rem to px
|
||||
getPixels(rem: number) {
|
||||
return this.fontSizeService.remToPx(rem);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**API:**
|
||||
- `get` - Readonly signal of current font size
|
||||
- `getPx` - Computed signal of font size in pixels
|
||||
- `set(size: FontSize)` - Sets font size
|
||||
- `remToPx(rem: number)` - Converts rem to pixels
|
||||
- `fontSizeEffect` - Effect that syncs font size to document
|
||||
|
||||
**Types:**
|
||||
```typescript
|
||||
type FontSize = 'small' | 'medium' | 'large';
|
||||
// Maps to: 14px | 16px | 18px
|
||||
```
|
||||
|
||||
### NotificationsService
|
||||
|
||||
Manages application notifications with read status tracking.
|
||||
|
||||
```typescript
|
||||
import { NotificationsService, Notification } from '@isa/shell/common';
|
||||
|
||||
@Component({...})
|
||||
export class MyComponent {
|
||||
notificationsService = inject(NotificationsService);
|
||||
|
||||
// Read all notifications
|
||||
notifications = this.notificationsService.get;
|
||||
|
||||
// Get unread count
|
||||
unreadCount = this.notificationsService.unreadCount;
|
||||
|
||||
// Add notification
|
||||
notify() {
|
||||
this.notificationsService.add({
|
||||
id: 'unique-id',
|
||||
group: 'Orders',
|
||||
title: 'New Order',
|
||||
message: 'Order #123 received',
|
||||
timestamp: Date.now(),
|
||||
action: {
|
||||
type: 'navigate',
|
||||
label: 'View',
|
||||
target: 'internal',
|
||||
route: '/orders/123'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Mark as read
|
||||
markRead(id: string) {
|
||||
this.notificationsService.markAsRead(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**API:**
|
||||
- `get` - Readonly signal of all notifications
|
||||
- `unreadCount` - Computed signal of unread count
|
||||
- `add(notification: Notification)` - Adds notification
|
||||
- `remove(id: NotificationId)` - Removes notification
|
||||
- `clear()` - Removes all notifications
|
||||
- `markAsRead(id: NotificationId)` - Marks single notification as read
|
||||
- `markAllAsRead()` - Marks all notifications as read
|
||||
|
||||
## Types
|
||||
|
||||
### Notification
|
||||
|
||||
```typescript
|
||||
type Notification = {
|
||||
id: string | number;
|
||||
group: string;
|
||||
title: string;
|
||||
message: string;
|
||||
action: NotificationAction;
|
||||
markedAsRead?: number; // timestamp
|
||||
timestamp: number;
|
||||
};
|
||||
```
|
||||
|
||||
### NotificationAction
|
||||
|
||||
```typescript
|
||||
type NotificationAction =
|
||||
| NotificationActionNavigate
|
||||
| NotificationActionCallback;
|
||||
|
||||
type NotificationActionNavigate = {
|
||||
type: 'navigate';
|
||||
label: string;
|
||||
target: 'internal' | 'external';
|
||||
route: string;
|
||||
};
|
||||
|
||||
type NotificationActionCallback = {
|
||||
type: 'callback';
|
||||
label: string;
|
||||
callback: () => void;
|
||||
};
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```typescript
|
||||
import {
|
||||
NavigationService,
|
||||
FontSizeService,
|
||||
FontSize,
|
||||
NotificationsService,
|
||||
Notification
|
||||
} from '@isa/shell/common';
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
**Internal:**
|
||||
- `@isa/core/logging` - Logger factory
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npx nx test shell-common
|
||||
```
|
||||
|
||||
## Related Libraries
|
||||
|
||||
- [`@isa/shell/header`](../header) - Uses all services
|
||||
- [`@isa/shell/layout`](../layout) - Uses NavigationService
|
||||
- [`@isa/shell/notifications`](../notifications) - Uses NotificationsService
|
||||
- [`@isa/shell/navigation`](../navigation) - Uses NavigationService
|
||||
34
libs/shell/common/eslint.config.cjs
Normal file
34
libs/shell/common/eslint.config.cjs
Normal file
@@ -0,0 +1,34 @@
|
||||
const nx = require('@nx/eslint-plugin');
|
||||
const baseConfig = require('../../../eslint.config.js');
|
||||
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'shell',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'shell',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
20
libs/shell/common/project.json
Normal file
20
libs/shell/common/project.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "shell-common",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/shell/common/src",
|
||||
"prefix": "shell",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/vite:test",
|
||||
"outputs": ["{options.reportsDirectory}"],
|
||||
"options": {
|
||||
"reportsDirectory": "../../../coverage/libs/shell/common"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
libs/shell/common/src/index.ts
Normal file
3
libs/shell/common/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './lib/navigation.service';
|
||||
export * from './lib/font-size.service';
|
||||
export * from './lib/notifications.service';
|
||||
48
libs/shell/common/src/lib/font-size.service.ts
Normal file
48
libs/shell/common/src/lib/font-size.service.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
computed,
|
||||
DOCUMENT,
|
||||
effect,
|
||||
inject,
|
||||
Injectable,
|
||||
signal,
|
||||
RendererFactory2,
|
||||
} from '@angular/core';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
export type FontSize = 'small' | 'medium' | 'large';
|
||||
|
||||
const FONT_SIZE_PX_MAP: Record<FontSize, number> = {
|
||||
small: 14,
|
||||
medium: 16,
|
||||
large: 18,
|
||||
};
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FontSizeService {
|
||||
#logger = logger({ service: 'FontSizeService' });
|
||||
#state = signal<FontSize>('medium');
|
||||
|
||||
#document = inject(DOCUMENT);
|
||||
|
||||
#renderer = inject(RendererFactory2).createRenderer(this.#document, null);
|
||||
|
||||
readonly get = this.#state.asReadonly();
|
||||
|
||||
readonly getPx = computed(() => FONT_SIZE_PX_MAP[this.#state()]);
|
||||
|
||||
set(size: FontSize): void {
|
||||
this.#logger.debug('Font size changed', () => ({ size }));
|
||||
this.#state.set(size);
|
||||
}
|
||||
|
||||
readonly remToPx = (rem: number) => rem * this.getPx();
|
||||
|
||||
readonly fontSizeEffect = effect(() => {
|
||||
const fontSize = this.#state();
|
||||
this.#renderer.setStyle(
|
||||
this.#document.documentElement,
|
||||
'font-size',
|
||||
`${FONT_SIZE_PX_MAP[fontSize]}px`,
|
||||
);
|
||||
});
|
||||
}
|
||||
20
libs/shell/common/src/lib/navigation.service.ts
Normal file
20
libs/shell/common/src/lib/navigation.service.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class NavigationService {
|
||||
#logger = logger({ service: 'NavigationService' });
|
||||
#state = signal<boolean>(false);
|
||||
|
||||
readonly get = this.#state.asReadonly();
|
||||
|
||||
toggle(): void {
|
||||
this.#state.update((state) => !state);
|
||||
this.#logger.debug('Navigation toggled', () => ({ isOpen: this.#state() }));
|
||||
}
|
||||
|
||||
set(state: boolean): void {
|
||||
this.#logger.debug('Navigation state set', () => ({ state }));
|
||||
this.#state.set(state);
|
||||
}
|
||||
}
|
||||
89
libs/shell/common/src/lib/notifications.service.ts
Normal file
89
libs/shell/common/src/lib/notifications.service.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { computed, Injectable, signal } from '@angular/core';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
type Timestamp = number;
|
||||
|
||||
type NotificationId = string | number;
|
||||
|
||||
export type Notification = {
|
||||
id: NotificationId;
|
||||
group: string;
|
||||
title: string;
|
||||
message: string;
|
||||
action: NotificationAction;
|
||||
markedAsRead?: Timestamp;
|
||||
timestamp: Timestamp;
|
||||
};
|
||||
|
||||
export type NotificationActionBase = {
|
||||
label: string;
|
||||
type: 'navigate' | 'callback';
|
||||
};
|
||||
|
||||
export type NotificationActionNavigate = NotificationActionBase & {
|
||||
type: 'navigate';
|
||||
target: 'internal' | 'external';
|
||||
route: string;
|
||||
};
|
||||
|
||||
export type NotificationActionCallback = NotificationActionBase & {
|
||||
type: 'callback';
|
||||
callback: () => void;
|
||||
};
|
||||
|
||||
export type NotificationAction =
|
||||
| NotificationActionNavigate
|
||||
| NotificationActionCallback;
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class NotificationsService {
|
||||
#logger = logger({ service: 'NotificationsService' });
|
||||
#state = signal<Notification[]>([]);
|
||||
|
||||
readonly get = this.#state.asReadonly();
|
||||
|
||||
add(notification: Notification): void {
|
||||
this.#logger.debug('Notification added', () => ({
|
||||
id: notification.id,
|
||||
group: notification.group,
|
||||
}));
|
||||
this.#state.update((notifications) => [...notifications, notification]);
|
||||
}
|
||||
|
||||
remove(id: NotificationId): void {
|
||||
this.#logger.debug('Notification removed', () => ({ id }));
|
||||
this.#state.update((notifications) =>
|
||||
notifications.filter((notification) => notification.id !== id),
|
||||
);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.#logger.debug('All notifications cleared');
|
||||
this.#state.set([]);
|
||||
}
|
||||
|
||||
markAsRead(id: NotificationId): void {
|
||||
this.#logger.debug('Notification marked as read', () => ({ id }));
|
||||
this.#state.update((notifications) =>
|
||||
notifications.map((notification) =>
|
||||
notification.id === id
|
||||
? { ...notification, markedAsRead: Date.now() }
|
||||
: notification,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
markAllAsRead(): void {
|
||||
this.#logger.debug('All notifications marked as read');
|
||||
this.#state.update((notifications) =>
|
||||
notifications.map((notification) => ({
|
||||
...notification,
|
||||
markedAsRead: Date.now(),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
readonly unreadCount = computed(() =>
|
||||
this.#state().filter((notification) => !notification.markedAsRead).length,
|
||||
);
|
||||
}
|
||||
13
libs/shell/common/src/test-setup.ts
Normal file
13
libs/shell/common/src/test-setup.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import '@angular/compiler';
|
||||
import '@analogjs/vitest-angular/setup-zone';
|
||||
|
||||
import {
|
||||
BrowserTestingModule,
|
||||
platformBrowserTesting,
|
||||
} from '@angular/platform-browser/testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserTestingModule,
|
||||
platformBrowserTesting(),
|
||||
);
|
||||
30
libs/shell/common/tsconfig.json
Normal file
30
libs/shell/common/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
27
libs/shell/common/tsconfig.lib.json
Normal file
27
libs/shell/common/tsconfig.lib.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/test-setup.ts",
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx"
|
||||
],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
29
libs/shell/common/tsconfig.spec.json
Normal file
29
libs/shell/common/tsconfig.spec.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"files": ["src/test-setup.ts"]
|
||||
}
|
||||
27
libs/shell/common/vite.config.mts
Normal file
27
libs/shell/common/vite.config.mts
Normal file
@@ -0,0 +1,27 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
import angular from '@analogjs/vite-plugin-angular';
|
||||
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../../node_modules/.vite/libs/shell/common',
|
||||
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||
// Uncomment this if you are using workers.
|
||||
// worker: {
|
||||
// plugins: [ nxViteTsPaths() ],
|
||||
// },
|
||||
test: {
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
setupFiles: ['src/test-setup.ts'],
|
||||
reporters: ['default'],
|
||||
coverage: {
|
||||
reportsDirectory: '../../../coverage/libs/shell/common',
|
||||
provider: 'v8' as const,
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -1,7 +1,92 @@
|
||||
# shell-header
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
> **Type:** Feature Library
|
||||
> **Domain:** Shell
|
||||
> **Path:** `libs/shell/header`
|
||||
|
||||
## Running unit tests
|
||||
## Overview
|
||||
|
||||
Run `nx test shell-header` to execute the unit tests.
|
||||
Application header component providing navigation, accessibility controls, and user actions. Displays on tablet/desktop viewports as the primary toolbar.
|
||||
|
||||
## Features
|
||||
|
||||
- Navigation toggle button (tablet/desktop responsive)
|
||||
- Font size selector for accessibility
|
||||
- Logout button with authentication integration
|
||||
- Notifications toggle with overlay panel
|
||||
|
||||
## Installation
|
||||
|
||||
```typescript
|
||||
import { ShellHeaderComponent } from '@isa/shell/header';
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'app-layout',
|
||||
standalone: true,
|
||||
imports: [ShellHeaderComponent],
|
||||
template: `<shell-header />`
|
||||
})
|
||||
export class AppLayoutComponent {}
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### ShellHeaderComponent
|
||||
|
||||
**Selector:** `shell-header`
|
||||
|
||||
Main header container that orchestrates sub-components based on viewport breakpoint.
|
||||
|
||||
### Internal Components
|
||||
|
||||
| Component | Selector | Purpose |
|
||||
|-----------|----------|---------|
|
||||
| `ShellNavigationToggleComponent` | `shell-navigation-toggle` | Toggles navigation drawer state |
|
||||
| `ShellFontSizeSelectorComponent` | `shell-font-size-selector` | Accessibility font size control |
|
||||
| `ShellLogoutButtonComponent` | `shell-logout-button` | Triggers user logout |
|
||||
| `ShellNotificationsToggleComponent` | `shell-notifications-toggle` | Opens notifications panel overlay |
|
||||
|
||||
## E2E Testing
|
||||
|
||||
| Element | data-what | data-which | Purpose |
|
||||
|---------|-----------|------------|---------|
|
||||
| Header | `header` | `shell-header` | Main header container |
|
||||
| Navigation toggle | `button` | `navigation-toggle` | Menu button |
|
||||
| Font size selector | `fieldset` | `font-size-selector` | Font size controls |
|
||||
| Logout button | `button` | `logout-button` | Logout action |
|
||||
| Notifications toggle | `button` | `notifications-toggle` | Notifications trigger |
|
||||
| Notifications panel | `panel` | `notifications-panel` | Notifications overlay |
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Semantic `<header>` with `role="banner"`
|
||||
- Dynamic `aria-label` and `aria-expanded` on toggle buttons
|
||||
- Font size selector uses `role="radiogroup"`
|
||||
- Notifications panel uses `role="dialog"` with `aria-haspopup`
|
||||
|
||||
## Dependencies
|
||||
|
||||
**Internal:**
|
||||
- `@isa/shell/common` - Navigation, font size, notifications services
|
||||
- `@isa/shell/notifications` - Notifications panel component
|
||||
- `@isa/ui/buttons` - IconButtonComponent, InfoButtonComponent
|
||||
- `@isa/ui/layout` - Breakpoint utilities
|
||||
- `@isa/core/auth` - AuthService
|
||||
- `@isa/core/logging` - Logger factory
|
||||
- `@isa/icons` - Navigation icons
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npx nx test shell-header
|
||||
```
|
||||
|
||||
## Related Libraries
|
||||
|
||||
- [`@isa/shell/layout`](../layout) - Parent layout component
|
||||
- [`@isa/shell/common`](../common) - Shared shell services
|
||||
- [`@isa/shell/notifications`](../notifications) - Notifications display
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './lib/shell-header/shell-header.component';
|
||||
export * from './lib/shell-header.component';
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.selector {
|
||||
@apply relative flex rounded-full bg-isa-neutral-300;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
@apply absolute top-0 size-12 rounded-full bg-isa-neutral-700 transition-transform duration-200 ease-out;
|
||||
}
|
||||
|
||||
.option {
|
||||
@apply relative flex h-12 w-12 cursor-pointer items-center justify-center transition-colors duration-200;
|
||||
}
|
||||
|
||||
.option.selected {
|
||||
@apply text-isa-neutral-300;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<fieldset
|
||||
class="selector"
|
||||
data-what="fieldset"
|
||||
data-which="font-size-selector"
|
||||
role="radiogroup"
|
||||
aria-label="Schriftgröße auswählen"
|
||||
>
|
||||
<div class="indicator" [style.transform]="'translateX(' + indicatorOffset() + 'rem)'"></div>
|
||||
|
||||
@for (option of fontSizeOptions; track option.value) {
|
||||
<input
|
||||
type="radio"
|
||||
class="sr-only"
|
||||
[id]="'font-size-' + option.value"
|
||||
[value]="option.value"
|
||||
name="font-size"
|
||||
[ngModel]="fontSizeService.get()"
|
||||
(ngModelChange)="onFontSizeChange($event)"
|
||||
[attr.data-what]="'radio'"
|
||||
[attr.data-which]="'font-size-' + option.value"
|
||||
/>
|
||||
<label
|
||||
class="option"
|
||||
[for]="'font-size-' + option.value"
|
||||
[class.selected]="fontSizeService.get() === option.value"
|
||||
[attr.aria-label]="option.label"
|
||||
[attr.data-what]="'label'"
|
||||
[attr.data-which]="'font-size-label-' + option.value"
|
||||
>
|
||||
<ng-icon name="isaNavigationFontsize" [size]="option.iconSize" />
|
||||
</label>
|
||||
}
|
||||
</fieldset>
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { isaNavigationFontsize } from '@isa/icons';
|
||||
import { FontSize, FontSizeService } from '@isa/shell/common';
|
||||
|
||||
interface FontSizeOption {
|
||||
value: FontSize;
|
||||
label: string;
|
||||
iconSize: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'shell-font-size-selector',
|
||||
templateUrl: './font-size-selector.component.html',
|
||||
styleUrl: './font-size-selector.component.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [FormsModule, NgIcon],
|
||||
providers: [
|
||||
provideIcons({
|
||||
isaNavigationFontsize,
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class ShellFontSizeSelectorComponent {
|
||||
#logger = logger({ component: 'ShellFontSizeSelectorComponent' });
|
||||
|
||||
readonly fontSizeService = inject(FontSizeService);
|
||||
|
||||
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' },
|
||||
];
|
||||
|
||||
readonly #offsetMap: Record<FontSize, number> = {
|
||||
small: 0,
|
||||
medium: 3,
|
||||
large: 6,
|
||||
};
|
||||
|
||||
readonly indicatorOffset = computed(
|
||||
() => this.#offsetMap[this.fontSizeService.get()],
|
||||
);
|
||||
|
||||
onFontSizeChange(size: FontSize): void {
|
||||
this.#logger.debug('Font size changed', () => ({ size }));
|
||||
this.fontSizeService.set(size);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { InfoButtonComponent } from '@isa/ui/buttons';
|
||||
import { AuthService } from '@isa/core/auth';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { isaNavigationLogout } from '@isa/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-logout-button',
|
||||
template: `<ui-info-button
|
||||
(click)="logout()"
|
||||
data-what="button"
|
||||
data-which="logout-button"
|
||||
aria-label="Abmelden"
|
||||
>
|
||||
<ng-icon uiInfoButtonIcon name="isaNavigationLogout" />
|
||||
<span class="isa-text-body-2-bold" uiInfoButtonLabel>NEU</span>
|
||||
</ui-info-button>`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [InfoButtonComponent, NgIcon],
|
||||
providers: [provideIcons({ isaNavigationLogout })],
|
||||
})
|
||||
export class ShellLogoutButtonComponent {
|
||||
#logger = logger({ component: 'ShellLogoutButtonComponent' });
|
||||
#authService = inject(AuthService);
|
||||
|
||||
logout(): void {
|
||||
this.#logger.info('User logging out');
|
||||
this.#authService.logout();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { isaActionClose, isaNavigationSidemenu } from '@isa/icons';
|
||||
import { NavigationService } from '@isa/shell/common';
|
||||
import {
|
||||
IconButtonComponent,
|
||||
IconButtonColor,
|
||||
IconButtonSize,
|
||||
} from '@isa/ui/buttons';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-navigation-toggle',
|
||||
template: `<ui-icon-button
|
||||
[name]="iconName()"
|
||||
[color]="IconButtonColor.Primary"
|
||||
[size]="IconButtonSize.Large"
|
||||
(click)="toggle()"
|
||||
data-what="button"
|
||||
data-which="navigation-toggle"
|
||||
[attr.aria-label]="ariaLabel()"
|
||||
[attr.aria-expanded]="navigationService.get()"
|
||||
/>`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [IconButtonComponent],
|
||||
providers: [provideIcons({ isaNavigationSidemenu, isaActionClose })],
|
||||
})
|
||||
export class ShellNavigationToggleComponent {
|
||||
#logger = logger({ component: 'ShellNavigationToggleComponent' });
|
||||
|
||||
readonly navigationService = inject(NavigationService);
|
||||
|
||||
readonly IconButtonColor = IconButtonColor;
|
||||
readonly IconButtonSize = IconButtonSize;
|
||||
|
||||
iconName = computed(() => {
|
||||
const open = this.navigationService.get();
|
||||
return open ? 'isaActionClose' : 'isaNavigationSidemenu';
|
||||
});
|
||||
|
||||
ariaLabel = computed(() =>
|
||||
this.navigationService.get() ? 'Menü schließen' : 'Menü öffnen',
|
||||
);
|
||||
|
||||
toggle(): void {
|
||||
this.navigationService.toggle();
|
||||
this.#logger.debug('Navigation toggled', () => ({
|
||||
isOpen: this.navigationService.get(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<ui-icon-button
|
||||
cdkOverlayOrigin
|
||||
#trigger="cdkOverlayOrigin"
|
||||
[name]="icon()"
|
||||
[color]="IconButtonColor.Tertiary"
|
||||
[size]="IconButtonSize.Large"
|
||||
[disabled]="!hasNotifications()"
|
||||
(click)="toggle()"
|
||||
data-what="button"
|
||||
data-which="notifications-toggle"
|
||||
[attr.aria-label]="ariaLabel()"
|
||||
[attr.aria-expanded]="isOpen()"
|
||||
aria-haspopup="true"
|
||||
/>
|
||||
|
||||
<ng-template
|
||||
cdkConnectedOverlay
|
||||
[cdkConnectedOverlayOrigin]="trigger"
|
||||
[cdkConnectedOverlayOpen]="isOpen()"
|
||||
[cdkConnectedOverlayPositions]="positions"
|
||||
[cdkConnectedOverlayOffsetY]="12"
|
||||
[cdkConnectedOverlayHasBackdrop]="true"
|
||||
cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop"
|
||||
(backdropClick)="close()"
|
||||
(detach)="close()"
|
||||
>
|
||||
<div
|
||||
class="p-4 bg-isa-white rounded-2xl max-h-96 overflow-y-auto shadow-lg"
|
||||
data-what="panel"
|
||||
data-which="notifications-panel"
|
||||
role="dialog"
|
||||
aria-label="Benachrichtigungen"
|
||||
>
|
||||
<shell-notifications />
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedPosition } from '@angular/cdk/overlay';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { isaNavigationMessage, isaNavigationMessageUnread } from '@isa/icons';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import {
|
||||
IconButtonComponent,
|
||||
IconButtonColor,
|
||||
IconButtonSize,
|
||||
} from '@isa/ui/buttons';
|
||||
import { NotificationsService } from '@isa/shell/common';
|
||||
import { ShellNotificationsComponent } from '@isa/shell/notifications';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-notifications-toggle',
|
||||
templateUrl: './notifications-toggle.component.html',
|
||||
styleUrl: './notifications-toggle.component.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [IconButtonComponent, ShellNotificationsComponent, CdkOverlayOrigin, CdkConnectedOverlay],
|
||||
providers: [
|
||||
provideIcons({ isaNavigationMessage, isaNavigationMessageUnread }),
|
||||
],
|
||||
})
|
||||
export class ShellNotificationsToggleComponent {
|
||||
#logger = logger({ component: 'ShellNotificationsToggleComponent' });
|
||||
|
||||
readonly IconButtonColor = IconButtonColor;
|
||||
readonly IconButtonSize = IconButtonSize;
|
||||
|
||||
readonly notificationsService = inject(NotificationsService);
|
||||
|
||||
isOpen = signal(false);
|
||||
|
||||
readonly positions: ConnectedPosition[] = [
|
||||
// Bottom right
|
||||
{ originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top' },
|
||||
// Bottom left
|
||||
{ originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top' },
|
||||
// Left top
|
||||
{ originX: 'start', originY: 'top', overlayX: 'end', overlayY: 'top' },
|
||||
// Left bottom
|
||||
{ originX: 'start', originY: 'bottom', overlayX: 'end', overlayY: 'bottom' },
|
||||
];
|
||||
|
||||
toggle(): void {
|
||||
this.isOpen.update((open) => !open);
|
||||
this.#logger.debug('Notifications panel toggled', () => ({
|
||||
isOpen: this.isOpen(),
|
||||
}));
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.isOpen.set(false);
|
||||
this.#logger.debug('Notifications panel closed');
|
||||
}
|
||||
|
||||
hasNotifications = computed(() => this.notificationsService.get().length > 0);
|
||||
|
||||
unreadNotifications = computed(
|
||||
() => this.notificationsService.unreadCount() > 0,
|
||||
);
|
||||
|
||||
icon = computed(() =>
|
||||
this.unreadNotifications()
|
||||
? 'isaNavigationMessageUnread'
|
||||
: 'isaNavigationMessage',
|
||||
);
|
||||
|
||||
ariaLabel = computed(() => {
|
||||
if (!this.hasNotifications()) return 'Keine Benachrichtigungen';
|
||||
return this.isOpen() ? 'Benachrichtigungen schließen' : 'Benachrichtigungen öffnen';
|
||||
});
|
||||
}
|
||||
14
libs/shell/header/src/lib/shell-header.component.html
Normal file
14
libs/shell/header/src/lib/shell-header.component.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<header
|
||||
class="flex items-center gap-2 px-4 desktop:px-6 py-2 bg-isa-white shadow-[0_2px_6px_0_rgba(0,0,0,0.1)]"
|
||||
data-what="header"
|
||||
data-which="shell-header"
|
||||
role="banner"
|
||||
>
|
||||
@if (isTablet()) {
|
||||
<shell-navigation-toggle />
|
||||
}
|
||||
<div class="grow"></div>
|
||||
<shell-font-size-selector />
|
||||
<shell-logout-button />
|
||||
<shell-notifications-toggle />
|
||||
</header>
|
||||
22
libs/shell/header/src/lib/shell-header.component.ts
Normal file
22
libs/shell/header/src/lib/shell-header.component.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { ShellNavigationToggleComponent } from './components/navigation-toggle.component';
|
||||
import { Breakpoint, breakpoint } from '@isa/ui/layout';
|
||||
import { ShellFontSizeSelectorComponent } from './components/font-size-selector/font-size-selector.component';
|
||||
import { ShellLogoutButtonComponent } from './components/logout-button.component';
|
||||
import { ShellNotificationsToggleComponent } from './components/notifications-toggle/notifications-toggle.component';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-header',
|
||||
templateUrl: './shell-header.component.html',
|
||||
styleUrls: ['./shell-header.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
ShellNavigationToggleComponent,
|
||||
ShellFontSizeSelectorComponent,
|
||||
ShellLogoutButtonComponent,
|
||||
ShellNotificationsToggleComponent,
|
||||
],
|
||||
})
|
||||
export class ShellHeaderComponent {
|
||||
readonly isTablet = breakpoint(Breakpoint.Tablet);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<p>ShellHeader works!</p>
|
||||
@@ -1,21 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ShellHeaderComponent } from './shell-header.component';
|
||||
|
||||
describe('ShellHeaderComponent', () => {
|
||||
let component: ShellHeaderComponent;
|
||||
let fixture: ComponentFixture<ShellHeaderComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ShellHeaderComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ShellHeaderComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-shell-header',
|
||||
imports: [CommonModule],
|
||||
templateUrl: './shell-header.component.html',
|
||||
styleUrl: './shell-header.component.css',
|
||||
})
|
||||
export class ShellHeaderComponent {}
|
||||
@@ -1,7 +1,91 @@
|
||||
# shell-layout
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
> **Type:** Feature Library
|
||||
> **Domain:** Shell
|
||||
> **Path:** `libs/shell/layout`
|
||||
|
||||
## Running unit tests
|
||||
## Overview
|
||||
|
||||
Run `nx test shell-layout` to execute the unit tests.
|
||||
Root layout component for the application shell. Provides the main structural wrapper including network status banner and header.
|
||||
|
||||
## Features
|
||||
|
||||
- Network status banner with offline/online state transitions
|
||||
- Header integration via `@isa/shell/header`
|
||||
- Content projection for main page content
|
||||
|
||||
## Installation
|
||||
|
||||
```typescript
|
||||
import { ShellLayoutComponent } from '@isa/shell/layout';
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [ShellLayoutComponent],
|
||||
template: `
|
||||
<shell-layout>
|
||||
<router-outlet />
|
||||
</shell-layout>
|
||||
`
|
||||
})
|
||||
export class AppComponent {}
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### ShellLayoutComponent
|
||||
|
||||
**Selector:** `shell-layout`
|
||||
|
||||
Root layout wrapper that structures the application shell.
|
||||
|
||||
**Template Structure:**
|
||||
```html
|
||||
<shell-network-status-banner />
|
||||
<shell-header />
|
||||
<main>
|
||||
<ng-content></ng-content>
|
||||
</main>
|
||||
```
|
||||
|
||||
### NetworkStatusBannerComponent
|
||||
|
||||
**Selector:** `shell-network-status-banner`
|
||||
|
||||
Displays connectivity status banners for network state changes.
|
||||
|
||||
**Behavior:**
|
||||
- **Offline:** Persistent red banner when network connection is lost
|
||||
- **Online:** Green success banner on reconnection, auto-dismisses after 2.5 seconds
|
||||
- Only shows after actual state change (not on initial load when online)
|
||||
|
||||
**Exported Constants:**
|
||||
- `ONLINE_BANNER_DISPLAY_DURATION_MS` - Banner display duration (2500ms)
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Uses `text-isa-neutral-900` host class for proper contrast
|
||||
- Network status banner announces connectivity changes
|
||||
|
||||
## Dependencies
|
||||
|
||||
**Internal:**
|
||||
- `@isa/shell/header` - Header component
|
||||
- `@isa/core/connectivity` - Network status service
|
||||
- `@isa/core/logging` - Logger factory
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npx nx test shell-layout
|
||||
```
|
||||
|
||||
## Related Libraries
|
||||
|
||||
- [`@isa/shell/header`](../header) - Header component
|
||||
- [`@isa/core/connectivity`](../../core/connectivity) - Network status service
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<shell-network-status-banner />
|
||||
<shell-header />
|
||||
<main>
|
||||
<ng-content></ng-content>
|
||||
</main>
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { NetworkStatusBannerComponent } from './components/network-status-banner.component';
|
||||
|
||||
import { ShellHeaderComponent } from '@isa/shell/header';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-layout',
|
||||
standalone: true,
|
||||
templateUrl: './shell-layout.component.html',
|
||||
styleUrls: ['./shell-layout.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NetworkStatusBannerComponent],
|
||||
imports: [NetworkStatusBannerComponent, ShellHeaderComponent],
|
||||
host: {
|
||||
class: 'text-isa-neutral-900',
|
||||
},
|
||||
})
|
||||
export class ShellLayoutComponent {}
|
||||
|
||||
98
libs/shell/notifications/README.md
Normal file
98
libs/shell/notifications/README.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# shell-notifications
|
||||
|
||||
> **Type:** Feature Library
|
||||
> **Domain:** Shell
|
||||
> **Path:** `libs/shell/notifications`
|
||||
|
||||
## Overview
|
||||
|
||||
Notification display component that renders grouped notifications with actions. Used within the header's notification toggle overlay panel.
|
||||
|
||||
## Features
|
||||
|
||||
- Grouped notification display with collapsible sections
|
||||
- Unread/read status indication
|
||||
- Relative timestamps (e.g., "5 minutes ago")
|
||||
- Action buttons with navigation or callback support
|
||||
|
||||
## Installation
|
||||
|
||||
```typescript
|
||||
import { ShellNotificationsComponent } from '@isa/shell/notifications';
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'app-notification-panel',
|
||||
standalone: true,
|
||||
imports: [ShellNotificationsComponent],
|
||||
template: `<shell-notifications />`
|
||||
})
|
||||
export class NotificationPanelComponent {}
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### ShellNotificationsComponent
|
||||
|
||||
**Selector:** `shell-notifications`
|
||||
|
||||
Main container that displays notifications grouped by category.
|
||||
|
||||
**Behavior:**
|
||||
- Groups notifications by `group` property
|
||||
- Sorts within groups: unread first, then by timestamp (newest first)
|
||||
- Empty state when no notifications
|
||||
|
||||
### ShellNotificationComponent
|
||||
|
||||
**Selector:** `shell-notification`
|
||||
|
||||
Individual notification card with action support.
|
||||
|
||||
**Inputs:**
|
||||
- `notification` (required): `Notification` object to display
|
||||
|
||||
**Outputs:**
|
||||
- `actionTriggered`: Emits when notification action is triggered
|
||||
|
||||
**Action Types:**
|
||||
- `navigate` - Navigates to internal or external route
|
||||
- `callback` - Executes custom callback function
|
||||
|
||||
## E2E Testing
|
||||
|
||||
| Element | data-what | data-which | Purpose |
|
||||
|---------|-----------|------------|---------|
|
||||
| Container | `container` | `notifications-list` | Main list wrapper |
|
||||
| Group header | `notification-group-header` | `{groupName}` | Group title |
|
||||
| Separator | `separator` | `separator-{groupName}` | Visual divider |
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Uses `role="feed"` for notification list
|
||||
- Group separators marked with `aria-hidden="true"`
|
||||
- `aria-label="Benachrichtigungen"` on main container
|
||||
|
||||
## Dependencies
|
||||
|
||||
**Internal:**
|
||||
- `@isa/shell/common` - NotificationsService, Notification type
|
||||
- `@isa/core/logging` - Logger factory
|
||||
- `@isa/icons` - Action icons
|
||||
|
||||
**External:**
|
||||
- `date-fns` - Relative time formatting
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npx nx test shell-notifications
|
||||
```
|
||||
|
||||
## Related Libraries
|
||||
|
||||
- [`@isa/shell/common`](../common) - NotificationsService
|
||||
- [`@isa/shell/header`](../header) - Header notification toggle
|
||||
34
libs/shell/notifications/eslint.config.cjs
Normal file
34
libs/shell/notifications/eslint.config.cjs
Normal file
@@ -0,0 +1,34 @@
|
||||
const nx = require('@nx/eslint-plugin');
|
||||
const baseConfig = require('../../../eslint.config.js');
|
||||
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'shell',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'shell',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
20
libs/shell/notifications/project.json
Normal file
20
libs/shell/notifications/project.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "shell-notifications",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/shell/notifications/src",
|
||||
"prefix": "shell",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/vite:test",
|
||||
"outputs": ["{options.reportsDirectory}"],
|
||||
"options": {
|
||||
"reportsDirectory": "../../../coverage/libs/shell/notifications"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
libs/shell/notifications/src/index.ts
Normal file
1
libs/shell/notifications/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './lib/shell-notifications.component';
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<article
|
||||
class="flex flex-col gap-4 rounded-lg p-4"
|
||||
[class.bg-isa-secondary-100]="!notification().markedAsRead"
|
||||
data-what="notification-item"
|
||||
[attr.data-which]="notification().id"
|
||||
[attr.data-unread]="!notification().markedAsRead"
|
||||
role="article"
|
||||
[attr.aria-label]="notification().title + ': ' + notification().message"
|
||||
>
|
||||
<!-- Content -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<h3 class="isa-text-body-2-bold text-isa-black">
|
||||
{{ notification().title }}
|
||||
</h3>
|
||||
<time
|
||||
class="isa-text-caption-regular flex-shrink-0 text-isa-neutral-600"
|
||||
[attr.datetime]="notification().timestamp | date: 'yyyy-MM-ddTHH:mm:ss'"
|
||||
[attr.aria-label]="'Received ' + relativeTime()"
|
||||
>
|
||||
{{ relativeTime() }}
|
||||
</time>
|
||||
</div>
|
||||
<p class="isa-text-body-2-bold text-isa-black">
|
||||
{{ notification().message }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-end">
|
||||
@if (!notification().markedAsRead) {
|
||||
<!-- Action button - primary -->
|
||||
<button
|
||||
type="button"
|
||||
class="isa-text-body-2-bold inline-flex items-center gap-2 rounded-full bg-isa-secondary-600 px-3 py-1.5 text-isa-white transition-colors hover:bg-isa-secondary-700"
|
||||
(click)="onAction()"
|
||||
data-what="notification-action-button"
|
||||
[attr.data-which]="notification().id"
|
||||
[attr.aria-label]="notification().action.label + ' for ' + notification().title"
|
||||
>
|
||||
<ng-icon name="isaActionCheck" size="12" />
|
||||
<span>{{ notification().action.label }}</span>
|
||||
</button>
|
||||
} @else {
|
||||
<!-- Action button - completed -->
|
||||
<button
|
||||
type="button"
|
||||
class="isa-text-body-2-bold inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-isa-accent-green transition-colors hover:bg-isa-neutral-100"
|
||||
(click)="onAction()"
|
||||
data-what="notification-action-button"
|
||||
[attr.data-which]="notification().id"
|
||||
[attr.aria-label]="notification().action.label + ' for ' + notification().title"
|
||||
>
|
||||
<ng-icon name="isaActionCheck" size="12" />
|
||||
<span>{{ notification().action.label }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
} from '@angular/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { Router } from '@angular/router';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { isaActionCheck } from '@isa/icons';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { Notification } from '@isa/shell/common';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-notification',
|
||||
templateUrl: './notification.component.html',
|
||||
styleUrl: './notification.component.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [DatePipe, NgIcon],
|
||||
providers: [provideIcons({ isaActionCheck })],
|
||||
})
|
||||
export class ShellNotificationComponent {
|
||||
readonly #logger = logger({ component: 'ShellNotificationComponent' });
|
||||
readonly #router = inject(Router);
|
||||
|
||||
notification = input.required<Notification>();
|
||||
actionTriggered = output<Notification>();
|
||||
|
||||
relativeTime = computed(() =>
|
||||
formatDistanceToNow(this.notification().timestamp, { addSuffix: true }),
|
||||
);
|
||||
|
||||
onAction(): void {
|
||||
const notification = this.notification();
|
||||
const action = notification.action;
|
||||
|
||||
this.#logger.debug('Notification action triggered', () => ({
|
||||
notificationId: notification.id,
|
||||
actionType: action.type,
|
||||
actionLabel: action.label,
|
||||
}));
|
||||
|
||||
if (action.type === 'navigate') {
|
||||
if (action.target === 'external') {
|
||||
window.open(action.route, '_blank');
|
||||
this.#logger.debug('Navigating to external route', () => ({
|
||||
route: action.route,
|
||||
}));
|
||||
} else {
|
||||
this.#router.navigate([action.route]);
|
||||
this.#logger.debug('Navigating to internal route', () => ({
|
||||
route: action.route,
|
||||
}));
|
||||
}
|
||||
} else if (action.type === 'callback') {
|
||||
action.callback();
|
||||
}
|
||||
|
||||
this.actionTriggered.emit(notification);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
@let grouped = groupedNotifications();
|
||||
<div
|
||||
class="flex flex-col gap-2"
|
||||
data-what="container"
|
||||
data-which="notifications-list"
|
||||
role="feed"
|
||||
aria-label="Benachrichtigungen"
|
||||
>
|
||||
@for (group of grouped | keyvalue; track group.key) {
|
||||
<div
|
||||
class="isa-text-caption-caps"
|
||||
data-what="notification-group-header"
|
||||
[attr.data-which]="group.key"
|
||||
>
|
||||
{{ group.key }}
|
||||
</div>
|
||||
@for (notification of group.value; track notification.id) {
|
||||
<shell-notification [notification]="notification" />
|
||||
}
|
||||
<hr
|
||||
class="my-2 border-isa-neutral-200"
|
||||
data-what="separator"
|
||||
[attr.data-which]="'separator-' + group.key"
|
||||
role="separator"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { NotificationsService } from '@isa/shell/common';
|
||||
import { ShellNotificationComponent } from './components/notification/notification.component';
|
||||
import { Notification } from '@isa/shell/common';
|
||||
import { KeyValuePipe } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-notifications',
|
||||
templateUrl: './shell-notifications.component.html',
|
||||
styleUrls: ['./shell-notifications.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ShellNotificationComponent, KeyValuePipe],
|
||||
})
|
||||
export class ShellNotificationsComponent {
|
||||
readonly notificationsService = inject(NotificationsService);
|
||||
|
||||
groupedNotifications = computed(() => {
|
||||
const notifications = this.notificationsService.get();
|
||||
|
||||
if (notifications.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const groups: Record<string, Notification[]> = {};
|
||||
for (const notification of notifications) {
|
||||
if (!groups[notification.group]) {
|
||||
groups[notification.group] = [];
|
||||
}
|
||||
groups[notification.group].push(notification);
|
||||
}
|
||||
|
||||
// Sort notifications by markedAsRead (unread first) and timestamp (newest first)
|
||||
for (const group in groups) {
|
||||
groups[group].sort((a, b) => {
|
||||
if (a.markedAsRead && !b.markedAsRead) return 1;
|
||||
if (!a.markedAsRead && b.markedAsRead) return -1;
|
||||
return b.timestamp - a.timestamp;
|
||||
});
|
||||
}
|
||||
|
||||
return groups;
|
||||
});
|
||||
}
|
||||
13
libs/shell/notifications/src/test-setup.ts
Normal file
13
libs/shell/notifications/src/test-setup.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import '@angular/compiler';
|
||||
import '@analogjs/vitest-angular/setup-zone';
|
||||
|
||||
import {
|
||||
BrowserTestingModule,
|
||||
platformBrowserTesting,
|
||||
} from '@angular/platform-browser/testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserTestingModule,
|
||||
platformBrowserTesting(),
|
||||
);
|
||||
30
libs/shell/notifications/tsconfig.json
Normal file
30
libs/shell/notifications/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
27
libs/shell/notifications/tsconfig.lib.json
Normal file
27
libs/shell/notifications/tsconfig.lib.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/test-setup.ts",
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx"
|
||||
],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
29
libs/shell/notifications/tsconfig.spec.json
Normal file
29
libs/shell/notifications/tsconfig.spec.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"files": ["src/test-setup.ts"]
|
||||
}
|
||||
27
libs/shell/notifications/vite.config.mts
Normal file
27
libs/shell/notifications/vite.config.mts
Normal file
@@ -0,0 +1,27 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
import angular from '@analogjs/vite-plugin-angular';
|
||||
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../../node_modules/.vite/libs/shell/notifications',
|
||||
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||
// Uncomment this if you are using workers.
|
||||
// worker: {
|
||||
// plugins: [ nxViteTsPaths() ],
|
||||
// },
|
||||
test: {
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
setupFiles: ['src/test-setup.ts'],
|
||||
reporters: ['default'],
|
||||
coverage: {
|
||||
reportsDirectory: '../../../coverage/libs/shell/notifications',
|
||||
provider: 'v8' as const,
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -145,9 +145,11 @@
|
||||
"libs/shared/quantity-control/src/index.ts"
|
||||
],
|
||||
"@isa/shared/scanner": ["libs/shared/scanner/src/index.ts"],
|
||||
"@isa/shell/common": ["libs/shell/common/src/index.ts"],
|
||||
"@isa/shell/header": ["libs/shell/header/src/index.ts"],
|
||||
"@isa/shell/layout": ["libs/shell/layout/src/index.ts"],
|
||||
"@isa/shell/navigation": ["libs/shell/navigation/src/index.ts"],
|
||||
"@isa/shell/notifications": ["libs/shell/notifications/src/index.ts"],
|
||||
"@isa/ui/bullet-list": ["libs/ui/bullet-list/src/index.ts"],
|
||||
"@isa/ui/buttons": ["libs/ui/buttons/src/index.ts"],
|
||||
"@isa/ui/carousel": ["libs/ui/carousel/src/index.ts"],
|
||||
|
||||
Reference in New Issue
Block a user