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.
Files changed (30) hide show
  1. package/assets/ui-ux-consultant/SKILL.md +844 -0
  2. package/assets/ui-ux-consultant/references/accessibility.md +175 -0
  3. package/assets/ui-ux-consultant/references/alt-libraries.md +90 -0
  4. package/assets/ui-ux-consultant/references/animations.md +448 -0
  5. package/assets/ui-ux-consultant/references/catalog/colors.md +91 -0
  6. package/assets/ui-ux-consultant/references/catalog/fonts.md +363 -0
  7. package/assets/ui-ux-consultant/references/catalog/products.md +340 -0
  8. package/assets/ui-ux-consultant/references/catalog/styles.md +165 -0
  9. package/assets/ui-ux-consultant/references/components.md +1116 -0
  10. package/assets/ui-ux-consultant/references/patterns.md +600 -0
  11. package/assets/ui-ux-consultant/references/performance.md +198 -0
  12. package/assets/ui-ux-consultant/references/stacks/astro.md +382 -0
  13. package/assets/ui-ux-consultant/references/stacks/flutter.md +308 -0
  14. package/assets/ui-ux-consultant/references/stacks/html-tailwind.md +415 -0
  15. package/assets/ui-ux-consultant/references/stacks/jetpack-compose.md +333 -0
  16. package/assets/ui-ux-consultant/references/stacks/laravel.md +521 -0
  17. package/assets/ui-ux-consultant/references/stacks/nextjs.md +275 -0
  18. package/assets/ui-ux-consultant/references/stacks/nuxt-ui.md +384 -0
  19. package/assets/ui-ux-consultant/references/stacks/nuxtjs.md +264 -0
  20. package/assets/ui-ux-consultant/references/stacks/react-native.md +346 -0
  21. package/assets/ui-ux-consultant/references/stacks/react.md +268 -0
  22. package/assets/ui-ux-consultant/references/stacks/shadcn.md +485 -0
  23. package/assets/ui-ux-consultant/references/stacks/svelte.md +429 -0
  24. package/assets/ui-ux-consultant/references/stacks/swiftui.md +336 -0
  25. package/assets/ui-ux-consultant/references/stacks/threejs.md +366 -0
  26. package/assets/ui-ux-consultant/references/stacks/vue.md +272 -0
  27. package/assets/ui-ux-consultant/references/theming.md +701 -0
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +130 -0
  30. 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
+ ```