@devmed555/angular-clean-architecture-cli 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +425 -0
  2. package/bin/index.js +100 -2
  3. package/generators.json +19 -9
  4. package/package.json +1 -1
  5. package/src/generators/core/files/auth/auth.guard.ts.template +14 -0
  6. package/src/generators/core/files/auth/auth.service.ts.template +137 -0
  7. package/src/generators/core/files/auth/login.component.ts.template +165 -0
  8. package/src/generators/core/files/auth/signup.component.ts.template +171 -0
  9. package/src/generators/core/files/guard/__name__.guard.ts.template +6 -0
  10. package/src/generators/core/files/interceptor/__name__.interceptor.ts.template +6 -0
  11. package/src/generators/core/files/language-selector/language-selector.component.ts.template +82 -0
  12. package/src/generators/core/files/menu/__name__.component.html.template +14 -0
  13. package/src/generators/core/files/menu/__name__.component.scss.template +66 -0
  14. package/src/generators/core/files/menu/__name__.component.ts.template +24 -0
  15. package/src/generators/core/files/navbar/__name__.component.html.template +45 -0
  16. package/src/generators/core/files/navbar/__name__.component.scss.template +134 -0
  17. package/src/generators/core/files/navbar/__name__.component.ts.template +38 -0
  18. package/src/generators/core/files/service/__name__.service.ts.template +26 -0
  19. package/src/generators/core/files/theme-selector/theme-selector.component.ts.template +49 -0
  20. package/src/generators/core/files/translate/translate.pipe.ts.template +15 -0
  21. package/src/generators/core/files/translate/translate.service.ts.template +24 -0
  22. package/src/generators/core/generator.d.ts +7 -0
  23. package/src/generators/core/generator.js +49 -0
  24. package/src/generators/core/generator.js.map +1 -0
  25. package/src/generators/core/schema.json +34 -0
  26. package/src/generators/feature/blueprint.schema.json +34 -0
  27. package/src/generators/feature/files/application/store.ts.template +135 -0
  28. package/src/generators/feature/files/domain/model.ts.template +9 -0
  29. package/src/generators/{clean-feature → feature}/files/infrastructure/service.ts.template +5 -5
  30. package/src/generators/feature/files/ui/__singularName__.component.html.template +109 -0
  31. package/src/generators/feature/files/ui/__singularName__.component.scss.template +162 -0
  32. package/src/generators/feature/files/ui/__singularName__.component.ts.template +131 -0
  33. package/src/generators/feature/files/ui/_theme.scss.template +35 -0
  34. package/src/generators/feature/files/ui/form/form.component.ts.template +122 -0
  35. package/src/generators/feature/generator.d.ts +4 -0
  36. package/src/generators/feature/generator.js +209 -0
  37. package/src/generators/feature/generator.js.map +1 -0
  38. package/src/generators/feature/schema.d.ts +5 -0
  39. package/src/generators/{clean-feature → feature}/schema.json +25 -21
  40. package/src/generators/shared/files/ui/__name__.component.ts.template +57 -0
  41. package/src/generators/shared/files/util/__name__.ts.template +7 -0
  42. package/src/generators/shared/generator.d.ts +7 -0
  43. package/src/generators/shared/generator.js +31 -0
  44. package/src/generators/shared/generator.js.map +1 -0
  45. package/src/generators/shared/schema.json +23 -0
  46. package/src/index.js +1 -0
  47. package/src/utils/string-utils.d.ts +16 -0
  48. package/src/utils/string-utils.js +33 -0
  49. package/src/utils/string-utils.js.map +1 -0
  50. package/src/generators/clean-feature/files/application/store.ts.template +0 -6
  51. package/src/generators/clean-feature/files/domain/model.ts.template +0 -4
  52. package/src/generators/clean-feature/files/ui/component.ts.template +0 -44
  53. package/src/generators/clean-feature/generator.d.ts +0 -4
  54. package/src/generators/clean-feature/generator.js +0 -89
  55. package/src/generators/clean-feature/generator.js.map +0 -1
  56. package/src/generators/clean-feature/schema.d.ts +0 -4
@@ -0,0 +1,137 @@
1
+ import { Injectable, signal, inject, computed } from '@angular/core';
2
+ import { HttpClient } from '@angular/common/http';
3
+ import { Router } from '@angular/router';
4
+ import { catchError, tap } from 'rxjs/operators';
5
+ import { of } from 'rxjs';
6
+ import { environment } from '../../../environments/environment';
7
+
8
+ export interface User {
9
+ id: string;
10
+ email: string;
11
+ name: string;
12
+ }
13
+
14
+ export interface LoginResponse {
15
+ access_token: string;
16
+ user: User;
17
+ }
18
+
19
+ const TOKEN_KEY = 'auth_token';
20
+ const USER_KEY = 'auth_user';
21
+
22
+ /**
23
+ * Auth Service - handles authentication with backend API
24
+ * Uses signals for reactive state management (Angular 21 zoneless compatible)
25
+ */
26
+ @Injectable({
27
+ providedIn: 'root'
28
+ })
29
+ export class AuthService {
30
+ private readonly http = inject(HttpClient);
31
+ private readonly router = inject(Router);
32
+ private readonly apiUrl = `${environment.apiUrl}/auth`;
33
+
34
+ // State signals
35
+ private readonly _currentUser = signal<User | null>(this.loadUserFromStorage());
36
+ private readonly _token = signal<string | null>(this.loadTokenFromStorage());
37
+ private readonly _loading = signal(false);
38
+ private readonly _error = signal<string | null>(null);
39
+
40
+ // Public readonly signals
41
+ readonly currentUser = this._currentUser.asReadonly();
42
+ readonly token = this._token.asReadonly();
43
+ readonly loading = this._loading.asReadonly();
44
+ readonly error = this._error.asReadonly();
45
+
46
+ // Computed signal for authentication status
47
+ readonly isAuthenticated = computed(() => !!this._token() && !!this._currentUser());
48
+
49
+ login(credentials: { email: string; password: string }) {
50
+ this._loading.set(true);
51
+ this._error.set(null);
52
+
53
+ return this.http.post<LoginResponse>(`${this.apiUrl}/login`, credentials).pipe(
54
+ tap(response => {
55
+ this.setSession(response);
56
+ this._loading.set(false);
57
+ }),
58
+ catchError(error => {
59
+ this._error.set(error.error?.message || 'Login failed');
60
+ this._loading.set(false);
61
+ return of(null);
62
+ })
63
+ );
64
+ }
65
+
66
+ signup(userData: { name: string; email: string; password: string }) {
67
+ this._loading.set(true);
68
+ this._error.set(null);
69
+
70
+ return this.http.post<LoginResponse>(`${this.apiUrl}/signup`, userData).pipe(
71
+ tap(response => {
72
+ this.setSession(response);
73
+ this._loading.set(false);
74
+ }),
75
+ catchError(error => {
76
+ this._error.set(error.error?.message || 'Signup failed');
77
+ this._loading.set(false);
78
+ return of(null);
79
+ })
80
+ );
81
+ }
82
+
83
+ logout() {
84
+ this.http.post(`${this.apiUrl}/logout`, {}).subscribe();
85
+ this.clearSession();
86
+ this.router.navigate(['/login']);
87
+ }
88
+
89
+ refreshUser() {
90
+ if (!this._token()) return;
91
+
92
+ this.http.get<User>(`${this.apiUrl}/me`).pipe(
93
+ catchError(() => {
94
+ this.clearSession();
95
+ return of(null);
96
+ })
97
+ ).subscribe(user => {
98
+ if (user) {
99
+ this._currentUser.set(user);
100
+ this.saveUserToStorage(user);
101
+ }
102
+ });
103
+ }
104
+
105
+ private setSession(response: LoginResponse) {
106
+ this._token.set(response.access_token);
107
+ this._currentUser.set(response.user);
108
+ this.saveTokenToStorage(response.access_token);
109
+ this.saveUserToStorage(response.user);
110
+ }
111
+
112
+ private clearSession() {
113
+ this._token.set(null);
114
+ this._currentUser.set(null);
115
+ localStorage.removeItem(TOKEN_KEY);
116
+ localStorage.removeItem(USER_KEY);
117
+ }
118
+
119
+ private loadTokenFromStorage(): string | null {
120
+ if (typeof localStorage === 'undefined') return null;
121
+ return localStorage.getItem(TOKEN_KEY);
122
+ }
123
+
124
+ private loadUserFromStorage(): User | null {
125
+ if (typeof localStorage === 'undefined') return null;
126
+ const userJson = localStorage.getItem(USER_KEY);
127
+ return userJson ? JSON.parse(userJson) : null;
128
+ }
129
+
130
+ private saveTokenToStorage(token: string) {
131
+ localStorage.setItem(TOKEN_KEY, token);
132
+ }
133
+
134
+ private saveUserToStorage(user: User) {
135
+ localStorage.setItem(USER_KEY, JSON.stringify(user));
136
+ }
137
+ }
@@ -0,0 +1,165 @@
1
+ import { Component, inject, ChangeDetectionStrategy, signal } from '@angular/core';
2
+ import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
3
+ import { MatFormFieldModule } from '@angular/material/form-field';
4
+ import { MatInputModule } from '@angular/material/input';
5
+ import { MatButtonModule } from '@angular/material/button';
6
+ import { MatCardModule } from '@angular/material/card';
7
+ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
8
+ import { RouterLink } from '@angular/router';
9
+ import { AuthService } from './auth.service';
10
+ import { Router } from '@angular/router';
11
+
12
+ /**
13
+ * Login component - connects to backend API
14
+ */
15
+ @Component({
16
+ selector: 'app-login',
17
+ imports: [
18
+ ReactiveFormsModule,
19
+ MatFormFieldModule,
20
+ MatInputModule,
21
+ MatButtonModule,
22
+ MatCardModule,
23
+ MatProgressSpinnerModule,
24
+ RouterLink
25
+ ],
26
+ template: `
27
+ @let emailErrors = loginForm.get('email')?.errors;
28
+ @let passwordErrors = loginForm.get('password')?.errors;
29
+ @let isSubmitDisabled = !formValid() || authService.loading();
30
+
31
+ <div class="auth-container">
32
+ <mat-card class="auth-card">
33
+ <mat-card-header>
34
+ <mat-card-title>Login</mat-card-title>
35
+ <mat-card-subtitle>Sign in to your account</mat-card-subtitle>
36
+ </mat-card-header>
37
+
38
+ <mat-card-content>
39
+ @if (authService.error()) {
40
+ <div class="error-banner">{{ authService.error() }}</div>
41
+ }
42
+
43
+ <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
44
+ <mat-form-field appearance="outline">
45
+ <mat-label>Email</mat-label>
46
+ <input matInput formControlName="email" type="email" placeholder="Enter your email">
47
+ @if (emailErrors?.['required']) {
48
+ <mat-error>Email is required</mat-error>
49
+ }
50
+ @if (emailErrors?.['email']) {
51
+ <mat-error>Please enter a valid email</mat-error>
52
+ }
53
+ </mat-form-field>
54
+
55
+ <mat-form-field appearance="outline">
56
+ <mat-label>Password</mat-label>
57
+ <input matInput formControlName="password" type="password" placeholder="Enter your password">
58
+ @if (passwordErrors?.['required']) {
59
+ <mat-error>Password is required</mat-error>
60
+ }
61
+ </mat-form-field>
62
+
63
+ <button
64
+ mat-raised-button
65
+ color="primary"
66
+ type="submit"
67
+ [disabled]="isSubmitDisabled"
68
+ class="submit-btn">
69
+ @if (authService.loading()) {
70
+ <mat-spinner diameter="20"></mat-spinner>
71
+ <span>Logging in...</span>
72
+ } @else {
73
+ Login
74
+ }
75
+ </button>
76
+ </form>
77
+
78
+ <div class="auth-footer">
79
+ <p>Don't have an account? <a routerLink="/signup">Sign up</a></p>
80
+ <p class="demo-hint">Demo: admin@example.com / admin123</p>
81
+ </div>
82
+ </mat-card-content>
83
+ </mat-card>
84
+ </div>
85
+ `,
86
+ styles: `
87
+ .auth-container {
88
+ display: flex;
89
+ justify-content: center;
90
+ align-items: center;
91
+ min-height: 80vh;
92
+ padding: 1rem;
93
+ }
94
+ .auth-card {
95
+ width: 100%;
96
+ max-width: 400px;
97
+ }
98
+ form {
99
+ display: flex;
100
+ flex-direction: column;
101
+ gap: 16px;
102
+ margin-top: 16px;
103
+ }
104
+ .submit-btn {
105
+ display: flex;
106
+ align-items: center;
107
+ justify-content: center;
108
+ gap: 8px;
109
+ height: 48px;
110
+ }
111
+ .error-banner {
112
+ background-color: #f44336;
113
+ color: white;
114
+ padding: 12px;
115
+ border-radius: 4px;
116
+ margin-bottom: 16px;
117
+ }
118
+ .auth-footer {
119
+ margin-top: 24px;
120
+ text-align: center;
121
+
122
+ a {
123
+ color: var(--color-primary);
124
+ text-decoration: none;
125
+ font-weight: 500;
126
+ }
127
+
128
+ .demo-hint {
129
+ margin-top: 8px;
130
+ font-size: 0.85rem;
131
+ color: var(--color-text-secondary);
132
+ }
133
+ }
134
+ `,
135
+ changeDetection: ChangeDetectionStrategy.OnPush
136
+ })
137
+ export class LoginComponent {
138
+ private readonly fb = inject(FormBuilder);
139
+ protected readonly authService = inject(AuthService);
140
+ private readonly router = inject(Router);
141
+
142
+ readonly formValid = signal(false);
143
+
144
+ readonly loginForm = this.fb.group({
145
+ email: ['', [Validators.required, Validators.email]],
146
+ password: ['', [Validators.required]]
147
+ });
148
+
149
+ constructor() {
150
+ this.loginForm.statusChanges.subscribe(() => {
151
+ this.formValid.set(this.loginForm.valid);
152
+ });
153
+ }
154
+
155
+ onSubmit(): void {
156
+ if (this.loginForm.valid) {
157
+ const { email, password } = this.loginForm.value;
158
+ this.authService.login({ email: email!, password: password! }).subscribe(response => {
159
+ if (response) {
160
+ this.router.navigate(['/']);
161
+ }
162
+ });
163
+ }
164
+ }
165
+ }
@@ -0,0 +1,171 @@
1
+ import { Component, inject, ChangeDetectionStrategy, signal } from '@angular/core';
2
+ import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
3
+ import { MatFormFieldModule } from '@angular/material/form-field';
4
+ import { MatInputModule } from '@angular/material/input';
5
+ import { MatButtonModule } from '@angular/material/button';
6
+ import { MatCardModule } from '@angular/material/card';
7
+ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
8
+ import { RouterLink } from '@angular/router';
9
+ import { AuthService } from './auth.service';
10
+ import { Router } from '@angular/router';
11
+
12
+ /**
13
+ * Signup component - connects to backend API
14
+ */
15
+ @Component({
16
+ selector: 'app-signup',
17
+ imports: [
18
+ ReactiveFormsModule,
19
+ MatFormFieldModule,
20
+ MatInputModule,
21
+ MatButtonModule,
22
+ MatCardModule,
23
+ MatProgressSpinnerModule,
24
+ RouterLink
25
+ ],
26
+ template: `
27
+ @let nameErrors = signupForm.get('name')?.errors;
28
+ @let emailErrors = signupForm.get('email')?.errors;
29
+ @let passwordErrors = signupForm.get('password')?.errors;
30
+ @let isSubmitDisabled = !formValid() || authService.loading();
31
+
32
+ <div class="auth-container">
33
+ <mat-card class="auth-card">
34
+ <mat-card-header>
35
+ <mat-card-title>Sign Up</mat-card-title>
36
+ <mat-card-subtitle>Create a new account</mat-card-subtitle>
37
+ </mat-card-header>
38
+
39
+ <mat-card-content>
40
+ @if (authService.error()) {
41
+ <div class="error-banner">{{ authService.error() }}</div>
42
+ }
43
+
44
+ <form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
45
+ <mat-form-field appearance="outline">
46
+ <mat-label>Name</mat-label>
47
+ <input matInput formControlName="name" placeholder="Enter your name">
48
+ @if (nameErrors?.['required']) {
49
+ <mat-error>Name is required</mat-error>
50
+ }
51
+ </mat-form-field>
52
+
53
+ <mat-form-field appearance="outline">
54
+ <mat-label>Email</mat-label>
55
+ <input matInput formControlName="email" type="email" placeholder="Enter your email">
56
+ @if (emailErrors?.['required']) {
57
+ <mat-error>Email is required</mat-error>
58
+ }
59
+ @if (emailErrors?.['email']) {
60
+ <mat-error>Please enter a valid email</mat-error>
61
+ }
62
+ </mat-form-field>
63
+
64
+ <mat-form-field appearance="outline">
65
+ <mat-label>Password</mat-label>
66
+ <input matInput formControlName="password" type="password" placeholder="Enter your password">
67
+ @if (passwordErrors?.['required']) {
68
+ <mat-error>Password is required</mat-error>
69
+ }
70
+ @if (passwordErrors?.['minlength']) {
71
+ <mat-error>Password must be at least 6 characters</mat-error>
72
+ }
73
+ </mat-form-field>
74
+
75
+ <button
76
+ mat-raised-button
77
+ color="primary"
78
+ type="submit"
79
+ [disabled]="isSubmitDisabled"
80
+ class="submit-btn">
81
+ @if (authService.loading()) {
82
+ <mat-spinner diameter="20"></mat-spinner>
83
+ <span>Creating account...</span>
84
+ } @else {
85
+ Sign Up
86
+ }
87
+ </button>
88
+ </form>
89
+
90
+ <div class="auth-footer">
91
+ <p>Already have an account? <a routerLink="/login">Login</a></p>
92
+ </div>
93
+ </mat-card-content>
94
+ </mat-card>
95
+ </div>
96
+ `,
97
+ styles: `
98
+ .auth-container {
99
+ display: flex;
100
+ justify-content: center;
101
+ align-items: center;
102
+ min-height: 80vh;
103
+ padding: 1rem;
104
+ }
105
+ .auth-card {
106
+ width: 100%;
107
+ max-width: 400px;
108
+ }
109
+ form {
110
+ display: flex;
111
+ flex-direction: column;
112
+ gap: 16px;
113
+ margin-top: 16px;
114
+ }
115
+ .submit-btn {
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ gap: 8px;
120
+ height: 48px;
121
+ }
122
+ .error-banner {
123
+ background-color: #f44336;
124
+ color: white;
125
+ padding: 12px;
126
+ border-radius: 4px;
127
+ margin-bottom: 16px;
128
+ }
129
+ .auth-footer {
130
+ margin-top: 24px;
131
+ text-align: center;
132
+
133
+ a {
134
+ color: var(--color-primary);
135
+ text-decoration: none;
136
+ font-weight: 500;
137
+ }
138
+ }
139
+ `,
140
+ changeDetection: ChangeDetectionStrategy.OnPush
141
+ })
142
+ export class SignupComponent {
143
+ private readonly fb = inject(FormBuilder);
144
+ protected readonly authService = inject(AuthService);
145
+ private readonly router = inject(Router);
146
+
147
+ readonly formValid = signal(false);
148
+
149
+ readonly signupForm = this.fb.group({
150
+ name: ['', [Validators.required]],
151
+ email: ['', [Validators.required, Validators.email]],
152
+ password: ['', [Validators.required, Validators.minLength(6)]]
153
+ });
154
+
155
+ constructor() {
156
+ this.signupForm.statusChanges.subscribe(() => {
157
+ this.formValid.set(this.signupForm.valid);
158
+ });
159
+ }
160
+
161
+ onSubmit(): void {
162
+ if (this.signupForm.valid) {
163
+ const { name, email, password } = this.signupForm.value;
164
+ this.authService.signup({ name: name!, email: email!, password: password! }).subscribe(response => {
165
+ if (response) {
166
+ this.router.navigate(['/']);
167
+ }
168
+ });
169
+ }
170
+ }
171
+ }
@@ -0,0 +1,6 @@
1
+ import { CanActivateFn } from '@angular/router';
2
+
3
+ export const <%= camelName %>Guard: CanActivateFn = (route, state) => {
4
+ // TODO: Implement guard logic
5
+ return true;
6
+ };
@@ -0,0 +1,6 @@
1
+ import { HttpInterceptorFn } from '@angular/common/http';
2
+
3
+ export const <%= camelName %>Interceptor: HttpInterceptorFn = (req, next) => {
4
+ // TODO: Implement interceptor logic
5
+ return next(req);
6
+ };
@@ -0,0 +1,82 @@
1
+ import { Component, inject, computed } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { MatButtonModule } from '@angular/material/button';
4
+ import { MatMenuModule } from '@angular/material/menu';
5
+ import { MatIconModule } from '@angular/material/icon';
6
+ import { TranslateService } from '../translate/translate.service';
7
+
8
+ @Component({
9
+ selector: 'app-language-selector',
10
+ imports: [CommonModule, MatButtonModule, MatMenuModule, MatIconModule],
11
+ template: `
12
+ <button mat-button [matMenuTriggerFor]="menu" class="lang-button">
13
+ <mat-icon>translate</mat-icon>
14
+ <span class="lang-label">{{ currentLangLabel() }}</span>
15
+ <mat-icon class="dropdown-icon">arrow_drop_down</mat-icon>
16
+ </button>
17
+
18
+ <mat-menu #menu="matMenu" xPosition="before">
19
+ @for (lang of languages; track lang.code) {
20
+ <button mat-menu-item (click)="changeLang(lang.code)" [class.active-lang]="translate.currentLang() === lang.code">
21
+ <mat-icon *ngIf="translate.currentLang() === lang.code">check</mat-icon>
22
+ <span>{{ lang.label }}</span>
23
+ </button>
24
+ }
25
+ </mat-menu>
26
+ `,
27
+ styles: [
28
+ `
29
+ .lang-button {
30
+ display: flex;
31
+ align-items: center;
32
+ padding: 0 12px;
33
+ height: 40px;
34
+ border-radius: 20px;
35
+ background: rgba(255, 255, 255, 0.1);
36
+ color: white;
37
+ transition: background 0.3s ease;
38
+ }
39
+
40
+ .lang-button:hover {
41
+ background: rgba(255, 255, 255, 0.2);
42
+ }
43
+
44
+ .lang-label {
45
+ margin: 0 4px;
46
+ font-weight: 500;
47
+ text-transform: uppercase;
48
+ font-size: 0.85rem;
49
+ }
50
+
51
+ .dropdown-icon {
52
+ margin: 0;
53
+ width: 18px;
54
+ height: 18px;
55
+ font-size: 18px;
56
+ }
57
+
58
+ .active-lang {
59
+ color: var(--primary-color, #3f51b5);
60
+ font-weight: bold;
61
+ }
62
+ `,
63
+ ],
64
+ })
65
+ export class LanguageSelectorComponent {
66
+ translate = inject(TranslateService);
67
+
68
+ languages = [
69
+ { code: 'en', label: 'English' },
70
+ { code: 'fr', label: 'Français' },
71
+ { code: 'es', label: 'Español' },
72
+ ];
73
+
74
+ currentLangLabel = computed(() => {
75
+ const current = this.translate.currentLang();
76
+ return this.languages.find((l) => l.code === current)?.label || current;
77
+ });
78
+
79
+ changeLang(lang: string) {
80
+ this.translate.setLanguage(lang);
81
+ }
82
+ }
@@ -0,0 +1,14 @@
1
+ <mat-nav-list>
2
+ <div mat-subheader>Features</div>
3
+ @for (feature of features; track feature.path) {
4
+ <a mat-list-item [routerLink]="['/', feature.path]" routerLinkActive="selected-nav">
5
+ <mat-icon matListItemIcon>extension</mat-icon>
6
+ <span matListItemTitle>{{ feature.label }}</span>
7
+ </a>
8
+ }
9
+ @if (features.length === 0) {
10
+ <div class="no-features-msg">
11
+ No features generated yet.
12
+ </div>
13
+ }
14
+ </mat-nav-list>
@@ -0,0 +1,66 @@
1
+ // Theme variables
2
+ @use '@angular/material' as mat;
3
+
4
+ :host {
5
+ display: block;
6
+ padding: 8px 0;
7
+ }
8
+
9
+ // Subheader styling (e.g. "Features")
10
+ div[mat-subheader] {
11
+ color: var(--color-text-secondary) !important;
12
+ font-weight: 700;
13
+ text-transform: uppercase;
14
+ font-size: 0.75rem;
15
+ letter-spacing: 1.2px;
16
+ line-height: 24px;
17
+ margin-bottom: 8px;
18
+ }
19
+
20
+ // List item styling
21
+ a[mat-list-item] {
22
+ margin: 0 8px;
23
+ border-radius: 8px;
24
+ transition: all 0.2s ease;
25
+ height: 48px;
26
+
27
+ [matListItemTitle] {
28
+ color: var(--color-text) !important;
29
+ font-weight: 500;
30
+ }
31
+
32
+ mat-icon {
33
+ color: var(--color-text-secondary) !important;
34
+ transition: color 0.2s ease;
35
+ }
36
+
37
+ &:hover {
38
+ background-color: var(--color-bg-elevated);
39
+
40
+ mat-icon {
41
+ color: var(--color-text) !important;
42
+ }
43
+ }
44
+
45
+ &.selected-nav {
46
+ background-color: rgba(63, 81, 181, 0.15);
47
+ border-left: 3px solid var(--color-primary);
48
+
49
+ [matListItemTitle] {
50
+ color: var(--color-primary) !important;
51
+ font-weight: 600;
52
+ }
53
+
54
+ mat-icon {
55
+ color: var(--color-primary) !important;
56
+ }
57
+ }
58
+ }
59
+
60
+ .no-features-msg {
61
+ padding: 16px;
62
+ color: var(--color-text-secondary);
63
+ font-style: italic;
64
+ font-size: 0.9rem;
65
+ text-align: center;
66
+ }
@@ -0,0 +1,24 @@
1
+ import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
2
+ import { RouterModule, Router } from '@angular/router';
3
+ import { MatListModule } from '@angular/material/list';
4
+ import { MatIconModule } from '@angular/material/icon';
5
+
6
+ @Component({
7
+ selector: 'app-menu',
8
+ imports: [RouterModule, MatListModule, MatIconModule],
9
+ templateUrl: './<%= name %>.component.html',
10
+ styleUrls: ['./<%= name %>.component.scss'],
11
+ changeDetection: ChangeDetectionStrategy.OnPush
12
+ })
13
+ export class <%= pascalName %>Component {
14
+ private readonly router = inject(Router);
15
+
16
+ get features() {
17
+ return this.router.config
18
+ .filter(route => route.path && route.path !== '')
19
+ .map(route => ({
20
+ path: route.path,
21
+ label: route.data?.['label'] || route.path?.split('-').map(p => p.charAt(0).toUpperCase() + p.slice(1)).join(' ')
22
+ }));
23
+ }
24
+ }