Compare commits

...

7 Commits

Author SHA1 Message Date
Lorenz Hilpert
5fe85282e7 📝 docs(shell): update READMEs and library reference
Update documentation for shell-header, shell-layout, shell-notifications,
and shell-common libraries with API references, E2E attributes, and
accessibility documentation.
2025-12-03 21:18:34 +01:00
Lorenz Hilpert
9a8eac3f9a 🎨 style(icons): reformat icons file and add new icons
Reformat SVG exports for consistent code style.
Add new icons: isaFiliale, isaFilialeLocation, isaArtikel variants.
2025-12-03 21:18:16 +01:00
Lorenz Hilpert
93752efb9d feat(shell-layout): integrate shell-header component
Add ShellHeaderComponent to layout and configure path aliases for new
shell-common and shell-notifications libraries.
2025-12-03 21:17:36 +01:00
Lorenz Hilpert
0c546802fa feat(core-auth): add AuthService for logout functionality
Add AuthService wrapping OAuthService logout with proper logging.
Refactor RoleService to use private class fields (#) convention.
2025-12-03 21:17:15 +01:00
Lorenz Hilpert
3ed3d0b466 ♻️ refactor(shell-header): restructure with modular sub-components
Reorganize header into focused sub-components:
- ShellNavigationToggleComponent: drawer toggle with dynamic icon
- ShellFontSizeSelectorComponent: accessibility font size control
- ShellLogoutButtonComponent: logout with AuthService integration
- ShellNotificationsToggleComponent: notification panel overlay

Add E2E testing attributes and ARIA accessibility support
2025-12-03 21:16:57 +01:00
Lorenz Hilpert
daf79d55a5 feat(shell-notifications): add notification display component
Add feature library for rendering grouped notifications with:
- Grouped notification display with collapsible sections
- Unread/read status indication
- Relative timestamps via date-fns
- Action buttons supporting navigation and callback types
2025-12-03 21:16:33 +01:00
Lorenz Hilpert
062a8044f2 feat(shell-common): add shared shell services library
Add new util library providing state management services for shell components:
- NavigationService: navigation drawer open/closed state
- FontSizeService: application-wide font size with document sync
- NotificationsService: notification state with read status tracking
2025-12-03 21:16:14 +01:00
52 changed files with 1873 additions and 306 deletions

View File

@@ -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

View File

@@ -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';

View 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();
}
}

View File

@@ -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;
}
}

View File

File diff suppressed because one or more lines are too long

201
libs/shell/common/README.md Normal file
View 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

View 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: {},
},
];

View 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"
}
}
}

View File

@@ -0,0 +1,3 @@
export * from './lib/navigation.service';
export * from './lib/font-size.service';
export * from './lib/notifications.service';

View 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`,
);
});
}

View 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);
}
}

View 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,
);
}

View 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(),
);

View 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"
}
]
}

View 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"]
}

View 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"]
}

View 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,
},
},
}));

View File

@@ -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

View File

@@ -1 +1 @@
export * from './lib/shell-header/shell-header.component';
export * from './lib/shell-header.component';

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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(),
}));
}
}

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -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>

View File

@@ -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';
});
}

View 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>

View 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);
}

View File

@@ -1 +0,0 @@
<p>ShellHeader works!</p>

View File

@@ -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();
});
});

View File

@@ -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 {}

View File

@@ -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

View File

@@ -1,4 +1,5 @@
<shell-network-status-banner />
<shell-header />
<main>
<ng-content></ng-content>
</main>

View File

@@ -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 {}

View 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

View 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: {},
},
];

View 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"
}
}
}

View File

@@ -0,0 +1 @@
export * from './lib/shell-notifications.component';

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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;
});
}

View 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(),
);

View 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"
}
]
}

View 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"]
}

View 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"]
}

View 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,
},
},
}));

View File

@@ -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"],