mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
✨ 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
This commit is contained in:
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,
|
||||
},
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user