ui-ux-consultant-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/ui-ux-consultant/SKILL.md +844 -0
- package/assets/ui-ux-consultant/references/accessibility.md +175 -0
- package/assets/ui-ux-consultant/references/alt-libraries.md +90 -0
- package/assets/ui-ux-consultant/references/animations.md +448 -0
- package/assets/ui-ux-consultant/references/catalog/colors.md +91 -0
- package/assets/ui-ux-consultant/references/catalog/fonts.md +363 -0
- package/assets/ui-ux-consultant/references/catalog/products.md +340 -0
- package/assets/ui-ux-consultant/references/catalog/styles.md +165 -0
- package/assets/ui-ux-consultant/references/components.md +1116 -0
- package/assets/ui-ux-consultant/references/patterns.md +600 -0
- package/assets/ui-ux-consultant/references/performance.md +198 -0
- package/assets/ui-ux-consultant/references/stacks/astro.md +382 -0
- package/assets/ui-ux-consultant/references/stacks/flutter.md +308 -0
- package/assets/ui-ux-consultant/references/stacks/html-tailwind.md +415 -0
- package/assets/ui-ux-consultant/references/stacks/jetpack-compose.md +333 -0
- package/assets/ui-ux-consultant/references/stacks/laravel.md +521 -0
- package/assets/ui-ux-consultant/references/stacks/nextjs.md +275 -0
- package/assets/ui-ux-consultant/references/stacks/nuxt-ui.md +384 -0
- package/assets/ui-ux-consultant/references/stacks/nuxtjs.md +264 -0
- package/assets/ui-ux-consultant/references/stacks/react-native.md +346 -0
- package/assets/ui-ux-consultant/references/stacks/react.md +268 -0
- package/assets/ui-ux-consultant/references/stacks/shadcn.md +485 -0
- package/assets/ui-ux-consultant/references/stacks/svelte.md +429 -0
- package/assets/ui-ux-consultant/references/stacks/swiftui.md +336 -0
- package/assets/ui-ux-consultant/references/stacks/threejs.md +366 -0
- package/assets/ui-ux-consultant/references/stacks/vue.md +272 -0
- package/assets/ui-ux-consultant/references/theming.md +701 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +130 -0
- package/package.json +51 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Angular CDK A11y and Accessibility Patterns
|
|
2
|
+
|
|
3
|
+
## Section 1: Angular CDK A11y Module
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { A11yModule } from '@angular/cdk/a11y';
|
|
7
|
+
// or individual:
|
|
8
|
+
import { FocusTrap, FocusMonitor, LiveAnnouncer } from '@angular/cdk/a11y';
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Section 2: Focus Management
|
|
14
|
+
|
|
15
|
+
**Trap focus inside modals/dialogs:**
|
|
16
|
+
```html
|
|
17
|
+
<div cdkTrapFocus cdkTrapFocusAutoCapture>
|
|
18
|
+
<h2>Dialog Title</h2>
|
|
19
|
+
<button cdkFocusInitial>First focused element</button>
|
|
20
|
+
<!-- Tab cycles within this div -->
|
|
21
|
+
</div>
|
|
22
|
+
```
|
|
23
|
+
Note: `MatDialog` handles this automatically. Use `cdkTrapFocus` for custom overlays.
|
|
24
|
+
|
|
25
|
+
**FocusMonitor — detect keyboard vs mouse focus:**
|
|
26
|
+
```typescript
|
|
27
|
+
private focusMonitor = inject(FocusMonitor);
|
|
28
|
+
private elementRef = inject(ElementRef);
|
|
29
|
+
|
|
30
|
+
ngAfterViewInit() {
|
|
31
|
+
this.focusMonitor.monitor(this.elementRef, true).subscribe(origin => {
|
|
32
|
+
// origin: 'keyboard' | 'mouse' | 'touch' | 'program' | null
|
|
33
|
+
if (origin === 'keyboard') {
|
|
34
|
+
// Show visible focus indicator
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**LiveAnnouncer — announce dynamic content to screen readers:**
|
|
41
|
+
```typescript
|
|
42
|
+
private announcer = inject(LiveAnnouncer);
|
|
43
|
+
|
|
44
|
+
async save() {
|
|
45
|
+
await this.saveData();
|
|
46
|
+
this.announcer.announce('Changes saved successfully', 'polite');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async delete() {
|
|
50
|
+
await this.deleteItem();
|
|
51
|
+
this.announcer.announce('Item deleted', 'assertive'); // immediate
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Section 3: ARIA Patterns
|
|
58
|
+
|
|
59
|
+
**Current page in nav:**
|
|
60
|
+
```html
|
|
61
|
+
<mat-nav-list>
|
|
62
|
+
@for (link of navLinks; track link.path) {
|
|
63
|
+
<a mat-list-item [routerLink]="link.path" routerLinkActive="active"
|
|
64
|
+
[attr.aria-current]="isActive(link.path) ? 'page' : null">
|
|
65
|
+
{{ link.label }}
|
|
66
|
+
</a>
|
|
67
|
+
}
|
|
68
|
+
</mat-nav-list>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Loading states:**
|
|
72
|
+
```html
|
|
73
|
+
<div role="status" aria-live="polite" aria-label="Loading content">
|
|
74
|
+
@if (loading()) { <mat-spinner /> }
|
|
75
|
+
</div>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Buttons that toggle:**
|
|
79
|
+
```html
|
|
80
|
+
<button mat-icon-button [attr.aria-expanded]="isOpen()" [attr.aria-label]="isOpen() ? 'Collapse menu' : 'Expand menu'"
|
|
81
|
+
(click)="isOpen.update(v => !v)">
|
|
82
|
+
<mat-icon>{{ isOpen() ? 'expand_less' : 'expand_more' }}</mat-icon>
|
|
83
|
+
</button>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Icon buttons need labels:**
|
|
87
|
+
```html
|
|
88
|
+
<!-- BAD -->
|
|
89
|
+
<button mat-icon-button><mat-icon>delete</mat-icon></button>
|
|
90
|
+
|
|
91
|
+
<!-- GOOD -->
|
|
92
|
+
<button mat-icon-button aria-label="Delete item"><mat-icon>delete</mat-icon></button>
|
|
93
|
+
<!-- or: -->
|
|
94
|
+
<button mat-icon-button [matTooltip]="'Delete'" [attr.aria-label]="'Delete'">
|
|
95
|
+
<mat-icon>delete</mat-icon>
|
|
96
|
+
</button>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Data tables:**
|
|
100
|
+
```html
|
|
101
|
+
<table mat-table [dataSource]="data()" aria-label="Users list">
|
|
102
|
+
<ng-container matColumnDef="name">
|
|
103
|
+
<th mat-header-cell *matHeaderCellDef scope="col">Name</th>
|
|
104
|
+
<td mat-cell *matCellDef="let row">{{ row.name }}</td>
|
|
105
|
+
</ng-container>
|
|
106
|
+
</table>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Section 4: Keyboard Navigation
|
|
112
|
+
|
|
113
|
+
**Skip links** (first element on page):
|
|
114
|
+
```html
|
|
115
|
+
<!-- app.component.html - very first element -->
|
|
116
|
+
<a href="#main-content" class="skip-link">Skip to main content</a>
|
|
117
|
+
<mat-sidenav-container>
|
|
118
|
+
<mat-sidenav>...</mat-sidenav>
|
|
119
|
+
<mat-sidenav-content>
|
|
120
|
+
<main id="main-content">
|
|
121
|
+
<router-outlet />
|
|
122
|
+
</main>
|
|
123
|
+
</mat-sidenav-content>
|
|
124
|
+
</mat-sidenav-container>
|
|
125
|
+
```
|
|
126
|
+
```scss
|
|
127
|
+
.skip-link {
|
|
128
|
+
position: absolute; transform: translateY(-100%);
|
|
129
|
+
&:focus { transform: translateY(0); }
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Manage focus on route change:**
|
|
134
|
+
```typescript
|
|
135
|
+
constructor() {
|
|
136
|
+
inject(Router).events.pipe(
|
|
137
|
+
filter(e => e instanceof NavigationEnd),
|
|
138
|
+
takeUntilDestroyed(),
|
|
139
|
+
).subscribe(() => {
|
|
140
|
+
// Move focus to main heading after navigation
|
|
141
|
+
const heading = document.querySelector('h1');
|
|
142
|
+
if (heading) { heading.setAttribute('tabindex', '-1'); heading.focus(); }
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Section 5: Color Contrast Requirements
|
|
150
|
+
|
|
151
|
+
| Text Type | Minimum Contrast | Enhanced |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| Normal text (< 18pt) | 4.5:1 (AA) | 7:1 (AAA) |
|
|
154
|
+
| Large text (≥ 18pt bold or ≥ 24pt) | 3:1 (AA) | 4.5:1 (AAA) |
|
|
155
|
+
| UI components / icons | 3:1 (AA) | — |
|
|
156
|
+
| Decorative elements | No requirement | — |
|
|
157
|
+
|
|
158
|
+
Angular Material 3 palettes are designed to meet AA by default. Always verify when using custom colors. Use: https://webaim.org/resources/contrastchecker/
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Section 6: Accessibility Checklist
|
|
163
|
+
|
|
164
|
+
| Check | Implementation |
|
|
165
|
+
|---|---|
|
|
166
|
+
| All icon buttons have aria-label | `[attr.aria-label]="'Action description'"` |
|
|
167
|
+
| Form fields have labels | `<mat-label>` inside `<mat-form-field>` |
|
|
168
|
+
| Errors are associated | `<mat-error>` auto-associates with input |
|
|
169
|
+
| Images have alt text | `alt="..."` always; `alt=""` for decorative |
|
|
170
|
+
| Focus is visible | Angular Material handles this; test with keyboard |
|
|
171
|
+
| Skip link present | First element in app.component.html |
|
|
172
|
+
| Dynamic content announced | `LiveAnnouncer` for async updates |
|
|
173
|
+
| Color not sole indicator | Icons + text + pattern alongside color |
|
|
174
|
+
| Reduced motion respected | CSS `prefers-reduced-motion` media query |
|
|
175
|
+
| ARIA current on nav | `[attr.aria-current]="isActive ? 'page' : null"` |
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Angular UI Library Decision Guide
|
|
2
|
+
|
|
3
|
+
## Decision Matrix
|
|
4
|
+
|
|
5
|
+
| Criteria | Angular Material | NG-ZORRO | PrimeNG | Tailwind CSS |
|
|
6
|
+
|---|---|---|---|---|
|
|
7
|
+
| Best for | Consumer apps, Google-style | Enterprise admin, data-heavy | Data grids, charts, reports | Custom design systems |
|
|
8
|
+
| Component count | 35+ | 70+ | 90+ | 0 (utility only) |
|
|
9
|
+
| Design spec | Material Design 3 | Ant Design | Custom/Fluent-ish | None |
|
|
10
|
+
| Bundle size | Small | Medium | Large | Very small |
|
|
11
|
+
| Theming | M3 token-based | CSS vars + Less | CSS vars + styled-components | Tailwind config |
|
|
12
|
+
| Accessibility | Excellent (CDK) | Good | Good | Manual |
|
|
13
|
+
| Angular version | 17+ recommended | 17+/21+ | 17+ | Any |
|
|
14
|
+
| Data table | Basic mat-table | Advanced + virtual scroll | Most powerful | Manual |
|
|
15
|
+
| Tree/Hierarchical | CDK tree | Yes | Yes | Manual |
|
|
16
|
+
| Rich text / editors | No | No | Yes | No |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Angular Material — When to Choose
|
|
21
|
+
|
|
22
|
+
- Consumer-facing apps, PWAs, mobile-first
|
|
23
|
+
- Google/Material aesthetic preferred
|
|
24
|
+
- Need best-in-class accessibility (Angular CDK underpins everything)
|
|
25
|
+
- Small team that wants a battle-tested system
|
|
26
|
+
- SSR / hydration support is important
|
|
27
|
+
- **Install:** `ng add @angular/material`
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## NG-ZORRO — When to Choose
|
|
32
|
+
|
|
33
|
+
- Enterprise admin dashboards with complex layouts
|
|
34
|
+
- Need: advanced data tables, tree components, complex forms, rich menu systems
|
|
35
|
+
- Ant Design aesthetic (enterprise-grade, clean)
|
|
36
|
+
- Heavy data display requirements
|
|
37
|
+
- **Install:** `ng add ng-zorro-antd`
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// app.config.ts
|
|
41
|
+
import { provideNzI18n, en_US } from 'ng-zorro-antd/i18n';
|
|
42
|
+
providers: [provideNzI18n(en_US)]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Top NG-ZORRO components beyond Angular Material: `nz-table` (virtual scroll, expandable rows), `nz-tree-select`, `nz-transfer`, `nz-cascader`, `nz-date-picker` (range picker), `nz-upload`
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## PrimeNG — When to Choose
|
|
50
|
+
|
|
51
|
+
- Heavy data analysis apps with charts + grids
|
|
52
|
+
- Need: advanced data table (filtering, grouping, frozen columns), `p-chart` (Chart.js), `p-tree`, rich text editor
|
|
53
|
+
- Large component variety needed quickly
|
|
54
|
+
- **Install:** `npm install primeng`
|
|
55
|
+
|
|
56
|
+
Top PrimeNG components: `p-table` (most feature-complete Angular data table), `p-chart`, `p-treeTable`, `p-fileUpload`, `p-calendar` (with time picker), `p-multiSelect`
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Tailwind CSS + Angular — When to Choose
|
|
61
|
+
|
|
62
|
+
- Custom brand design system (no Material/Ant aesthetic)
|
|
63
|
+
- Rapid prototyping or utility-first workflow
|
|
64
|
+
- Combining with headless UI (Angular CDK primitives)
|
|
65
|
+
- **Install:**
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm install tailwindcss @tailwindcss/vite
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// vite.config.ts (or angular.json for build-based setup)
|
|
73
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
74
|
+
plugins: [tailwindcss()]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```css
|
|
78
|
+
/* styles.css */
|
|
79
|
+
@import "tailwindcss";
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Pair with Angular CDK for accessible headless components (dialogs, listboxes, etc.)
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Mixing Libraries
|
|
87
|
+
|
|
88
|
+
- **Angular Material + Tailwind:** Works well. Material for complex components (dialogs, forms), Tailwind for layout and custom styling. Avoid using Tailwind on Material components (conflicts with `::ng-deep` warnings).
|
|
89
|
+
- **NG-ZORRO + custom Tailwind layout:** Common pattern. NG-ZORRO components, Tailwind for page layout.
|
|
90
|
+
- **Never mix Angular Material + NG-ZORRO + PrimeNG:** Bundle bloat, style conflicts, accessibility inconsistency.
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
# Angular Animations Reference
|
|
2
|
+
|
|
3
|
+
Angular Animations module guide with motion design principles.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Section 1: Setup
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// main.ts or app.config.ts
|
|
11
|
+
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
|
12
|
+
|
|
13
|
+
export const appConfig: ApplicationConfig = {
|
|
14
|
+
providers: [provideAnimationsAsync()],
|
|
15
|
+
};
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`provideAnimationsAsync()` is preferred over `provideAnimations()` — it defers animation loading until first interaction, improving initial load performance.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Section 2: Core Animation API
|
|
23
|
+
|
|
24
|
+
### Imports
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import {
|
|
28
|
+
trigger, state, style, transition, animate,
|
|
29
|
+
keyframes, query, stagger, group, sequence,
|
|
30
|
+
} from '@angular/animations';
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Building Blocks
|
|
34
|
+
|
|
35
|
+
| Function | Purpose |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `trigger(name, [...])` | Named animation attached to a template element |
|
|
38
|
+
| `state(name, style)` | A named style state the element can be in |
|
|
39
|
+
| `transition('a => b', [...])` | Animation to run when moving between two states |
|
|
40
|
+
| `animate('timing', style)` | The actual tween — duration, easing, target styles |
|
|
41
|
+
| `query(selector, [...])` | Target child elements within an animation |
|
|
42
|
+
| `stagger(delay, [...])` | Add incremental delays to a group of animated elements |
|
|
43
|
+
| `group([...])` | Run multiple animations in parallel |
|
|
44
|
+
| `sequence([...])` | Run multiple animations one after another |
|
|
45
|
+
| `keyframes([...])` | Define multi-step animations within a single animate() |
|
|
46
|
+
|
|
47
|
+
### Timing Syntax
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
'200ms' — 200ms linear
|
|
51
|
+
'200ms ease-out' — 200ms with easing
|
|
52
|
+
'200ms 50ms ease-in' — 200ms, delayed 50ms, with easing
|
|
53
|
+
'200ms cubic-bezier(0.4,0,0.2,1)' — custom cubic bezier
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Special Transition Aliases
|
|
57
|
+
|
|
58
|
+
| Alias | Meaning |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `:enter` | Element added to DOM (`void => *`) |
|
|
61
|
+
| `:leave` | Element removed from DOM (`* => void`) |
|
|
62
|
+
| `* <=> *` | Any state change in either direction |
|
|
63
|
+
| `void => *` | Explicit enter (same as `:enter`) |
|
|
64
|
+
| `* => void` | Explicit leave (same as `:leave`) |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Section 3: Common Animations (Copy-Paste Ready)
|
|
69
|
+
|
|
70
|
+
### Fade In/Out
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
export const fadeInOut = trigger('fadeInOut', [
|
|
74
|
+
transition(':enter', [
|
|
75
|
+
style({ opacity: 0 }),
|
|
76
|
+
animate('200ms ease-in', style({ opacity: 1 })),
|
|
77
|
+
]),
|
|
78
|
+
transition(':leave', [
|
|
79
|
+
animate('150ms ease-out', style({ opacity: 0 })),
|
|
80
|
+
]),
|
|
81
|
+
]);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Slide In from Right
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
export const slideInRight = trigger('slideInRight', [
|
|
88
|
+
transition(':enter', [
|
|
89
|
+
style({ transform: 'translateX(100%)', opacity: 0 }),
|
|
90
|
+
animate('250ms cubic-bezier(0.4, 0, 0.2, 1)', style({ transform: 'translateX(0)', opacity: 1 })),
|
|
91
|
+
]),
|
|
92
|
+
transition(':leave', [
|
|
93
|
+
animate('200ms cubic-bezier(0.4, 0, 0.6, 1)', style({ transform: 'translateX(100%)', opacity: 0 })),
|
|
94
|
+
]),
|
|
95
|
+
]);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Slide In from Bottom
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
export const slideInBottom = trigger('slideInBottom', [
|
|
102
|
+
transition(':enter', [
|
|
103
|
+
style({ transform: 'translateY(24px)', opacity: 0 }),
|
|
104
|
+
animate('250ms cubic-bezier(0.4, 0, 0.2, 1)', style({ transform: 'translateY(0)', opacity: 1 })),
|
|
105
|
+
]),
|
|
106
|
+
transition(':leave', [
|
|
107
|
+
animate('200ms cubic-bezier(0.4, 0, 0.6, 1)', style({ transform: 'translateY(24px)', opacity: 0 })),
|
|
108
|
+
]),
|
|
109
|
+
]);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Expand/Collapse (Accordion)
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
export const expandCollapse = trigger('expandCollapse', [
|
|
116
|
+
state('collapsed', style({ height: '0', overflow: 'hidden', opacity: 0 })),
|
|
117
|
+
state('expanded', style({ height: '*', overflow: 'hidden', opacity: 1 })),
|
|
118
|
+
transition('collapsed <=> expanded', animate('250ms cubic-bezier(0.4, 0, 0.2, 1)')),
|
|
119
|
+
]);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Usage in template:
|
|
123
|
+
```html
|
|
124
|
+
<div [@expandCollapse]="isExpanded() ? 'expanded' : 'collapsed'">
|
|
125
|
+
<ng-content />
|
|
126
|
+
</div>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Scale In (for cards, modals, popovers)
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
export const scaleIn = trigger('scaleIn', [
|
|
133
|
+
transition(':enter', [
|
|
134
|
+
style({ transform: 'scale(0.95)', opacity: 0 }),
|
|
135
|
+
animate('200ms cubic-bezier(0.4, 0, 0.2, 1)', style({ transform: 'scale(1)', opacity: 1 })),
|
|
136
|
+
]),
|
|
137
|
+
transition(':leave', [
|
|
138
|
+
animate('150ms cubic-bezier(0.4, 0, 0.6, 1)', style({ transform: 'scale(0.95)', opacity: 0 })),
|
|
139
|
+
]),
|
|
140
|
+
]);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### List Stagger (animate items entering a list)
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
export const listStagger = trigger('listStagger', [
|
|
147
|
+
transition('* => *', [
|
|
148
|
+
query(':enter', [
|
|
149
|
+
style({ opacity: 0, transform: 'translateY(-8px)' }),
|
|
150
|
+
stagger(60, [
|
|
151
|
+
animate('200ms ease-out', style({ opacity: 1, transform: 'translateY(0)' })),
|
|
152
|
+
]),
|
|
153
|
+
], { optional: true }),
|
|
154
|
+
]),
|
|
155
|
+
]);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Usage:
|
|
159
|
+
```html
|
|
160
|
+
<!-- Apply trigger to the container, not individual items -->
|
|
161
|
+
<ul [@listStagger]="items().length">
|
|
162
|
+
@for (item of items(); track item.id) {
|
|
163
|
+
<li>{{ item.name }}</li>
|
|
164
|
+
}
|
|
165
|
+
</ul>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Route Transition
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
export const routeTransition = trigger('routeTransition', [
|
|
172
|
+
transition('* <=> *', [
|
|
173
|
+
query(':enter', [style({ opacity: 0, transform: 'translateY(8px)' })], { optional: true }),
|
|
174
|
+
query(':leave', [animate('150ms ease-in', style({ opacity: 0 }))], { optional: true }),
|
|
175
|
+
query(':enter', [animate('200ms 50ms ease-out', style({ opacity: 1, transform: 'translateY(0)' }))], { optional: true }),
|
|
176
|
+
]),
|
|
177
|
+
]);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
App component setup:
|
|
181
|
+
```typescript
|
|
182
|
+
// app.component.ts
|
|
183
|
+
getRouteAnimationData(outlet: RouterOutlet) {
|
|
184
|
+
return outlet?.activatedRouteData?.['animation'];
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```html
|
|
189
|
+
<!-- app.component.html -->
|
|
190
|
+
<div [@routeTransition]="getRouteAnimationData(outlet)">
|
|
191
|
+
<router-outlet #outlet="outlet" />
|
|
192
|
+
</div>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Route data:
|
|
196
|
+
```typescript
|
|
197
|
+
{ path: 'home', component: HomeComponent, data: { animation: 'home' } }
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Shake (validation error feedback)
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
export const shake = trigger('shake', [
|
|
204
|
+
transition('* => shake', [
|
|
205
|
+
animate('400ms', keyframes([
|
|
206
|
+
style({ transform: 'translateX(0)', offset: 0 }),
|
|
207
|
+
style({ transform: 'translateX(-8px)', offset: 0.2 }),
|
|
208
|
+
style({ transform: 'translateX(8px)', offset: 0.4 }),
|
|
209
|
+
style({ transform: 'translateX(-6px)', offset: 0.6 }),
|
|
210
|
+
style({ transform: 'translateX(6px)', offset: 0.8 }),
|
|
211
|
+
style({ transform: 'translateX(0)', offset: 1 }),
|
|
212
|
+
])),
|
|
213
|
+
]),
|
|
214
|
+
]);
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Usage:
|
|
218
|
+
```html
|
|
219
|
+
<form [@shake]="shakeState()" (@shake.done)="shakeState.set('idle')">
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Section 4: Applying Animations in Components
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
@Component({
|
|
228
|
+
standalone: true,
|
|
229
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
230
|
+
animations: [fadeInOut, listStagger, slideInRight, scaleIn],
|
|
231
|
+
template: `
|
|
232
|
+
@if (visible()) {
|
|
233
|
+
<div @fadeInOut class="panel">Content</div>
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
<ul [@listStagger]="items().length">
|
|
237
|
+
@for (item of items(); track item.id) {
|
|
238
|
+
<li>{{ item.name }}</li>
|
|
239
|
+
}
|
|
240
|
+
</ul>
|
|
241
|
+
|
|
242
|
+
@if (showPanel()) {
|
|
243
|
+
<aside @slideInRight class="side-panel">
|
|
244
|
+
Details...
|
|
245
|
+
</aside>
|
|
246
|
+
}
|
|
247
|
+
`,
|
|
248
|
+
})
|
|
249
|
+
export class MyComponent {
|
|
250
|
+
readonly visible = signal(true);
|
|
251
|
+
readonly showPanel = signal(false);
|
|
252
|
+
readonly items = signal<Item[]>([]);
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Disabling Animations Programmatically
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
// Useful for tests or reduced-motion
|
|
260
|
+
@HostBinding('@.disabled')
|
|
261
|
+
get animationsDisabled() {
|
|
262
|
+
return this.prefersReducedMotion;
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Section 5: Respecting `prefers-reduced-motion`
|
|
269
|
+
|
|
270
|
+
### CSS Approach (Global — Recommended)
|
|
271
|
+
|
|
272
|
+
```scss
|
|
273
|
+
// styles.scss
|
|
274
|
+
@media (prefers-reduced-motion: reduce) {
|
|
275
|
+
*,
|
|
276
|
+
*::before,
|
|
277
|
+
*::after {
|
|
278
|
+
animation-duration: 0.01ms !important;
|
|
279
|
+
animation-iteration-count: 1 !important;
|
|
280
|
+
transition-duration: 0.01ms !important;
|
|
281
|
+
scroll-behavior: auto !important;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### TypeScript Approach (Per Component)
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
// Check preference at component level
|
|
290
|
+
readonly prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
291
|
+
|
|
292
|
+
// React to OS setting changes at runtime
|
|
293
|
+
private motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
294
|
+
readonly reducedMotion = signal(this.motionQuery.matches);
|
|
295
|
+
|
|
296
|
+
constructor() {
|
|
297
|
+
this.motionQuery.addEventListener('change', e => {
|
|
298
|
+
this.reducedMotion.set(e.matches);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Use in template
|
|
303
|
+
animationDuration() {
|
|
304
|
+
return this.reducedMotion() ? '0ms' : '250ms';
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Angular Animation with Reduced Motion Guard
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
export const safeFadeIn = trigger('safeFadeIn', [
|
|
312
|
+
transition(':enter', [
|
|
313
|
+
style({ opacity: 0 }),
|
|
314
|
+
animate('{{ duration }}ms ease-in', style({ opacity: 1 })),
|
|
315
|
+
], { params: { duration: 200 } }),
|
|
316
|
+
]);
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
```html
|
|
320
|
+
<div [@safeFadeIn]="{ value: '', params: { duration: reducedMotion() ? 0 : 200 } }">
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Section 6: Motion Design Rules
|
|
326
|
+
|
|
327
|
+
| Rule | Guideline |
|
|
328
|
+
|---|---|
|
|
329
|
+
| Duration: micro | 100–150ms — immediate feedback (tap, hover) |
|
|
330
|
+
| Duration: standard | 200–300ms — most UI transitions |
|
|
331
|
+
| Duration: complex | 300–500ms — route changes, modals, large reveals |
|
|
332
|
+
| Never exceed | 500ms for UI transitions (feels sluggish) |
|
|
333
|
+
| Easing: enter | `ease-out` — `cubic-bezier(0, 0, 0.2, 1)` — starts fast, decelerates |
|
|
334
|
+
| Easing: exit | `ease-in` — `cubic-bezier(0.4, 0, 1, 1)` — starts slow, accelerates |
|
|
335
|
+
| Easing: standard | `ease-in-out` — `cubic-bezier(0.4, 0, 0.2, 1)` — Material standard |
|
|
336
|
+
| Spatial motion | Enter from direction of origin; exit toward destination |
|
|
337
|
+
| Avoid | Simultaneous enter + leave in the same space (stagger them) |
|
|
338
|
+
| Avoid | Animating `width`/`height` directly (causes layout thrash); use `transform` and `opacity` |
|
|
339
|
+
| Prefer | `transform` and `opacity` — GPU-composited, no layout reflow |
|
|
340
|
+
| Always | Test with `prefers-reduced-motion: reduce` |
|
|
341
|
+
|
|
342
|
+
### Easing Cheat Sheet
|
|
343
|
+
|
|
344
|
+
```
|
|
345
|
+
ease-out (decelerate — for entering elements):
|
|
346
|
+
cubic-bezier(0, 0, 0.2, 1)
|
|
347
|
+
|
|
348
|
+
ease-in (accelerate — for exiting elements):
|
|
349
|
+
cubic-bezier(0.4, 0, 1, 1)
|
|
350
|
+
|
|
351
|
+
ease-in-out (standard — for elements changing state):
|
|
352
|
+
cubic-bezier(0.4, 0, 0.2, 1)
|
|
353
|
+
|
|
354
|
+
spring-like (bouncy, expressive):
|
|
355
|
+
cubic-bezier(0.34, 1.56, 0.64, 1)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Distance Guidelines
|
|
359
|
+
|
|
360
|
+
| Element size | Recommended travel distance |
|
|
361
|
+
|---|---|
|
|
362
|
+
| Small (icon, chip) | 4–8px |
|
|
363
|
+
| Medium (card, panel) | 8–16px |
|
|
364
|
+
| Large (page, modal) | 16–32px or 100% (slide in from edge) |
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Section 7: Angular Material Built-in Animations
|
|
369
|
+
|
|
370
|
+
Angular Material components already have animations baked in. Do not re-animate them.
|
|
371
|
+
|
|
372
|
+
| Component | Built-in animation |
|
|
373
|
+
|---|---|
|
|
374
|
+
| `MatDialog` | Scale + fade on open/close |
|
|
375
|
+
| `MatSnackBar` | Slide up from bottom |
|
|
376
|
+
| `MatSidenav` | Slide in from side |
|
|
377
|
+
| `MatExpansionPanel` | Height expand/collapse |
|
|
378
|
+
| `MatTooltip` | Fade in/out |
|
|
379
|
+
| `MatMenu` | Scale + fade |
|
|
380
|
+
| `MatSelect` | Dropdown expand |
|
|
381
|
+
| `MatProgressBar` | Indeterminate shimmer |
|
|
382
|
+
| `MatChip` | Scale on add/remove |
|
|
383
|
+
|
|
384
|
+
These all respect `provideAnimationsAsync()` and `prefers-reduced-motion` automatically via Angular Material's internal animation config.
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## Section 8: Animation Testing
|
|
389
|
+
|
|
390
|
+
### Disable in Unit Tests
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
// In TestBed setup
|
|
394
|
+
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
|
395
|
+
|
|
396
|
+
TestBed.configureTestingModule({
|
|
397
|
+
imports: [NoopAnimationsModule], // replaces BrowserAnimationsModule
|
|
398
|
+
});
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Flush Animations in Tests
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
import { fakeAsync, tick } from '@angular/core/testing';
|
|
405
|
+
|
|
406
|
+
it('should show element after animation', fakeAsync(() => {
|
|
407
|
+
component.visible.set(true);
|
|
408
|
+
fixture.detectChanges();
|
|
409
|
+
tick(200); // advance past animation duration
|
|
410
|
+
fixture.detectChanges();
|
|
411
|
+
expect(fixture.nativeElement.querySelector('.panel')).toBeTruthy();
|
|
412
|
+
}));
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## Section 9: Reusable Animation File Pattern
|
|
418
|
+
|
|
419
|
+
Centralize all animations to avoid duplication:
|
|
420
|
+
|
|
421
|
+
```
|
|
422
|
+
src/
|
|
423
|
+
app/
|
|
424
|
+
shared/
|
|
425
|
+
animations/
|
|
426
|
+
fade.animations.ts
|
|
427
|
+
slide.animations.ts
|
|
428
|
+
list.animations.ts
|
|
429
|
+
route.animations.ts
|
|
430
|
+
index.ts ← barrel export
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
// shared/animations/index.ts
|
|
435
|
+
export * from './fade.animations';
|
|
436
|
+
export * from './slide.animations';
|
|
437
|
+
export * from './list.animations';
|
|
438
|
+
export * from './route.animations';
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
// In any component
|
|
443
|
+
import { fadeInOut, slideInRight, listStagger } from '@shared/animations';
|
|
444
|
+
|
|
445
|
+
@Component({
|
|
446
|
+
animations: [fadeInOut, slideInRight, listStagger],
|
|
447
|
+
})
|
|
448
|
+
```
|