Refactor API and enhance Angular integration
- Removed `ciTargetName` from `nx.json`. - Updated `package.json` to include new dependencies: `@types/seedrandom`, `fast-check`, `happy-dom`, and `@nestjs/schedule`. - Modified `pnpm-lock.yaml` to reflect the addition of new packages and their versions. - Improved project documentation in `PROJECT_CONTEXT.md` to clarify the use of Zod schemas and Angular framework decisions. - Introduced new Angular components and patterns in the `.agents/skills/frontend-angular` directory, including examples and reference materials for Angular 21+ features.
This commit is contained in:
@@ -1,4 +1,15 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(node -e *)",
|
||||
"Bash(node --input-type=module)",
|
||||
"Bash(2>&1)",
|
||||
"Bash(pnpm *)"
|
||||
]
|
||||
},
|
||||
"enabledPlugins": {
|
||||
"nx@nx-claude-plugins": true
|
||||
},
|
||||
"extraKnownMarketplaces": {
|
||||
"nx-claude-plugins": {
|
||||
"source": {
|
||||
@@ -6,8 +17,5 @@
|
||||
"repo": "nrwl/nx-ai-agents-config"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enabledPlugins": {
|
||||
"nx@nx-claude-plugins": true
|
||||
}
|
||||
}
|
||||
|
||||
10
.claude/skills/frontend-angular/.skillfish.json
Normal file
10
.claude/skills/frontend-angular/.skillfish.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": 2,
|
||||
"name": "frontend-angular",
|
||||
"owner": "ai-enhanced-engineer",
|
||||
"repo": "aiee-team",
|
||||
"path": "skills/frontend-angular",
|
||||
"branch": "main",
|
||||
"sha": "9742140f3ba1399701796d131407d6f41983074f",
|
||||
"source": "manual"
|
||||
}
|
||||
190
.claude/skills/frontend-angular/SKILL.md
Normal file
190
.claude/skills/frontend-angular/SKILL.md
Normal file
@@ -0,0 +1,190 @@
|
||||
---
|
||||
name: frontend-angular
|
||||
description: Modern Angular 21+ patterns including signals, standalone components, zoneless change detection, and new control flow syntax. Use for Angular architecture decisions or implementing components with latest APIs.
|
||||
trigger-terms: Angular 21+, signals, standalone components, zoneless, control flow, @if, @for
|
||||
---
|
||||
|
||||
# Angular Production Patterns
|
||||
|
||||
Production-ready patterns for Angular 21+ applications with zoneless change detection.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Signal-Based Reactivity
|
||||
|
||||
```typescript
|
||||
import { Component, signal, computed, effect } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-counter',
|
||||
standalone: true,
|
||||
template: `
|
||||
<button (click)="increment()">
|
||||
{{ count() }} × 2 = {{ doubled() }}
|
||||
</button>
|
||||
`
|
||||
})
|
||||
export class CounterComponent {
|
||||
count = signal(0);
|
||||
doubled = computed(() => this.count() * 2);
|
||||
|
||||
constructor() {
|
||||
effect(() => console.log('Count changed:', this.count()));
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.count.update(n => n + 1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Key APIs
|
||||
|
||||
| API | Purpose |
|
||||
|-----|---------|
|
||||
| `signal()` | Reactive state declaration |
|
||||
| `computed()` | Derived values (auto-tracked) |
|
||||
| `effect()` | Side effects on signal changes |
|
||||
| `input()` | Component inputs (replaces @Input) |
|
||||
| `output()` | Component outputs (replaces @Output) |
|
||||
| `model()` | Two-way bindable signals |
|
||||
|
||||
## Modern Control Flow
|
||||
|
||||
```html
|
||||
<!-- Conditionals -->
|
||||
@if (isLoading()) {
|
||||
<app-spinner />
|
||||
} @else if (hasError()) {
|
||||
<app-error [message]="error()" />
|
||||
} @else {
|
||||
<app-content [data]="data()" />
|
||||
}
|
||||
|
||||
<!-- Loops with tracking -->
|
||||
@for (item of items(); track item.id) {
|
||||
<app-item [data]="item" />
|
||||
} @empty {
|
||||
<p>No items found</p>
|
||||
}
|
||||
|
||||
<!-- Switch -->
|
||||
@switch (status()) {
|
||||
@case ('pending') { <app-pending /> }
|
||||
@case ('active') { <app-active /> }
|
||||
@default { <app-unknown /> }
|
||||
}
|
||||
```
|
||||
|
||||
## Standalone Architecture
|
||||
|
||||
```typescript
|
||||
// No NgModules - components declare their dependencies
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterModule, MetricsComponent],
|
||||
template: `...`
|
||||
})
|
||||
export class DashboardComponent {}
|
||||
|
||||
// Bootstrapping
|
||||
bootstrapApplication(AppComponent, {
|
||||
providers: [
|
||||
provideRouter(routes),
|
||||
provideHttpClient(withInterceptors([authInterceptor]))
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
## Component I/O (Angular 21+)
|
||||
|
||||
```typescript
|
||||
@Component({...})
|
||||
export class UserCardComponent {
|
||||
// Input signal (required)
|
||||
user = input.required<User>();
|
||||
|
||||
// Input with default
|
||||
showAvatar = input(true);
|
||||
|
||||
// Output
|
||||
selected = output<User>();
|
||||
|
||||
// Two-way binding
|
||||
isExpanded = model(false);
|
||||
|
||||
onSelect() {
|
||||
this.selected.emit(this.user());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Targets
|
||||
|
||||
| Metric | Target |
|
||||
|--------|--------|
|
||||
| LCP | < 2.5s |
|
||||
| INP | < 200ms |
|
||||
| CLS | < 0.1 |
|
||||
| Initial Bundle | < 250KB |
|
||||
|
||||
## When NOT to Use Angular
|
||||
|
||||
- Simple interactivity → Vanilla JS
|
||||
- Static marketing site → Astro/11ty
|
||||
- < 100KB JS budget → Svelte or Web Components
|
||||
- React ecosystem dependency → React
|
||||
|
||||
## Signal Testing Patterns
|
||||
|
||||
| Pattern | Use Case |
|
||||
|---------|----------|
|
||||
| PLATFORM_ID mocking | Prevent constructor side effects in SSR tests |
|
||||
| WritableSignal with `.set()` | Control signal state without reassignment |
|
||||
| NG0100 prevention | Initialize signals before `detectChanges()` |
|
||||
|
||||
## Signal-Based Service Pattern
|
||||
|
||||
Private writable signal → public readonly → update in `tap()` → component injects and reads:
|
||||
|
||||
```typescript
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AnalyticsService {
|
||||
private _summary = signal<Summary | null>(null);
|
||||
readonly summary = this._summary.asReadonly();
|
||||
|
||||
private _loading = signal(false);
|
||||
readonly loading = this._loading.asReadonly();
|
||||
|
||||
getSummary() {
|
||||
this._loading.set(true);
|
||||
return this.http.get<Summary>('/api/analytics/summary').pipe(
|
||||
tap(data => {
|
||||
this._summary.set(data);
|
||||
this._loading.set(false);
|
||||
}),
|
||||
catchError(err => {
|
||||
this._loading.set(false);
|
||||
throw err;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Component usage:**
|
||||
```typescript
|
||||
@Component({...})
|
||||
export class DashboardComponent {
|
||||
private analytics = inject(AnalyticsService);
|
||||
summary = this.analytics.summary;
|
||||
loading = this.analytics.loading;
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern:** Matches AuthService pattern for consistency across services.
|
||||
|
||||
See `examples.md` for full testing code patterns.
|
||||
|
||||
See `reference.md` for component libraries.
|
||||
83
.claude/skills/frontend-angular/examples.md
Normal file
83
.claude/skills/frontend-angular/examples.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Angular Examples
|
||||
|
||||
Production-ready component implementations for Angular 21+.
|
||||
|
||||
## Dashboard Metrics Component
|
||||
|
||||
```typescript
|
||||
import { Component, input, computed } from '@angular/core';
|
||||
import { DecimalPipe, PercentPipe } from '@angular/common';
|
||||
|
||||
interface Metric {
|
||||
label: string;
|
||||
value: number;
|
||||
format: 'number' | 'currency' | 'percent';
|
||||
trend: 'up' | 'down' | 'stable';
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-metrics-widget',
|
||||
standalone: true,
|
||||
imports: [DecimalPipe, PercentPipe],
|
||||
template: `
|
||||
<div class="metrics-grid" role="region" aria-labelledby="metrics-heading">
|
||||
<h2 id="metrics-heading" class="sr-only">Key Metrics</h2>
|
||||
|
||||
@for (metric of metrics(); track metric.label) {
|
||||
<article
|
||||
class="metric-card"
|
||||
[attr.aria-label]="metric.label + ': ' + formatValue(metric)">
|
||||
<span class="label">{{ metric.label }}</span>
|
||||
<span class="value">{{ formatValue(metric) }}</span>
|
||||
<span class="trend" [class]="metric.trend">
|
||||
@switch (metric.trend) {
|
||||
@case ('up') { ↑ }
|
||||
@case ('down') { ↓ }
|
||||
@default { → }
|
||||
}
|
||||
</span>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.metric-card {
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
background: var(--surface-card);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.trend.up { color: var(--green-500); }
|
||||
.trend.down { color: var(--red-500); }
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class MetricsWidgetComponent {
|
||||
metrics = input.required<Metric[]>();
|
||||
|
||||
formatValue(metric: Metric): string {
|
||||
switch (metric.format) {
|
||||
case 'currency':
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
}).format(metric.value);
|
||||
case 'percent':
|
||||
return `${(metric.value * 100).toFixed(1)}%`;
|
||||
default:
|
||||
return metric.value.toLocaleString();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
174
.claude/skills/frontend-angular/reference.md
Normal file
174
.claude/skills/frontend-angular/reference.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Angular Reference
|
||||
|
||||
Detailed patterns and component library guidance for Angular 21+.
|
||||
|
||||
## Zoneless Change Detection (Default in v21)
|
||||
|
||||
```typescript
|
||||
// No Zone.js needed - signals trigger updates automatically
|
||||
bootstrapApplication(AppComponent, {
|
||||
providers: [
|
||||
// Zoneless is default in Angular 21+
|
||||
// Only add this if you need Zone.js for legacy code:
|
||||
// provideZoneChangeDetection()
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Smaller bundle (no Zone.js ~100KB)
|
||||
- Predictable change detection
|
||||
- Better debugging (no Zone.js stack traces)
|
||||
- Signals automatically schedule updates
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
```typescript
|
||||
// Modern inject() function (preferred)
|
||||
@Component({...})
|
||||
export class UserService {
|
||||
private http = inject(HttpClient);
|
||||
private router = inject(Router);
|
||||
}
|
||||
|
||||
// Constructor injection (still valid)
|
||||
constructor(private http: HttpClient) {}
|
||||
```
|
||||
|
||||
## HTTP Client Patterns
|
||||
|
||||
```typescript
|
||||
// Functional interceptors (Angular 21+)
|
||||
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
const token = inject(AuthService).getToken();
|
||||
|
||||
if (token) {
|
||||
req = req.clone({
|
||||
setHeaders: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
}
|
||||
|
||||
return next(req);
|
||||
};
|
||||
|
||||
// Registration
|
||||
provideHttpClient(
|
||||
withInterceptors([authInterceptor, loggingInterceptor])
|
||||
)
|
||||
```
|
||||
|
||||
## Resource API (Async Data)
|
||||
|
||||
```typescript
|
||||
// Signal-based async data loading
|
||||
@Component({...})
|
||||
export class UsersComponent {
|
||||
private usersService = inject(UsersService);
|
||||
|
||||
searchQuery = signal('');
|
||||
|
||||
usersResource = resource({
|
||||
request: () => ({ query: this.searchQuery() }),
|
||||
loader: async ({ request }) => {
|
||||
return this.usersService.search(request.query);
|
||||
}
|
||||
});
|
||||
|
||||
// In template:
|
||||
// @if (usersResource.isLoading()) { ... }
|
||||
// @if (usersResource.hasValue()) { ... }
|
||||
// @if (usersResource.error()) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
## Component Libraries Comparison
|
||||
|
||||
| Library | Cost | Components | Best For |
|
||||
|---------|------|------------|----------|
|
||||
| **PrimeNG** | Free (MIT) | 80+ | B2B dashboards, forms |
|
||||
| **Kendo UI** | $1,028+/dev/year | 110+ | Enterprise, data grids |
|
||||
| **Angular Material** | Free | ~40 | Material Design apps |
|
||||
|
||||
## Reactive Forms with Signals
|
||||
|
||||
```typescript
|
||||
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule],
|
||||
template: `
|
||||
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||
<input formControlName="email" />
|
||||
@if (form.controls.email.errors?.['required']) {
|
||||
<span class="error">Email is required</span>
|
||||
}
|
||||
<button type="submit" [disabled]="form.invalid">Submit</button>
|
||||
</form>
|
||||
`
|
||||
})
|
||||
export class LoginComponent {
|
||||
private fb = inject(FormBuilder);
|
||||
|
||||
form = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
password: ['', [Validators.required, Validators.minLength(8)]]
|
||||
});
|
||||
|
||||
onSubmit() {
|
||||
if (this.form.valid) {
|
||||
console.log(this.form.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Router Patterns
|
||||
|
||||
```typescript
|
||||
// Route configuration
|
||||
export const routes: Routes = [
|
||||
{ path: '', component: HomeComponent },
|
||||
{
|
||||
path: 'dashboard',
|
||||
loadComponent: () => import('./dashboard/dashboard.component')
|
||||
.then(m => m.DashboardComponent),
|
||||
canActivate: [authGuard]
|
||||
},
|
||||
{ path: '**', component: NotFoundComponent }
|
||||
];
|
||||
|
||||
// Functional guard
|
||||
export const authGuard: CanActivateFn = (route, state) => {
|
||||
const auth = inject(AuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
if (auth.isAuthenticated()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return router.createUrlTree(['/login'], {
|
||||
queryParams: { returnUrl: state.url }
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
// Global error handler
|
||||
@Injectable()
|
||||
export class GlobalErrorHandler implements ErrorHandler {
|
||||
private snackBar = inject(MatSnackBar);
|
||||
|
||||
handleError(error: Error) {
|
||||
console.error('Unhandled error:', error);
|
||||
this.snackBar.open('An error occurred', 'Dismiss', {
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Provider
|
||||
{ provide: ErrorHandler, useClass: GlobalErrorHandler }
|
||||
```
|
||||
Reference in New Issue
Block a user