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,600 @@
1
+ # Angular UI/UX Patterns Reference
2
+
3
+ Angular UI/UX patterns for signals, forms, routing, and component architecture.
4
+
5
+ ---
6
+
7
+ ## Section 1: Signals-Based UI Patterns (Angular 17+)
8
+
9
+ ### Pattern: Signal-Driven List with Loading/Empty/Error States
10
+
11
+ ```typescript
12
+ @Component({
13
+ selector: 'app-user-list',
14
+ standalone: true,
15
+ changeDetection: ChangeDetectionStrategy.OnPush,
16
+ imports: [MatProgressBarModule, MatListModule, CommonModule],
17
+ template: `
18
+ @if (loading()) {
19
+ <mat-progress-bar mode="indeterminate" />
20
+ } @else if (error()) {
21
+ <div class="error">{{ error() }}</div>
22
+ } @else if (users().length === 0) {
23
+ <p class="empty">No users found.</p>
24
+ } @else {
25
+ <mat-list>
26
+ @for (user of users(); track user.id) {
27
+ <mat-list-item>{{ user.name }}</mat-list-item>
28
+ }
29
+ </mat-list>
30
+ }
31
+ `,
32
+ })
33
+ export class UserListComponent {
34
+ private userService = inject(UserService);
35
+
36
+ readonly loading = signal(false);
37
+ readonly error = signal<string | null>(null);
38
+ readonly users = signal<User[]>([]);
39
+
40
+ ngOnInit() { this.load(); }
41
+
42
+ async load() {
43
+ this.loading.set(true);
44
+ this.error.set(null);
45
+ try {
46
+ this.users.set(await this.userService.getAll());
47
+ } catch (e) {
48
+ this.error.set('Failed to load users. Try again.');
49
+ } finally {
50
+ this.loading.set(false);
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ ### Pattern: Optimistic UI with Rollback
57
+
58
+ ```typescript
59
+ async toggleFavorite(item: Item) {
60
+ const snapshot = this.items();
61
+ // Optimistic update
62
+ this.items.update(list =>
63
+ list.map(i => i.id === item.id ? { ...i, isFavorite: !i.isFavorite } : i)
64
+ );
65
+ try {
66
+ await this.api.toggleFavorite(item.id);
67
+ } catch {
68
+ this.items.set(snapshot); // rollback
69
+ this.snackBar.open('Update failed. Changes reverted.', 'OK', { duration: 5000 });
70
+ }
71
+ }
72
+ ```
73
+
74
+ ### Pattern: Derived/Computed State
75
+
76
+ ```typescript
77
+ readonly searchQuery = signal('');
78
+ readonly allItems = signal<Item[]>([]);
79
+ readonly filteredItems = computed(() => {
80
+ const q = this.searchQuery().toLowerCase();
81
+ return q ? this.allItems().filter(i => i.name.toLowerCase().includes(q)) : this.allItems();
82
+ });
83
+ readonly totalCount = computed(() => this.filteredItems().length);
84
+ ```
85
+
86
+ ### Pattern: RxJS to Signals Bridge
87
+
88
+ ```typescript
89
+ private destroy$ = inject(DestroyRef);
90
+
91
+ // Convert Observable to signal (use in constructor or ngOnInit)
92
+ readonly currentUser = toSignal(this.authService.currentUser$, { initialValue: null });
93
+
94
+ // Subscribe with auto-cleanup
95
+ this.someObservable$
96
+ .pipe(takeUntilDestroyed(this.destroy$))
97
+ .subscribe(value => this.mySignal.set(value));
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Section 2: Smart/Dumb Component Pattern
103
+
104
+ ### Smart (Container) Component
105
+
106
+ ```typescript
107
+ @Component({
108
+ selector: 'app-dashboard-page',
109
+ standalone: true,
110
+ changeDetection: ChangeDetectionStrategy.OnPush,
111
+ imports: [DashboardStatsComponent, RecentActivityComponent],
112
+ template: `
113
+ <app-dashboard-stats [stats]="stats()" (refresh)="loadStats()" />
114
+ <app-recent-activity [activities]="activities()" />
115
+ `,
116
+ })
117
+ export class DashboardPageComponent {
118
+ private dashboardService = inject(DashboardService);
119
+ readonly stats = toSignal(this.dashboardService.stats$);
120
+ readonly activities = toSignal(this.dashboardService.recentActivities$);
121
+ loadStats() { this.dashboardService.refresh(); }
122
+ }
123
+ ```
124
+
125
+ ### Dumb (Presentation) Component
126
+
127
+ ```typescript
128
+ @Component({
129
+ selector: 'app-dashboard-stats',
130
+ standalone: true,
131
+ changeDetection: ChangeDetectionStrategy.OnPush,
132
+ // No injected services — pure input/output
133
+ template: `...`,
134
+ })
135
+ export class DashboardStatsComponent {
136
+ readonly stats = input<Stats | undefined>();
137
+ readonly refresh = output<void>();
138
+ }
139
+ ```
140
+
141
+ **Rules:**
142
+ - Dumb components have zero injected services.
143
+ - All data comes via `input()`.
144
+ - All events go via `output()`.
145
+ - Easy to test, easy to reuse.
146
+
147
+ ---
148
+
149
+ ## Section 3: Angular Router UX Patterns
150
+
151
+ ### Lazy Loading with Loading UX
152
+
153
+ ```typescript
154
+ // app.routes.ts
155
+ export const routes: Routes = [
156
+ {
157
+ path: 'dashboard',
158
+ loadComponent: () => import('./dashboard/dashboard.component').then(m => m.DashboardComponent),
159
+ },
160
+ {
161
+ path: 'settings',
162
+ loadChildren: () => import('./settings/settings.routes').then(m => m.SETTINGS_ROUTES),
163
+ },
164
+ ];
165
+ ```
166
+
167
+ ### Route-Level Loading Indicator
168
+
169
+ ```typescript
170
+ // app.component.ts
171
+ readonly isNavigating = signal(false);
172
+
173
+ constructor() {
174
+ const router = inject(Router);
175
+ router.events.pipe(
176
+ takeUntilDestroyed(),
177
+ ).subscribe(event => {
178
+ if (event instanceof NavigationStart) this.isNavigating.set(true);
179
+ if (event instanceof NavigationEnd || event instanceof NavigationCancel) this.isNavigating.set(false);
180
+ });
181
+ }
182
+ ```
183
+
184
+ ```html
185
+ <!-- app.component.html -->
186
+ @if (isNavigating()) {
187
+ <mat-progress-bar mode="indeterminate" class="nav-loader" />
188
+ }
189
+ <router-outlet />
190
+ ```
191
+
192
+ ### Data Prefetch with Resolvers
193
+
194
+ ```typescript
195
+ // dashboard.resolver.ts
196
+ export const dashboardResolver: ResolveFn<DashboardData> = (route, state) => {
197
+ return inject(DashboardService).getData();
198
+ };
199
+
200
+ // dashboard.routes.ts
201
+ { path: 'dashboard', component: DashboardComponent, resolve: { data: dashboardResolver } }
202
+
203
+ // dashboard.component.ts
204
+ readonly data = toSignal(inject(ActivatedRoute).data.pipe(map(d => d['data'])));
205
+ ```
206
+
207
+ ### Functional Route Guards
208
+
209
+ ```typescript
210
+ export const authGuard: CanActivateFn = (route, state) => {
211
+ const auth = inject(AuthService);
212
+ const router = inject(Router);
213
+ if (auth.isLoggedIn()) return true;
214
+ return router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });
215
+ };
216
+ ```
217
+
218
+ ### Skeleton Loaders During Route Transition
219
+
220
+ Show skeleton UI immediately, replace with real content when data arrives:
221
+
222
+ ```html
223
+ @if (data(); as d) {
224
+ <app-dashboard-content [data]="d" />
225
+ } @else {
226
+ <app-dashboard-skeleton /> <!-- CSS skeleton shimmer -->
227
+ }
228
+ ```
229
+
230
+ ---
231
+
232
+ ## Section 4: Dialog & Sheet Patterns
233
+
234
+ ### Opening a Dialog
235
+
236
+ ```typescript
237
+ private dialog = inject(MatDialog);
238
+ private destroyRef = inject(DestroyRef);
239
+
240
+ openEditDialog(item: Item) {
241
+ const ref = this.dialog.open(EditItemDialogComponent, {
242
+ width: '480px',
243
+ maxWidth: '95vw',
244
+ data: { item },
245
+ });
246
+
247
+ ref.afterClosed().pipe(
248
+ filter(result => !!result),
249
+ takeUntilDestroyed(this.destroyRef),
250
+ ).subscribe(updatedItem => this.updateItem(updatedItem));
251
+ }
252
+ ```
253
+
254
+ ### Dialog Component
255
+
256
+ ```typescript
257
+ @Component({
258
+ standalone: true,
259
+ imports: [MatDialogModule, MatButtonModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule],
260
+ template: `
261
+ <h2 mat-dialog-title>Edit Item</h2>
262
+ <mat-dialog-content>
263
+ <mat-form-field appearance="outline">
264
+ <mat-label>Name</mat-label>
265
+ <input matInput [formControl]="nameControl" />
266
+ <mat-error>Name is required</mat-error>
267
+ </mat-form-field>
268
+ </mat-dialog-content>
269
+ <mat-dialog-actions align="end">
270
+ <button mat-button mat-dialog-close>Cancel</button>
271
+ <button mat-flat-button color="primary" [disabled]="!nameControl.valid" (click)="save()">Save</button>
272
+ </mat-dialog-actions>
273
+ `,
274
+ })
275
+ export class EditItemDialogComponent {
276
+ private dialogRef = inject(MatDialogRef<EditItemDialogComponent>);
277
+ private data = inject<{ item: Item }>(MAT_DIALOG_DATA);
278
+
279
+ readonly nameControl = new FormControl(this.data.item.name, Validators.required);
280
+
281
+ save() {
282
+ if (this.nameControl.valid) {
283
+ this.dialogRef.close({ ...this.data.item, name: this.nameControl.value });
284
+ }
285
+ }
286
+ }
287
+ ```
288
+
289
+ ### Bottom Sheet Pattern
290
+
291
+ ```typescript
292
+ private bottomSheet = inject(MatBottomSheet);
293
+
294
+ openOptions(item: Item) {
295
+ const ref = this.bottomSheet.open(ItemOptionsSheetComponent, { data: { item } });
296
+ ref.afterDismissed().pipe(
297
+ filter(action => !!action),
298
+ ).subscribe(action => this.handleAction(action, item));
299
+ }
300
+ ```
301
+
302
+ ---
303
+
304
+ ## Section 5: Reactive Forms Patterns
305
+
306
+ ### Typed Form Group
307
+
308
+ ```typescript
309
+ interface UserForm {
310
+ name: FormControl<string>;
311
+ email: FormControl<string>;
312
+ role: FormControl<'admin' | 'user'>;
313
+ }
314
+
315
+ readonly form = new FormGroup<UserForm>({
316
+ name: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
317
+ email: new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.email] }),
318
+ role: new FormControl('user', { nonNullable: true }),
319
+ });
320
+ ```
321
+
322
+ ### Form Validation with Signal-Driven Error Display
323
+
324
+ ```typescript
325
+ // In component
326
+ readonly nameError = computed(() => {
327
+ const ctrl = this.form.controls.name;
328
+ if (ctrl.hasError('required')) return 'Name is required';
329
+ if (ctrl.hasError('minlength')) return 'Name must be at least 2 characters';
330
+ return null;
331
+ });
332
+ ```
333
+
334
+ ```html
335
+ <mat-form-field appearance="outline">
336
+ <mat-label>Name</mat-label>
337
+ <input matInput formControlName="name" />
338
+ @if (nameError()) {
339
+ <mat-error>{{ nameError() }}</mat-error>
340
+ }
341
+ </mat-form-field>
342
+ ```
343
+
344
+ ### Async Validator (e.g., Check Username Availability)
345
+
346
+ ```typescript
347
+ const usernameAvailable: AsyncValidatorFn = (ctrl) => {
348
+ return inject(UserService).checkUsername(ctrl.value).pipe(
349
+ map(available => available ? null : { usernameTaken: true }),
350
+ catchError(() => of(null)),
351
+ );
352
+ };
353
+ ```
354
+
355
+ ### Form Submit with Loading State
356
+
357
+ ```typescript
358
+ readonly saving = signal(false);
359
+
360
+ async submit() {
361
+ if (this.form.invalid || this.saving()) return;
362
+ this.saving.set(true);
363
+ try {
364
+ await this.service.save(this.form.getRawValue());
365
+ this.snackBar.open('Saved!', undefined, { duration: 3000 });
366
+ this.dialogRef.close(true);
367
+ } catch {
368
+ this.snackBar.open('Save failed. Please try again.', 'OK', { duration: 5000 });
369
+ } finally {
370
+ this.saving.set(false);
371
+ }
372
+ }
373
+ ```
374
+
375
+ ```html
376
+ <button mat-flat-button color="primary" [disabled]="form.invalid || saving()" (click)="submit()">
377
+ @if (saving()) { <mat-spinner diameter="18" /> } @else { Save }
378
+ </button>
379
+ ```
380
+
381
+ ---
382
+
383
+ ## Section 6: Responsive Layout Pattern
384
+
385
+ ### Adaptive Sidenav
386
+
387
+ ```typescript
388
+ readonly isHandset = toSignal(
389
+ inject(BreakpointObserver).observe(Breakpoints.Handset).pipe(map(r => r.matches)),
390
+ { initialValue: false }
391
+ );
392
+ ```
393
+
394
+ ```html
395
+ <mat-sidenav-container>
396
+ <mat-sidenav
397
+ [mode]="isHandset() ? 'over' : 'side'"
398
+ [opened]="!isHandset()"
399
+ >
400
+ <mat-nav-list>...</mat-nav-list>
401
+ </mat-sidenav>
402
+ <mat-sidenav-content>
403
+ <router-outlet />
404
+ </mat-sidenav-content>
405
+ </mat-sidenav-container>
406
+ ```
407
+
408
+ ### Responsive Grid with CSS Grid
409
+
410
+ ```scss
411
+ .card-grid {
412
+ display: grid;
413
+ gap: 1rem;
414
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
415
+
416
+ @media (max-width: 600px) {
417
+ grid-template-columns: 1fr;
418
+ }
419
+ }
420
+ ```
421
+
422
+ ### Breakpoint Constants (Angular CDK)
423
+
424
+ ```typescript
425
+ import { Breakpoints } from '@angular/cdk/layout';
426
+ // Breakpoints.Handset — phones portrait + landscape
427
+ // Breakpoints.Tablet — tablets portrait + landscape
428
+ // Breakpoints.Web — desktops
429
+ // Breakpoints.HandsetPortrait
430
+ // Breakpoints.TabletLandscape
431
+ ```
432
+
433
+ ---
434
+
435
+ ## Section 7: Infinite Scroll with CDK Virtual Scroll
436
+
437
+ ```typescript
438
+ // Use CdkVirtualScrollViewport for large lists
439
+ import { ScrollingModule } from '@angular/cdk/scrolling';
440
+ ```
441
+
442
+ ```html
443
+ <cdk-virtual-scroll-viewport itemSize="64" class="list-viewport">
444
+ <mat-list-item *cdkVirtualFor="let item of items; trackBy: trackById">
445
+ {{ item.name }}
446
+ </mat-list-item>
447
+ </cdk-virtual-scroll-viewport>
448
+ ```
449
+
450
+ ```scss
451
+ .list-viewport { height: 400px; }
452
+ ```
453
+
454
+ For paginated infinite scroll (load-more on scroll):
455
+
456
+ ```typescript
457
+ readonly page = signal(0);
458
+ readonly allItems = signal<Item[]>([]);
459
+ readonly hasMore = signal(true);
460
+
461
+ async loadMore() {
462
+ if (!this.hasMore()) return;
463
+ const next = await this.service.getPage(this.page());
464
+ this.allItems.update(prev => [...prev, ...next.items]);
465
+ this.hasMore.set(next.hasMore);
466
+ this.page.update(p => p + 1);
467
+ }
468
+ ```
469
+
470
+ ---
471
+
472
+ ## Section 8: Error Boundary & Global Error Handling
473
+
474
+ ### Global HTTP Error Interceptor
475
+
476
+ ```typescript
477
+ export const errorInterceptor: HttpInterceptorFn = (req, next) => {
478
+ return next(req).pipe(
479
+ catchError((error: HttpErrorResponse) => {
480
+ const snackBar = inject(MatSnackBar);
481
+ if (error.status === 401) {
482
+ inject(Router).navigate(['/login']);
483
+ } else if (error.status >= 500) {
484
+ snackBar.open('Server error. Please try again later.', 'OK', { duration: 5000 });
485
+ }
486
+ return throwError(() => error);
487
+ }),
488
+ );
489
+ };
490
+
491
+ // app.config.ts
492
+ providers: [provideHttpClient(withInterceptors([errorInterceptor]))]
493
+ ```
494
+
495
+ ### Component-Level Error State
496
+
497
+ ```typescript
498
+ readonly error = signal<string | null>(null);
499
+
500
+ async load() {
501
+ this.error.set(null);
502
+ try {
503
+ this.data.set(await this.service.get());
504
+ } catch (e: unknown) {
505
+ if (e instanceof HttpErrorResponse && e.status === 404) {
506
+ this.error.set('Item not found.');
507
+ } else {
508
+ this.error.set('Something went wrong. Please try again.');
509
+ }
510
+ }
511
+ }
512
+ ```
513
+
514
+ ---
515
+
516
+ ## Section 9: Accessibility Patterns
517
+
518
+ ### Focus Management After Dialog Close
519
+
520
+ ```typescript
521
+ // Store focused element before opening, restore after close
522
+ openDialog(triggerEl: HTMLElement) {
523
+ const ref = this.dialog.open(MyDialogComponent);
524
+ ref.afterClosed().subscribe(() => triggerEl.focus());
525
+ }
526
+ ```
527
+
528
+ ### ARIA Live Regions for Dynamic Content
529
+
530
+ ```html
531
+ <!-- Announce async updates to screen readers -->
532
+ <div aria-live="polite" aria-atomic="true" class="sr-only">
533
+ @if (statusMessage()) { {{ statusMessage() }} }
534
+ </div>
535
+ ```
536
+
537
+ ```scss
538
+ .sr-only {
539
+ position: absolute;
540
+ width: 1px;
541
+ height: 1px;
542
+ padding: 0;
543
+ overflow: hidden;
544
+ clip: rect(0, 0, 0, 0);
545
+ white-space: nowrap;
546
+ border: 0;
547
+ }
548
+ ```
549
+
550
+ ### Keyboard Navigation in Custom Components
551
+
552
+ ```typescript
553
+ @HostListener('keydown', ['$event'])
554
+ onKeydown(event: KeyboardEvent) {
555
+ switch (event.key) {
556
+ case 'ArrowDown': this.focusNext(); break;
557
+ case 'ArrowUp': this.focusPrev(); break;
558
+ case 'Enter':
559
+ case ' ': this.select(); event.preventDefault(); break;
560
+ case 'Escape': this.close(); break;
561
+ }
562
+ }
563
+ ```
564
+
565
+ ---
566
+
567
+ ## Section 10: Performance Patterns
568
+
569
+ ### OnPush + Signals (Default for All New Components)
570
+
571
+ Always use `ChangeDetectionStrategy.OnPush` with signals. Angular's signal-based reactivity only triggers re-render when signal values change — no zone.js overhead.
572
+
573
+ ```typescript
574
+ @Component({
575
+ changeDetection: ChangeDetectionStrategy.OnPush,
576
+ // signals automatically schedule re-renders
577
+ })
578
+ ```
579
+
580
+ ### trackBy in @for
581
+
582
+ Always provide `track` in `@for` to avoid full list re-renders:
583
+
584
+ ```html
585
+ @for (item of items(); track item.id) {
586
+ <app-item [item]="item" />
587
+ }
588
+ ```
589
+
590
+ ### Defer Block for Below-the-Fold Content
591
+
592
+ ```html
593
+ @defer (on viewport) {
594
+ <app-heavy-chart [data]="chartData()" />
595
+ } @placeholder {
596
+ <div class="chart-placeholder">Loading chart...</div>
597
+ } @loading (minimum 500ms) {
598
+ <mat-spinner />
599
+ }
600
+ ```