Compare commits

...

7 Commits

Author SHA1 Message Date
Lorenz Hilpert
eca1e5b8b1 feat(shell-tabs): improve collapse behavior with scroll and delay
- Add 1-second delay before collapsing when mouse moves away (desktop)
- Add scroll-to-top detection: expand when page is near top (<50px)
- Add scroll direction detection for tablet: expand on scroll up, collapse on scroll down
- Scroll direction requires 50px threshold before triggering
- Mouse proximity detection disabled on tablet devices
2025-12-11 15:04:41 +01:00
Lorenz Hilpert
e9fc791dea 📝 docs: update library-reference date 2025-12-11 13:55:02 +01:00
Lorenz Hilpert
d9604572b3 🩹 fix(common-title-management): add undefined check for activeTabId
Ensure activeTabId is not undefined before patching tab name.
2025-12-11 13:54:55 +01:00
Lorenz Hilpert
3d217ae83a 💫 feat(shell-tabs): add subtle animation for active tab
- Add scale pulse animation when tab becomes active
- Respect prefers-reduced-motion for accessibility
- Prevent animation during fade-slide-out transitions
2025-12-11 13:54:49 +01:00
Lorenz Hilpert
33026c064f feat(core-tabs): add pattern-based URL exclusion and tab deactivation guard
- Change URL blacklist from exact match to prefix matching
- Rename HISTORY_BLACKLIST_URLS to HISTORY_BLACKLIST_PATTERNS
- Add deactivateTab() method to TabService
- Add deactivateTabGuard for routes outside tab context
- Apply guard to dashboard route in app.routes.ts
- Update README with new features and API documentation

Routes like /dashboard and /kunde/dashboard now properly
deactivate the active tab when navigated to.
2025-12-11 13:54:39 +01:00
Lorenz Hilpert
43e4a6bf64 ♻️ refactor(claude): migrate skills to new skill-creator format
Recreate all 11 skills using the updated skill-creator tooling:
- api-sync, arch-docs, architecture-validator, css-animations
- git-workflow, library-creator, logging, state-patterns
- tailwind, template-standards, test-migration

Key changes:
- Updated YAML frontmatter structure
- Reorganized logging references into references/ subdirectory
- Applied progressive disclosure patterns where applicable
2025-12-11 12:27:15 +01:00
Lorenz Hilpert
7612394ba1 ♻️ refactor(claude): reorganize and rename skills for clarity
Consolidate and rename skills to shorter, more intuitive names:
- css-keyframes-animations → css-animations
- api-sync-manager → api-sync
- architecture-documentation → arch-docs
- jest-vitest-patterns → test-migration
- reactive-state-patterns → state-patterns
- library-scaffolder → library-creator

Merge related skills:
- angular-template + html-template → template-standards
- angular-effects-alternatives + ngrx-resource-api → state-patterns

Remove obsolete skills:
- architecture-enforcer (merged into architecture-validator)
- circular-dependency-resolver (merged into architecture-validator)
- standalone-component-migrator (merged into migration-specialist agent)
- swagger-sync-manager (replaced by api-sync)
- api-change-analyzer (merged into api-sync)
- type-safety-engineer (content distributed to relevant skills)
- test-migration-specialist (replaced by migration-specialist agent)

Add migration-specialist agent for standalone and test migrations.
Update all cross-references in CLAUDE.md and agents.
2025-12-11 11:30:05 +01:00
60 changed files with 5781 additions and 5812 deletions

View File

@@ -1,6 +1,6 @@
---
name: angular-developer
description: Implements Angular code (components, services, stores, pipes, directives, guards) for 2-5 file features. Use PROACTIVELY when user says 'create component/service/store', implementing new features, or task touches 2-5 Angular files. Auto-loads angular-template, html-template, logging, tailwind skills.
description: Implements Angular code (components, services, stores, pipes, directives, guards) for 2-5 file features. Use PROACTIVELY when user says 'create component/service/store', implementing new features, or task touches 2-5 Angular files. Auto-loads template-standards, logging, tailwind skills.
tools: Read, Write, Edit, Bash, Grep, Skill
model: sonnet
---
@@ -12,8 +12,7 @@ You are a specialized Angular developer focused on creating high-quality, mainta
**IMMEDIATELY load these skills at the start of every task:**
```
/skill angular-template
/skill html-template
/skill template-standards
/skill logging
/skill tailwind
```
@@ -174,7 +173,7 @@ npx nx test [project-name]
```
✓ Feature created: UserProfileComponent
✓ Files: component.ts (150), template (85), store (65), tests (18/18 passing)
✓ Skills applied: angular-template, html-template, logging, tailwind
✓ Skills applied: template-standards, logging, tailwind
Key points:
- Used signalStore with Resource API for async profile loading
@@ -220,14 +219,13 @@ Files created:
- All passing (18/18)
Skills applied:
angular-template: @if/@for syntax, @defer for lazy sections
✓ html-template: data-what/data-which, ARIA attributes
✓ template-standards: @if/@for syntax, @defer, data-what/data-which, ARIA attributes
✓ logging: logger() factory with lazy evaluation in all files
✓ tailwind: ISA color palette, consistent spacing
Architecture decisions:
- Chose Resource API over manual loading for better race condition handling
- Used computed signals for validation instead of effects (per angular-effects-alternatives skill)
- Used computed signals for validation instead of effects (per state-patterns skill)
- Single store for entire profile feature (not separate stores per concern)
Integration requirements:

View File

@@ -0,0 +1,549 @@
---
name: migration-specialist
description: Modernizes Angular libraries with standalone component migration and/or Jest→Vitest test framework migration. Use PROACTIVELY when user mentions "migrate to standalone", "convert to Vitest", "modernize library", or references the 40 remaining Jest-based libraries. Safe incremental approach with validation.
tools: Read, Write, Edit, Bash, Grep, Glob, Skill
model: sonnet
---
You are a specialized migration engineer focused on modernizing Angular libraries in the ISA-Frontend monorepo through two key migration workflows.
## Automatic Skill Loading
**IMMEDIATELY load these skills at start:**
```
/skill template-standards
/skill logging
```
**Load for test migrations:**
```
/skill test-migration (syntax mappings for Jest→Vitest)
```
**Load additional skills as needed:**
```
/skill architecture-validator (if checking import boundaries)
/skill state-patterns (if modernizing state management)
```
## When to Use This Agent
**✅ Use migration-specialist when:**
- Converting NgModule components to standalone
- Migrating tests from Jest + Spectator to Vitest + Angular Testing Library
- Modernizing an entire library (both migrations)
- User references the ~40 remaining Jest-based libraries
- Updating routes to use lazy-loaded standalone components
**❌ Do NOT use when:**
- Creating new components (use angular-developer)
- Refactoring 5+ files without migration pattern (use refactor-engineer)
- Writing tests from scratch (use test-writer)
- Simple bug fixes or single file edits
**Examples:**
**✅ Good fit:**
```
"Migrate customer-profile library to standalone components and Vitest"
→ Analyzes dependencies, converts components, updates tests
```
**❌ Poor fit:**
```
"Create a new user dashboard component"
→ Use angular-developer agent
"Refactor all 15 checkout files to use new API"
→ Use refactor-engineer (not a migration pattern)
```
## Your Mission
Execute migrations safely while keeping implementation details in YOUR context. Return summaries based on response_format parameter.
## Migration Type Selection
Determine which migration workflow(s) to execute:
| Scenario | Action |
|----------|--------|
| User mentions standalone/NgModule | Standalone Migration Only |
| User mentions Jest/Vitest/Spectator | Test Migration Only |
| User wants "full modernization" | Both (standalone first, then tests) |
| Library has both old patterns | Both (standalone first, then tests) |
## Workflow
### 1. Intake & Analysis
**Parse the briefing:**
- Library name and path
- Migration type(s) requested
- Current state (NgModule? Jest? Both?)
- **response_format**: "concise" (default) or "detailed"
**Analyze current state:**
```bash
# Check if Jest or Vitest
grep -l "jest.config" libs/[path]/
# Check if standalone components exist
grep -r "standalone: true" libs/[path]/src/
# Check for Spectator usage
grep -r "createComponentFactory\|createServiceFactory" libs/[path]/src/
# Count test files
find libs/[path] -name "*.spec.ts" | wc -l
# Check project.json for executor
cat libs/[path]/project.json | grep executor
```
---
# Standalone Component Migration
### Step 1: Analyze Component Dependencies
1. **Read Component File**
- Identify component decorator configuration
- Check if already standalone (skip if true)
2. **Analyze Template**
- Scan for directives: `*ngIf`, `*ngFor`, `*ngSwitch` → CommonModule
- Scan for forms: `ngModel`, `formControl` → FormsModule or ReactiveFormsModule
- Scan for built-in pipes: `async`, `date`, `json` → CommonModule
- Scan for custom components: identify all component selectors
- Scan for router: `routerLink`, `router-outlet` → RouterModule
3. **Find Parent NgModule**
- Search for NgModule that declares this component
- Note current imports for reference
### Step 2: Convert Component to Standalone
```typescript
// BEFORE
@Component({
selector: 'app-my-component',
templateUrl: './my-component.component.html'
})
export class MyComponent { }
// AFTER
@Component({
selector: 'app-my-component',
standalone: true,
imports: [
CommonModule,
FormsModule,
RouterModule,
ChildComponent,
CustomPipe
],
templateUrl: './my-component.component.html'
})
export class MyComponent { }
```
### Step 3: Update Parent NgModule
Remove from declarations, add to imports if exported:
```typescript
// BEFORE
@NgModule({
declarations: [MyComponent, OtherComponent],
imports: [CommonModule],
exports: [MyComponent]
})
// AFTER
@NgModule({
declarations: [OtherComponent],
imports: [CommonModule, MyComponent],
exports: [MyComponent]
})
```
If NgModule becomes empty, consider removing it entirely.
### Step 4: Update Routes (if applicable)
Convert to lazy-loaded standalone component:
```typescript
// BEFORE
{ path: 'feature', component: MyComponent }
// AFTER
{
path: 'feature',
loadComponent: () => import('./my-component.component').then(m => m.MyComponent)
}
```
### Step 5: Update Tests
```typescript
// BEFORE
TestBed.configureTestingModule({
declarations: [MyComponent],
imports: [CommonModule, FormsModule]
});
// AFTER
TestBed.configureTestingModule({
imports: [MyComponent] // Component imports its own dependencies
});
```
### Step 6: Optional - Migrate to Modern Control Flow
If requested, convert to new Angular control flow syntax:
```html
<!-- OLD -->
<div *ngIf="condition">Content</div>
<div *ngFor="let item of items; trackBy: trackById">{{ item.name }}</div>
<!-- NEW -->
@if (condition) {
<div>Content</div>
}
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
}
```
### Common Import Patterns
| Template Usage | Required Import |
|---------------|-----------------|
| `*ngIf`, `*ngFor`, `*ngSwitch` | `CommonModule` |
| `ngModel` | `FormsModule` |
| `formControl`, `formGroup` | `ReactiveFormsModule` |
| `routerLink`, `router-outlet` | `RouterModule` |
| `async`, `date`, `json` pipes | `CommonModule` |
| Custom components | Direct component import |
| Custom pipes | Direct pipe import |
---
# Test Framework Migration
**Current Status**: ~40 libraries use Jest (65.6%), ~21 use Vitest (34.4%)
### Step 1: Pre-Migration Analysis
1. **Analyze Library Structure**
```bash
# Check current executor
cat libs/[path]/project.json | grep -A5 '"test"'
# Count test files
find libs/[path] -name "*.spec.ts" | wc -l
# Check for Spectator
grep -r "createComponentFactory\|createServiceFactory" libs/[path]/src/
```
2. **Determine Library Depth**
- 2 levels: `libs/feature/ui` → `../../`
- 3 levels: `libs/feature/data-access/api` → `../../../`
- 4 levels: `libs/feature/shell/data-access/store` → `../../../../`
### Step 2: Update Test Configuration
**Update project.json:**
```json
{
"test": {
"executor": "@nx/vite:test",
"options": {
"configFile": "vite.config.mts"
}
}
}
```
**Create vite.config.mts:**
```typescript
/// <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
// @ts-expect-error - Vitest reporter tuple types have complex inference issues
defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/[path]',
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
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',
['junit', { outputFile: '../../../testresults/junit-[library-name].xml' }],
],
coverage: {
reportsDirectory: '../../../coverage/libs/[path]',
provider: 'v8' as const,
reporter: ['text', 'cobertura'],
},
},
}));
```
### Step 3: Migrate Test Files
For each `.spec.ts` file:
**1. Update Imports:**
```typescript
// REMOVE
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
// ADD
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach, vi } from 'vitest';
```
**2. Convert Component Tests:**
```typescript
// OLD (Spectator)
const createComponent = createComponentFactory({
component: MyComponent,
imports: [CommonModule],
mocks: [MyService]
});
let spectator: Spectator<MyComponent>;
beforeEach(() => spectator = createComponent());
it('should display title', () => {
spectator.setInput('title', 'Test');
expect(spectator.query('h1')).toHaveText('Test');
});
// NEW (Angular Testing Utilities)
describe('MyComponent', () => {
let fixture: ComponentFixture<MyComponent>;
let component: MyComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyComponent, CommonModule],
providers: [{ provide: MyService, useValue: mockService }]
}).compileComponents();
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
});
it('should display title', () => {
fixture.componentRef.setInput('title', 'Test');
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('h1').textContent).toContain('Test');
});
});
```
**3. Update Mock Patterns:**
- `jest.fn()` → `vi.fn()`
- `jest.spyOn()` → `vi.spyOn()`
- `jest.mock()` → `vi.mock()`
**4. Update Matchers:**
- `toHaveText('x')` → `expect(el.textContent).toContain('x')`
- `toExist()` → `toBeTruthy()`
### Step 4: Clean Up
1. **Remove Jest Files**
- Delete `jest.config.ts` if present
2. **Update test-setup.ts**
```typescript
// Replace jest-preset-angular setup with:
import '@analogjs/vitest-angular/setup-zone';
```
---
## Validation (Both Migrations)
**Run after each migration phase:**
```bash
# Type check
npx tsc --noEmit
# Run tests
npx nx test [library-name] --skip-nx-cache
# Lint check
npx nx lint [library-name]
```
**Provide progress updates:**
```
Phase 1: Converting components to standalone...
→ MyComponent: ✓ standalone + imports
→ OtherComponent: ✓ standalone + imports
✓ 5 components converted
Phase 2: Updating routes...
→ Routes: ✓ loadComponent syntax
✓ Route configuration updated
Phase 3: Running validation...
→ TypeScript: ✓ No errors
→ Tests: ✓ 18/18 passing
→ Lint: ✓ Passed
✓ Validation complete
```
---
## Reporting (Response Format Based)
**If response_format = "concise" (default):**
```
✓ Migration complete: [library-name]
Standalone Migration:
- Components: 5 converted
- Routes: Updated to loadComponent
- NgModule: Removed (empty)
Test Migration:
- Framework: Jest + Spectator → Vitest + Angular Testing Library
- Files migrated: 8
- Tests: 45/45 passing
Validation: ✓ TypeScript, ✓ Tests, ✓ Lint
```
**If response_format = "detailed":**
```
✓ Migration complete: [library-name]
=== Standalone Component Migration ===
Components migrated: 5
- MyComponent: standalone + CommonModule, FormsModule
- OtherComponent: standalone + CommonModule, RouterModule
- ChildComponent: standalone + CommonModule
- FormComponent: standalone + ReactiveFormsModule
- ListComponent: standalone + CommonModule
Routes updated: 3
- /feature → loadComponent(() => import(...))
- /feature/detail → loadComponent(() => import(...))
- /feature/edit → loadComponent(() => import(...))
NgModule changes:
- FeatureModule: Removed (all components now standalone)
- SharedModule: Updated imports array
=== Test Framework Migration ===
Framework: Jest + Spectator → Vitest + Angular Testing Library
Files migrated: 8
Configuration:
- project.json: ✓ Updated to @nx/vite:test
- vite.config.mts: ✓ Created (depth: 3 levels)
- test-setup.ts: ✓ Updated to vitest-angular
Test conversions:
- Component tests: 5 files (Spectator → TestBed)
- Service tests: 2 files (SpectatorService → TestBed.inject)
- Pipe tests: 1 file (direct testing)
Mock updates:
- jest.fn() → vi.fn(): 23 occurrences
- jest.spyOn() → vi.spyOn(): 8 occurrences
CI/CD Integration:
- JUnit XML: ✓ testresults/junit-[name].xml
- Cobertura XML: ✓ coverage/libs/[path]/cobertura-coverage.xml
=== Validation ===
✓ TypeScript: No errors
✓ Tests: 45/45 passing (100%)
✓ Lint: No errors
✓ Build: Successful
Cleanup:
- jest.config.ts: Deleted
- Spectator imports: Removed
Remaining Jest libraries: XX/40
Migration progress: XX% complete
```
---
## Error Handling
### Standalone Migration Issues
**Circular dependencies:**
- Extract shared interfaces to util library
- Use dependency injection for services
**Missing imports causing template errors:**
- Check browser console for specific errors
- Verify all template dependencies in imports array
**Route lazy loading fails:**
- Verify component is exported
- Check import path is correct
### Test Migration Issues
**Tests fail after migration:**
- Check `fixture.detectChanges()` is called after setting inputs
- Verify async tests use `async/await` properly
**Mocks not working:**
- Verify `vi.fn()` syntax
- Check providers array in TestBed
**Coverage files not generated:**
- Verify path depth in vite.config.mts
- Check reporters include `'cobertura'`
---
## Anti-Patterns to Avoid
❌ Converting without analyzing dependencies first
❌ Leaving old NgModule alongside standalone components
❌ Skipping test updates after standalone conversion
❌ Using wrong path depth in vite.config.mts
❌ Missing fixture.detectChanges() in converted tests
❌ Batch converting without incremental validation
## Context Efficiency
**Keep main context clean:**
- Use Glob/Grep for discovery
- Report summaries, not full file contents
- Iterate on errors internally
- Return only the migration summary
**Token budget target:** Keep execution under 30K tokens through surgical reads and compressed outputs.

View File

@@ -1,417 +0,0 @@
---
name: angular-effects-alternatives
description: This skill should be used when writing Angular code with signals and effects. Use when deciding whether to use effect(), computed(), or reactive patterns for state management. Applies to all Angular components and services using signals, especially when considering effect() for state propagation, data synchronization, or reactive flows. Essential for code review of effect usage and refactoring imperative patterns to declarative alternatives.
---
# Angular Effects Alternatives
## Overview
This skill guides proper usage of Angular's `effect()` and provides declarative alternatives for common patterns. Effects are frequently misused for state propagation, leading to circular updates, maintenance issues, and violations of reactive principles. This skill prevents anti-patterns and promotes maintainable, declarative code.
## When to Use Effects (Valid Use Cases)
Effects are **primarily for rendering content that cannot be rendered through data binding**. Valid use cases are limited to:
### 1. Logging
Recording application events or debugging:
```typescript
effect(() => {
const error = this.error();
if (error) {
console.error('Error occurred:', error);
}
});
```
### 2. Canvas Painting
Custom graphics rendering (e.g., Angular Three library, Chart.js integration):
```typescript
effect(() => {
const data = this.chartData();
this.renderCanvas(data);
});
```
### 3. Custom DOM Behavior
Imperative APIs that require direct DOM manipulation:
```typescript
effect(() => {
const message = this.notificationMessage();
if (message) {
this.snackBar.open(message, 'Close');
}
});
```
**Key principle:** Data binding is the preferred way to display data. Effects should only be used when data binding is insufficient.
## Understanding Auto-Tracking
Angular automatically tracks signals accessed during effect execution, **even within called methods**:
```typescript
effect(() => {
this.logError(); // Signal tracking happens inside this method
});
logError(): void {
const error = this.error(); // This signal is automatically tracked
if (error) {
console.error(error);
}
}
```
**Implication:** Auto-tracking makes effect dependencies non-obvious and hard to maintain. This is a primary reason to avoid effects for state management.
## When NOT to Use Effects (Anti-Patterns)
### ❌ Anti-Pattern 1: State Propagation
**NEVER use effects to propagate state changes to other state:**
```typescript
// ❌ WRONG - Anti-pattern
effect(() => {
const value = this.source();
this.derived.set(value * 2);
});
```
**Problems:**
- Risk of circular updates and infinite loops
- Hard to maintain due to implicit tracking
- Violates declarative reactive principles
- Inappropriate glitch-free semantics
### ❌ Anti-Pattern 2: Synchronizing Signals
```typescript
// ❌ WRONG - Anti-pattern
effect(() => {
const filter = this.filter();
this.loadData(filter);
});
```
### ❌ Anti-Pattern 3: Event Emulation
```typescript
// ❌ WRONG - Anti-pattern
effect(() => {
const count = this.itemCount();
this.countChanged.emit(count);
});
```
**Why signals ≠ events:** Signals are designed to be glitch-free, collapsing multiple updates. This makes them inappropriate for representing discrete events.
## Decision Tree: Effect vs Alternative
```
Need to react to signal changes?
├─ Synchronous derivation?
│ └─ ✅ Use computed()
├─ Asynchronous derivation?
│ └─ ✅ Use Resource API
├─ Complex reactive flow with race conditions?
│ └─ ✅ Use RxJS (toObservable + operators + toSignal)
├─ Event-based trigger (not state change)?
│ └─ ✅ React to event directly, not signal
├─ Need RxJS operators with signals?
│ └─ ✅ Use reactive helpers (rxMethod, deriveAsync)
└─ Rendering non-data-bound content (logging, canvas, imperative API)?
└─ ✅ Use effect()
```
## Recommended Alternatives
### Alternative 1: Use `computed()` for Synchronous Derivations
**When to use:** Deriving new state synchronously from existing signals.
```typescript
// ✅ CORRECT - Declarative
const derived = computed(() => {
return this.baseSignal() * 2;
});
const fullName = computed(() => {
return `${this.firstName()} ${this.lastName()}`;
});
```
**Benefits:**
- Declarative and maintainable
- Automatic dependency tracking
- Memoized and efficient
- No risk of circular updates
### Alternative 2: Use Resource API for Asynchronous Derivations
**When to use:** Loading data based on reactive parameters.
```typescript
// ✅ CORRECT - Declarative async state
readonly itemsResource = resource({
params: this.filter,
loader: ({ params, abortSignal }) => {
return this.dataService.load(params, abortSignal);
}
});
readonly items = computed(() => this.itemsResource.value() ?? []);
```
**Benefits:**
- Automatic race condition handling
- Built-in loading/error states
- Declarative parameter tracking
- Cancellation support
**See also:** `ngrx-resource-api` skill for detailed Resource API patterns.
### Alternative 3: React to Events, Not State Changes
**When to use:** User interactions or DOM events should trigger actions.
```typescript
// ❌ WRONG - Reacting to signal change
effect(() => {
const searchTerm = this.searchTerm();
this.search(searchTerm);
});
// ✅ CORRECT - React to event
<input (input)="search($event.target.value)" />
// Component
search(term: string): void {
this.searchTerm.set(term);
this.performSearch(term);
}
```
**Benefits:**
- Clear causality (event → action)
- No auto-tracking complexity
- Explicit control flow
### Alternative 4: RxJS Integration
**When to use:** Complex reactive flows requiring operators like `debounceTime`, `switchMap`, `combineLatest`.
```typescript
// ✅ CORRECT - RxJS for complex flows
readonly searchTerm = signal('');
readonly searchTerm$ = toObservable(this.searchTerm);
readonly results$ = this.searchTerm$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.searchService.search(term))
);
readonly results = toSignal(this.results$, { initialValue: [] });
```
**Benefits:**
- Full RxJS operator ecosystem
- Race condition prevention (`switchMap`)
- Powerful composition
- Type-safe streams
**Pattern:** Signal → Observable (toObservable) → RxJS operators → Signal (toSignal)
### Alternative 5: Reactive Helpers
**When to use:** Need RxJS operators but prefer signal-centric API.
#### Using `rxMethod` (NgRx Signal Store)
```typescript
readonly loadItem = rxMethod<number>(
pipe(
tap(() => patchState(this, { loading: true })),
switchMap(id => this.service.findById(id)),
tap(item => patchState(this, { item, loading: false }))
)
);
// Call with signal or value
constructor() {
this.loadItem(this.selectedId);
}
```
#### Using `deriveAsync` (ngxtension)
```typescript
readonly data = deriveAsync(() => {
const filter = this.filter();
return this.service.load(filter);
});
```
**Benefits:**
- Signal-friendly API
- RxJS operator support
- Cleaner than manual Observable conversion
- Automatic subscription management
## The `explicitEffect` Consideration
Some libraries provide `explicitEffect` to restrict auto-tracking:
```typescript
// Uses untracked() internally to limit tracking
explicitEffect(this.id, (id) => {
this.store.load(id);
});
```
**Evaluation:**
- ✅ Mitigates auto-tracking drawbacks
- ✅ Makes dependencies explicit
- ❌ Still imperative (not declarative)
- ❌ Doesn't solve circular update risks
- ❌ Less idiomatic than reactive alternatives
**Recommendation:** Prefer declarative patterns (computed, Resource API, RxJS) over `explicitEffect`.
## Common Refactoring Patterns
### Pattern 1: Effect for State Sync → computed()
```typescript
// ❌ Before
effect(() => {
const x = this.x();
const y = this.y();
this.sum.set(x + y);
});
// ✅ After
readonly sum = computed(() => this.x() + this.y());
```
### Pattern 2: Effect for Async Load → Resource API
```typescript
// ❌ Before
effect(() => {
const id = this.selectedId();
this.loadItem(id);
});
// ✅ After
readonly itemResource = resource({
params: this.selectedId,
loader: ({ params }) => this.service.loadItem(params)
});
```
### Pattern 3: Effect for Debounced Search → RxJS
```typescript
// ❌ Before
effect(() => {
const term = this.searchTerm();
// No way to debounce within effect
this.search(term);
});
// ✅ After
readonly searchTerm$ = toObservable(this.searchTerm);
readonly results = toSignal(
this.searchTerm$.pipe(
debounceTime(300),
switchMap(term => this.searchService.search(term))
),
{ initialValue: [] }
);
```
### Pattern 4: Effect for Event Notification → Direct Event Handling
```typescript
// ❌ Before
effect(() => {
const value = this.value();
this.valueChange.emit(value);
});
// ✅ After
updateValue(newValue: string): void {
this.value.set(newValue);
this.valueChange.emit(newValue);
}
```
## Code Review Checklist
When reviewing code with `effect()`, ask:
- [ ] Is this for rendering non-data-bound content? (logging, canvas, imperative APIs)
- **YES:** Effect is appropriate
- **NO:** Continue checklist
- [ ] Is this synchronous state derivation?
- **YES:** Use `computed()` instead
- [ ] Is this asynchronous data loading?
- **YES:** Use Resource API instead
- [ ] Does this need RxJS operators (debounce, switchMap, etc.)?
- **YES:** Use RxJS integration or reactive helpers instead
- [ ] Is this reacting to a user event?
- **YES:** Handle event directly instead
- [ ] Could this cause circular updates?
- **YES:** Refactor immediately - this will cause bugs
## Anti-Pattern Detection Rules
Flag any effect that:
1. **Calls `set()` or `update()` on signals** - Likely state propagation anti-pattern
2. **Calls service methods that update state** - Hidden state propagation
3. **Emits events based on signal changes** - Signal/event semantic mismatch
4. **Has try/catch for async operations** - Should use Resource API
5. **Would benefit from debouncing/throttling** - Should use RxJS
## Migration Strategy
When converting effects to alternatives:
1. **Identify effect purpose** - State derivation? Async load? Event handling?
2. **Choose appropriate alternative** - Use decision tree above
3. **Implement replacement** - Follow patterns in this skill
4. **Test thoroughly** - Ensure reactive flow works correctly
5. **Remove effect** - Clean up unused code
## Key Principles
1. **Effects are for side effects, not state management**
2. **Prefer declarative over imperative**
3. **Use computed() for sync, Resource API for async**
4. **React to events, not state changes**
5. **RxJS for complex reactive flows**
6. **Auto-tracking is powerful but opaque - avoid when possible**
## When in Doubt
Ask: "Can the user see this without an effect using data binding?"
- **YES:** Don't use effect, use data binding
- **NO:** Effect might be appropriate (but verify against decision tree)

View File

@@ -1,240 +0,0 @@
---
name: angular-template
description: This skill should be used when writing or reviewing Angular component templates. It provides guidance on modern Angular 20+ template syntax including control flow (@if, @for, @switch, @defer), content projection (ng-content), template references (ng-template, ng-container), variable declarations (@let), and expression binding. Use when creating components, refactoring to modern syntax, implementing lazy loading, or reviewing template best practices.
---
# Angular Template
Guide for modern Angular 20+ template patterns: control flow, lazy loading, projection, and binding.
## When to Use
- Creating/reviewing component templates
- Refactoring legacy `*ngIf/*ngFor/*ngSwitch` to modern syntax
- Implementing `@defer` lazy loading
- Designing reusable components with `ng-content`
- Template performance optimization
**Related Skills:** These skills work together when writing Angular templates:
- **[html-template](../html-template/SKILL.md)** - E2E testing attributes (`data-what`, `data-which`) and ARIA accessibility
- **[tailwind](../tailwind/SKILL.md)** - ISA design system styling (colors, typography, spacing, layout)
- **[logging](../logging/SKILL.md)** - MANDATORY logging in all Angular files using `@isa/core/logging`
## Control Flow (Angular 17+)
### @if / @else if / @else
```typescript
@if (user.isAdmin()) {
<app-admin-dashboard />
} @else if (user.isEditor()) {
<app-editor-dashboard />
} @else {
<app-viewer-dashboard />
}
// Store result with 'as'
@if (user.profile?.settings; as settings) {
<p>Theme: {{settings.theme}}</p>
}
```
### @for with @empty
```typescript
@for (product of products(); track product.id) {
<app-product-card [product]="product" />
} @empty {
<p>No products available</p>
}
```
**CRITICAL:** Always provide `track` expression:
- Best: `track item.id` or `track item.uuid`
- Static lists: `track $index`
- **NEVER:** `track identity(item)` (causes full re-render)
**Contextual variables:** `$count`, `$index`, `$first`, `$last`, `$even`, `$odd`
### @switch
```typescript
@switch (viewMode()) {
@case ('grid') { <app-grid-view /> }
@case ('list') { <app-list-view /> }
@default { <app-grid-view /> }
}
```
## @defer Lazy Loading
### Basic Usage
```typescript
@defer (on viewport) {
<app-heavy-chart />
} @placeholder (minimum 500ms) {
<div class="skeleton"></div>
} @loading (after 100ms; minimum 1s) {
<mat-spinner />
} @error {
<p>Failed to load</p>
}
```
### Triggers
| Trigger | Use Case |
|---------|----------|
| `idle` (default) | Non-critical features |
| `viewport` | Below-the-fold content |
| `interaction` | User-initiated (click/keydown) |
| `hover` | Tooltips/popovers |
| `timer(Xs)` | Delayed content |
| `when(expr)` | Custom condition |
**Multiple triggers:** `@defer (on interaction; on timer(5s))`
**Prefetching:** `@defer (on interaction; prefetch on idle)`
### Requirements
- Components **MUST be standalone**
- No `@ViewChild`/`@ContentChild` references
- Reserve space in `@placeholder` to prevent layout shift
### Best Practices
- ✅ Defer below-the-fold content
- ❌ Never defer above-the-fold (harms LCP)
- ❌ Avoid `immediate`/`timer` during initial render (harms TTI)
- Test with network throttling
## Content Projection
### Single Slot
```typescript
@Component({
selector: 'ui-card',
template: `<div class="card"><ng-content></ng-content></div>`
})
```
### Multi-Slot with Selectors
```typescript
@Component({
template: `
<header><ng-content select="card-header"></ng-content></header>
<main><ng-content select="card-body"></ng-content></main>
<footer><ng-content></ng-content></footer> <!-- default slot -->
`
})
```
**Usage:**
```html
<ui-card>
<card-header><h3>Title</h3></card-header>
<card-body><p>Content</p></card-body>
<button>Action</button> <!-- goes to default slot -->
</ui-card>
```
**Fallback content:** `<ng-content select="title">Default Title</ng-content>`
**Aliasing:** `<h3 ngProjectAs="card-header">Title</h3>`
### CRITICAL Constraint
`ng-content` **always instantiates** (even if hidden). For conditional projection, use `ng-template` + `NgTemplateOutlet`.
## Template References
### ng-template
```html
<ng-template #userCard let-user="userData" let-index="i">
<div class="user">#{{index}}: {{user.name}}</div>
</ng-template>
<ng-container
*ngTemplateOutlet="userCard; context: {userData: currentUser(), i: 0}">
</ng-container>
```
**Access in component:**
```typescript
myTemplate = viewChild<TemplateRef<unknown>>('myTemplate');
```
### ng-container
Groups elements without DOM footprint:
```html
<p>
Hero's name is
<ng-container @if="hero()">{{hero().name}}</ng-container>.
</p>
```
## Variables
### @let (Angular 18.1+)
```typescript
@let userName = user().name;
@let greeting = 'Hello, ' + userName;
@let asyncData = data$ | async;
<h1>{{greeting}}</h1>
```
**Scoped to current view** (not hoisted to parent/sibling).
### Template References (#)
```html
<input #emailInput type="email" />
<button (click)="sendEmail(emailInput.value)">Send</button>
<app-datepicker #startDate />
<button (click)="startDate.open()">Open</button>
```
## Binding Patterns
**Property:** `[disabled]="!isValid()"`
**Attribute:** `[attr.aria-label]="label()"` `[attr.data-what]="'card'"`
**Event:** `(click)="save()"` `(input)="onInput($event)"`
**Two-way:** `[(ngModel)]="userName"`
**Class:** `[class.active]="isActive()"` or `[class]="{active: isActive()}"`
**Style:** `[style.width.px]="width()"` or `[style]="{color: textColor()}"`
## Best Practices
1. **Use signals:** `isExpanded = signal(false)`
2. **Prefer control flow over directives:** Use `@if` not `*ngIf`
3. **Keep expressions simple:** Use `computed()` for complex logic
4. **Testing & Accessibility:** Always add E2E and ARIA attributes (see **[html-template](../html-template/SKILL.md)** skill)
5. **Track expressions:** Required in `@for`, use unique IDs
## Migration
| Legacy | Modern |
|--------|--------|
| `*ngIf="condition"` | `@if (condition) { }` |
| `*ngFor="let item of items"` | `@for (item of items; track item.id) { }` |
| `[ngSwitch]` | `@switch (value) { @case ('a') { } }` |
**CLI migration:** `ng generate @angular/core:control-flow`
## Reference Files
For detailed examples and edge cases, see:
- `references/control-flow-reference.md` - @if/@for/@switch patterns
- `references/defer-patterns.md` - Lazy loading strategies
- `references/projection-patterns.md` - Advanced ng-content
- `references/template-reference.md` - ng-template/ng-container
Search with: `grep -r "pattern" references/`

View File

@@ -1,151 +0,0 @@
---
name: api-change-analyzer
description: This skill should be used when checking for breaking changes before API regeneration, assessing backend API update impact, or user mentions "check breaking changes", "API diff", "impact assessment". Analyzes Swagger/OpenAPI spec changes, categorizes as breaking/compatible/warnings, and provides migration strategies.
---
# API Change Analyzer
## Overview
Analyze Swagger/OpenAPI specification changes to detect breaking changes before regeneration. Provides detailed comparison, impact analysis, and migration recommendations.
## When to Use This Skill
Invoke when user wants to:
- Check API changes before regeneration
- Assess impact of backend updates
- Plan migration for breaking changes
- Mentioned "breaking changes" or "API diff"
## Analysis Workflow
### Step 1: Backup and Generate Temporarily
```bash
cp -r generated/swagger/[api-name] /tmp/[api-name].backup
npm run generate:swagger:[api-name]
```
### Step 2: Compare Files
```bash
diff -u /tmp/[api-name].backup/models.ts generated/swagger/[api-name]/models.ts
diff -u /tmp/[api-name].backup/services.ts generated/swagger/[api-name]/services.ts
```
### Step 3: Categorize Changes
**🔴 Breaking (Critical):**
- Removed properties from response models
- Changed property types (string → number)
- Removed endpoints
- Optional → required fields
- Removed enum values
**✅ Compatible (Safe):**
- Added properties (non-breaking)
- New endpoints
- Added optional parameters
- New enum values
**⚠️ Warnings (Review):**
- Property renamed (old removed + new added)
- Changed default values
- Changed validation rules
- Added required request fields
### Step 4: Analyze Impact
For each breaking change, use `Explore` agent to find usages:
```bash
# Example: Find usages of removed property
grep -r "removedProperty" libs/*/data-access --include="*.ts"
```
List:
- Affected files
- Services impacted
- Estimated refactoring effort
### Step 5: Generate Migration Strategy
Based on severity:
**High Impact (multiple breaking changes):**
1. Create migration branch
2. Document all changes
3. Update services incrementally
4. Comprehensive testing
**Medium Impact:**
1. Fix compilation errors
2. Update affected tests
3. Deploy with monitoring
**Low Impact:**
1. Minor updates
2. Deploy
### Step 6: Create Report
```
API Breaking Changes Analysis
==============================
API: [api-name]
Analysis Date: [timestamp]
📊 Summary
----------
Breaking Changes: XX
Warnings: XX
Compatible Changes: XX
🔴 Breaking Changes
-------------------
1. Removed Property: OrderResponse.deliveryDate
Files Affected: 2
- libs/oms/data-access/src/lib/services/order.service.ts:45
- libs/oms/feature/order-detail/src/lib/component.ts:78
Impact: Medium
Fix: Remove references or use alternativeDate
2. Type Changed: ProductResponse.price (string → number)
Files Affected: 1
- libs/catalogue/data-access/src/lib/services/product.service.ts:32
Impact: High
Fix: Update parsing logic
⚠️ Warnings
-----------
1. Possible Rename: CustomerResponse.customerName → fullName
Action: Verify with backend team
✅ Compatible Changes
---------------------
1. Added Property: OrderResponse.estimatedDelivery
2. New Endpoint: GET /api/v2/orders/bulk
💡 Migration Strategy
---------------------
Approach: [High/Medium/Low Impact]
Estimated Effort: [hours]
Steps: [numbered list]
🎯 Recommendation
-----------------
[Proceed with sync / Fix critical issues first / Coordinate with backend]
```
### Step 7: Cleanup
```bash
rm -rf /tmp/[api-name].backup
# Or restore if needed
```
## References
- CLAUDE.md API Integration
- Semantic Versioning: https://semver.org

View File

@@ -0,0 +1,362 @@
---
name: api-sync
description: This skill should be used when regenerating Swagger/OpenAPI TypeScript API clients with breaking change detection. Handles generation of all 10 API clients (or specific ones), pre-generation impact analysis, Unicode cleanup, TypeScript validation, and affected test execution. Use when user requests "API sync", "regenerate swagger", "check breaking changes", or indicates backend API changes.
---
# API Sync Manager
## Overview
Automate the complete lifecycle of TypeScript API client regeneration from Swagger/OpenAPI specifications. Provides pre-generation breaking change detection, automatic post-processing, impact analysis, validation, and migration recommendations for all 10 API clients in the ISA-Frontend monorepo.
## Available APIs
availability-api, cat-search-api, checkout-api, crm-api, eis-api, inventory-api, isa-api, oms-api, print-api, wws-api
## Unified Sync Workflow
### Step 1: Pre-Generation Check
```bash
# Check uncommitted changes
git status generated/swagger/
# Verify no manual edits will be lost
git diff generated/swagger/
```
If uncommitted changes exist, warn user and ask to proceed. If manual edits detected, strongly recommend committing first.
### Step 2: Pre-Generation Breaking Change Detection
Generate to temporary location to compare without affecting working directory:
```bash
# Backup current state
cp -r generated/swagger/[api-name] /tmp/[api-name].backup
# Generate to temp location for comparison
npm run generate:swagger:[api-name]
```
**Compare Models and Services:**
```bash
diff -u /tmp/[api-name].backup/models.ts generated/swagger/[api-name]/models.ts
diff -u /tmp/[api-name].backup/services.ts generated/swagger/[api-name]/services.ts
```
**Categorize Changes:**
**🔴 Breaking (Critical):**
- Removed properties from response models
- Changed property types (string → number, object → array)
- Removed endpoints
- Optional → required fields in request models
- Removed enum values
- Changed endpoint paths or HTTP methods
**⚠️ Warnings (Review Required):**
- Property renamed (old removed + new added)
- Changed default values
- Changed validation rules (min/max length, pattern)
- Added required request fields
- Changed parameter locations (query → body)
**✅ Compatible (Safe):**
- Added properties to response models
- New endpoints
- Added optional parameters
- New enum values
- Required → optional fields
### Step 3: Impact Analysis
For each breaking or warning change, analyze codebase impact:
```bash
# Find all imports from affected API
grep -r "from '@generated/swagger/[api-name]" libs/ --include="*.ts"
# Find usages of specific removed/changed items
grep -r "[RemovedType|removedProperty|removedMethod]" libs/*/data-access --include="*.ts"
```
**Document:**
- Affected files (with line numbers)
- Services impacted
- Components/stores using affected services
- Estimated refactoring effort (hours)
### Step 4: Generate Migration Strategy
Based on breaking change severity:
**High Impact (>5 breaking changes or critical endpoints):**
1. Create dedicated migration branch from develop
2. Document all changes in migration guide
3. Update data-access services incrementally
4. Update affected stores and components
5. Comprehensive test coverage
6. Coordinate deployment with backend team
**Medium Impact (2-5 breaking changes):**
1. Fix TypeScript compilation errors
2. Update affected data-access services
3. Update tests
4. Run affected test suite
5. Deploy with monitoring
**Low Impact (1 breaking change, minor impact):**
1. Apply minimal updates
2. Verify tests pass
3. Deploy normally
### Step 5: Execute Generation
If user approves proceeding after impact analysis:
```bash
# All APIs
npm run generate:swagger
# Specific API (if api-name provided)
npm run generate:swagger:[api-name]
```
### Step 6: Verify Unicode Cleanup
Automatic cleanup via `tools/fix-files.js` should execute during generation. Verify:
```bash
# Scan for remaining Unicode issues
grep -r "\\\\u00" generated/swagger/ || echo "✅ No Unicode issues"
```
If issues remain, run manually:
```bash
node tools/fix-files.js
```
### Step 7: TypeScript Validation
```bash
# Full TypeScript compilation check
npx tsc --noEmit
# If errors, show affected files
npx tsc --noEmit | head -20
```
Document compilation errors:
- Missing properties
- Type mismatches
- Incompatible signatures
### Step 8: Run Affected Tests
```bash
# Run tests for affected projects
npx nx affected:test --skip-nx-cache --base=HEAD~1
# Lint affected projects
npx nx affected:lint --base=HEAD~1
```
Monitor test failures and categorize:
- Mock data mismatches (update fixtures)
- Type assertion failures (update test types)
- Integration test failures (API contract changes)
### Step 9: Generate Comprehensive Report
```
API Sync Manager Report
=======================
API: [api-name | all]
Sync Date: [timestamp]
Generation: ✅ Success / ❌ Failed
📊 Change Summary
-----------------
Breaking Changes: XX
Warnings: XX
Compatible Changes: XX
Files Modified: XX
🔴 Breaking Changes
-------------------
1. [API Name] - Removed Property: OrderResponse.deliveryDate
Impact: Medium (2 files affected)
Files:
- libs/oms/data-access/src/lib/services/order.service.ts:45
- libs/oms/feature/order-detail/src/lib/component.ts:78
Fix Strategy: Remove references or use alternativeDate field
Estimated Effort: 30 minutes
2. [API Name] - Type Changed: ProductResponse.price (string → number)
Impact: High (1 file + cascading changes)
Files:
- libs/catalogue/data-access/src/lib/services/product.service.ts:32
Fix Strategy: Remove string parsing, update price calculations
Estimated Effort: 1 hour
⚠️ Warnings (Review Required)
------------------------------
1. [API Name] - Possible Rename: CustomerResponse.customerName → fullName
Action: Verify with backend team if intentional
Migration: Update references if confirmed rename
2. [API Name] - New Required Field: CreateOrderRequest.taxId
Action: Update order creation flows to provide taxId
Impact: 3 order creation forms
✅ Compatible Changes
---------------------
1. [API Name] - Added Property: OrderResponse.estimatedDelivery
2. [API Name] - New Endpoint: GET /api/v2/orders/bulk
3. [API Name] - Added Optional Parameter: includeArchived to GET /orders
📊 Validation Results
---------------------
TypeScript Compilation: ✅ Pass / ❌ XX errors
Unicode Cleanup: ✅ Complete
Tests: XX/XX passing (YY affected)
Lint: ✅ Pass / ⚠️ XX warnings
❌ Test Failures (if any)
-------------------------
1. order.service.spec.ts - Mock response missing deliveryDate
Fix: Update mock fixture
2. product.component.spec.ts - Price type assertion failed
Fix: Change expect(price).toBe("10.99") → expect(price).toBe(10.99)
💡 Migration Strategy
---------------------
Approach: [High/Medium/Low Impact]
Estimated Total Effort: [hours]
Steps:
1. [Fix compilation errors in data-access services]
2. [Update test mocks and fixtures]
3. [Update components using affected properties]
4. [Run full test suite]
5. [Deploy with backend coordination]
🎯 Recommendation
-----------------
[One of the following:]
✅ Safe to proceed - Only compatible changes detected
⚠️ Proceed with caution - Fix breaking changes before deployment
🔴 Coordinate with backend - High impact changes require migration plan
🛑 Block regeneration - Critical breaking changes, backend rollback needed
📋 Next Steps
-------------
[Specific actions user should take]
- [ ] Fix compilation errors in [files]
- [ ] Update test mocks in [files]
- [ ] Coordinate deployment timing with backend team
- [ ] Monitor [specific endpoints] after deployment
```
### Step 10: Cleanup
```bash
# Remove temporary backup
rm -rf /tmp/[api-name].backup
# Or restore if generation needs to be reverted
# cp -r /tmp/[api-name].backup generated/swagger/[api-name]
```
## Error Handling
**Generation Fails:**
- Check OpenAPI spec URLs in package.json scripts
- Verify backend API is accessible
- Check for malformed OpenAPI specification
**Unicode Cleanup Fails:**
- Run `node tools/fix-files.js` manually
- Check for new Unicode patterns not covered by script
**TypeScript Compilation Errors:**
- Review breaking changes section in report
- Update affected data-access services first
- Fix cascading type errors in consumers
**Test Failures:**
- Update mock data to match new API contracts
- Fix type assertions in tests
- Update integration test expectations
**Diff Detection Issues:**
- Ensure clean git state before running
- Check file permissions on generated/ directory
- Verify backup location is writable
## Advanced Usage
**Compare Specific Endpoints Only:**
```bash
# Extract and compare specific interface
grep -A 20 "interface OrderResponse" /tmp/[api-name].backup/models.ts
grep -A 20 "interface OrderResponse" generated/swagger/[api-name]/models.ts
```
**Bulk API Sync with Change Detection:**
Run pre-generation detection for all APIs before regenerating:
```bash
for api in availability-api cat-search-api checkout-api crm-api eis-api inventory-api isa-api oms-api print-api wws-api; do
echo "Checking $api..."
# Run detection workflow for each
done
```
Generate consolidated report across all APIs before committing.
**Rollback Procedure:**
```bash
# If sync causes critical issues
git checkout generated/swagger/[api-name]
git clean -fd generated/swagger/[api-name]
# Or restore from backup
cp -r generated/swagger.backup.[timestamp]/* generated/swagger/
```
## Integration with Git Workflow
**Recommended Commit Message:**
```
feat(api): sync [api-name] API client [TASK-####]
Breaking changes:
- Removed OrderResponse.deliveryDate (use estimatedDelivery)
- Changed ProductResponse.price type (string → number)
Compatible changes:
- Added OrderResponse.estimatedDelivery
- New endpoint GET /api/v2/orders/bulk
```
**Branch Strategy:**
- Low impact: Commit to feature branch directly
- Medium impact: Create `sync/[api-name]-[date]` branch
- High impact: Create `migration/[api-name]-[version]` branch with migration guide
## References
- CLAUDE.md API Integration section
- package.json swagger generation scripts
- tools/fix-files.js for Unicode cleanup logic
- Semantic Versioning: https://semver.org

View File

@@ -1,37 +1,31 @@
---
name: architecture-documentation
name: arch-docs
description: Generate architecture documentation (C4, Arc42, ADRs, PlantUML). Auto-invoke when user mentions "architecture docs", "C4 model", "ADR", "document architecture", "system design", or "create architecture diagram".
---
# Architecture Documentation Skill
# Architecture Documentation
Generate comprehensive architecture documentation using modern frameworks and best practices.
## When to Use
- Creating or updating architecture documentation
- Generating C4 model diagrams (Context, Container, Component, Code)
- Writing Architecture Decision Records (ADRs)
- Documenting system design and component relationships
- Creating PlantUML or Mermaid diagrams
## Available Frameworks
### C4 Model
Best for: Visualizing software architecture at different abstraction levels
Visualize software architecture at different abstraction levels.
Levels:
**Levels:**
1. **Context** - System landscape and external actors
2. **Container** - High-level technology choices (apps, databases, etc.)
3. **Component** - Internal structure of containers
4. **Code** - Class/module level detail (optional)
See: `@references/c4-model.md` for patterns and examples
**When to use:** Visualizing system architecture, creating diagrams for stakeholders at different technical levels.
See `references/c4-model.md` for detailed patterns and examples.
### Arc42 Template
Best for: Comprehensive architecture documentation
Comprehensive architecture documentation covering all aspects.
Sections:
**Sections:**
1. Introduction and Goals
2. Constraints
3. Context and Scope
@@ -45,16 +39,22 @@ Sections:
11. Risks and Technical Debt
12. Glossary
See: `@references/arc42.md` for template structure
**When to use:** Creating complete architecture documentation, documenting system-wide concerns, establishing quality goals.
See `references/arc42.md` for complete template structure.
### Architecture Decision Records (ADRs)
Best for: Documenting individual architectural decisions
Document individual architectural decisions with context and consequences.
See: `@references/adr-template.md` for format and examples
**When to use:** Recording significant decisions, documenting trade-offs, tracking architectural evolution.
See `references/adr-template.md` for format and examples.
## Workflow
### 1. Discovery Phase
Understand the system structure:
```bash
# Find existing architecture files
find . -name "*architecture*" -o -name "*.puml" -o -name "*.mmd"
@@ -73,7 +73,7 @@ ls -la docs/adr/ docs/decisions/
- Map data flow from API definitions
### 3. Documentation Phase
Based on the request, create appropriate documentation:
Choose appropriate output format based on request:
**For C4 diagrams:**
```
@@ -100,7 +100,7 @@ docs/architecture/
└── ...
```
## ISA-Frontend Specific Context
## ISA-Frontend Context
### Monorepo Structure
- **apps/**: Angular applications
@@ -169,3 +169,9 @@ graph TD
3. **Keep ADRs Atomic** - One decision per ADR
4. **Version Control** - Commit documentation with code changes
5. **Review Regularly** - Architecture docs decay; schedule reviews
## References
- `references/c4-model.md` - C4 model patterns, templates, and ISA-Frontend domain structure
- `references/arc42.md` - Complete Arc42 template with all 12 sections
- `references/adr-template.md` - ADR format with examples and naming conventions

View File

@@ -1,208 +0,0 @@
---
name: architecture-enforcer
description: This skill should be used when checking architecture compliance, validating layer boundaries (Feature→Feature violations), detecting circular dependencies, or user mentions "check architecture", "validate boundaries", "check imports". Validates import boundaries and architectural rules in ISA-Frontend monorepo.
---
# Architecture Enforcer
## Overview
Validate and enforce architectural boundaries in the monorepo. Checks import rules, detects violations, generates dependency graphs, and suggests refactoring.
## When to Use This Skill
Invoke when user wants to:
- Validate import boundaries
- Check architectural rules
- Find dependency violations
- Mentioned "check architecture" or "validate imports"
## Architectural Rules
**✅ Allowed:**
- Feature → Data Access
- Feature → UI
- Feature → Util
- Data Access → Util
**❌ Forbidden:**
- Feature → Feature
- Data Access → Feature
- UI → Feature
- Cross-domain (OMS ↔ Remission)
## Enforcement Workflow
### Step 1: Run Nx Dependency Checks
```bash
# Lint all (includes boundary checks)
npx nx run-many --target=lint --all
# Or specific library
npx nx lint [library-name]
```
### Step 2: Generate Dependency Graph
```bash
# Visual graph
npx nx graph
# Focus on specific project
npx nx graph --focus=[library-name]
# Affected projects
npx nx affected:graph
```
### Step 3: Scan for Violations
**Check for Circular Dependencies:**
Use `Explore` agent to find A→B→A patterns.
**Check Layer Violations:**
```bash
# Find feature-to-feature imports
grep -r "from '@isa/[^/]*/feature" libs/*/feature/ --include="*.ts"
```
**Check Relative Imports:**
```bash
# Should use path aliases, not relative
grep -r "from '\.\./\.\./\.\." libs/ --include="*.ts"
```
**Check Direct Swagger Imports:**
```bash
# Should go through data-access
grep -r "from '@generated/swagger" libs/*/feature/ --include="*.ts"
```
### Step 4: Categorize Violations
**🔴 Critical:**
- Circular dependencies
- Feature → Feature
- Data Access → Feature
- Cross-domain dependencies
**⚠️ Warnings:**
- Relative imports (should use aliases)
- Missing tags in project.json
- Deep import paths
** Info:**
- Potential shared utilities
### Step 5: Generate Violation Report
For each violation:
```
📍 libs/oms/feature/return-search/src/lib/component.ts:12
🔴 Layer Violation
❌ Feature importing from another feature
Import: import { OrderList } from '@isa/oms/feature-order-list';
Issue: Feature libraries should not depend on other features
Fix: Move shared component to @isa/shared/* or @isa/ui/*
```
### Step 6: Suggest Refactoring
**For repeated patterns:**
- Create shared library for common components
- Extract shared utilities to util library
- Move API clients to data-access layer
- Create facade services
### Step 7: Visualize Problems
```bash
npx nx graph --focus=[problematic-library]
```
### Step 8: Generate Report
```
Import Boundary Analysis
========================
Scope: [All | Specific library]
📊 Summary
----------
Total violations: XX
🔴 Critical: XX
⚠️ Warnings: XX
Info: XX
🔍 Violations by Type
---------------------
Layer violations: XX
Domain violations: XX
Circular dependencies: XX
Path alias violations: XX
🔴 Critical Violations
----------------------
1. [File:Line]
Issue: Feature → Feature dependency
Fix: Extract to @isa/shared/component-name
2. [File:Line]
Issue: Circular dependency
Fix: Extract interface to util library
💡 Refactoring Recommendations
-------------------------------
1. Create @isa/shared/order-components
- Move: [list of shared components]
- Benefits: Reusable, breaks circular deps
2. Extract interfaces to @isa/oms/util
- Move: [list of interfaces]
- Benefits: Breaks circular dependencies
📈 Dependency Graph
-------------------
npx nx graph --focus=[library]
🎯 Next Steps
-------------
1. Fix critical violations
2. Update ESLint config
3. Refactor shared components
4. Re-run: architecture-enforcer
```
## Common Fixes
**Circular Dependencies:**
```typescript
// Extract shared interface to util
// @isa/oms/util
export interface OrderId { id: string; }
// Both services import from util
import { OrderId } from '@isa/oms/util';
```
**Layer Violations:**
```typescript
// Move shared component from feature to ui
// Before: @isa/oms/feature-shared
// After: @isa/ui/order-components
```
**Path Alias Usage:**
```typescript
// BEFORE: import { Service } from '../../../data-access/src/lib/service';
// AFTER: import { Service } from '@isa/oms/data-access';
```
## References
- CLAUDE.md Architecture section
- Nx enforce-module-boundaries: https://nx.dev/nx-api/eslint-plugin/documents/enforce-module-boundaries
- tsconfig.base.json (path aliases)

View File

@@ -0,0 +1,590 @@
---
name: architecture-validator
description: This skill should be used when validating architecture compliance, checking import boundaries, detecting circular dependencies, finding layer violations (Feature→Feature), or user mentions "check architecture", "validate boundaries", "circular dependencies", "dependency cycles". Validates architectural rules, detects cycles using graph analysis, and provides fix strategies for the ISA-Frontend monorepo.
---
# Architecture Validator
## Overview
Validate and enforce architectural boundaries in the ISA-Frontend monorepo. Detects import boundary violations, circular dependencies, layer violations, and cross-domain dependencies. Provides automated fix strategies using graph analysis, dependency injection, interface extraction, and shared code refactoring.
## Architectural Rules
**Allowed Dependencies:**
- Feature → Data Access
- Feature → UI
- Feature → Util
- Data Access → Util
**Forbidden Dependencies:**
- Feature → Feature
- Data Access → Feature
- UI → Feature
- Cross-domain (OMS ↔ Remission)
**Severity Levels:**
🔴 **Critical (Must Fix):**
- Circular dependencies in services/data-access
- Feature → Feature dependencies
- Data Access → Feature dependencies
- Cross-domain violations
⚠️ **Warning (Should Fix):**
- Component-to-component cycles
- Relative import paths (should use aliases)
- Missing architectural tags in project.json
- Deep import paths
**Info (Review):**
- Type-only circular references (may be acceptable)
- Potential shared utilities
- Test file circular imports
## Validation Workflow
### Step 1: Run Automated Checks
**Nx Dependency Validation:**
```bash
# Lint all projects (includes boundary checks)
npx nx run-many --target=lint --all
# Or specific library
npx nx lint [library-name]
# Check for circular dependency warnings
npx nx run-many --target=lint --all 2>&1 | grep -i "circular"
```
**TypeScript Compilation Check:**
```bash
# Detect cycles through compilation
npx tsc --noEmit --strict 2>&1 | grep -i "circular\|cycle"
```
**Madge Analysis (if installed):**
```bash
# Install globally if needed
npm install -g madge
# Detect circular dependencies
madge --circular --extensions ts libs/
# Generate visual dependency graph
madge --circular --image circular-deps.svg libs/
```
### Step 2: Generate Dependency Graph
```bash
# Visual interactive graph
npx nx graph
# Focus on specific problematic project
npx nx graph --focus=[library-name]
# Show only affected projects
npx nx affected:graph
```
### Step 3: Scan for Specific Violations
**Feature-to-Feature Imports:**
```bash
grep -r "from '@isa/[^/]*/feature" libs/*/feature/ --include="*.ts"
```
**Relative Import Paths:**
```bash
# Should use path aliases, not relative paths
grep -r "from '\.\./\.\./\.\." libs/ --include="*.ts"
```
**Direct Swagger Imports in Features:**
```bash
# Features should go through data-access layer
grep -r "from '@generated/swagger" libs/*/feature/ --include="*.ts"
```
**Cross-Domain Imports:**
```bash
# OMS should not import from Remission and vice versa
grep -r "from '@isa/remission" libs/oms/ --include="*.ts"
grep -r "from '@isa/oms" libs/remission/ --include="*.ts"
```
### Step 4: Analyze Each Violation
For each detected issue, categorize and document:
```
📍 Violation Location
libs/oms/feature-return-search/src/lib/component.ts:12
🔴 Violation Type: Feature → Feature Layer Violation
Import Statement:
import { OrderList } from '@isa/oms/feature-order-list';
Issue Analysis:
Feature libraries should not depend on other features.
This creates tight coupling and prevents independent deployment.
Cycle Path (if circular):
1. libs/oms/data-access/src/lib/services/order.service.ts
→ imports OrderValidator
2. libs/oms/data-access/src/lib/validators/order.validator.ts
→ imports OrderService
3. Back to order.service.ts (CYCLE)
Severity: 🔴 Critical
Files Involved: 2
```
### Step 5: Choose Fix Strategy
**Strategy 1: Extract to Shared Library**
Best for: Components, utilities, or types used across multiple features
```typescript
// BEFORE (Violation)
// @isa/oms/feature-return-search imports from @isa/oms/feature-order-list
import { OrderList } from '@isa/oms/feature-order-list';
// AFTER (Fixed)
// Create @isa/ui/order-components or @isa/oms/ui-order
export { OrderList } from './order-list.component';
// Both features now import from shared UI library
import { OrderList } from '@isa/ui/order-components';
```
**Strategy 2: Interface Extraction**
Best for: Breaking circular dependencies with type-only references
```typescript
// BEFORE (Circular: order.ts ↔ customer.ts)
// order.ts
import { Customer } from './customer';
export class Order {
customer: Customer;
}
// customer.ts
import { Order } from './order';
export class Customer {
orders: Order[];
}
// AFTER (Fixed)
// Create order.interface.ts
export interface IOrder {
id: string;
customerId: string;
}
// Create customer.interface.ts
export interface ICustomer {
id: string;
orderIds: string[];
}
// order.ts imports only ICustomer
import { ICustomer } from './customer.interface';
export class Order implements IOrder {
customer: ICustomer;
}
// customer.ts imports only IOrder
import { IOrder } from './order.interface';
export class Customer implements ICustomer {
orders: IOrder[];
}
```
**Strategy 3: Dependency Injection (Lazy Loading)**
Best for: Service circular dependencies where both need each other
```typescript
// BEFORE (Circular)
import { ServiceB } from './service-b';
export class ServiceA {
constructor(private serviceB: ServiceB) {}
}
// AFTER (Fixed)
import { Injector } from '@angular/core';
import type { ServiceB } from './service-b'; // type-only import
export class ServiceA {
private serviceB!: ServiceB;
constructor(private injector: Injector) {
// Lazy injection breaks the cycle
setTimeout(() => {
this.serviceB = this.injector.get(ServiceB);
});
}
}
```
**Strategy 4: Extract to Util Library**
Best for: Shared types, interfaces, constants, or pure functions
```typescript
// BEFORE (Circular)
// order.service.ts imports validator.ts
// validator.ts imports order.service.ts
// AFTER (Fixed)
// Create @isa/oms/util/order-types.ts
export interface OrderData {
id: string;
amount: number;
}
export const ORDER_STATUS = {
PENDING: 'pending',
COMPLETED: 'completed'
} as const;
// order.service.ts imports types
import { OrderData, ORDER_STATUS } from '@isa/oms/util/order-types';
// validator.ts imports types
import { OrderData } from '@isa/oms/util/order-types';
// No more cycle
```
**Strategy 5: Forward References (Angular Components)**
Best for: Angular component circular references
```typescript
// Use forwardRef for components
import { forwardRef } from '@angular/core';
@Component({
selector: 'parent-component',
standalone: true,
imports: [forwardRef(() => ChildComponent)]
})
export class ParentComponent {}
```
**Strategy 6: Move to Data Access Layer**
Best for: Features directly importing API clients
```typescript
// BEFORE (Violation)
// Feature directly imports Swagger client
import { OrderApi } from '@generated/swagger/oms';
// AFTER (Fixed)
// Create data-access service
// @isa/oms/data-access/order-data.service.ts
@Injectable({ providedIn: 'root' })
export class OrderDataService {
private api = inject(OrderApi);
loadOrders() {
return this.api.getOrders();
}
}
// Feature imports data-access
import { OrderDataService } from '@isa/oms/data-access';
```
### Step 6: Implement Fixes
Apply chosen strategy:
1. Create new files if needed (util library, interfaces, shared components)
2. Update imports in all affected files
3. Remove circular or violating imports
4. Update architectural tags in project.json if needed
5. Document the refactoring decision
### Step 7: Validate Fixes
Run complete validation suite:
```bash
# Check cycles resolved
madge --circular --extensions ts libs/
# TypeScript compilation
npx tsc --noEmit
# Run affected tests
npx nx affected:test --skip-nx-cache
# Lint affected projects
npx nx affected:lint
# Visual verification
npx nx graph --focus=[fixed-library]
```
### Step 8: Generate Comprehensive Report
```
Architecture Validation Report
==============================
Analysis Date: [timestamp]
Scope: [All | Specific library | Affected projects]
📊 Executive Summary
--------------------
Total violations: XX
🔴 Critical: XX (must fix)
⚠️ Warning: XX (should fix)
Info: XX (review)
Fixed: XX/XX
Remaining: XX
🔍 Violations by Category
-------------------------
Layer violations: XX
- Feature → Feature: XX
- Data Access → Feature: XX
- UI → Feature: XX
Circular dependencies: XX
- Service cycles: XX
- Component cycles: XX
- Model cycles: XX
Import violations: XX
- Relative imports: XX
- Direct Swagger imports: XX
- Deep import paths: XX
Domain violations: XX
- OMS ↔ Remission: XX
🔴 Critical Violations
----------------------
1. Feature → Feature Dependency
📍 libs/oms/feature-return-search/src/lib/component.ts:12
❌ import { OrderList } from '@isa/oms/feature-order-list'
Issue: Feature libraries should not depend on other features
Impact: Creates tight coupling, prevents independent deployment
Fix Strategy: Extract to Shared UI Library
Action: Create @isa/ui/order-components
Files to Move: OrderList, OrderDetails (2 components)
Effort: ~30 minutes
Status: ✅ FIXED
2. Circular Dependency (Service Layer)
📍 Cycle detected in data-access layer
Path:
1. libs/oms/data-access/src/lib/services/order.service.ts:8
→ imports OrderValidator
2. libs/oms/data-access/src/lib/validators/order.validator.ts:15
→ imports OrderService
3. Back to order.service.ts (CYCLE COMPLETES)
Issue: Service circular reference prevents proper initialization
Impact: Runtime errors, initialization failures
Fix Strategy: Extract to Util Library
Action: Create @isa/oms/util/order-types.ts
Move: OrderData interface, ORDER_STATUS constants
Effort: ~15 minutes
Status: ✅ FIXED
⚠️ Warnings
-----------
1. Relative Import Paths
📍 libs/oms/feature-order/src/lib/component.ts:5
❌ import { Service } from '../../../data-access/src/lib/service'
✅ import { Service } from '@isa/oms/data-access'
Count: 12 occurrences
Fix: Replace with path aliases from tsconfig.base.json
Status: 🔄 IN PROGRESS
💡 Fix Strategies Applied
--------------------------
Strategy | Count | Success Rate
----------------------------|-------|-------------
Extract to Shared Library | 3 | 100%
Interface Extraction | 2 | 100%
Extract to Util Library | 4 | 100%
Dependency Injection | 1 | 100%
Move to Data Access | 2 | 100%
📈 Dependency Graph Analysis
-----------------------------
Before: XX dependencies (YY violations)
After: XX dependencies (ZZ violations)
Improvement: XX% reduction in violations
View graph: npx nx graph --focus=[library]
✅ Validation Results
---------------------
- Madge check: ✅ No circular dependencies
- TypeScript: ✅ Compilation successful
- Tests: ✅ 125/125 passing
- Lint: ✅ All projects passing
- Nx graph: ✅ No forbidden dependencies
🎯 Remaining Work
-----------------
1. Fix XX relative import warnings
2. Add architectural tags to XX libraries
3. Review XX type-only circular references
🛡️ Prevention Recommendations
------------------------------
1. Enable ESLint rule: import/no-cycle
2. Add pre-commit hook for cycle detection
3. Regular architecture reviews (monthly)
4. Update onboarding docs with architecture rules
5. Create library scaffolder with proper tags
📚 Architecture Documentation
------------------------------
- Nx boundaries: https://nx.dev/nx-api/eslint-plugin/documents/enforce-module-boundaries
- ISA-Frontend architecture: docs/architecture.md
- Path aliases: tsconfig.base.json
- Library reference: docs/library-reference.md
```
## Prevention Strategies
### ESLint Configuration
Add to `.eslintrc.json`:
```json
{
"rules": {
"import/no-cycle": ["error", { "maxDepth": 1 }],
"@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "type:feature",
"onlyDependOnLibsWithTags": [
"type:data-access",
"type:ui",
"type:util"
]
}
]
}
]
}
}
```
### Pre-commit Hook
Create `.husky/pre-commit`:
```bash
#!/bin/bash
echo "🔍 Checking for circular dependencies..."
madge --circular --extensions ts libs/
if [ $? -ne 0 ]; then
echo "❌ Circular dependencies detected"
echo "Run 'npx nx run architecture-validator' to fix"
exit 1
fi
echo "✅ No circular dependencies found"
```
### Nx Project Tags
Ensure all libraries have proper tags in `project.json`:
```json
{
"tags": [
"domain:oms",
"type:feature"
]
}
```
## Common Patterns and Solutions
### Pattern: Shared Components Between Features
**Problem:** Multiple features need the same component
**Solution:** Create shared UI library
```bash
npx nx g @nx/angular:library ui/order-components \
--directory=libs/ui/order-components \
--tags=type:ui,scope:shared
```
### Pattern: Service Mutual Dependency
**Problem:** ServiceA needs ServiceB, ServiceB needs ServiceA
**Solution:** Use lazy injection with Injector or extract shared interface
### Pattern: Type Circular Reference
**Problem:** Model A references Model B, Model B references Model A
**Solution:** Extract interfaces to separate files, use type-only imports
### Pattern: Feature Needs API Client
**Problem:** Feature directly imports Swagger-generated client
**Solution:** Create data-access service as abstraction layer
## Tool Reference
**Nx Commands:**
- `npx nx graph` - Visual dependency graph
- `npx nx lint [project]` - Lint with boundary checks
- `npx nx affected:graph` - Show affected projects
**Madge Commands:**
- `madge --circular --extensions ts libs/` - Detect cycles
- `madge --image graph.svg libs/` - Generate visual graph
- `madge --json libs/ > deps.json` - Export dependency data
**Grep Patterns:**
- Feature→Feature: `grep -r "from '@isa/[^/]*/feature" libs/*/feature/`
- Relative imports: `grep -r "from '\.\./\.\./\.\." libs/`
- Swagger direct: `grep -r "from '@generated/swagger" libs/*/feature/`
## References
- Nx enforce-module-boundaries: https://nx.dev/nx-api/eslint-plugin/documents/enforce-module-boundaries
- Madge circular dependency detection: https://github.com/pahen/madge
- ESLint import plugin: https://github.com/import-js/eslint-plugin-import
- CLAUDE.md Architecture section
- ISA-Frontend architecture docs: docs/architecture.md
- Path aliases configuration: tsconfig.base.json

View File

@@ -1,249 +0,0 @@
---
name: circular-dependency-resolver
description: This skill should be used when build fails with circular import warnings, user mentions "circular dependencies" or "dependency cycles", or fixing A→B→C→A import cycles. Detects and resolves circular dependencies using graph algorithms with DI, interface extraction, and shared code fix strategies.
---
# Circular Dependency Resolver
## Overview
Detect and resolve circular dependencies using graph analysis, multiple fix strategies, and automated validation. Prevents runtime and build issues caused by dependency cycles.
## When to Use This Skill
Invoke when user:
- Mentions "circular dependencies"
- Has import cycle errors
- Requests dependency analysis
- Build fails with circular import warnings
## Resolution Workflow
### Step 1: Detect Circular Dependencies
**Using Nx:**
```bash
npx nx run-many --target=lint --all 2>&1 | grep -i "circular"
```
**Using madge (if installed):**
```bash
npm install -g madge
madge --circular --extensions ts libs/
madge --circular --image circular-deps.svg libs/
```
**Using TypeScript:**
```bash
npx tsc --noEmit --strict 2>&1 | grep -i "circular\|cycle"
```
### Step 2: Analyze Each Cycle
For each cycle found:
```
📍 Circular Dependency Detected
Cycle Path:
1. libs/oms/data-access/src/lib/services/order.service.ts
→ imports OrderValidator
2. libs/oms/data-access/src/lib/validators/order.validator.ts
→ imports OrderService
3. Back to order.service.ts
Type: Service-Validator circular reference
Severity: 🔴 Critical
Files Involved: 2
```
### Step 3: Categorize by Severity
**🔴 Critical (Must Fix):**
- Service-to-service cycles
- Data-access layer cycles
- Store dependencies creating cycles
**⚠️ Warning (Should Fix):**
- Component-to-component cycles
- Model cross-references
- Utility function cycles
** Info (Review):**
- Type-only circular references (may be acceptable)
- Test file circular imports
### Step 4: Choose Fix Strategy
**Strategy 1: Extract to Shared Utility**
```typescript
// BEFORE (Circular)
// order.service.ts imports validator.ts
// validator.ts imports order.service.ts
// AFTER (Fixed)
// Create @isa/oms/util/types.ts
export interface OrderData { id: string; }
// order.service.ts imports types
// validator.ts imports types
// No more cycle
```
**Strategy 2: Dependency Injection (Lazy)**
```typescript
// BEFORE
import { ServiceB } from './service-b';
export class ServiceA {
constructor(private serviceB: ServiceB) {}
}
// AFTER
import { Injector } from '@angular/core';
export class ServiceA {
private serviceB!: ServiceB;
constructor(private injector: Injector) {
setTimeout(() => {
this.serviceB = this.injector.get(ServiceB);
});
}
}
```
**Strategy 3: Interface Extraction**
```typescript
// BEFORE (Models with circular reference)
// order.ts ↔ customer.ts
// AFTER
// order.interface.ts - no imports
export interface IOrder { customerId: string; }
// customer.interface.ts - no imports
export interface ICustomer { orderIds: string[]; }
// order.ts imports only ICustomer
// customer.ts imports only IOrder
```
**Strategy 4: Move Shared Code**
```typescript
// BEFORE
// feature-a imports feature-b
// feature-b imports feature-a
// AFTER
// Extract to @isa/shared/[name]
// Both features import from shared
```
**Strategy 5: Forward References (Angular)**
```typescript
// Use forwardRef for components
import { forwardRef } from '@angular/core';
@Component({
imports: [forwardRef(() => ChildComponent)]
})
```
### Step 5: Implement Fix
Apply chosen strategy:
1. Create new files if needed (util library, interfaces)
2. Update imports in both files
3. Remove circular import
### Step 6: Validate Fix
```bash
# Check cycle resolved
madge --circular --extensions ts libs/
# TypeScript compilation
npx tsc --noEmit
# Run tests
npx nx affected:test --skip-nx-cache
# Lint
npx nx affected:lint
```
### Step 7: Generate Report
```
Circular Dependency Resolution
===============================
Analysis Date: [timestamp]
📊 Summary
----------
Circular dependencies found: XX
🔴 Critical: XX
⚠️ Warning: XX
Fixed: XX
🔍 Detected Cycles
------------------
🔴 Critical Cycle #1 (FIXED)
Path: order.service → validator → order.service
Strategy Used: Extract to Util Library
Created: @isa/oms/util/order-types.ts
Files Modified: 2
Status: ✅ Resolved
⚠️ Warning Cycle #2 (FIXED)
Path: component-a → component-b → component-a
Strategy Used: Move Shared to @isa/ui
Created: @isa/ui/shared-component
Files Modified: 3
Status: ✅ Resolved
💡 Fix Strategies Applied
--------------------------
1. Extract to Util: XX cycles
2. Interface Extraction: XX cycles
3. Move Shared Code: XX cycles
4. Dependency Injection: XX cycles
✅ Validation
-------------
- madge check: ✅ No cycles
- TypeScript: ✅ Compiles
- Tests: ✅ XX/XX passing
- Lint: ✅ Passed
🎯 Prevention Tips
------------------
1. Add ESLint rule: import/no-cycle
2. Pre-commit hook for cycle detection
3. Regular architecture reviews
```
## Prevention
**ESLint Configuration:**
```json
{
"import/no-cycle": ["error", { "maxDepth": 1 }]
}
```
**Pre-commit Hook:**
```bash
#!/bin/bash
madge --circular --extensions ts libs/
if [ $? -ne 0 ]; then
echo "❌ Circular dependencies detected"
exit 1
fi
```
## References
- Madge: https://github.com/pahen/madge
- Nx dependency graph: https://nx.dev/features/explore-graph
- ESLint import plugin: https://github.com/import-js/eslint-plugin-import

View File

@@ -1,25 +1,14 @@
---
name: css-keyframes-animations
name: css-animations
description: This skill should be used when writing or reviewing CSS animations in Angular components. Use when creating entrance/exit animations, implementing @keyframes instead of @angular/animations, applying timing functions and fill modes, creating staggered animations, or ensuring GPU-accelerated performance. Essential for modern Angular 20+ components using animate.enter/animate.leave directives and converting legacy Angular animations to native CSS.
---
# CSS @keyframes Animations
# CSS Animations
## Overview
Implement native CSS @keyframes animations for Angular applications, replacing @angular/animations with GPU-accelerated, zero-bundle-size alternatives. This skill provides comprehensive guidance on creating performant entrance/exit animations, staggered effects, and proper timing configurations.
## When to Use This Skill
Apply this skill when:
- **Writing Angular components** with entrance/exit animations
- **Converting @angular/animations** to native CSS @keyframes
- **Implementing animate.enter/animate.leave** in Angular 20+ templates
- **Creating staggered animations** for lists or collections
- **Debugging animation issues** (snap-back, wrong starting positions, choppy playback)
- **Optimizing animation performance** for GPU acceleration
- **Reviewing animation code** for accessibility and best practices
## Quick Start
### Basic Animation Setup
@@ -311,41 +300,6 @@ element.addEventListener('animationend', (e) => {
});
```
## Resources
### references/keyframes-guide.md
Comprehensive deep-dive covering:
- Complete @keyframes syntax reference
- Detailed timing functions and cubic-bezier curves
- Advanced techniques (direction, play-state, @starting-style)
- Performance optimization strategies
- Extensive common patterns library
- Debugging techniques and troubleshooting
**When to reference:** Complex animation requirements, custom easing curves, advanced techniques, performance optimization, or learning comprehensive details.
### assets/animations.css
Ready-to-use CSS file with common animation patterns:
- Fade animations (in/out)
- Slide animations (up/down/left/right)
- Scale animations (in/out)
- Utility animations (spin, shimmer, shake, breathe, attention-pulse)
- Toast/notification animations
- Accessibility (@media prefers-reduced-motion)
**Usage:** Copy this file to project and import in component styles or global styles:
```css
@import 'path/to/animations.css';
```
Then use classes directly:
```html
<div animate.enter="fade-in" animate.leave="slide-out-down">
```
## Migration from @angular/animations
### Before (Angular Animations)
@@ -390,3 +344,38 @@ import { trigger, state, style, transition, animate } from '@angular/animations'
- GPU hardware acceleration
- Standard CSS (transferable skills)
- Better performance
## Resources
### references/keyframes-guide.md
Comprehensive deep-dive covering:
- Complete @keyframes syntax reference
- Detailed timing functions and cubic-bezier curves
- Advanced techniques (direction, play-state, @starting-style)
- Performance optimization strategies
- Extensive common patterns library
- Debugging techniques and troubleshooting
**When to reference:** Complex animation requirements, custom easing curves, advanced techniques, performance optimization, or learning comprehensive details.
### assets/animations.css
Ready-to-use CSS file with common animation patterns:
- Fade animations (in/out)
- Slide animations (up/down/left/right)
- Scale animations (in/out)
- Utility animations (spin, shimmer, shake, breathe, attention-pulse)
- Toast/notification animations
- Accessibility (@media prefers-reduced-motion)
**Usage:** Copy this file to project and import in component styles or global styles:
```css
@import 'path/to/animations.css';
```
Then use classes directly:
```html
<div animate.enter="fade-in" animate.leave="slide-out-down">
```

View File

@@ -1,299 +0,0 @@
---
name: html-template
description: This skill should be used when writing or reviewing HTML templates to ensure proper E2E testing attributes (data-what, data-which) and ARIA accessibility attributes are included. Use when creating interactive elements like buttons, inputs, links, forms, dialogs, or any HTML markup requiring testing and accessibility compliance. Works seamlessly with the angular-template skill.
---
# HTML Template - Testing & Accessibility Attributes
This skill should be used when writing or reviewing HTML templates to ensure proper testing and accessibility attributes are included.
## When to Use This Skill
Use this skill when:
- Writing or modifying Angular component templates
- Creating any HTML templates or markup
- Reviewing code for testing and accessibility compliance
- Adding interactive elements (buttons, inputs, links, etc.)
- Implementing forms, lists, navigation, or dialogs
**Works seamlessly with:**
- **[angular-template](../angular-template/SKILL.md)** - Angular template syntax, control flow, and modern patterns
- **[tailwind](../tailwind/SKILL.md)** - ISA design system styling for visual design
- **[logging](../logging/SKILL.md)** - MANDATORY logging in all Angular components using `@isa/core/logging`
## Overview
This skill provides comprehensive guidance for two critical HTML attribute categories:
### 1. E2E Testing Attributes
Enable automated end-to-end testing by providing stable selectors for QA automation:
- **`data-what`**: Semantic description of element's purpose
- **`data-which`**: Unique identifier for specific instances
- **`data-*`**: Additional contextual information
### 2. ARIA Accessibility Attributes
Ensure web applications are accessible to all users, including those using assistive technologies:
- **Roles**: Define element purpose (button, navigation, dialog, etc.)
- **Properties**: Provide additional context (aria-label, aria-describedby)
- **States**: Indicate dynamic states (aria-expanded, aria-disabled)
- **Live Regions**: Announce dynamic content changes
## Why Both Are Essential
- **E2E Attributes**: Enable reliable automated testing without brittle CSS or XPath selectors
- **ARIA Attributes**: Ensure compliance with WCAG standards and improve user experience for people with disabilities
- **Together**: Create robust, testable, and accessible web applications
## Quick Reference
### Button Example
```html
<button
type="button"
(click)="onSubmit()"
data-what="submit-button"
data-which="registration-form"
aria-label="Submit registration form">
Submit
</button>
```
### Input Example
```html
<input
type="text"
[(ngModel)]="email"
data-what="email-input"
data-which="registration-form"
aria-label="Email address"
aria-describedby="email-hint"
aria-required="true" />
<span id="email-hint">We'll never share your email</span>
```
### Dynamic List Example
```html
@for (item of items; track item.id) {
<li
(click)="selectItem(item)"
data-what="list-item"
[attr.data-which]="item.id"
[attr.data-status]="item.status"
[attr.aria-label]="'Select ' + item.name"
role="button"
tabindex="0">
{{ item.name }}
</li>
}
```
### Link Example
```html
<a
[routerLink]="['/orders', orderId]"
data-what="order-link"
[attr.data-which]="orderId"
[attr.aria-label]="'View order ' + orderNumber">
View Order #{{ orderNumber }}
</a>
```
### Dialog Example
```html
<div
class="dialog"
data-what="confirmation-dialog"
data-which="delete-item"
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description">
<h2 id="dialog-title">Confirm Deletion</h2>
<p id="dialog-description">Are you sure you want to delete this item?</p>
<button
(click)="confirm()"
data-what="confirm-button"
data-which="delete-dialog"
aria-label="Confirm deletion">
Delete
</button>
<button
(click)="cancel()"
data-what="cancel-button"
data-which="delete-dialog"
aria-label="Cancel deletion">
Cancel
</button>
</div>
```
## Common Patterns by Element Type
### Interactive Elements That Need Attributes
**Required attributes for:**
- Buttons (`<button>`, `<ui-button>`, custom button components)
- Form inputs (`<input>`, `<textarea>`, `<select>`)
- Links (`<a>`, `[routerLink]`)
- Clickable elements (elements with `(click)` handlers)
- Custom interactive components
- List items in dynamic lists
- Navigation items
- Dialog/modal controls
### Naming Conventions
**E2E `data-what` patterns:**
- `*-button` (submit-button, cancel-button, delete-button)
- `*-input` (email-input, search-input, quantity-input)
- `*-link` (product-link, order-link, customer-link)
- `*-item` (list-item, menu-item, card-item)
- `*-dialog` (confirm-dialog, error-dialog, info-dialog)
- `*-dropdown` (status-dropdown, category-dropdown)
**E2E `data-which` guidelines:**
- Use unique identifiers: `data-which="primary"`, `data-which="customer-list"`
- Bind dynamically for lists: `[attr.data-which]="item.id"`
- Combine with context: `data-which="customer-{{ customerId }}-edit"`
**ARIA role patterns:**
- Interactive elements: `button`, `link`, `menuitem`
- Structural: `navigation`, `main`, `complementary`, `contentinfo`
- Widget: `dialog`, `alertdialog`, `tooltip`, `tablist`, `tab`
- Landmark: `banner`, `search`, `form`, `region`
## Best Practices
### E2E Attributes
1. ✅ Add to ALL interactive elements
2. ✅ Use kebab-case for `data-what` values
3. ✅ Ensure `data-which` is unique within the view
4. ✅ Use Angular binding for dynamic values: `[attr.data-*]`
5. ✅ Avoid including sensitive data in attributes
6. ✅ Document complex attribute patterns in template comments
### ARIA Attributes
1. ✅ Use semantic HTML first (use `<button>` instead of `<div role="button">`)
2. ✅ Provide text alternatives for all interactive elements
3. ✅ Ensure proper keyboard navigation (tabindex, focus management)
4. ✅ Use `aria-label` when visual label is missing
5. ✅ Use `aria-labelledby` to reference existing visible labels
6. ✅ Keep ARIA attributes in sync with visual states
7. ✅ Test with screen readers (NVDA, JAWS, VoiceOver)
### Combined Best Practices
1. ✅ Add both E2E and ARIA attributes to every interactive element
2. ✅ Keep attributes close together in the HTML for readability
3. ✅ Update tests to use `data-what` and `data-which` selectors
4. ✅ Validate coverage: all interactive elements should have both types
5. ✅ Review with QA and accessibility teams
## Detailed References
For comprehensive guides, examples, and patterns, see:
- **[E2E Testing Attributes](references/e2e-attributes.md)** - Complete E2E attribute patterns and conventions
- **[ARIA Accessibility Attributes](references/aria-attributes.md)** - Comprehensive ARIA guidance and WCAG compliance
- **[Combined Patterns](references/combined-patterns.md)** - Real-world examples with both attribute types
## Project-Specific Links
- **Testing Guidelines**: `docs/guidelines/testing.md` - Project testing standards including E2E attributes
- **CLAUDE.md**: Project conventions and requirements
- **Angular Template Skill**: `.claude/skills/angular-template` - For Angular-specific syntax
## Validation Checklist
Before considering template complete:
- [ ] All buttons have `data-what`, `data-which`, and `aria-label`
- [ ] All inputs have `data-what`, `data-which`, and appropriate ARIA attributes
- [ ] All links have `data-what`, `data-which`, and descriptive ARIA labels
- [ ] Dynamic lists use `[attr.data-*]` bindings with unique identifiers
- [ ] Dialogs have proper ARIA roles and relationships
- [ ] Forms have proper field associations and error announcements
- [ ] Interactive elements are keyboard accessible (tabindex where needed)
- [ ] No duplicate `data-which` values within the same view
- [ ] Screen reader testing completed (if applicable)
## Example: Complete Form
```html
<form
data-what="registration-form"
data-which="user-signup"
role="form"
aria-labelledby="form-title">
<h2 id="form-title">User Registration</h2>
<div class="form-field">
<label for="username-input">Username</label>
<input
id="username-input"
type="text"
[(ngModel)]="username"
data-what="username-input"
data-which="registration-form"
aria-required="true"
aria-describedby="username-hint" />
<span id="username-hint">Must be at least 3 characters</span>
</div>
<div class="form-field">
<label for="email-input">Email</label>
<input
id="email-input"
type="email"
[(ngModel)]="email"
data-what="email-input"
data-which="registration-form"
aria-required="true"
[attr.aria-invalid]="emailError ? 'true' : null"
aria-describedby="email-error" />
@if (emailError) {
<span
id="email-error"
role="alert"
aria-live="polite">
{{ emailError }}
</span>
}
</div>
<div class="form-actions">
<button
type="submit"
(click)="onSubmit()"
data-what="submit-button"
data-which="registration-form"
[attr.aria-disabled]="!isValid"
aria-label="Submit registration form">
Register
</button>
<button
type="button"
(click)="onCancel()"
data-what="cancel-button"
data-which="registration-form"
aria-label="Cancel registration">
Cancel
</button>
</div>
</form>
```
## Remember
- **Always use both E2E and ARIA attributes together**
- **E2E attributes enable automated testing** - your QA team relies on them
- **ARIA attributes enable accessibility** - legal requirement and right thing to do
- **Test with real users and assistive technologies** - automated checks aren't enough
- **Keep attributes up-to-date** - maintain as code changes
---
**This skill works automatically with Angular templates. Both E2E and ARIA attributes should be added to every interactive element.**

View File

@@ -1,50 +1,43 @@
---
name: library-scaffolder
name: library-creator
description: This skill should be used when creating feature/data-access/ui/util libraries or user says "create library", "new library", "scaffold library". Creates new Angular libraries in ISA-Frontend monorepo with proper Nx configuration, Vitest setup, architectural tags, and path aliases.
---
# Library Scaffolder
# Library Creator
## Overview
Automate the creation of new Angular libraries following ISA-Frontend conventions. This skill handles the complete scaffolding workflow including Nx generation, Vitest configuration with CI/CD integration, path alias verification, and initial validation.
## When to Use This Skill
Invoke this skill when:
- User requests creating a new library
- User mentions "new library", "scaffold library", or "create feature"
- User wants to add a new domain/layer/feature to the monorepo
Automate the creation of new Angular libraries following ISA-Frontend conventions. This skill handles the complete scaffolding workflow including Nx generation, Vitest configuration with CI/CD integration, architectural tags, path alias verification, and initial validation.
## Required Parameters
User must provide:
Collect these parameters from the user:
- **domain**: Domain name (oms, remission, checkout, ui, core, shared, utils)
- **layer**: Layer type (feature, data-access, ui, util)
- **name**: Library name in kebab-case
## Scaffolding Workflow
## Library Creation Workflow
### Step 1: Validate Input
1. **Verify Domain**
**Verify Domain:**
- Use `docs-researcher` to check `docs/library-reference.md`
- Ensure domain follows existing patterns
2. **Validate Layer**
**Validate Layer:**
- Must be one of: feature, data-access, ui, util
3. **Check Name**
**Check Name:**
- Must be kebab-case
- Must not conflict with existing libraries
4. **Determine Path Depth**
**Determine Path Depth:**
- 3 levels: `libs/domain/layer/name``../../../`
- 4 levels: `libs/domain/type/layer/name``../../../../`
### Step 2: Run Dry-Run
Execute Nx generator with `--dry-run`:
Execute Nx generator with `--dry-run` to preview changes:
```bash
npx nx generate @nx/angular:library \
@@ -118,7 +111,7 @@ cat libs/[domain]/[layer]/[name]/project.json | jq '.tags'
### Step 5: Configure Vitest with JUnit and Cobertura
Update `libs/[path]/vite.config.mts`:
Update `libs/[path]/vite.config.mts` with this template:
```typescript
/// <reference types='vitest' />
@@ -152,20 +145,20 @@ defineConfig(() => ({
}));
```
**Critical**: Adjust path depth based on library location.
**Critical**: Adjust path depth (`../../../` or `../../../../`) based on library location.
### Step 6: Verify Configuration
1. **Check Path Alias**
**Check Path Alias:**
- Verify `tsconfig.base.json` was updated
- Should have: `"@isa/[domain]/[layer]/[name]": ["libs/[domain]/[layer]/[name]/src/index.ts"]`
2. **Run Initial Test**
**Run Initial Test:**
```bash
npx nx test [library-name] --coverage.enabled=true --skip-nx-cache
```
3. **Verify CI/CD Files Created**
**Verify CI/CD Files Created:**
- JUnit XML: `testresults/junit-[library-name].xml`
- Cobertura XML: `coverage/libs/[path]/cobertura-coverage.xml`
@@ -193,6 +186,8 @@ Add entry to `docs/library-reference.md` under appropriate domain:
### Step 9: Run Full Validation
Execute validation commands to ensure library is properly configured:
```bash
# Lint (includes boundary checks)
npx nx lint [library-name]
@@ -209,6 +204,8 @@ npx nx graph --focus=[library-name]
### Step 10: Generate Creation Report
Provide this structured report to the user:
```
Library Created Successfully
============================
@@ -251,18 +248,23 @@ Run lint to check for violations: npx nx lint [library-name]
## Error Handling
**Issue: Path depth mismatch**
**Path depth mismatch:**
- Count directory levels from workspace root
- Adjust `../` in outputFile and reportsDirectory
**Issue: TypeScript errors in vite.config.mts**
**TypeScript errors in vite.config.mts:**
- Add `// @ts-expect-error` before `defineConfig()`
**Issue: Path alias not working**
**Path alias not working:**
- Check tsconfig.base.json
- Run `npx nx reset`
- Restart TypeScript server
**Library already exists:**
- Check `tsconfig.base.json` for existing path alias
- Use Grep to search for existing library name
- Suggest alternative name to user
## References
- docs/guidelines/testing.md (Vitest, JUnit, Cobertura sections)
@@ -270,6 +272,6 @@ Run lint to check for violations: npx nx lint [library-name]
- CLAUDE.md (Library Organization, Testing Framework sections)
- eslint.config.js (@nx/enforce-module-boundaries configuration)
- scripts/add-library-tags.js (automatic tagging script)
- .claude/skills/architecture-enforcer (boundary validation)
- .claude/skills/architecture-validator (boundary validation)
- Nx Angular Library Generator: https://nx.dev/nx-api/angular/generators/library
- Nx Enforce Module Boundaries: https://nx.dev/nx-api/eslint-plugin/documents/enforce-module-boundaries

View File

@@ -3,17 +3,10 @@ name: logging
description: This skill should be used when working with Angular components, directives, services, pipes, guards, or TypeScript classes. Logging is MANDATORY in all Angular files. Implements @isa/core/logging with logger() factory pattern, appropriate log levels, lazy evaluation for performance, error handling, and avoids console.log and common mistakes.
---
# Logging Helper Skill
# Logging
Ensures consistent and efficient logging using `@isa/core/logging` library.
## When to Use
- Adding logging to new components/services
- Refactoring existing logging code
- Reviewing code for proper logging patterns
- Debugging logging issues
## Core Principles
### 1. Always Use Factory Pattern
@@ -228,45 +221,14 @@ describe('MyComponent', () => {
});
```
## Code Review Checklist
- [ ] Uses `logger()` factory, not `LoggingService` injection
- [ ] Appropriate log level for each message
- [ ] Context functions for expensive operations
- [ ] No sensitive information (passwords, tokens, PII)
- [ ] No `console.log` statements
- [ ] Error logs include error object
- [ ] No logging in tight loops
- [ ] Component/service identified in context
- [ ] E2E attributes on interactive elements
## Quick Reference
```typescript
// Import
import { logger, provideLoggerContext } from '@isa/core/logging';
// Create logger
#logger = logger(); // Basic
#logger = logger({ component: 'Name' }); // Static context
#logger = logger(() => ({ id: this.id })); // Dynamic context
// Log messages
this.#logger.trace('Detailed trace');
this.#logger.debug('Debug info');
this.#logger.info('General info', () => ({ key: value }));
this.#logger.warn('Warning');
this.#logger.error('Error', error, () => ({ context }));
// Component context
@Component({
providers: [provideLoggerContext({ feature: 'users' })]
})
```
## Additional Resources
- Full documentation: `libs/core/logging/README.md`
- Examples: `.claude/skills/logging-helper/examples.md`
- Quick reference: `.claude/skills/logging-helper/reference.md`
- Troubleshooting: `.claude/skills/logging-helper/troubleshooting.md`
**For more detailed information:**
- **API signatures and patterns**: See [references/api-reference.md](references/api-reference.md) for complete API documentation
- **Real-world examples**: See [references/examples.md](references/examples.md) for components, services, guards, interceptors, and more
- **Troubleshooting**: See [references/troubleshooting.md](references/troubleshooting.md) for common issues and solutions
**Project documentation:**
- Full library documentation: `libs/core/logging/README.md`

View File

@@ -1,4 +1,4 @@
# Logging Quick Reference
# Logging API Reference
## API Signatures

View File

@@ -1,287 +0,0 @@
---
name: ngrx-resource-api
description: This skill should be used when implementing Angular's Resource API with NgRx Signal Store for reactive data management. Use when creating signal stores that load data reactively, need automatic race condition prevention, or require declarative resource management without RxJS. Applies to data-access libraries, feature stores with API integration, and components needing reactive filtering or pagination.
---
# NgRx Resource API
## Overview
This skill enables integration of Angular's Resource API with NgRx Signal Store to create reactive data flows without RxJS while automatically preventing race conditions. The Resource API handles concurrent request management declaratively, eliminating manual `switchMap` or `takeUntilDestroyed` patterns.
## Core Architectural Concepts
### Reactive Flow Graph
Establish three clear interaction points in the store:
1. **Filter signals trigger resource loading** - Parameter changes automatically reload resources
2. **Methods explicitly invoke operations** - Use `signalMethod` for user-triggered actions
3. **Computed signals derive view models** - Transform loaded data for component consumption
### The withProps Pattern for Dependency Injection
Inject services via `withProps` with underscore-prefixed properties to mark them as internal implementation details:
```typescript
withProps(() => ({
_dataService: inject(DataService),
_notificationService: inject(ToastService),
}))
```
**Benefits:**
- Centralizes dependency injection in one location
- Clear distinction between internal (prefixed) and public properties
- Services available to all subsequent feature sections
## Implementation Steps
### Step 1: Inject Services with withProps
Create the initial `withProps` section to inject required services:
```typescript
export const MyStore = signalStore(
withProps(() => ({
_dataService: inject(DataService),
_toastService: inject(ToastService),
})),
// ... additional features
);
```
**Naming convention:** Prefix all injected services with underscore (`_`) to indicate internal use.
### Step 2: Define Filter State
Add state properties that will serve as resource parameters:
```typescript
withState({
filter: {
searchTerm: '',
category: '',
} as MyFilter,
})
```
### Step 3: Configure Resources
In a subsequent `withProps` section, create resources that reference the injected services and state:
```typescript
withProps((store) => ({
_itemsResource: resource({
params: store.filter,
loader: (loaderParams) => {
const filter = loaderParams.params;
const abortSignal = loaderParams.abortSignal;
return store._dataService.loadItems(filter, abortSignal);
}
})
}))
```
**Key points:**
- Resources automatically reload when `params` signal changes
- `abortSignal` enables automatic cancellation of in-flight requests
- Loader must return a Promise (use `.findPromise()` if service returns Observable)
### Step 4: Expose Read-Only Resources (Optional)
If the resource should be accessible to consumers, expose it as read-only:
```typescript
withProps((store) => ({
itemsResource: store._itemsResource.asReadonly(),
}))
```
**Pattern:** Internal resources use underscore prefix, public versions are read-only without prefix.
### Step 5: Create Signal Methods for Updates
Use `signalMethod` for actions that update state and trigger resource reloads:
```typescript
withMethods((store) => ({
updateFilter: signalMethod<MyFilter>((filter) => {
patchState(store, { filter });
}),
refresh: () => {
store._itemsResource.reload();
}
}))
```
**Important:** `signalMethod` implementations are **untracked by convention** - they don't re-execute when signals change. This provides explicit control flow.
### Step 6: Add Error Handling
Use `withHooks` to react to resource errors:
```typescript
withHooks({
onInit: (store) => {
effect(() => {
const error = store._itemsResource.error();
if (error) {
store._toastService.show('Error: ' + getMessage(error));
}
});
}
})
```
**Pattern:** Error effects should be read-only - they observe errors and trigger side effects, but don't modify state.
## Common Patterns
### Template Integration with linkedSignal
For two-way form binding that synchronizes with the store:
```typescript
export class MyComponent {
#store = inject(MyStore);
// Create linked signal for form field
searchTerm = linkedSignal(() => this.#store.filter().searchTerm);
// Combine form fields into filter object
#linkedFilter = computed(() => ({
searchTerm: this.searchTerm(),
// ... other fields
}));
constructor() {
// Sync form changes back to store
this.#store.updateFilter(this.#linkedFilter);
}
}
```
**Benefits:**
- Two-way binding for forms
- Automatic store synchronization
- Type-safe filter construction
### Working with Resource Data
Access resource data through resource signals:
```typescript
withComputed((store) => ({
items: computed(() => store._itemsResource.value() ?? []),
isLoading: computed(() => store._itemsResource.isLoading()),
hasError: computed(() => store._itemsResource.hasError()),
}))
```
**Available signals:**
- `value()` - The loaded data (undefined while loading)
- `isLoading()` - Loading state boolean
- `hasError()` - Error state boolean
- `error()` - Error object if present
- `status()` - Overall status: 'idle' | 'loading' | 'resolved' | 'error'
### Updating Resource Data Locally
For temporary working copies before server writes:
```typescript
withMethods((store) => ({
updateLocalItem: (id: string, changes: Partial<Item>) => {
store._itemsResource.update((currentItems) => {
return currentItems.map(item =>
item.id === id ? { ...item, ...changes } : item
);
});
}
}))
```
**Note:** This pattern feels unconventional but aligns with maintaining temporary working copies before server persistence.
### Multiple Resources in One Store
Combine multiple resources for complex data requirements:
```typescript
withProps((store) => ({
_itemsResource: resource({
params: store.filter,
loader: (params) => store._dataService.loadItems(params.params, params.abortSignal)
}),
_detailsResource: resource({
params: store.selectedId,
loader: (params) => {
if (!params.params) return Promise.resolve(null);
return store._dataService.loadDetails(params.params, params.abortSignal);
}
})
}))
```
**Pattern:** Each resource has independent params and loading state, but can share service instances.
## Important Considerations
### Race Condition Prevention
The Resource API automatically handles race conditions:
- New requests automatically cancel pending requests
- No need for `switchMap`, `takeUntilDestroyed`, or manual abort handling
- Declarative parameter changes trigger clean cancellation
### Untracked Signal Methods
`signalMethod` implementations deliberately skip reactive tracking:
- Provides explicit, predictable control flow
- Prevents unexpected re-executions from signal changes
- Makes side effects obvious at call sites
### Loader Function Requirements
Loaders must:
- Return a `Promise` (not Observable)
- Accept and pass through the `abortSignal` to enable cancellation
- Handle the signal in the underlying HTTP call
**Converting Observables:**
```typescript
loader: (params) => {
return firstValueFrom(
this._service.load$(params.params)
.pipe(takeUntilDestroyed(this._destroyRef))
);
}
```
### Resource Lifecycle
Resources maintain their own state machine:
1. **Idle** - Initial state before first load
2. **Loading** - Request in progress
3. **Resolved** - Data loaded successfully
4. **Error** - Request failed
State transitions automatically trigger reactive updates to dependent computeds and effects.
## When to Use This Pattern
**Use Resource API with Signal Store when:**
- Loading data based on reactive filter/search parameters
- Need automatic race condition handling for concurrent requests
- Want declarative data loading without RxJS subscriptions
- Building stores with frequently changing query parameters
- Implementing pagination, filtering, or search features
**Consider alternatives when:**
- Simple one-time data loads (use `rxMethod` or direct service calls)
- Complex Observable chains with multiple operators needed
- Need fine-grained control over request timing/caching
- Working with WebSocket or SSE streams (use `rxMethod` instead)

View File

@@ -22,6 +22,28 @@ equipped with procedural knowledge that no model can fully possess.
3. Domain expertise - Company-specific knowledge, schemas, business logic
4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks
## Core Principles
### Concise is Key
The context window is a public good. Skills share the context window with everything else Claude needs: system prompt, conversation history, other Skills' metadata, and the actual user request.
**Default assumption: Claude is already very smart.** Only add context Claude doesn't already have. Challenge each piece of information: "Does Claude really need this explanation?" and "Does this paragraph justify its token cost?"
Prefer concise examples over verbose explanations.
### Set Appropriate Degrees of Freedom
Match the level of specificity to the task's fragility and variability:
**High freedom (text-based instructions)**: Use when multiple approaches are valid, decisions depend on context, or heuristics guide the approach.
**Medium freedom (pseudocode or scripts with parameters)**: Use when a preferred pattern exists, some variation is acceptable, or configuration affects behavior.
**Low freedom (specific scripts, few parameters)**: Use when operations are fragile and error-prone, consistency is critical, or a specific sequence must be followed.
Think of Claude as exploring a path: a narrow bridge with cliffs needs specific guardrails (low freedom), while an open field allows many routes (high freedom).
### Anatomy of a Skill
Every skill consists of a required SKILL.md file and optional bundled resources:
@@ -41,7 +63,10 @@ skill-name/
#### SKILL.md (required)
**Metadata Quality:** The `name` and `description` in YAML frontmatter determine when Claude will use the skill. Be specific about what the skill does and when to use it. Use the third-person (e.g. "This skill should be used when..." instead of "Use this skill when...").
Every SKILL.md consists of:
- **Frontmatter** (YAML): Contains `name` and `description` fields. These are the only fields that Claude reads to determine when the skill gets used, thus it is very important to be clear and comprehensive in describing what the skill is, and when it should be used.
- **Body** (Markdown): Instructions and guidance for using the skill. Only loaded AFTER the skill triggers (if at all).
#### Bundled Resources (optional)
@@ -74,19 +99,118 @@ Files not intended to be loaded into context, but rather used within the output
- **Use cases**: Templates, images, icons, boilerplate code, fonts, sample documents that get copied or modified
- **Benefits**: Separates output resources from documentation, enables Claude to use files without loading them into context
#### What to Not Include in a Skill
A skill should only contain essential files that directly support its functionality. Do NOT create extraneous documentation or auxiliary files, including:
- README.md
- INSTALLATION_GUIDE.md
- QUICK_REFERENCE.md
- CHANGELOG.md
- etc.
The skill should only contain the information needed for an AI agent to do the job at hand. It should not contain auxilary context about the process that went into creating it, setup and testing procedures, user-facing documentation, etc. Creating additional documentation files just adds clutter and confusion.
### Progressive Disclosure Design Principle
Skills use a three-level loading system to manage context efficiently:
1. **Metadata (name + description)** - Always in context (~100 words)
2. **SKILL.md body** - When skill triggers (<5k words)
3. **Bundled resources** - As needed by Claude (Unlimited*)
3. **Bundled resources** - As needed by Claude (Unlimited because scripts can be executed without reading into context window)
*Unlimited because scripts can be executed without reading into context window.
#### Progressive Disclosure Patterns
Keep SKILL.md body to the essentials and under 500 lines to minimize context bloat. Split content into separate files when approaching this limit. When splitting out content into other files, it is very important to reference them from SKILL.md and describe clearly when to read them, to ensure the reader of the skill knows they exist and when to use them.
**Key principle:** When a skill supports multiple variations, frameworks, or options, keep only the core workflow and selection guidance in SKILL.md. Move variant-specific details (patterns, examples, configuration) into separate reference files.
**Pattern 1: High-level guide with references**
```markdown
# PDF Processing
## Quick start
Extract text with pdfplumber:
[code example]
## Advanced features
- **Form filling**: See [FORMS.md](FORMS.md) for complete guide
- **API reference**: See [REFERENCE.md](REFERENCE.md) for all methods
- **Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns
```
Claude loads FORMS.md, REFERENCE.md, or EXAMPLES.md only when needed.
**Pattern 2: Domain-specific organization**
For Skills with multiple domains, organize content by domain to avoid loading irrelevant context:
```
bigquery-skill/
├── SKILL.md (overview and navigation)
└── reference/
├── finance.md (revenue, billing metrics)
├── sales.md (opportunities, pipeline)
├── product.md (API usage, features)
└── marketing.md (campaigns, attribution)
```
When a user asks about sales metrics, Claude only reads sales.md.
Similarly, for skills supporting multiple frameworks or variants, organize by variant:
```
cloud-deploy/
├── SKILL.md (workflow + provider selection)
└── references/
├── aws.md (AWS deployment patterns)
├── gcp.md (GCP deployment patterns)
└── azure.md (Azure deployment patterns)
```
When the user chooses AWS, Claude only reads aws.md.
**Pattern 3: Conditional details**
Show basic content, link to advanced content:
```markdown
# DOCX Processing
## Creating documents
Use docx-js for new documents. See [DOCX-JS.md](DOCX-JS.md).
## Editing documents
For simple edits, modify the XML directly.
**For tracked changes**: See [REDLINING.md](REDLINING.md)
**For OOXML details**: See [OOXML.md](OOXML.md)
```
Claude reads REDLINING.md or OOXML.md only when the user needs those features.
**Important guidelines:**
- **Avoid deeply nested references** - Keep references one level deep from SKILL.md. All reference files should link directly from SKILL.md.
- **Structure longer reference files** - For files longer than 100 lines, include a table of contents at the top so Claude can see the full scope when previewing.
## Skill Creation Process
To create a skill, follow the "Skill Creation Process" in order, skipping steps only if there is a clear reason why they are not applicable.
Skill creation involves these steps:
1. Understand the skill with concrete examples
2. Plan reusable skill contents (scripts, references, assets)
3. Initialize the skill (run init_skill.py)
4. Edit the skill (implement resources and write SKILL.md)
5. Package the skill (run package_skill.py)
6. Iterate based on real usage
Follow these steps in order, skipping only if there is a clear reason why they are not applicable.
### Step 1: Understanding the Skill with Concrete Examples
@@ -154,27 +278,48 @@ After initialization, customize or remove the generated SKILL.md and example fil
### Step 4: Edit the Skill
When editing the (newly-generated or existing) skill, remember that the skill is being created for another instance of Claude to use. Focus on including information that would be beneficial and non-obvious to Claude. Consider what procedural knowledge, domain-specific details, or reusable assets would help another Claude instance execute these tasks more effectively.
When editing the (newly-generated or existing) skill, remember that the skill is being created for another instance of Claude to use. Include information that would be beneficial and non-obvious to Claude. Consider what procedural knowledge, domain-specific details, or reusable assets would help another Claude instance execute these tasks more effectively.
#### Learn Proven Design Patterns
Consult these helpful guides based on your skill's needs:
- **Multi-step processes**: See references/workflows.md for sequential workflows and conditional logic
- **Specific output formats or quality standards**: See references/output-patterns.md for template and example patterns
These files contain established best practices for effective skill design.
#### Start with Reusable Skill Contents
To begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`.
Also, delete any example files and directories not needed for the skill. The initialization script creates example files in `scripts/`, `references/`, and `assets/` to demonstrate structure, but most skills won't need all of them.
Added scripts must be tested by actually running them to ensure there are no bugs and that the output matches what is expected. If there are many similar scripts, only a representative sample needs to be tested to ensure confidence that they all work while balancing time to completion.
Any example files and directories not needed for the skill should be deleted. The initialization script creates example files in `scripts/`, `references/`, and `assets/` to demonstrate structure, but most skills won't need all of them.
#### Update SKILL.md
**Writing Style:** Write the entire skill using **imperative/infinitive form** (verb-first instructions), not second person. Use objective, instructional language (e.g., "To accomplish X, do Y" rather than "You should do X" or "If you need to do X"). This maintains consistency and clarity for AI consumption.
**Writing Guidelines:** Always use imperative/infinitive form.
To complete SKILL.md, answer the following questions:
##### Frontmatter
1. What is the purpose of the skill, in a few sentences?
2. When should the skill be used?
3. In practice, how should Claude use the skill? All reusable skill contents developed above should be referenced so that Claude knows how to use them.
Write the YAML frontmatter with `name` and `description`:
- `name`: The skill name
- `description`: This is the primary triggering mechanism for your skill, and helps Claude understand when to use the skill.
- Include both what the Skill does and specific triggers/contexts for when to use it.
- Include all "when to use" information here - Not in the body. The body is only loaded after triggering, so "When to Use This Skill" sections in the body are not helpful to Claude.
- Example description for a `docx` skill: "Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. Use when Claude needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks"
Do not include any other fields in YAML frontmatter.
##### Body
Write instructions for using the skill and its bundled resources.
### Step 5: Packaging a Skill
Once the skill is ready, it should be packaged into a distributable zip file that gets shared with the user. The packaging process automatically validates the skill first to ensure it meets all requirements:
Once development of the skill is complete, it must be packaged into a distributable .skill file that gets shared with the user. The packaging process automatically validates the skill first to ensure it meets all requirements:
```bash
scripts/package_skill.py <path/to/skill-folder>
@@ -189,12 +334,13 @@ scripts/package_skill.py <path/to/skill-folder> ./dist
The packaging script will:
1. **Validate** the skill automatically, checking:
- YAML frontmatter format and required fields
- Skill naming conventions and directory structure
- Description completeness and quality
- File organization and resource references
2. **Package** the skill if validation passes, creating a zip file named after the skill (e.g., `my-skill.zip`) that includes all files and maintains the proper directory structure for distribution.
2. **Package** the skill if validation passes, creating a .skill file named after the skill (e.g., `my-skill.skill`) that includes all files and maintains the proper directory structure for distribution. The .skill file is a zip file with a .skill extension.
If validation fails, the script will report the errors and exit without creating a package. Fix any validation errors and run the packaging command again.
@@ -203,6 +349,7 @@ If validation fails, the script will report the errors and exit without creating
After testing the skill, users may request improvements. Often this happens right after using the skill, with fresh context of how the skill performed.
**Iteration workflow:**
1. Use the skill on real tasks
2. Notice struggles or inefficiencies
3. Identify how SKILL.md or bundled resources should be updated

View File

@@ -0,0 +1,82 @@
# Output Patterns
Use these patterns when skills need to produce consistent, high-quality output.
## Template Pattern
Provide templates for output format. Match the level of strictness to your needs.
**For strict requirements (like API responses or data formats):**
```markdown
## Report structure
ALWAYS use this exact template structure:
# [Analysis Title]
## Executive summary
[One-paragraph overview of key findings]
## Key findings
- Finding 1 with supporting data
- Finding 2 with supporting data
- Finding 3 with supporting data
## Recommendations
1. Specific actionable recommendation
2. Specific actionable recommendation
```
**For flexible guidance (when adaptation is useful):**
```markdown
## Report structure
Here is a sensible default format, but use your best judgment:
# [Analysis Title]
## Executive summary
[Overview]
## Key findings
[Adapt sections based on what you discover]
## Recommendations
[Tailor to the specific context]
Adjust sections as needed for the specific analysis type.
```
## Examples Pattern
For skills where output quality depends on seeing examples, provide input/output pairs:
```markdown
## Commit message format
Generate commit messages following these examples:
**Example 1:**
Input: Added user authentication with JWT tokens
Output:
```
feat(auth): implement JWT-based authentication
Add login endpoint and token validation middleware
```
**Example 2:**
Input: Fixed bug where dates displayed incorrectly in reports
Output:
```
fix(reports): correct date formatting in timezone conversion
Use UTC timestamps consistently across report generation
```
Follow this style: type(scope): brief description, then detailed explanation.
```
Examples help Claude understand the desired style and level of detail more clearly than descriptions alone.

View File

@@ -0,0 +1,28 @@
# Workflow Patterns
## Sequential Workflows
For complex tasks, break operations into clear, sequential steps. It is often helpful to give Claude an overview of the process towards the beginning of SKILL.md:
```markdown
Filling a PDF form involves these steps:
1. Analyze the form (run analyze_form.py)
2. Create field mapping (edit fields.json)
3. Validate mapping (run validate_fields.py)
4. Fill the form (run fill_form.py)
5. Verify output (run verify_output.py)
```
## Conditional Workflows
For tasks with branching logic, guide Claude through decision points:
```markdown
1. Determine the modification type:
**Creating new content?** → Follow "Creation workflow" below
**Editing existing content?** → Follow "Editing workflow" below
2. Creation workflow: [steps]
3. Editing workflow: [steps]
```

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Skill Packager - Creates a distributable zip file of a skill folder
Skill Packager - Creates a distributable .skill file of a skill folder
Usage:
python utils/package_skill.py <path/to/skill-folder> [output-directory]
@@ -18,14 +18,14 @@ from quick_validate import validate_skill
def package_skill(skill_path, output_dir=None):
"""
Package a skill folder into a zip file.
Package a skill folder into a .skill file.
Args:
skill_path: Path to the skill folder
output_dir: Optional output directory for the zip file (defaults to current directory)
output_dir: Optional output directory for the .skill file (defaults to current directory)
Returns:
Path to the created zip file, or None if error
Path to the created .skill file, or None if error
"""
skill_path = Path(skill_path).resolve()
@@ -61,11 +61,11 @@ def package_skill(skill_path, output_dir=None):
else:
output_path = Path.cwd()
zip_filename = output_path / f"{skill_name}.zip"
skill_filename = output_path / f"{skill_name}.skill"
# Create the zip file
# Create the .skill file (zip format)
try:
with zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
with zipfile.ZipFile(skill_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
# Walk through the skill directory
for file_path in skill_path.rglob('*'):
if file_path.is_file():
@@ -74,11 +74,11 @@ def package_skill(skill_path, output_dir=None):
zipf.write(file_path, arcname)
print(f" Added: {arcname}")
print(f"\n✅ Successfully packaged skill to: {zip_filename}")
return zip_filename
print(f"\n✅ Successfully packaged skill to: {skill_filename}")
return skill_filename
except Exception as e:
print(f"❌ Error creating zip file: {e}")
print(f"❌ Error creating .skill file: {e}")
return None

View File

@@ -6,6 +6,7 @@ Quick validation script for skills - minimal version
import sys
import os
import re
import yaml
from pathlib import Path
def validate_skill(skill_path):
@@ -27,31 +28,60 @@ def validate_skill(skill_path):
if not match:
return False, "Invalid frontmatter format"
frontmatter = match.group(1)
frontmatter_text = match.group(1)
# Parse YAML frontmatter
try:
frontmatter = yaml.safe_load(frontmatter_text)
if not isinstance(frontmatter, dict):
return False, "Frontmatter must be a YAML dictionary"
except yaml.YAMLError as e:
return False, f"Invalid YAML in frontmatter: {e}"
# Define allowed properties
ALLOWED_PROPERTIES = {'name', 'description', 'license', 'allowed-tools', 'metadata'}
# Check for unexpected properties (excluding nested keys under metadata)
unexpected_keys = set(frontmatter.keys()) - ALLOWED_PROPERTIES
if unexpected_keys:
return False, (
f"Unexpected key(s) in SKILL.md frontmatter: {', '.join(sorted(unexpected_keys))}. "
f"Allowed properties are: {', '.join(sorted(ALLOWED_PROPERTIES))}"
)
# Check required fields
if 'name:' not in frontmatter:
if 'name' not in frontmatter:
return False, "Missing 'name' in frontmatter"
if 'description:' not in frontmatter:
if 'description' not in frontmatter:
return False, "Missing 'description' in frontmatter"
# Extract name for validation
name_match = re.search(r'name:\s*(.+)', frontmatter)
if name_match:
name = name_match.group(1).strip()
name = frontmatter.get('name', '')
if not isinstance(name, str):
return False, f"Name must be a string, got {type(name).__name__}"
name = name.strip()
if name:
# Check naming convention (hyphen-case: lowercase with hyphens)
if not re.match(r'^[a-z0-9-]+$', name):
return False, f"Name '{name}' should be hyphen-case (lowercase letters, digits, and hyphens only)"
if name.startswith('-') or name.endswith('-') or '--' in name:
return False, f"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens"
# Check name length (max 64 characters per spec)
if len(name) > 64:
return False, f"Name is too long ({len(name)} characters). Maximum is 64 characters."
# Extract and validate description
desc_match = re.search(r'description:\s*(.+)', frontmatter)
if desc_match:
description = desc_match.group(1).strip()
description = frontmatter.get('description', '')
if not isinstance(description, str):
return False, f"Description must be a string, got {type(description).__name__}"
description = description.strip()
if description:
# Check for angle brackets
if '<' in description or '>' in description:
return False, "Description cannot contain angle brackets (< or >)"
# Check description length (max 1024 characters per spec)
if len(description) > 1024:
return False, f"Description is too long ({len(description)} characters). Maximum is 1024 characters."
return True, "Skill is valid!"

View File

@@ -1,212 +0,0 @@
---
name: standalone-component-migrator
description: This skill should be used when converting Angular NgModule-based components to standalone architecture. It handles dependency analysis, template scanning, route refactoring, and test updates. Use this skill when the user requests component migration to standalone, mentions "convert to standalone", or wants to modernize Angular components to the latest patterns.
---
# Standalone Component Migrator
## Overview
Automate the conversion of Angular components from NgModule-based architecture to standalone components with explicit imports. This skill analyzes component dependencies, updates routing configurations, migrates tests, and optionally converts to modern Angular control flow syntax (@if, @for, @switch).
## When to Use This Skill
Invoke this skill when:
- User requests component conversion to standalone
- User mentions "migrate to standalone" or "modernize component"
- User wants to remove NgModule declarations
- User references Angular's standalone component architecture
## Migration Workflow
### Step 1: Analyze Component Dependencies
1. **Read Component File**
- Identify component decorator configuration
- Note selector, template path, style paths
- Check if already standalone
2. **Analyze Template**
- Read template file (HTML)
- Scan for directives: `*ngIf`, `*ngFor`, `*ngSwitch` → requires CommonModule
- Scan for forms: `ngModel`, `formControl` → requires FormsModule or ReactiveFormsModule
- Scan for built-in pipes: `async`, `date`, `json` → CommonModule
- Scan for custom components: identify all component selectors
- Scan for router directives: `routerLink`, `router-outlet` → RouterModule
3. **Find Parent NgModule**
- Search for NgModule that declares this component
- Read NgModule file to understand current imports
### Step 2: Convert Component to Standalone
Add `standalone: true` and explicit imports array:
```typescript
// BEFORE
import { Component } from '@angular/core';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.component.html'
})
export class MyComponent { }
// AFTER
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { ChildComponent } from './child.component';
import { CustomPipe } from '@isa/utils/pipes';
@Component({
selector: 'app-my-component',
standalone: true,
imports: [
CommonModule,
FormsModule,
RouterModule,
ChildComponent,
CustomPipe
],
templateUrl: './my-component.component.html'
})
export class MyComponent { }
```
### Step 3: Update Parent NgModule
Remove component from declarations, add to imports if exported:
```typescript
// BEFORE
@NgModule({
declarations: [MyComponent, OtherComponent],
imports: [CommonModule],
exports: [MyComponent]
})
// AFTER
@NgModule({
declarations: [OtherComponent],
imports: [CommonModule, MyComponent], // Import standalone component
exports: [MyComponent]
})
```
If NgModule becomes empty (no declarations), consider removing it entirely.
### Step 4: Update Routes (if applicable)
Convert to lazy-loaded standalone component:
```typescript
// BEFORE
const routes: Routes = [
{ path: 'feature', component: MyComponent }
];
// AFTER (lazy loading)
const routes: Routes = [
{
path: 'feature',
loadComponent: () => import('./my-component.component').then(m => m.MyComponent)
}
];
```
### Step 5: Update Tests
Convert test configuration:
```typescript
// BEFORE
TestBed.configureTestingModule({
declarations: [MyComponent],
imports: [CommonModule, FormsModule]
});
// AFTER
TestBed.configureTestingModule({
imports: [MyComponent] // Component imports its own dependencies
});
```
### Step 6: Optional - Migrate to Modern Control Flow
If requested, convert to new Angular control flow syntax:
```typescript
// OLD
<div *ngIf="condition">Content</div>
<div *ngFor="let item of items; trackBy: trackById">{{ item.name }}</div>
<div [ngSwitch]="value">
<div *ngSwitchCase="'a'">A</div>
<div *ngSwitchDefault>Default</div>
</div>
// NEW
@if (condition) {
<div>Content</div>
}
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
}
@switch (value) {
@case ('a') { <div>A</div> }
@default { <div>Default</div> }
}
```
### Step 7: Validate and Test
1. **Compile Check**
```bash
npx tsc --noEmit
```
2. **Run Tests**
```bash
npx nx test [library-name] --skip-nx-cache
```
3. **Lint Check**
```bash
npx nx lint [library-name]
```
4. **Verify Application Runs**
```bash
npm start
```
## Common Import Patterns
| Template Usage | Required Import |
|---------------|-----------------|
| `*ngIf`, `*ngFor`, `*ngSwitch` | `CommonModule` |
| `ngModel` | `FormsModule` |
| `formControl`, `formGroup` | `ReactiveFormsModule` |
| `routerLink`, `router-outlet` | `RouterModule` |
| `async`, `date`, `json` pipes | `CommonModule` |
| Custom components | Direct component import |
| Custom pipes | Direct pipe import |
## Error Handling
**Issue: Circular dependencies**
- Extract shared interfaces to util library
- Use dependency injection for services
- Avoid component A importing component B when B imports A
**Issue: Missing imports causing template errors**
- Check browser console for specific errors
- Verify all template dependencies are in imports array
- Use Angular Language Service in IDE for hints
## References
- Angular Standalone Components: https://angular.dev/guide/components/importing
- Modern Control Flow: https://angular.dev/guide/templates/control-flow
- CLAUDE.md Component Architecture section

View File

@@ -0,0 +1,678 @@
---
name: state-patterns
description: This skill should be used when writing Angular code with signals, effects, and NgRx Signal Store. Use when deciding whether to use effect(), computed(), or reactive patterns for state management. Applies when implementing Resource API with Signal Store for reactive data loading, preventing race conditions, and creating declarative async flows. Essential for code review of effect usage, refactoring imperative patterns to declarative alternatives, and building stores with reactive filter/search parameters.
---
# Reactive State Patterns
## Overview
This skill guides proper usage of Angular's `effect()`, provides declarative alternatives for common patterns, and enables integration of Angular's Resource API with NgRx Signal Store. Effects are frequently misused for state propagation, leading to circular updates and maintenance issues. The Resource API provides race-condition-free async data loading that integrates seamlessly with Signal Store.
## When to Use Effects (Valid Use Cases)
Effects are **primarily for rendering content that cannot be rendered through data binding**. Valid use cases are limited to:
### 1. Logging
Recording application events or debugging:
```typescript
effect(() => {
const error = this.error();
if (error) {
console.error('Error occurred:', error);
}
});
```
### 2. Canvas Painting
Custom graphics rendering (e.g., Angular Three library, Chart.js integration):
```typescript
effect(() => {
const data = this.chartData();
this.renderCanvas(data);
});
```
### 3. Custom DOM Behavior
Imperative APIs that require direct DOM manipulation:
```typescript
effect(() => {
const message = this.notificationMessage();
if (message) {
this.snackBar.open(message, 'Close');
}
});
```
**Key principle:** Data binding is the preferred way to display data. Effects should only be used when data binding is insufficient.
## Understanding Auto-Tracking
Angular automatically tracks signals accessed during effect execution, **even within called methods**:
```typescript
effect(() => {
this.logError(); // Signal tracking happens inside this method
});
logError(): void {
const error = this.error(); // This signal is automatically tracked
if (error) {
console.error(error);
}
}
```
**Implication:** Auto-tracking makes effect dependencies non-obvious and hard to maintain. This is a primary reason to avoid effects for state management.
## Effect Anti-Patterns (NEVER Use)
### ❌ Anti-Pattern 1: State Propagation
**NEVER use effects to propagate state changes to other state:**
```typescript
// ❌ WRONG - Anti-pattern
effect(() => {
const value = this.source();
this.derived.set(value * 2);
});
```
**Problems:**
- Risk of circular updates and infinite loops
- Hard to maintain due to implicit tracking
- Violates declarative reactive principles
- Inappropriate glitch-free semantics
### ❌ Anti-Pattern 2: Synchronizing Signals
```typescript
// ❌ WRONG - Anti-pattern
effect(() => {
const filter = this.filter();
this.loadData(filter);
});
```
### ❌ Anti-Pattern 3: Event Emulation
```typescript
// ❌ WRONG - Anti-pattern
effect(() => {
const count = this.itemCount();
this.countChanged.emit(count);
});
```
**Why signals ≠ events:** Signals are designed to be glitch-free, collapsing multiple updates. This makes them inappropriate for representing discrete events.
## Decision Tree: Effect vs Alternative
```
Need to react to signal changes?
├─ Synchronous derivation?
│ └─ ✅ Use computed()
├─ Asynchronous derivation?
│ └─ ✅ Use Resource API
├─ Complex reactive flow with race conditions?
│ └─ ✅ Use RxJS (toObservable + operators + toSignal)
├─ Event-based trigger (not state change)?
│ └─ ✅ React to event directly, not signal
├─ Need RxJS operators with signals?
│ └─ ✅ Use reactive helpers (rxMethod, deriveAsync)
└─ Rendering non-data-bound content (logging, canvas, imperative API)?
└─ ✅ Use effect()
```
## Recommended Alternatives
### Alternative 1: Use `computed()` for Synchronous Derivations
**When to use:** Deriving new state synchronously from existing signals.
```typescript
// ✅ CORRECT - Declarative
const derived = computed(() => {
return this.baseSignal() * 2;
});
const fullName = computed(() => {
return `${this.firstName()} ${this.lastName()}`;
});
```
**Benefits:**
- Declarative and maintainable
- Automatic dependency tracking
- Memoized and efficient
- No risk of circular updates
### Alternative 2: Use Resource API for Asynchronous Derivations
**When to use:** Loading data based on reactive parameters.
```typescript
// ✅ CORRECT - Declarative async state
readonly itemsResource = resource({
params: this.filter,
loader: ({ params, abortSignal }) => {
return this.dataService.load(params, abortSignal);
}
});
readonly items = computed(() => this.itemsResource.value() ?? []);
```
**Benefits:**
- Automatic race condition handling
- Built-in loading/error states
- Declarative parameter tracking
- Cancellation support
### Alternative 3: React to Events, Not State Changes
**When to use:** User interactions or DOM events should trigger actions.
```typescript
// ❌ WRONG - Reacting to signal change
effect(() => {
const searchTerm = this.searchTerm();
this.search(searchTerm);
});
// ✅ CORRECT - React to event
<input (input)="search($event.target.value)" />
// Component
search(term: string): void {
this.searchTerm.set(term);
this.performSearch(term);
}
```
**Benefits:**
- Clear causality (event → action)
- No auto-tracking complexity
- Explicit control flow
### Alternative 4: RxJS Integration
**When to use:** Complex reactive flows requiring operators like `debounceTime`, `switchMap`, `combineLatest`.
```typescript
// ✅ CORRECT - RxJS for complex flows
readonly searchTerm = signal('');
readonly searchTerm$ = toObservable(this.searchTerm);
readonly results$ = this.searchTerm$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.searchService.search(term))
);
readonly results = toSignal(this.results$, { initialValue: [] });
```
**Benefits:**
- Full RxJS operator ecosystem
- Race condition prevention (`switchMap`)
- Powerful composition
- Type-safe streams
**Pattern:** Signal → Observable (toObservable) → RxJS operators → Signal (toSignal)
### Alternative 5: Reactive Helpers
**When to use:** Need RxJS operators but prefer signal-centric API.
#### Using `rxMethod` (NgRx Signal Store)
```typescript
readonly loadItem = rxMethod<number>(
pipe(
tap(() => patchState(this, { loading: true })),
switchMap(id => this.service.findById(id)),
tap(item => patchState(this, { item, loading: false }))
)
);
// Call with signal or value
constructor() {
this.loadItem(this.selectedId);
}
```
#### Using `deriveAsync` (ngxtension)
```typescript
readonly data = deriveAsync(() => {
const filter = this.filter();
return this.service.load(filter);
});
```
**Benefits:**
- Signal-friendly API
- RxJS operator support
- Cleaner than manual Observable conversion
- Automatic subscription management
## Resource API with NgRx Signal Store
### Core Architectural Concepts
Establish three clear interaction points in the store:
1. **Filter signals trigger resource loading** - Parameter changes automatically reload resources
2. **Methods explicitly invoke operations** - Use `signalMethod` for user-triggered actions
3. **Computed signals derive view models** - Transform loaded data for component consumption
### The withProps Pattern for Dependency Injection
Inject services via `withProps` with underscore-prefixed properties to mark them as internal implementation details:
```typescript
withProps(() => ({
_dataService: inject(DataService),
_notificationService: inject(ToastService),
}))
```
**Benefits:**
- Centralizes dependency injection in one location
- Clear distinction between internal (prefixed) and public properties
- Services available to all subsequent feature sections
### Implementation Steps
#### Step 1: Inject Services with withProps
Create the initial `withProps` section to inject required services:
```typescript
export const MyStore = signalStore(
withProps(() => ({
_dataService: inject(DataService),
_toastService: inject(ToastService),
})),
// ... additional features
);
```
**Naming convention:** Prefix all injected services with underscore (`_`) to indicate internal use.
#### Step 2: Define Filter State
Add state properties that will serve as resource parameters:
```typescript
withState({
filter: {
searchTerm: '',
category: '',
} as MyFilter,
})
```
#### Step 3: Configure Resources
In a subsequent `withProps` section, create resources that reference the injected services and state:
```typescript
withProps((store) => ({
_itemsResource: resource({
params: store.filter,
loader: (loaderParams) => {
const filter = loaderParams.params;
const abortSignal = loaderParams.abortSignal;
return store._dataService.loadItems(filter, abortSignal);
}
})
}))
```
**Key points:**
- Resources automatically reload when `params` signal changes
- `abortSignal` enables automatic cancellation of in-flight requests
- Loader must return a Promise (use `.findPromise()` if service returns Observable)
#### Step 4: Expose Read-Only Resources (Optional)
If the resource should be accessible to consumers, expose it as read-only:
```typescript
withProps((store) => ({
itemsResource: store._itemsResource.asReadonly(),
}))
```
**Pattern:** Internal resources use underscore prefix, public versions are read-only without prefix.
#### Step 5: Create Signal Methods for Updates
Use `signalMethod` for actions that update state and trigger resource reloads:
```typescript
withMethods((store) => ({
updateFilter: signalMethod<MyFilter>((filter) => {
patchState(store, { filter });
}),
refresh: () => {
store._itemsResource.reload();
}
}))
```
**Important:** `signalMethod` implementations are **untracked by convention** - they don't re-execute when signals change. This provides explicit control flow.
#### Step 6: Add Error Handling
Use `withHooks` to react to resource errors:
```typescript
withHooks({
onInit: (store) => {
effect(() => {
const error = store._itemsResource.error();
if (error) {
store._toastService.show('Error: ' + getMessage(error));
}
});
}
})
```
**Pattern:** Error effects should be read-only - they observe errors and trigger side effects, but don't modify state.
### Common Resource API Patterns
#### Template Integration with linkedSignal
For two-way form binding that synchronizes with the store:
```typescript
export class MyComponent {
#store = inject(MyStore);
// Create linked signal for form field
searchTerm = linkedSignal(() => this.#store.filter().searchTerm);
// Combine form fields into filter object
#linkedFilter = computed(() => ({
searchTerm: this.searchTerm(),
// ... other fields
}));
constructor() {
// Sync form changes back to store
this.#store.updateFilter(this.#linkedFilter);
}
}
```
**Benefits:**
- Two-way binding for forms
- Automatic store synchronization
- Type-safe filter construction
#### Working with Resource Data
Access resource data through resource signals:
```typescript
withComputed((store) => ({
items: computed(() => store._itemsResource.value() ?? []),
isLoading: computed(() => store._itemsResource.isLoading()),
hasError: computed(() => store._itemsResource.hasError()),
}))
```
**Available signals:**
- `value()` - The loaded data (undefined while loading)
- `isLoading()` - Loading state boolean
- `hasError()` - Error state boolean
- `error()` - Error object if present
- `status()` - Overall status: 'idle' | 'loading' | 'resolved' | 'error'
#### Updating Resource Data Locally
For temporary working copies before server writes:
```typescript
withMethods((store) => ({
updateLocalItem: (id: string, changes: Partial<Item>) => {
store._itemsResource.update((currentItems) => {
return currentItems.map(item =>
item.id === id ? { ...item, ...changes } : item
);
});
}
}))
```
**Note:** This pattern feels unconventional but aligns with maintaining temporary working copies before server persistence.
#### Multiple Resources in One Store
Combine multiple resources for complex data requirements:
```typescript
withProps((store) => ({
_itemsResource: resource({
params: store.filter,
loader: (params) => store._dataService.loadItems(params.params, params.abortSignal)
}),
_detailsResource: resource({
params: store.selectedId,
loader: (params) => {
if (!params.params) return Promise.resolve(null);
return store._dataService.loadDetails(params.params, params.abortSignal);
}
})
}))
```
**Pattern:** Each resource has independent params and loading state, but can share service instances.
### Important Resource API Considerations
#### Race Condition Prevention
The Resource API automatically handles race conditions:
- New requests automatically cancel pending requests
- No need for `switchMap`, `takeUntilDestroyed`, or manual abort handling
- Declarative parameter changes trigger clean cancellation
#### Untracked Signal Methods
`signalMethod` implementations deliberately skip reactive tracking:
- Provides explicit, predictable control flow
- Prevents unexpected re-executions from signal changes
- Makes side effects obvious at call sites
#### Loader Function Requirements
Loaders must:
- Return a `Promise` (not Observable)
- Accept and pass through the `abortSignal` to enable cancellation
- Handle the signal in the underlying HTTP call
**Converting Observables:**
```typescript
loader: (params) => {
return firstValueFrom(
this._service.load$(params.params)
.pipe(takeUntilDestroyed(this._destroyRef))
);
}
```
#### Resource Lifecycle
Resources maintain their own state machine:
1. **Idle** - Initial state before first load
2. **Loading** - Request in progress
3. **Resolved** - Data loaded successfully
4. **Error** - Request failed
State transitions automatically trigger reactive updates to dependent computeds and effects.
## Common Refactoring Patterns
### Pattern 1: Effect for State Sync → computed()
```typescript
// ❌ Before
effect(() => {
const x = this.x();
const y = this.y();
this.sum.set(x + y);
});
// ✅ After
readonly sum = computed(() => this.x() + this.y());
```
### Pattern 2: Effect for Async Load → Resource API
```typescript
// ❌ Before
effect(() => {
const id = this.selectedId();
this.loadItem(id);
});
// ✅ After
readonly itemResource = resource({
params: this.selectedId,
loader: ({ params }) => this.service.loadItem(params)
});
```
### Pattern 3: Effect for Debounced Search → RxJS
```typescript
// ❌ Before
effect(() => {
const term = this.searchTerm();
// No way to debounce within effect
this.search(term);
});
// ✅ After
readonly searchTerm$ = toObservable(this.searchTerm);
readonly results = toSignal(
this.searchTerm$.pipe(
debounceTime(300),
switchMap(term => this.searchService.search(term))
),
{ initialValue: [] }
);
```
### Pattern 4: Effect for Event Notification → Direct Event Handling
```typescript
// ❌ Before
effect(() => {
const value = this.value();
this.valueChange.emit(value);
});
// ✅ After
updateValue(newValue: string): void {
this.value.set(newValue);
this.valueChange.emit(newValue);
}
```
## Code Review Checklist
When reviewing code with `effect()`, ask:
- [ ] Is this for rendering non-data-bound content? (logging, canvas, imperative APIs)
- **YES:** Effect is appropriate
- **NO:** Continue checklist
- [ ] Is this synchronous state derivation?
- **YES:** Use `computed()` instead
- [ ] Is this asynchronous data loading?
- **YES:** Use Resource API instead
- [ ] Does this need RxJS operators (debounce, switchMap, etc.)?
- **YES:** Use RxJS integration or reactive helpers instead
- [ ] Is this reacting to a user event?
- **YES:** Handle event directly instead
- [ ] Could this cause circular updates?
- **YES:** Refactor immediately - this will cause bugs
## Anti-Pattern Detection Rules
Flag any effect that:
1. **Calls `set()` or `update()` on signals** - Likely state propagation anti-pattern
2. **Calls service methods that update state** - Hidden state propagation
3. **Emits events based on signal changes** - Signal/event semantic mismatch
4. **Has try/catch for async operations** - Should use Resource API
5. **Would benefit from debouncing/throttling** - Should use RxJS
## When to Use Each Pattern
**Use computed() when:**
- Synchronous state derivation
- No async operations needed
- Memoization desired
**Use Resource API with Signal Store when:**
- Loading data based on reactive filter/search parameters
- Need automatic race condition handling for concurrent requests
- Want declarative data loading without RxJS subscriptions
- Building stores with frequently changing query parameters
- Implementing pagination, filtering, or search features
**Use RxJS integration when:**
- Complex reactive flows with multiple operators
- Need debouncing, throttling, or custom timing control
- Working with multiple Observable sources
- Require fine-grained control over subscription lifecycle
**Use reactive helpers when:**
- Prefer signal-centric API but need RxJS power
- Simple async operations with basic operators
- Want automatic subscription management
**Consider alternatives to Resource API when:**
- Simple one-time data loads (use `rxMethod` or direct service calls)
- Complex Observable chains with multiple operators needed
- Need fine-grained control over request timing/caching
- Working with WebSocket or SSE streams (use `rxMethod` instead)
## Key Principles
1. **Effects are for side effects, not state management**
2. **Prefer declarative over imperative**
3. **Use computed() for sync, Resource API for async**
4. **React to events, not state changes**
5. **RxJS for complex reactive flows**
6. **Auto-tracking is powerful but opaque - avoid when possible**
## When in Doubt
Ask: "Can the user see this without an effect using data binding?"
- **YES:** Don't use effect, use data binding
- **NO:** Effect might be appropriate (but verify against decision tree)

View File

@@ -1,134 +0,0 @@
---
name: swagger-sync-manager
description: This skill should be used when regenerating Swagger/OpenAPI TypeScript API clients in the ISA-Frontend monorepo. It handles generation of all 10 API clients (or specific ones), Unicode cleanup, breaking change detection, TypeScript validation, and affected test execution. Use this skill when the user requests API sync, mentions "regenerate swagger", or indicates backend API changes.
---
# Swagger Sync Manager
## Overview
Automate the regeneration of TypeScript API clients from Swagger/OpenAPI specifications. Handles 10 API clients with automatic post-processing, breaking change detection, impact analysis, and validation.
## When to Use This Skill
Invoke when user requests:
- API client regeneration
- "sync swagger" or "update API clients"
- Backend API changes need frontend updates
## Available APIs
availability-api, cat-search-api, checkout-api, crm-api, eis-api, inventory-api, isa-api, oms-api, print-api, wws-api
## Sync Workflow
### Step 1: Pre-Generation Check
```bash
# Check uncommitted changes
git status generated/swagger/
```
If changes exist, warn user and ask to proceed.
### Step 2: Backup Current State (Optional)
```bash
cp -r generated/swagger generated/swagger.backup.$(date +%s)
```
### Step 3: Run Generation
```bash
# All APIs
npm run generate:swagger
# Specific API (if api-name provided)
npm run generate:swagger:[api-name]
```
### Step 4: Verify Unicode Cleanup
Check `tools/fix-files.js` executed. Scan for remaining Unicode issues:
```bash
grep -r "\\\\u00" generated/swagger/ || echo "✅ No Unicode issues"
```
### Step 5: Detect Breaking Changes
For each modified API:
```bash
git diff generated/swagger/[api-name]/
```
Identify:
- 🔴 Removed properties
- 🔴 Changed types
- 🔴 Removed endpoints
- ✅ Added properties (safe)
- ✅ New endpoints (safe)
### Step 6: Impact Analysis
Use `Explore` agent to find affected files:
- Search for imports from `@generated/swagger/[api-name]`
- List data-access services using changed APIs
- Estimate refactoring scope
### Step 7: Validate
```bash
# TypeScript compilation
npx tsc --noEmit
# Run affected tests
npx nx affected:test --skip-nx-cache
# Lint affected
npx nx affected:lint
```
### Step 8: Generate Report
```
Swagger Sync Complete
=====================
APIs Regenerated: [all | specific]
Files Changed: XX
Breaking Changes: XX
🔴 Breaking Changes
-------------------
- [API]: [Property removed/type changed]
- Affected files: [list]
✅ Compatible Changes
---------------------
- [API]: [New properties/endpoints]
📊 Validation
-------------
TypeScript: ✅/❌
Tests: XX/XX passing
Lint: ✅/❌
💡 Next Steps
-------------
[Fix breaking changes / Deploy]
```
## Error Handling
**Generation fails**: Check OpenAPI spec URLs in package.json
**Unicode cleanup fails**: Run `node tools/fix-files.js` manually
**TypeScript errors**: Review breaking changes, update affected services
## References
- CLAUDE.md API Integration section
- package.json swagger generation scripts

View File

@@ -9,29 +9,6 @@ description: This skill should be used when working with Tailwind CSS styling in
Assist with applying the ISA-specific Tailwind CSS design system throughout the ISA-Frontend Angular monorepo. This skill provides comprehensive knowledge of custom utilities, color palettes, typography classes, button variants, and layout patterns specific to this project.
## When to Use This Skill
Invoke this skill when:
- **After** checking `libs/ui/**` for existing components (always check first!)
- Styling layout and spacing for components
- Choosing appropriate color values for custom elements
- Applying typography classes to text content
- Determining spacing, layout, or responsive breakpoints
- Customizing or extending existing UI components
- Ensuring design system consistency
- Questions about which Tailwind utility classes are available
**Important**: This skill provides Tailwind utilities. Always prefer using components from `@isa/ui/*` libraries before applying custom Tailwind styles.
**Works together with:**
- **[angular-template](../angular-template/SKILL.md)** - Angular template syntax, control flow (@if, @for, @defer), and binding patterns
- **[html-template](../html-template/SKILL.md)** - E2E testing attributes (`data-what`, `data-which`) and ARIA accessibility
When building Angular components, these three skills work together:
1. Use **angular-template** for Angular syntax and control flow
2. Use **html-template** for `data-*` and ARIA attributes
3. Use **tailwind** (this skill) for styling with the ISA design system
## Core Design System Principles
### 0. Component Libraries First (Most Important)

View File

@@ -0,0 +1,472 @@
---
name: template-standards
description: This skill should be used when writing or reviewing Angular component templates. It provides comprehensive guidance on modern Angular 20+ template syntax (control flow, defer, projection, bindings), E2E testing attributes (data-what, data-which), and ARIA accessibility attributes. Use when creating components, refactoring to modern syntax, implementing lazy loading, adding testing attributes, ensuring accessibility compliance, or reviewing template best practices.
---
# Template Standards
Comprehensive guide for Angular templates covering modern syntax, E2E testing attributes, and ARIA accessibility.
## Overview
This skill combines three essential aspects of Angular template development:
1. **Modern Angular Syntax** - Control flow (@if, @for, @switch), lazy loading (@defer), content projection, variable declarations, and bindings
2. **E2E Testing Attributes** - Stable selectors (data-what, data-which) for automated testing
3. **ARIA Accessibility** - Semantic roles, properties, and states for assistive technologies
**Every interactive element MUST include both E2E and ARIA attributes.**
**Related Skills:**
- **tailwind** - ISA design system styling (colors, typography, spacing, layout)
- **logging** - MANDATORY logging in all Angular files using `@isa/core/logging`
## Part 1: Angular Template Syntax
### Control Flow (Angular 17+)
#### @if / @else if / @else
```typescript
@if (user.isAdmin()) {
<app-admin-dashboard />
} @else if (user.isEditor()) {
<app-editor-dashboard />
} @else {
<app-viewer-dashboard />
}
// Store result with 'as'
@if (user.profile?.settings; as settings) {
<p>Theme: {{settings.theme}}</p>
}
```
#### @for with @empty
```typescript
@for (product of products(); track product.id) {
<app-product-card [product]="product" />
} @empty {
<p>No products available</p>
}
```
**CRITICAL:** Always provide `track` expression:
- Best: `track item.id` or `track item.uuid`
- Static lists: `track $index`
- **NEVER:** `track identity(item)` (causes full re-render)
**Contextual variables:** `$count`, `$index`, `$first`, `$last`, `$even`, `$odd`
#### @switch
```typescript
@switch (viewMode()) {
@case ('grid') { <app-grid-view /> }
@case ('list') { <app-list-view /> }
@default { <app-grid-view /> }
}
```
**See [control-flow-reference.md](references/control-flow-reference.md) for advanced patterns including nested loops, complex conditions, and filter strategies.**
### @defer Lazy Loading
#### Basic Usage
```typescript
@defer (on viewport) {
<app-heavy-chart />
} @placeholder (minimum 500ms) {
<div class="skeleton"></div>
} @loading (after 100ms; minimum 1s) {
<mat-spinner />
} @error {
<p>Failed to load</p>
}
```
#### Common Triggers
| Trigger | Use Case |
|---------|----------|
| `idle` (default) | Non-critical features |
| `viewport` | Below-the-fold content |
| `interaction` | User-initiated (click/keydown) |
| `hover` | Tooltips/popovers |
| `timer(Xs)` | Delayed content |
| `when(expr)` | Custom condition |
**Multiple triggers:** `@defer (on interaction; on timer(5s))`
**Prefetching:** `@defer (on interaction; prefetch on idle)`
#### Critical Requirements
- Components **MUST be standalone**
- No `@ViewChild`/`@ContentChild` references
- Reserve space in `@placeholder` to prevent layout shift
- Never defer above-the-fold content (harms LCP)
- Avoid `immediate`/`timer` during initial render (harms TTI)
**See [defer-patterns.md](references/defer-patterns.md) for performance optimization, Core Web Vitals impact, bundle size reduction strategies, and real-world examples.**
### Content Projection
#### Single Slot
```typescript
@Component({
selector: 'ui-card',
template: `<div class="card"><ng-content></ng-content></div>`
})
```
#### Multi-Slot with Selectors
```typescript
@Component({
template: `
<header><ng-content select="card-header"></ng-content></header>
<main><ng-content select="card-body"></ng-content></main>
<footer><ng-content></ng-content></footer> <!-- default slot -->
`
})
```
**Usage:**
```html
<ui-card>
<card-header><h3>Title</h3></card-header>
<card-body><p>Content</p></card-body>
<button>Action</button> <!-- goes to default slot -->
</ui-card>
```
**CRITICAL Constraint:** `ng-content` **always instantiates** (even if hidden). For conditional projection, use `ng-template` + `NgTemplateOutlet`.
**See [projection-patterns.md](references/projection-patterns.md) for conditional projection, template-based projection, querying projected content, and modal/form field examples.**
### Template References
#### ng-template
```html
<ng-template #userCard let-user="userData" let-index="i">
<div class="user">#{{index}}: {{user.name}}</div>
</ng-template>
<ng-container
*ngTemplateOutlet="userCard; context: {userData: currentUser(), i: 0}">
</ng-container>
```
**Access in component:**
```typescript
myTemplate = viewChild<TemplateRef<unknown>>('myTemplate');
```
#### ng-container
Groups elements without DOM footprint:
```html
<p>
Hero's name is
<ng-container @if="hero()">{{hero().name}}</ng-container>.
</p>
```
**See [template-reference.md](references/template-reference.md) for programmatic rendering, ViewContainerRef patterns, context scoping, and common pitfalls.**
### Variables
#### @let (Angular 18.1+)
```typescript
@let userName = user().name;
@let greeting = 'Hello, ' + userName;
@let asyncData = data$ | async;
<h1>{{greeting}}</h1>
```
**Scoped to current view** (not hoisted to parent/sibling).
#### Template References (#)
```html
<input #emailInput type="email" />
<button (click)="sendEmail(emailInput.value)">Send</button>
<app-datepicker #startDate />
<button (click)="startDate.open()">Open</button>
```
### Binding Patterns
**Property:** `[disabled]="!isValid()"`
**Attribute:** `[attr.aria-label]="label()"` `[attr.data-what]="'card'"`
**Event:** `(click)="save()"` `(input)="onInput($event)"`
**Two-way:** `[(ngModel)]="userName"`
**Class:** `[class.active]="isActive()"` or `[class]="{active: isActive()}"`
**Style:** `[style.width.px]="width()"` or `[style]="{color: textColor()}"`
## Part 2: E2E Testing Attributes
### Purpose
Enable automated end-to-end testing by providing stable selectors for QA automation:
- **`data-what`**: Semantic description of element's purpose (e.g., `submit-button`, `email-input`)
- **`data-which`**: Unique identifier for specific instances (e.g., `registration-form`, `customer-123`)
- **`data-*`**: Additional contextual information (e.g., `data-status="active"`)
### Naming Conventions
**`data-what` patterns:**
- `*-button` (submit-button, cancel-button, delete-button)
- `*-input` (email-input, search-input, quantity-input)
- `*-link` (product-link, order-link, customer-link)
- `*-item` (list-item, menu-item, card-item)
- `*-dialog` (confirm-dialog, error-dialog, info-dialog)
- `*-dropdown` (status-dropdown, category-dropdown)
**`data-which` guidelines:**
- Use unique identifiers: `data-which="primary"`, `data-which="customer-list"`
- Bind dynamically for lists: `[attr.data-which]="item.id"`
- Combine with context: `data-which="customer-{{ customerId }}-edit"`
### Best Practices
1. ✅ Add to ALL interactive elements
2. ✅ Use kebab-case for `data-what` values
3. ✅ Ensure `data-which` is unique within the view
4. ✅ Use Angular binding for dynamic values: `[attr.data-*]`
5. ✅ Avoid including sensitive data in attributes
**See [e2e-attributes.md](references/e2e-attributes.md) for complete patterns by element type (buttons, inputs, links, lists, tables, dialogs), dynamic attribute bindings, testing integration examples, and validation strategies.**
## Part 3: ARIA Accessibility Attributes
### Purpose
Ensure web applications are accessible to all users, including those using assistive technologies:
- **Roles**: Define element purpose (button, navigation, dialog, etc.)
- **Properties**: Provide additional context (aria-label, aria-describedby)
- **States**: Indicate dynamic states (aria-expanded, aria-disabled)
- **Live Regions**: Announce dynamic content changes
### Role Patterns
- Interactive elements: `button`, `link`, `menuitem`
- Structural: `navigation`, `main`, `complementary`, `contentinfo`
- Widget: `dialog`, `alertdialog`, `tooltip`, `tablist`, `tab`
- Landmark: `banner`, `search`, `form`, `region`
### Best Practices
1. ✅ Use semantic HTML first (use `<button>` instead of `<div role="button">`)
2. ✅ Provide text alternatives for all interactive elements
3. ✅ Ensure proper keyboard navigation (tabindex, focus management)
4. ✅ Use `aria-label` when visual label is missing
5. ✅ Use `aria-labelledby` to reference existing visible labels
6. ✅ Keep ARIA attributes in sync with visual states
7. ✅ Test with screen readers (NVDA, JAWS, VoiceOver)
**See [aria-attributes.md](references/aria-attributes.md) for comprehensive role reference, property and state attributes, live regions, keyboard navigation patterns, WCAG compliance requirements, and testing strategies.**
## Part 4: Combined Examples
### Button with All Attributes
```html
<button
type="button"
(click)="onSubmit()"
data-what="submit-button"
data-which="registration-form"
aria-label="Submit registration form"
[attr.aria-disabled]="!isValid()">
Submit
</button>
```
### Input with All Attributes
```html
<input
type="text"
[(ngModel)]="email"
data-what="email-input"
data-which="registration-form"
aria-label="Email address"
aria-describedby="email-hint"
aria-required="true"
[attr.aria-invalid]="emailError ? 'true' : null" />
<span id="email-hint">We'll never share your email</span>
```
### Dynamic List with All Attributes
```typescript
@for (item of items(); track item.id) {
<li
(click)="selectItem(item)"
data-what="list-item"
[attr.data-which]="item.id"
[attr.data-status]="item.status"
[attr.aria-label]="'Select ' + item.name"
role="button"
tabindex="0"
(keydown.enter)="selectItem(item)"
(keydown.space)="selectItem(item)">
{{ item.name }}
</li>
}
```
### Dialog with All Attributes
```html
<div
class="dialog"
data-what="confirmation-dialog"
data-which="delete-item"
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description">
<h2 id="dialog-title">Confirm Deletion</h2>
<p id="dialog-description">Are you sure you want to delete this item?</p>
<button
(click)="confirm()"
data-what="confirm-button"
data-which="delete-dialog"
aria-label="Confirm deletion">
Delete
</button>
<button
(click)="cancel()"
data-what="cancel-button"
data-which="delete-dialog"
aria-label="Cancel deletion">
Cancel
</button>
</div>
```
**See [combined-patterns.md](references/combined-patterns.md) for complete form examples, product listings, shopping carts, modal dialogs, navigation patterns, data tables, search interfaces, notifications, and multi-step forms with all attributes properly applied.**
## Validation Checklist
Before considering template complete:
### Angular Syntax
- [ ] Using modern control flow (@if, @for, @switch) instead of *ngIf/*ngFor/*ngSwitch
- [ ] All @for loops have proper `track` expressions (prefer item.id over $index)
- [ ] Complex expressions moved to `computed()` in component
- [ ] @defer used for below-the-fold or heavy components
- [ ] Content projection using ng-content with proper selectors (if applicable)
- [ ] Using @let for template-scoped variables (Angular 18.1+)
### E2E Attributes
- [ ] All buttons have `data-what` and `data-which`
- [ ] All inputs have `data-what` and `data-which`
- [ ] All links have `data-what` and `data-which`
- [ ] Dynamic lists use `[attr.data-*]` bindings with unique identifiers
- [ ] No duplicate `data-which` values within the same view
### ARIA Accessibility
- [ ] All interactive elements have appropriate ARIA labels
- [ ] Proper roles assigned (button, navigation, dialog, etc.)
- [ ] Form fields associated with labels (id/for or aria-labelledby)
- [ ] Error messages use `role="alert"` and `aria-live="polite"`
- [ ] Dialogs have `role="dialog"`, `aria-modal`, and label relationships
- [ ] Dynamic state changes reflected in ARIA attributes
- [ ] Keyboard accessibility (tabindex, enter/space handlers where needed)
### Combined Standards
- [ ] Every interactive element has BOTH E2E and ARIA attributes
- [ ] Attributes organized logically (Angular directives → data-* → aria-*)
- [ ] Dynamic bindings use `[attr.*]` syntax correctly
- [ ] No accessibility violations (semantic HTML preferred over ARIA)
## Migration Guide
### From Legacy Angular Syntax
| Legacy | Modern |
|--------|--------|
| `*ngIf="condition"` | `@if (condition) { }` |
| `*ngFor="let item of items"` | `@for (item of items; track item.id) { }` |
| `[ngSwitch]` | `@switch (value) { @case ('a') { } }` |
**CLI migration:** `ng generate @angular/core:control-flow`
### Adding E2E and ARIA to Existing Templates
1. **Identify interactive elements**: buttons, inputs, links, clickable divs
2. **Add E2E attributes**: `data-what` (semantic type), `data-which` (unique identifier)
3. **Add ARIA attributes**: appropriate role, label, and state attributes
4. **Test**: verify selectors work in E2E tests, validate with screen readers
## Reference Files
For detailed examples and advanced patterns, see:
### Angular Syntax References
- `references/control-flow-reference.md` - Advanced @if/@for/@switch patterns, nested loops, filtering
- `references/defer-patterns.md` - Lazy loading strategies, Core Web Vitals, performance optimization
- `references/projection-patterns.md` - Advanced ng-content, conditional projection, template-based patterns
- `references/template-reference.md` - ng-template/ng-container, programmatic rendering, ViewContainerRef
### E2E Testing References
- `references/e2e-attributes.md` - Complete E2E attribute patterns, naming conventions, testing integration
### ARIA Accessibility References
- `references/aria-attributes.md` - Comprehensive ARIA guidance, roles, properties, states, WCAG compliance
### Combined References
- `references/combined-patterns.md` - Real-world examples with Angular + E2E + ARIA integrated
Search with: `grep -r "pattern" references/`
## Quick Reference Summary
**Every interactive element needs:**
1. **Angular binding** (event, property, attribute)
2. **`data-what`** (semantic type: submit-button, email-input)
3. **`data-which`** (unique identifier: registration-form, user-123)
4. **ARIA attribute** (role, aria-label, aria-describedby)
**Template Pattern:**
```html
<button
(click)="action()"
data-what="[type]-button"
data-which="[context]"
aria-label="[descriptive label]">
Text
</button>
```
**Dynamic Pattern:**
```typescript
@for (item of items(); track item.id) {
<div
(click)="select(item)"
data-what="item-card"
[attr.data-which]="item.id"
[attr.aria-label]="'Select ' + item.name">
{{ item.name }}
</div>
}
```
---
**This skill combines Angular template syntax, E2E testing attributes, and ARIA accessibility into a unified standard. Apply all three aspects to every template.**

View File

@@ -1,344 +0,0 @@
---
name: test-migration-specialist
description: This skill should be used when migrating Angular libraries from Jest + Spectator to Vitest + Angular Testing Utilities. It handles test configuration updates, test file refactoring, mock pattern conversion, and validation. Use this skill when the user requests test framework migration, specifically for the 40 remaining Jest-based libraries in the ISA-Frontend monorepo.
---
# Test Migration Specialist
## Overview
Automate the migration of Angular library tests from Jest + Spectator to Vitest + Angular Testing Utilities. This skill handles the complete migration workflow including configuration updates, test file refactoring, dependency management, and validation.
**Current Migration Status**: 40 libraries use Jest (65.6%), 21 libraries use Vitest (34.4%)
## When to Use This Skill
Invoke this skill when:
- User requests test migration for a specific library
- User mentions "migrate tests" or "Jest to Vitest"
- User wants to update test framework for a library
- User references the 40 remaining libraries to migrate
## Migration Workflow
### Step 1: Pre-Migration Analysis
Before making any changes, analyze the current state:
1. **Read Testing Guidelines**
- Use `docs-researcher` agent to read `docs/guidelines/testing.md`
- Understand migration patterns and best practices
- Note JUnit and Cobertura configuration requirements
2. **Analyze Library Structure**
- Read `libs/[path]/project.json` to identify current test executor
- Count test files using Glob: `**/*.spec.ts`
- Scan for Spectator usage patterns using Grep: `createComponentFactory|createServiceFactory|Spectator`
- Identify complex mocking scenarios (ng-mocks, jest.mock patterns)
3. **Determine Library Depth**
- Calculate directory levels from workspace root
- This affects relative paths in vite.config.mts (../../../ vs ../../../../)
### Step 2: Update Test Configuration
Update the library's test configuration to use Vitest:
1. **Update project.json**
Replace Jest executor with Vitest:
```json
{
"test": {
"executor": "@nx/vite:test",
"options": {
"configFile": "vite.config.mts"
}
}
}
```
2. **Create vite.config.mts**
Create configuration with JUnit and Cobertura reporters:
```typescript
/// <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
// @ts-expect-error - Vitest reporter tuple types have complex inference issues
defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/[path]',
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
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',
['junit', { outputFile: '../../../testresults/junit-[library-name].xml' }],
],
coverage: {
reportsDirectory: '../../../coverage/libs/[path]',
provider: 'v8' as const,
reporter: ['text', 'cobertura'],
},
},
}));
```
**Critical**: Adjust `../../../` depth based on library location
### Step 3: Migrate Test Files
For each `.spec.ts` file, perform these conversions:
1. **Update Imports**
```typescript
// REMOVE
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
// ADD
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach } from 'vitest';
```
2. **Convert Component Tests**
```typescript
// OLD (Spectator)
const createComponent = createComponentFactory({
component: MyComponent,
imports: [CommonModule],
mocks: [MyService]
});
let spectator: Spectator<MyComponent>;
beforeEach(() => spectator = createComponent());
it('should display title', () => {
spectator.setInput('title', 'Test');
expect(spectator.query('h1')).toHaveText('Test');
});
// NEW (Angular Testing Utilities)
describe('MyComponent', () => {
let fixture: ComponentFixture<MyComponent>;
let component: MyComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyComponent, CommonModule],
providers: [{ provide: MyService, useValue: mockService }]
}).compileComponents();
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
});
it('should display title', () => {
component.title = 'Test';
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('h1').textContent).toContain('Test');
});
});
```
3. **Convert Service Tests**
```typescript
// OLD (Spectator)
const createService = createServiceFactory({
service: MyService,
mocks: [HttpClient]
});
// NEW (Angular Testing Utilities)
describe('MyService', () => {
let service: MyService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [MyService]
});
service = TestBed.inject(MyService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
});
```
4. **Update Mock Patterns**
- Replace `jest.fn()` → `vi.fn()`
- Replace `jest.spyOn()` → `vi.spyOn()`
- Replace `jest.mock()` → `vi.mock()`
- For complex mocks, use `ng-mocks` library if needed
5. **Update Matchers**
- Replace Spectator matchers (`toHaveText`, `toExist`) with standard Jest/Vitest matchers
- Use `expect().toBeTruthy()`, `expect().toContain()`, etc.
### Step 4: Verify E2E Attributes
Check component templates for E2E testing attributes:
1. **Scan Templates**
Use Grep to find templates: `**/*.html`
2. **Validate Attributes**
Ensure interactive elements have:
- `data-what`: Semantic description (e.g., "submit-button")
- `data-which`: Unique identifier (e.g., "form-primary")
- Dynamic `data-*` for list items: `[attr.data-item-id]="item.id"`
3. **Add Missing Attributes**
If missing, add them to components. See `dev:add-e2e-attrs` command or use that skill.
### Step 5: Run Tests and Validate
Execute tests to verify migration:
1. **Run Tests**
```bash
npx nx test [library-name] --skip-nx-cache
```
2. **Run with Coverage**
```bash
npx nx test [library-name] --coverage.enabled=true --skip-nx-cache
```
3. **Verify Output Files**
Check that CI/CD integration files are created:
- JUnit XML: `testresults/junit-[library-name].xml`
- Cobertura XML: `coverage/libs/[path]/cobertura-coverage.xml`
4. **Address Failures**
If tests fail:
- Review test conversion (common issues: missing fixture.detectChanges(), incorrect selectors)
- Check mock configurations
- Verify imports are correct
- Ensure async tests use proper patterns
### Step 6: Clean Up
Remove legacy configurations:
1. **Remove Jest Files**
- Delete `jest.config.ts` or `jest.config.js` if present
- Remove Jest-specific setup files
2. **Update Dependencies**
- Note if Spectator can be removed (check if other libs still use it)
- Note if Jest can be removed (check if other libs still use it)
- Don't actually remove from package.json unless all libs migrated
3. **Update Documentation**
Update library README.md with new test commands:
```markdown
## Testing
This library uses Vitest + Angular Testing Utilities.
```bash
# Run tests
npx nx test [library-name] --skip-nx-cache
# Run with coverage
npx nx test [library-name] --coverage.enabled=true --skip-nx-cache
```
```
### Step 7: Generate Migration Report
Provide comprehensive migration summary:
```
Test Migration Complete
=======================
Library: [library-name]
Framework: Jest + Spectator → Vitest + Angular Testing Utilities
📊 Migration Statistics
-----------------------
Test files migrated: XX
Component tests: XX
Service tests: XX
Total test cases: XX
✅ Test Results
---------------
Passing: XX/XX (100%)
Coverage: XX%
📝 Configuration
----------------
- project.json: ✅ Updated to @nx/vite:test
- vite.config.mts: ✅ Created with JUnit + Cobertura
- E2E attributes: ✅ Validated
📁 CI/CD Integration
--------------------
- JUnit XML: ✅ testresults/junit-[name].xml
- Cobertura XML: ✅ coverage/libs/[path]/cobertura-coverage.xml
🧹 Cleanup
----------
- Jest config removed: ✅
- README updated: ✅
💡 Next Steps
-------------
1. Verify tests in CI/CD pipeline
2. Monitor for any edge cases
3. Consider migrating related libraries
📚 Remaining Libraries
----------------------
Jest libraries remaining: XX/40
Progress: XX% complete
```
## Error Handling
### Common Migration Issues
**Issue 1: Tests fail after migration**
- Check `fixture.detectChanges()` is called after setting inputs
- Verify async tests use `async/await` properly
- Check component imports are correct (standalone components)
**Issue 2: Mocks not working**
- Verify `vi.fn()` syntax is correct
- Check providers array in TestBed configuration
- For complex mocks, consider using `ng-mocks`
**Issue 3: Coverage files not generated**
- Verify path depth in vite.config.mts matches library location
- Check reporters array includes `'cobertura'`
- Ensure `provider: 'v8'` is set
**Issue 4: Type errors in vite.config.mts**
- Add `// @ts-expect-error` comment before `defineConfig()`
- This is expected due to Vitest reporter type complexity
## References
Use `docs-researcher` agent to access:
- `docs/guidelines/testing.md` - Comprehensive migration guide with examples
- `CLAUDE.md` - Testing Framework section for project conventions
**Key Documentation Sections:**
- Vitest Configuration with JUnit and Cobertura
- Angular Testing Utilities examples
- Migration patterns and best practices
- E2E attribute requirements

View File

@@ -1,346 +0,0 @@
# Jest to Vitest Migration Patterns
## Overview
This reference provides syntax mappings and patterns for migrating tests from Jest (with Spectator) to Vitest (with Angular Testing Library).
## Configuration Migration
### Jest Config → Vitest Config
**Before (jest.config.ts):**
```typescript
export default {
displayName: 'my-lib',
preset: '../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
coverageDirectory: '../../coverage/libs/my-lib',
transform: {
'^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular',
},
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
],
};
```
**After (vitest.config.ts):**
```typescript
import { defineConfig } from 'vitest/config';
import angular from '@analogjs/vite-plugin-angular';
export default defineConfig({
plugins: [angular()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['src/test-setup.ts'],
include: ['**/*.spec.ts'],
reporters: ['default'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
},
});
```
### Test Setup Migration
**Before (test-setup.ts - Jest):**
```typescript
import 'jest-preset-angular/setup-jest';
```
**After (test-setup.ts - Vitest):**
```typescript
import '@analogjs/vitest-angular/setup-zone';
```
## Import Changes
### Test Function Imports
**Before (Jest):**
```typescript
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
```
**After (Vitest):**
```typescript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
```
### Mock Imports
**Before (Jest):**
```typescript
jest.fn()
jest.spyOn()
jest.mock()
jest.useFakeTimers()
```
**After (Vitest):**
```typescript
vi.fn()
vi.spyOn()
vi.mock()
vi.useFakeTimers()
```
## Mock Migration Patterns
### Function Mocks
**Before (Jest):**
```typescript
const mockFn = jest.fn();
const mockFnWithReturn = jest.fn().mockReturnValue('value');
const mockFnWithAsync = jest.fn().mockResolvedValue('async value');
```
**After (Vitest):**
```typescript
const mockFn = vi.fn();
const mockFnWithReturn = vi.fn().mockReturnValue('value');
const mockFnWithAsync = vi.fn().mockResolvedValue('async value');
```
### Spy Migration
**Before (Jest):**
```typescript
const spy = jest.spyOn(service, 'method');
spy.mockImplementation(() => 'mocked');
```
**After (Vitest):**
```typescript
const spy = vi.spyOn(service, 'method');
spy.mockImplementation(() => 'mocked');
```
### Module Mocks
**Before (Jest):**
```typescript
jest.mock('@isa/core/logging', () => ({
logger: jest.fn(() => ({
info: jest.fn(),
error: jest.fn(),
})),
}));
```
**After (Vitest):**
```typescript
vi.mock('@isa/core/logging', () => ({
logger: vi.fn(() => ({
info: vi.fn(),
error: vi.fn(),
})),
}));
```
## Spectator → Angular Testing Library
### Component Testing
**Before (Spectator):**
```typescript
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
describe('MyComponent', () => {
let spectator: Spectator<MyComponent>;
const createComponent = createComponentFactory({
component: MyComponent,
imports: [CommonModule],
providers: [
{ provide: MyService, useValue: mockService },
],
});
beforeEach(() => {
spectator = createComponent();
});
it('should render title', () => {
expect(spectator.query('.title')).toHaveText('Hello');
});
it('should handle click', () => {
spectator.click('.button');
expect(mockService.doSomething).toHaveBeenCalled();
});
});
```
**After (Angular Testing Library):**
```typescript
import { render, screen, fireEvent } from '@testing-library/angular';
describe('MyComponent', () => {
it('should render title', async () => {
await render(MyComponent, {
imports: [CommonModule],
providers: [
{ provide: MyService, useValue: mockService },
],
});
expect(screen.getByText('Hello')).toBeInTheDocument();
});
it('should handle click', async () => {
await render(MyComponent, {
providers: [{ provide: MyService, useValue: mockService }],
});
fireEvent.click(screen.getByRole('button'));
expect(mockService.doSomething).toHaveBeenCalled();
});
});
```
### Query Selectors
| Spectator | Angular Testing Library |
|-----------|------------------------|
| `spectator.query('.class')` | `screen.getByTestId()` or `screen.getByRole()` |
| `spectator.queryAll('.class')` | `screen.getAllByRole()` |
| `spectator.query('button')` | `screen.getByRole('button')` |
| `spectator.query('[data-testid]')` | `screen.getByTestId()` |
### Events
| Spectator | Angular Testing Library |
|-----------|------------------------|
| `spectator.click(element)` | `fireEvent.click(element)` or `await userEvent.click(element)` |
| `spectator.typeInElement(value, element)` | `await userEvent.type(element, value)` |
| `spectator.blur(element)` | `fireEvent.blur(element)` |
| `spectator.focus(element)` | `fireEvent.focus(element)` |
### Service Testing
**Before (Spectator):**
```typescript
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
describe('MyService', () => {
let spectator: SpectatorService<MyService>;
const createService = createServiceFactory({
service: MyService,
providers: [
{ provide: HttpClient, useValue: mockHttp },
],
});
beforeEach(() => {
spectator = createService();
});
it('should fetch data', () => {
spectator.service.getData().subscribe(data => {
expect(data).toEqual(expectedData);
});
});
});
```
**After (TestBed):**
```typescript
import { TestBed } from '@angular/core/testing';
describe('MyService', () => {
let service: MyService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
MyService,
{ provide: HttpClient, useValue: mockHttp },
],
});
service = TestBed.inject(MyService);
});
it('should fetch data', () => {
service.getData().subscribe(data => {
expect(data).toEqual(expectedData);
});
});
});
```
## Async Testing
### Observable Testing
**Before (Jest):**
```typescript
it('should emit values', (done) => {
service.data$.subscribe({
next: (value) => {
expect(value).toBe(expected);
done();
},
});
});
```
**After (Vitest):**
```typescript
import { firstValueFrom } from 'rxjs';
it('should emit values', async () => {
const value = await firstValueFrom(service.data$);
expect(value).toBe(expected);
});
```
### Timer Mocks
**Before (Jest):**
```typescript
jest.useFakeTimers();
service.startTimer();
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
jest.useRealTimers();
```
**After (Vitest):**
```typescript
vi.useFakeTimers();
service.startTimer();
vi.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
vi.useRealTimers();
```
## Common Matchers
| Jest | Vitest |
|------|--------|
| `expect(x).toBe(y)` | Same |
| `expect(x).toEqual(y)` | Same |
| `expect(x).toHaveBeenCalled()` | Same |
| `expect(x).toHaveBeenCalledWith(y)` | Same |
| `expect(x).toMatchSnapshot()` | `expect(x).toMatchSnapshot()` |
| `expect(x).toHaveText('text')` | `expect(x).toHaveTextContent('text')` (with jest-dom) |
## Migration Checklist
1. [ ] Update `vitest.config.ts`
2. [ ] Update `test-setup.ts`
3. [ ] Replace `jest.fn()` with `vi.fn()`
4. [ ] Replace `jest.spyOn()` with `vi.spyOn()`
5. [ ] Replace `jest.mock()` with `vi.mock()`
6. [ ] Replace Spectator with Angular Testing Library
7. [ ] Update queries to use accessible selectors
8. [ ] Update async patterns
9. [ ] Run tests and fix any remaining issues
10. [ ] Remove Jest dependencies from `package.json`

View File

@@ -0,0 +1,192 @@
---
name: test-migration
description: Reference patterns for Jest to Vitest test migration. Contains syntax mappings for jest→vi, Spectator→Angular Testing Library, and common matcher conversions. Auto-loaded by migration-specialist agent.
---
# Jest to Vitest Migration Patterns
Quick reference for test framework migration syntax.
## Import Changes
```typescript
// REMOVE
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
// ADD
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach, vi } from 'vitest';
```
## Mock Migration
| Jest | Vitest |
|------|--------|
| `jest.fn()` | `vi.fn()` |
| `jest.spyOn(obj, 'method')` | `vi.spyOn(obj, 'method')` |
| `jest.mock('module')` | `vi.mock('module')` |
| `jest.useFakeTimers()` | `vi.useFakeTimers()` |
| `jest.advanceTimersByTime(ms)` | `vi.advanceTimersByTime(ms)` |
| `jest.useRealTimers()` | `vi.useRealTimers()` |
| `jest.clearAllMocks()` | `vi.clearAllMocks()` |
| `jest.resetAllMocks()` | `vi.resetAllMocks()` |
## Spectator → TestBed
### Component Testing
```typescript
// OLD (Spectator)
const createComponent = createComponentFactory({
component: MyComponent,
imports: [CommonModule],
mocks: [MyService]
});
let spectator: Spectator<MyComponent>;
beforeEach(() => spectator = createComponent());
// NEW (TestBed)
let fixture: ComponentFixture<MyComponent>;
let component: MyComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyComponent],
providers: [{ provide: MyService, useValue: mockService }]
}).compileComponents();
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
});
```
### Service Testing
```typescript
// OLD (Spectator)
const createService = createServiceFactory({
service: MyService,
mocks: [HttpClient]
});
let spectator: SpectatorService<MyService>;
beforeEach(() => spectator = createService());
// NEW (TestBed)
let service: MyService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
MyService,
{ provide: HttpClient, useValue: mockHttp }
]
});
service = TestBed.inject(MyService);
});
```
## Query Selectors
| Spectator | Angular/Native |
|-----------|---------------|
| `spectator.query('.class')` | `fixture.nativeElement.querySelector('.class')` |
| `spectator.queryAll('.class')` | `fixture.nativeElement.querySelectorAll('.class')` |
| `spectator.query('button')` | `fixture.nativeElement.querySelector('button')` |
## Events
| Spectator | Native |
|-----------|--------|
| `spectator.click(element)` | `element.click()` + `fixture.detectChanges()` |
| `spectator.typeInElement(value, el)` | `el.value = value; el.dispatchEvent(new Event('input'))` |
| `spectator.blur(element)` | `element.dispatchEvent(new Event('blur'))` |
## Matchers
| Spectator | Standard |
|-----------|----------|
| `toHaveText('text')` | `expect(el.textContent).toContain('text')` |
| `toExist()` | `toBeTruthy()` |
| `toBeVisible()` | Check `!el.hidden && el.offsetParent !== null` |
| `toHaveClass('class')` | `expect(el.classList.contains('class')).toBe(true)` |
## Async Patterns
```typescript
// OLD (callback)
it('should emit', (done) => {
service.data$.subscribe(val => {
expect(val).toBe(expected);
done();
});
});
// NEW (async/await)
import { firstValueFrom } from 'rxjs';
it('should emit', async () => {
const val = await firstValueFrom(service.data$);
expect(val).toBe(expected);
});
```
## Input/Output Testing
```typescript
// Setting inputs (Angular 17.3+)
fixture.componentRef.setInput('title', 'Test');
fixture.detectChanges();
// Testing outputs
const emitSpy = vi.fn();
component.myOutput.subscribe(emitSpy);
// trigger action...
expect(emitSpy).toHaveBeenCalledWith(expectedValue);
```
## Configuration Files
### vite.config.mts Template
```typescript
/// <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
// @ts-expect-error - Vitest reporter tuple types have complex inference issues
defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/[path]', // Adjust depth!
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
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',
['junit', { outputFile: '../../../testresults/junit-[name].xml' }],
],
coverage: {
reportsDirectory: '../../../coverage/libs/[path]',
provider: 'v8' as const,
reporter: ['text', 'cobertura'],
},
},
}));
```
### test-setup.ts
```typescript
import '@analogjs/vitest-angular/setup-zone';
```
## Path Depth Reference
| Library Location | Relative Path Prefix |
|-----------------|---------------------|
| `libs/feature/ui` | `../../` |
| `libs/feature/data-access` | `../../` |
| `libs/domain/feature/ui` | `../../../` |
| `libs/domain/feature/data-access/store` | `../../../../` |

View File

@@ -1,199 +0,0 @@
---
name: type-safety-engineer
description: This skill should be used when improving TypeScript type safety by removing `any` types, adding Zod schemas for runtime validation, creating type guards, and strengthening strictness. Use this skill when the user wants to enhance type safety, mentions "fix any types", "add Zod validation", or requests type improvements for better code quality.
---
# Type Safety Engineer
## Overview
Enhance TypeScript type safety by eliminating `any` types, adding Zod schemas for runtime validation, creating type guards, and strengthening compiler strictness.
## When to Use This Skill
Invoke when user wants to:
- Remove `any` types
- Add runtime validation with Zod
- Improve type safety
- Mentioned "type safety" or "Zod schemas"
## Type Safety Workflow
### Step 1: Scan for Issues
```bash
# Find explicit any
grep -r ": any" libs/ --include="*.ts" | grep -v ".spec.ts"
# Find functions without return types
grep -r "^.*function.*{$" libs/ --include="*.ts" | grep -v ": "
# TypeScript strict mode check
npx tsc --noEmit --strict
```
### Step 2: Categorize Issues
**🔴 Critical:**
- `any` in API response handling
- `any` in service methods
- `any` in store state types
**⚠️ Important:**
- Missing return types
- Untyped parameters
- Weak types (`object`, `Function`)
** Moderate:**
- `any` in test files
- Loose array types
### Step 3: Add Zod Schemas for API Responses
```typescript
import { z } from 'zod';
// Define schema
const OrderItemSchema = z.object({
productId: z.string().uuid(),
quantity: z.number().int().positive(),
price: z.number().positive()
});
const OrderResponseSchema = z.object({
id: z.string().uuid(),
status: z.enum(['pending', 'confirmed', 'shipped']),
items: z.array(OrderItemSchema),
createdAt: z.string().datetime()
});
// Infer TypeScript type
type OrderResponse = z.infer<typeof OrderResponseSchema>;
// Runtime validation
const order = OrderResponseSchema.parse(apiResponse);
```
### Step 4: Replace `any` with Specific Types
**Pattern 1: Unknown + Type Guards**
```typescript
// BEFORE
function processData(data: any) {
return data.value;
}
// AFTER
function processData(data: unknown): string {
if (!isValidData(data)) {
throw new Error('Invalid data');
}
return data.value;
}
function isValidData(data: unknown): data is { value: string } {
return typeof data === 'object' && data !== null && 'value' in data;
}
```
**Pattern 2: Generic Types**
```typescript
// BEFORE
function findById(items: any[], id: string): any {
return items.find(item => item.id === id);
}
// AFTER
function findById<T extends { id: string }>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
```
### Step 5: Add Type Guards for API Data
```typescript
export function isOrderResponse(data: unknown): data is OrderResponse {
try {
OrderResponseSchema.parse(data);
return true;
} catch {
return false;
}
}
// Use in service
getOrder(id: string): Observable<OrderResponse> {
return this.http.get(`/api/orders/${id}`).pipe(
map(response => {
if (!isOrderResponse(response)) {
throw new Error('Invalid API response');
}
return response;
})
);
}
```
### Step 6: Validate Changes
```bash
npx tsc --noEmit --strict
npx nx affected:test --skip-nx-cache
npx nx affected:lint
```
### Step 7: Generate Report
```
Type Safety Improvements
========================
Path: [analyzed path]
🔍 Issues Found
---------------
`any` usages: XX → 0
Missing return types: XX → 0
Untyped parameters: XX → 0
✅ Improvements
---------------
- Added Zod schemas: XX
- Created type guards: XX
- Fixed `any` types: XX
- Added return types: XX
📈 Type Safety Score
--------------------
Before: XX%
After: XX% (+XX%)
💡 Recommendations
------------------
1. Enable stricter TypeScript options
2. Add validation to remaining APIs
```
## Common Patterns
**API Response Validation:**
```typescript
const schema = z.object({...});
type Type = z.infer<typeof schema>;
return this.http.get<unknown>(url).pipe(
map(response => schema.parse(response))
);
```
**Event Handlers:**
```typescript
// BEFORE: onClick(event: any)
// AFTER: onClick(event: MouseEvent)
```
## References
- Use `docs-researcher` for latest Zod documentation
- Zod: https://zod.dev
- TypeScript strict mode: https://www.typescriptlang.org/tsconfig#strict

View File

@@ -1,293 +0,0 @@
# Zod Patterns Reference
## Overview
Zod is a TypeScript-first schema validation library. Use it for runtime validation at system boundaries (API responses, form inputs, external data).
## Basic Schema Patterns
### Primitive Types
```typescript
import { z } from 'zod';
// Basic types
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
const dateSchema = z.date();
// With constraints
const emailSchema = z.string().email();
const positiveNumber = z.number().positive();
const nonEmptyString = z.string().min(1);
const optionalString = z.string().optional();
const nullableString = z.string().nullable();
```
### Object Schemas
```typescript
// Basic object
const userSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1),
age: z.number().int().positive().optional(),
role: z.enum(['admin', 'user', 'guest']),
createdAt: z.string().datetime(),
});
// Infer TypeScript type from schema
type User = z.infer<typeof userSchema>;
// Partial and Pick utilities
const partialUser = userSchema.partial(); // All fields optional
const requiredUser = userSchema.required(); // All fields required
const pickedUser = userSchema.pick({ email: true, name: true });
const omittedUser = userSchema.omit({ createdAt: true });
```
### Array Schemas
```typescript
// Basic array
const stringArray = z.array(z.string());
// With constraints
const nonEmptyArray = z.array(z.string()).nonempty();
const limitedArray = z.array(z.number()).min(1).max(10);
// Tuple (fixed length, different types)
const coordinate = z.tuple([z.number(), z.number()]);
```
## API Response Validation
### Pattern: Validate API Responses
```typescript
// Define response schema
const apiResponseSchema = z.object({
data: z.object({
items: z.array(userSchema),
total: z.number(),
page: z.number(),
pageSize: z.number(),
}),
meta: z.object({
timestamp: z.string().datetime(),
requestId: z.string().uuid(),
}),
});
// In Angular service
@Injectable({ providedIn: 'root' })
export class UserService {
#http = inject(HttpClient);
#logger = logger({ component: 'UserService' });
getUsers(): Observable<User[]> {
return this.#http.get('/api/users').pipe(
map((response) => {
const result = apiResponseSchema.safeParse(response);
if (!result.success) {
this.#logger.error('Invalid API response', {
errors: result.error.errors
});
throw new Error('Invalid API response');
}
return result.data.data.items;
})
);
}
}
```
### Pattern: Coerce Types
```typescript
// API returns string IDs, coerce to number
const productSchema = z.object({
id: z.coerce.number(), // "123" -> 123
price: z.coerce.number(), // "99.99" -> 99.99
inStock: z.coerce.boolean(), // "true" -> true
createdAt: z.coerce.date(), // "2024-01-01" -> Date
});
```
## Form Validation
### Pattern: Form Schema
```typescript
// Define form schema with custom error messages
const loginFormSchema = z.object({
email: z.string()
.email({ message: 'Invalid email address' }),
password: z.string()
.min(8, { message: 'Password must be at least 8 characters' })
.regex(/[A-Z]/, { message: 'Must contain uppercase letter' })
.regex(/[0-9]/, { message: 'Must contain number' }),
rememberMe: z.boolean().default(false),
});
// Use with Angular forms
type LoginForm = z.infer<typeof loginFormSchema>;
```
### Pattern: Cross-field Validation
```typescript
const passwordFormSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string(),
}).refine(
(data) => data.password === data.confirmPassword,
{
message: "Passwords don't match",
path: ['confirmPassword'], // Error path
}
);
```
## Type Guards
### Pattern: Create Type Guard from Schema
```typescript
// Schema
const customerSchema = z.object({
type: z.literal('customer'),
customerId: z.string(),
loyaltyPoints: z.number(),
});
// Type guard function
function isCustomer(value: unknown): value is z.infer<typeof customerSchema> {
return customerSchema.safeParse(value).success;
}
// Usage
if (isCustomer(data)) {
console.log(data.loyaltyPoints); // Type-safe access
}
```
### Pattern: Discriminated Unions
```typescript
const customerSchema = z.object({
type: z.literal('customer'),
customerId: z.string(),
});
const guestSchema = z.object({
type: z.literal('guest'),
sessionId: z.string(),
});
const userSchema = z.discriminatedUnion('type', [
customerSchema,
guestSchema,
]);
type User = z.infer<typeof userSchema>;
// User = { type: 'customer'; customerId: string } | { type: 'guest'; sessionId: string }
```
## Replacing `any` Types
### Before (unsafe)
```typescript
function processOrder(order: any) {
// No type safety
console.log(order.items.length);
console.log(order.customer.name);
}
```
### After (with Zod)
```typescript
const orderSchema = z.object({
id: z.string().uuid(),
items: z.array(z.object({
productId: z.string(),
quantity: z.number().positive(),
price: z.number().nonnegative(),
})),
customer: z.object({
name: z.string(),
email: z.string().email(),
}),
status: z.enum(['pending', 'confirmed', 'shipped', 'delivered']),
});
type Order = z.infer<typeof orderSchema>;
function processOrder(input: unknown): Order {
const order = orderSchema.parse(input); // Throws if invalid
console.log(order.items.length); // Type-safe
console.log(order.customer.name); // Type-safe
return order;
}
```
## Error Handling
### Pattern: Structured Error Handling
```typescript
const result = schema.safeParse(data);
if (!result.success) {
// Access formatted errors
const formatted = result.error.format();
// Access flat error list
const flat = result.error.flatten();
// Custom error mapping
const errors = result.error.errors.map(err => ({
field: err.path.join('.'),
message: err.message,
code: err.code,
}));
}
```
## Transform Patterns
### Pattern: Transform on Parse
```typescript
const userInputSchema = z.object({
email: z.string().email().transform(s => s.toLowerCase()),
name: z.string().transform(s => s.trim()),
tags: z.string().transform(s => s.split(',')),
});
// Input: { email: "USER@EXAMPLE.COM", name: " John ", tags: "a,b,c" }
// Output: { email: "user@example.com", name: "John", tags: ["a", "b", "c"] }
```
### Pattern: Default Values
```typescript
const configSchema = z.object({
theme: z.enum(['light', 'dark']).default('light'),
pageSize: z.number().default(20),
features: z.array(z.string()).default([]),
});
```
## Best Practices
1. **Define schemas at module boundaries** - API services, form handlers
2. **Use `safeParse` for error handling** - Don't let validation throw unexpectedly
3. **Infer types from schemas** - Single source of truth
4. **Add meaningful error messages** - Help debugging and user feedback
5. **Use transforms for normalization** - Clean data on parse
6. **Keep schemas close to usage** - Colocate with services/components

View File

@@ -39,18 +39,17 @@ Task involves external API? → AUTO-INVOKE docs-researcher
| Trigger | Auto-Invoke Skill |
|---------|-------------------|
| Writing Angular templates | `angular-template` |
| Writing HTML with interactivity | `html-template` |
| Writing Angular templates | `template-standards` |
| Applying Tailwind classes | `tailwind` |
| Writing any Angular code | `logging` |
| Creating CSS animations | `css-keyframes-animations` |
| Creating new library | `library-scaffolder` |
| Regenerating API clients | `swagger-sync-manager` |
| Creating CSS animations | `css-animations` |
| Creating new library | `library-creator` |
| Regenerating API clients | `api-sync` |
| Git operations | `git-workflow` |
**Skill chaining for Angular work:**
```
angular-template → html-template → logging → tailwind
template-standards → logging → tailwind
```
### Implementation Agents (Use for Complex Tasks)
@@ -75,7 +74,7 @@ angular-template → html-template → logging → tailwind
```
✅ "Researching [library] API..." → [auto-invokes docs-researcher]
✅ "Loading angular-template skill..." → [auto-invokes skill]
✅ "Loading template-standards skill..." → [auto-invokes skill]
✅ "Implementing based on current docs..."
✅ If fails: "Escalating research..." → [auto-invokes docs-researcher-advanced]
```
@@ -223,7 +222,7 @@ Extract only what's needed, discard the rest:
```
✓ Created 3 files
✓ Tests: 12/12 passing
✓ Skills applied: angular-template, logging
✓ Skills applied: template-standards, logging
```
<!-- nx configuration start-->

View File

@@ -25,6 +25,7 @@ import {
tabResolverFn,
processResolverFn,
hasTabIdGuard,
deactivateTabGuard,
} from '@isa/core/tabs';
export const routes: Routes = [
@@ -47,6 +48,7 @@ export const routes: Routes = [
path: 'dashboard',
loadChildren: () =>
import('@page/dashboard').then((m) => m.DashboardModule),
canActivate: [deactivateTabGuard],
data: {
matomo: {
title: 'Dashboard',

View File

@@ -1,6 +1,6 @@
# Library Reference Guide
> **Last Updated:** 2025-12-10
> **Last Updated:** 2025-12-11
> **Angular Version:** 20.3.6
> **Nx Version:** 21.3.2
> **Total Libraries:** 81

View File

@@ -80,7 +80,7 @@ export class IsaTitleStrategy extends TitleStrategy {
const tabService = this.#getTabService();
const activeTabId = tabService.activatedTabId();
if (activeTabId !== null) {
if (activeTabId !== null && activeTabId !== undefined) {
tabService.patchTab(activeTabId, {
name: pageTitle,
});

View File

@@ -32,6 +32,7 @@ The Core Tabs library provides a comprehensive solution for managing multiple ta
- **Metadata system** - Flexible per-tab metadata storage
- **Angular DevTools integration** - Debug tab state with Redux DevTools
- **Router resolver** - Automatic tab creation from route parameters
- **Tab deactivation guard** - Deactivate tabs when entering global routes
- **Injection helpers** - Convenient signal-based injection functions
- **Navigate back component** - Ready-to-use back button component
@@ -332,6 +333,20 @@ Activates a tab by ID and updates its activation timestamp.
this.#tabService.activateTab(42);
```
##### `deactivateTab(): void`
Deactivates the currently active tab by setting `activatedTabId` to null.
**Use Cases:**
- Navigating to global routes (dashboard, branch operations)
- Exiting tab context without closing the tab
**Example:**
```typescript
this.#tabService.deactivateTab();
// activatedTabId is now null
```
##### `patchTab(id: number, changes: PatchTabInput): void`
Partially updates a tab's properties.
@@ -666,6 +681,42 @@ export class CustomerComponent {
}
```
### Deactivate Tab Guard
#### `deactivateTabGuard: CanActivateFn`
Angular Router guard that deactivates the active tab when entering a route.
**Use Case:** Routes that exist outside of a process/tab context, such as dashboard pages or global branch operations.
**Behavior:**
- Sets `activatedTabId` to null
- Logs the deactivation with previous tab ID
- Always returns true (allows navigation)
**Route Configuration:**
```typescript
import { Routes } from '@angular/router';
import { deactivateTabGuard } from '@isa/core/tabs';
export const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./dashboard').then(m => m.DashboardModule),
canActivate: [deactivateTabGuard]
},
{
path: 'filiale/task-calendar',
loadChildren: () => import('./task-calendar').then(m => m.TaskCalendarModule),
canActivate: [deactivateTabGuard]
}
];
```
**Why use a guard instead of TabNavigationService?**
Tab activation happens in route resolvers (`tabResolverFn`), so tab deactivation should also happen in route guards for consistency. This keeps all tab activation/deactivation logic within the Angular Router lifecycle.
### NavigateBackButtonComponent
Ready-to-use back button component with automatic state management.
@@ -1209,12 +1260,18 @@ The service recognizes two URL patterns:
### URL Blacklist
Certain URLs are excluded from history tracking:
Certain URL patterns are excluded from history tracking using prefix matching:
```typescript
export const HISTORY_BLACKLIST_URLS = ['/kunde/dashboard'];
export const HISTORY_BLACKLIST_PATTERNS = ['/kunde/dashboard', '/dashboard'];
```
**Behavior:**
- Uses prefix matching (not exact match)
- `/kunde/dashboard` excludes `/kunde/dashboard`, `/kunde/dashboard/`, `/kunde/dashboard/stats`, etc.
- `/dashboard` excludes `/dashboard`, `/dashboard/overview`, etc.
- Does NOT exclude `/kunde/123/dashboard` (different prefix)
### Router Resolver Integration
```typescript
@@ -1495,18 +1552,6 @@ const { index, wasInvalid } = TabHistoryPruner.validateLocationIndex(
**Impact:** Medium risk for long-running sessions
#### 3. URL Blacklist Not Configurable (Low Priority)
**Current State:**
- Hardcoded blacklist in constants
- Cannot customize at runtime
**Proposed Solution:**
- Injection token for blacklist
- Merge with default blacklist
**Impact:** Low, blacklist rarely needs customization
### Future Enhancements
Potential improvements identified:

View File

@@ -9,3 +9,4 @@ export * from './lib/tab-config';
export * from './lib/helpers';
export * from './lib/has-tab-id.guard';
export * from './lib/tab-cleanup.guard';
export * from './lib/deactivate-tab.guard';

View File

@@ -0,0 +1,35 @@
import { inject } from '@angular/core';
import { CanActivateFn } from '@angular/router';
import { TabService } from './tab';
import { logger } from '@isa/core/logging';
/**
* Guard that deactivates the currently active tab when entering a route.
*
* Use this guard on routes that exist outside of a process/tab context,
* such as dashboard pages or global branch operations.
*
* @example
* ```typescript
* const routes: Routes = [
* {
* path: 'dashboard',
* loadChildren: () => import('./dashboard').then(m => m.DashboardModule),
* canActivate: [deactivateTabGuard],
* },
* ];
* ```
*/
export const deactivateTabGuard: CanActivateFn = () => {
const tabService = inject(TabService);
const log = logger({ guard: 'deactivateTabGuard' });
const previousTabId = tabService.activatedTabId();
if (previousTabId !== null) {
tabService.deactivateTab();
log.debug('Tab deactivated on route activation', () => ({ previousTabId }));
}
return true;
};

View File

@@ -6,16 +6,21 @@
*/
/**
* URLs that should not be added to tab navigation history.
* URL patterns that should not be added to tab navigation history.
*
* These routes are excluded to prevent cluttering the history stack with
* frequently visited pages that don't need to be tracked in tab history.
* These patterns are matched using prefix matching - any URL that starts with
* a pattern in this list will be excluded from history tracking.
*
* @example
* ```typescript
* // Dashboard routes are excluded because they serve as entry points
* // and don't represent meaningful navigation steps in a workflow
* // Dashboard routes are excluded because they don't run in a process context
* // Pattern '/kunde/dashboard' excludes:
* // - '/kunde/dashboard'
* // - '/kunde/dashboard/'
* // - '/kunde/dashboard/stats'
* // But NOT:
* // - '/kunde/123/dashboard'
* '/kunde/dashboard'
* ```
*/
export const HISTORY_BLACKLIST_URLS = ['/kunde/dashboard'];
export const HISTORY_BLACKLIST_PATTERNS = ['/kunde/dashboard', '/dashboard'];

View File

@@ -4,7 +4,7 @@ import { filter } from 'rxjs/operators';
import { TabService } from './tab';
import { TabLocation } from './schemas';
import { Title } from '@angular/platform-browser';
import { HISTORY_BLACKLIST_URLS } from './tab-navigation.constants';
import { HISTORY_BLACKLIST_PATTERNS } from './tab-navigation.constants';
/**
* Service that automatically syncs browser navigation events to tab location history.
@@ -39,7 +39,7 @@ export class TabNavigationService {
}
#syncNavigationToTab(event: NavigationEnd) {
// Skip blacklisted URLs
// Skip blacklisted URLs (tab deactivation handled by route guards)
if (this.#shouldSkipHistory(event.url)) {
return;
}
@@ -65,11 +65,14 @@ export class TabNavigationService {
/**
* Checks if a URL should be excluded from tab navigation history.
*
* @param url - The URL to check against the blacklist
* Uses prefix matching - a URL is excluded if it starts with any pattern
* in the blacklist. This allows excluding entire route trees.
*
* @param url - The URL to check against the blacklist patterns
* @returns true if the URL should be skipped, false otherwise
*/
#shouldSkipHistory(url: string): boolean {
return HISTORY_BLACKLIST_URLS.includes(url);
return HISTORY_BLACKLIST_PATTERNS.some(pattern => url.startsWith(pattern));
}
#getActiveTabId(url: string): number | null {

View File

@@ -104,6 +104,13 @@ export const TabService = signalStore(
store._logger.debug('Tab activated', () => ({ tabId: id }));
},
deactivateTab() {
const previousTabId = store.activatedTabId();
patchState(store, { activatedTabId: null });
store._logger.debug('Tab deactivated', () => ({
previousTabId,
}));
},
patchTab(id: number, changes: z.infer<typeof PatchTabSchema>) {
const currentTab = store.entityMap()[id];

View File

@@ -58,12 +58,21 @@ Individual tab item component.
## Compact Mode
The tabs bar uses mouse proximity detection to automatically switch display modes:
The tabs bar automatically switches between expanded and compact display modes based on user interaction.
- **Expanded mode** (mouse near): Full height tabs showing name and subtitle
- **Compact mode** (mouse away): Reduced height tabs with hidden subtitle
### Desktop Behavior
The proximity threshold is 50 pixels from the component edge.
- **Expanded** when mouse is within 50px of the tabs bar
- **Collapsed** with 1-second delay when mouse moves away
- **Expanded** when page is scrolled to top (< 50px from top)
### Tablet Behavior (≤ 1279px viewport)
- **Expanded** when page is scrolled to top (< 50px from top)
- **Expanded** when scrolling up (after 50px of upward scroll)
- **Collapsed** when scrolling down
Mouse proximity detection is disabled on tablet devices.
## Dependencies

View File

@@ -2,8 +2,32 @@
@apply block opacity-40 self-end;
}
:host.active {
:host.active:not(.fade-slide-out) {
@apply opacity-100;
animation: tabActivate 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
:host.active.fade-slide-out {
@apply opacity-100;
}
/* Active tab animation - subtle scale pulse */
@keyframes tabActivate {
0% {
transform: scale(1);
}
50% {
transform: scale(1.03);
}
100% {
transform: scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
:host.active:not(.fade-slide-out) {
animation: none;
}
}
a {

View File

@@ -1,14 +1,25 @@
import { Component, inject, ElementRef } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import {
Component,
computed,
effect,
ElementRef,
inject,
OnDestroy,
signal,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { Router } from '@angular/router';
import { logger } from '@isa/core/logging';
import { ShellTabItemComponent } from './components/shell-tab-item.component';
import { TabService } from '@isa/core/tabs';
import { CarouselComponent } from '@isa/ui/carousel';
import { IconButtonComponent, TextButtonComponent } from '@isa/ui/buttons';
import { provideIcons } from '@ng-icons/core';
import { isaActionPlus } from '@isa/icons';
import { TabsCollapsedService } from '@isa/shell/common';
import { IconButtonComponent, TextButtonComponent } from '@isa/ui/buttons';
import { CarouselComponent } from '@isa/ui/carousel';
import { Breakpoint, breakpoint } from '@isa/ui/layout';
import { provideIcons } from '@ng-icons/core';
import { fromEvent, map, startWith } from 'rxjs';
import { ShellTabItemComponent } from './components/shell-tab-item.component';
/**
* Distance in pixels from the component edge within which
@@ -16,6 +27,24 @@ import { Breakpoint, breakpoint } from '@isa/ui/layout';
*/
const PROXIMITY_THRESHOLD_PX = 50;
/**
* Scroll position threshold in pixels. Tabs expand when
* scroll position is below this value.
*/
const SCROLL_THRESHOLD_PX = 50;
/**
* Delay in milliseconds before collapsing tabs after
* the mouse moves away from the proximity zone.
*/
const COLLAPSE_DELAY_MS = 1000;
/**
* Minimum scroll distance in pixels required to detect
* a scroll direction change (tablet only).
*/
const SCROLL_DIRECTION_THRESHOLD_PX = 50;
/**
* Shell tabs bar component that displays all open tabs in a horizontal carousel.
*
@@ -49,8 +78,9 @@ const PROXIMITY_THRESHOLD_PX = 50;
'(document:mousemove)': 'onMouseMove($event)',
},
})
export class ShellTabsComponent {
export class ShellTabsComponent implements OnDestroy {
#logger = logger({ component: 'ShellTabsComponent' });
#document = inject(DOCUMENT);
#tablet = breakpoint(Breakpoint.Tablet);
#tabService = inject(TabService);
@@ -58,15 +88,83 @@ export class ShellTabsComponent {
#elementRef = inject(ElementRef);
#router = inject(Router);
/** Internal state: whether mouse is within proximity (desktop only). */
#mouseNear = signal(false);
/** Internal state: whether page is scrolled near top. */
#scrolledToTop = signal(true);
/** Internal state: whether user is scrolling up (tablet only). */
#scrollingUp = signal(false);
/** Last scroll position to detect direction. */
#lastScrollY = 0;
/** Anchor position when scroll direction changes (for threshold detection). */
#scrollAnchorY = 0;
/** Current scroll direction: 'up' | 'down' | null */
#currentDirection: 'up' | 'down' | null = null;
/** Timer ID for delayed collapse. */
#collapseTimer: number | undefined;
/** Tracks the current scroll position of the window. */
#scrollPosition = toSignal(
fromEvent(this.#document.defaultView!, 'scroll', { passive: true }).pipe(
startWith(null),
map(() => this.#document.defaultView?.scrollY ?? 0),
),
{ initialValue: 0 },
);
/**
* Computed collapsed state based on combined conditions.
* Desktop: Expanded when mouse is near OR scrolled to top.
* Tablet: Expanded when scrolled to top OR scrolling up.
*/
#shouldCollapse = computed(() => {
const isTablet = this.#tablet();
const mouseCondition = isTablet ? false : this.#mouseNear();
const scrollTopCondition = this.#scrolledToTop();
// Scroll direction only matters on tablet
const scrollUpCondition = isTablet ? this.#scrollingUp() : false;
return !(mouseCondition || scrollTopCondition || scrollUpCondition);
});
/** All tabs from the TabService. */
readonly tabs = this.#tabService.entities;
/** Whether tabs should display in compact mode (mouse is far from component). */
/** Whether tabs should display in compact mode. */
compact = this.#tabsCollapsedService.get;
constructor() {
// Sync scroll position to scrolledToTop and track direction
effect(() => {
const scrollY = this.#scrollPosition();
// Track scroll direction with threshold (for tablet)
this.#updateScrollDirection(scrollY);
// Track if at top
this.#scrolledToTop.set(scrollY < SCROLL_THRESHOLD_PX);
});
// Sync computed collapsed state to service
effect(() => {
this.#tabsCollapsedService.set(this.#shouldCollapse());
});
}
ngOnDestroy(): void {
this.#clearCollapseTimer();
}
/**
* Handles mouse movement to detect proximity to the tabs bar.
* Updates compact mode based on whether the mouse is near the component.
* Expands immediately when near, collapses with delay when far.
* Skipped on tablet devices (scroll-only behavior).
*/
onMouseMove(event: MouseEvent): void {
if (this.#tablet()) {
@@ -74,19 +172,75 @@ export class ShellTabsComponent {
}
const rect = this.#elementRef.nativeElement.getBoundingClientRect();
const mouseY = event.clientY;
const mouseX = event.clientX;
const isWithinX = mouseX >= rect.left && mouseX <= rect.right;
const isWithinX =
event.clientX >= rect.left && event.clientX <= rect.right;
const distanceY =
mouseY < rect.top
? rect.top - mouseY
: mouseY > rect.bottom
? mouseY - rect.bottom
event.clientY < rect.top
? rect.top - event.clientY
: event.clientY > rect.bottom
? event.clientY - rect.bottom
: 0;
const isNear = isWithinX && distanceY <= PROXIMITY_THRESHOLD_PX;
this.#tabsCollapsedService.set(!isNear);
if (isNear) {
this.#clearCollapseTimer();
this.#mouseNear.set(true);
} else if (this.#mouseNear()) {
this.#scheduleMouseLeave();
}
}
/** Schedules a delayed collapse when mouse leaves proximity zone. */
#scheduleMouseLeave(): void {
if (this.#collapseTimer !== undefined) {
return;
}
this.#collapseTimer = window.setTimeout(() => {
this.#mouseNear.set(false);
this.#collapseTimer = undefined;
}, COLLAPSE_DELAY_MS);
}
/** Cancels any pending collapse timer. */
#clearCollapseTimer(): void {
if (this.#collapseTimer !== undefined) {
clearTimeout(this.#collapseTimer);
this.#collapseTimer = undefined;
}
}
/**
* Updates scroll direction state with threshold detection.
* Only triggers scrollingUp after scrolling up at least SCROLL_DIRECTION_THRESHOLD_PX.
*/
#updateScrollDirection(scrollY: number): void {
// Detect direction based on movement from last position
const movingUp = scrollY < this.#lastScrollY;
const movingDown = scrollY > this.#lastScrollY;
this.#lastScrollY = scrollY;
if (movingUp) {
if (this.#currentDirection !== 'up') {
// Direction changed to up, set new anchor
this.#currentDirection = 'up';
this.#scrollAnchorY = scrollY;
}
// Check if scrolled up enough from anchor
const distanceFromAnchor = this.#scrollAnchorY - scrollY;
if (distanceFromAnchor >= SCROLL_DIRECTION_THRESHOLD_PX) {
this.#scrollingUp.set(true);
}
} else if (movingDown) {
if (this.#currentDirection !== 'down') {
// Direction changed to down, set new anchor
this.#currentDirection = 'down';
this.#scrollAnchorY = scrollY;
}
// Collapse immediately when scrolling down
this.#scrollingUp.set(false);
}
}
/** Closes all open tabs and navigates to the root route. */