ui-ux-consultant-cli 1.0.0-beta.1
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,844 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ui-ux-pro
|
|
3
|
+
description: >
|
|
4
|
+
Multi-framework UI/UX design intelligence. Use when building UI with any framework or platform.
|
|
5
|
+
Deep Angular coverage (Angular Material 3, signals, CDK, M3 theming, animations, accessibility).
|
|
6
|
+
Also covers: React, Next.js, Vue, Nuxt.js, Nuxt UI, Svelte, Astro, shadcn/ui, HTML+Tailwind,
|
|
7
|
+
Flutter, SwiftUI, React Native, Laravel, Jetpack Compose, Three.js.
|
|
8
|
+
Triggers on: "angular", "react", "next.js", "nextjs", "vue", "nuxt", "svelte", "astro",
|
|
9
|
+
"shadcn", "tailwind", "flutter", "swiftui", "react native", "laravel", "jetpack compose",
|
|
10
|
+
"three.js", "threejs", "r3f", "material", "component", "UI library", "design system",
|
|
11
|
+
"angular material", "ng-zorro", "primeng", "signals UI", "CDK", "hooks", "composable",
|
|
12
|
+
"useState", "useEffect", "Pinia", "Riverpod", "Provider", "StatefulWidget", "@Observable",
|
|
13
|
+
"app router", "server components", "blade", "livewire", "inertia", "webgl", "scene graph".
|
|
14
|
+
user-invokable: true
|
|
15
|
+
argument-hint: "[angular|react|vue|nextjs|svelte|astro|flutter|swiftui|react-native|laravel|jetpack|threejs|shadcn|nuxt|html-tailwind] [component|theme|layout|pattern|audit]"
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Framework Router
|
|
19
|
+
|
|
20
|
+
Detect the framework from context, then read the matching reference:
|
|
21
|
+
|
|
22
|
+
| Framework | Reference | Triggers |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| **Angular** | `references/` (7 files — deepest coverage) | angular, angular material, ng-zorro, primeng, signals, CDK, standalone component |
|
|
25
|
+
| **React** | `references/stacks/react.md` | react, jsx, useState, useEffect, vite+react, react hooks |
|
|
26
|
+
| **Next.js** | `references/stacks/nextjs.md` | next.js, nextjs, app router, RSC, server components, server actions |
|
|
27
|
+
| **Vue** | `references/stacks/vue.md` | vue, composition api, script setup, pinia, vue router |
|
|
28
|
+
| **Nuxt.js** | `references/stacks/nuxtjs.md` | nuxt, nuxt 3, auto-imports, useAsyncData, useFetch |
|
|
29
|
+
| **Nuxt UI** | `references/stacks/nuxt-ui.md` | nuxt ui, @nuxt/ui, u-button, u-card |
|
|
30
|
+
| **Svelte** | `references/stacks/svelte.md` | svelte, sveltekit, $state, $derived, $effect, runes |
|
|
31
|
+
| **Astro** | `references/stacks/astro.md` | astro, .astro, islands, content collections, astro components |
|
|
32
|
+
| **shadcn/ui** | `references/stacks/shadcn.md` | shadcn, radix ui, copy-paste components, cn(), cva() |
|
|
33
|
+
| **HTML+Tailwind** | `references/stacks/html-tailwind.md` | tailwind, utility css, no framework, vanilla html |
|
|
34
|
+
| **Flutter** | `references/stacks/flutter.md` | flutter, dart, widget, statelesswidget, provider, riverpod |
|
|
35
|
+
| **SwiftUI** | `references/stacks/swiftui.md` | swiftui, @state, @observable, navigationstack, xcode |
|
|
36
|
+
| **React Native** | `references/stacks/react-native.md` | react native, expo, flatlist, stylesheet, metro |
|
|
37
|
+
| **Laravel** | `references/stacks/laravel.md` | laravel, blade, livewire, inertia, eloquent |
|
|
38
|
+
| **Jetpack Compose** | `references/stacks/jetpack-compose.md` | jetpack compose, composable, remember, kotlin, android |
|
|
39
|
+
| **Three.js** | `references/stacks/threejs.md` | three.js, threejs, r3f, react three fiber, webgl, scene graph |
|
|
40
|
+
|
|
41
|
+
**For Angular requests:** Continue reading this SKILL.md — all Angular content is below.
|
|
42
|
+
**For other frameworks:** Read the matching stack file above, then return here for universal UX rules if needed.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Design Catalog
|
|
47
|
+
|
|
48
|
+
Use these when the user needs aesthetic direction, color, typography, or product-type guidance:
|
|
49
|
+
|
|
50
|
+
| Catalog | File | Use when |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| **UI Styles** | `references/catalog/styles.md` | User asks for a visual style, aesthetic, or "what style fits X" — 23 named styles |
|
|
53
|
+
| **Color Palettes** | `references/catalog/colors.md` | User needs brand colors, theme colors, or palette recommendations — 35 palettes |
|
|
54
|
+
| **Font Pairings** | `references/catalog/fonts.md` | User needs typography, heading+body font combos — 31 pairings with Google Fonts imports |
|
|
55
|
+
| **Product Types** | `references/catalog/products.md` | User describes what they're building — 41 product types with UI patterns and layout guidance |
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
# Angular UI/UX Design Intelligence
|
|
60
|
+
|
|
61
|
+
Angular-specific design guidance for building production-quality UIs. All examples use Angular 17+ standalone component syntax with signals-first patterns.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Section 1: Decision Tree — What Are You Building?
|
|
66
|
+
|
|
67
|
+
> These routes apply to **Angular** projects. For other frameworks see the Framework Router above.
|
|
68
|
+
|
|
69
|
+
Use this routing guide to load the right reference file before implementing:
|
|
70
|
+
|
|
71
|
+
| Building... | Read this |
|
|
72
|
+
|---|---|
|
|
73
|
+
| A new component (card, list, form, dialog, toolbar) | `references/components.md` |
|
|
74
|
+
| Design system / color theming / dark mode / typography | `references/theming.md` |
|
|
75
|
+
| Signals patterns, smart/dumb components, routing UX, state flows | `references/patterns.md` |
|
|
76
|
+
| Motion, page transitions, list animations, micro-interactions | `references/animations.md` |
|
|
77
|
+
| Performance: lazy loading, deferring, image optimization, SSR | `references/performance.md` |
|
|
78
|
+
| Accessibility audit, ARIA, keyboard navigation, screen readers | `references/accessibility.md` |
|
|
79
|
+
| Choosing between Angular Material, NG-ZORRO, PrimeNG, custom | `references/alt-libraries.md` |
|
|
80
|
+
|
|
81
|
+
When in doubt: start with `references/components.md`, then `references/patterns.md`.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Section 2: Design Philosophy
|
|
86
|
+
|
|
87
|
+
### Component-First Architecture
|
|
88
|
+
Angular's core mental model is composition of focused, testable components with clear `input()` / `output()` signal contracts. Good UI design in Angular means good component decomposition — each component should own a single visual responsibility and surface its API through typed signal inputs. Avoid "god components" that manage multiple concerns (e.g., a dashboard that owns its own HTTP calls, layout, and child state simultaneously). Aim for components under 200 lines of template and under 100 lines of class. When a template grows beyond two levels of conditional nesting, extract the inner content into a sub-component.
|
|
89
|
+
|
|
90
|
+
### Angular Material 3 as Default
|
|
91
|
+
Angular Material is the default recommendation for virtually all Angular UIs. It is the only Angular component library that is officially maintained by the Angular team, ships with full Material Design 3 spec compliance, and has first-class accessibility built in. Deviations — choosing NG-ZORRO for data-dense enterprise UIs, PrimeNG for e-commerce richness, or a fully custom system — require a deliberate justification. When in doubt, use Material. When the design spec calls for something Material cannot do, layer CDK primitives underneath your own components rather than pulling in a second library.
|
|
92
|
+
|
|
93
|
+
### Signals-First Reactive UI (Angular 17+)
|
|
94
|
+
`signal()`, `computed()`, and `effect()` replace most `subscribe()` patterns for local UI state. RxJS remains essential for async data pipelines (HTTP, WebSockets, complex event merging) but should terminate at the component boundary via `toSignal()`. Never expose `Observable` subscriptions that outlive component teardown — use `takeUntilDestroyed()` if you must subscribe manually, or prefer `toSignal()` which handles teardown automatically. Template control flow (`@if`, `@for`, `@switch`, `@defer`) replaces `*ngIf`, `*ngFor`, `*ngSwitch` structural directives and is more readable and slightly more efficient.
|
|
95
|
+
|
|
96
|
+
### OnPush by Default
|
|
97
|
+
Every new component should be created with `changeDetection: ChangeDetectionStrategy.OnPush`. Combined with signals, this eliminates virtually all unnecessary re-renders. Zone.js-based default change detection (the pre-17 default) runs on every async event in the entire application — a click handler, a setTimeout, an HTTP response. OnPush narrows updates to when inputs change or signals emit. This is not a micro-optimization; at scale it is the difference between a snappy UI and a laggy one. Set OnPush at component creation time — retrofitting it later is painful and error-prone.
|
|
98
|
+
|
|
99
|
+
### Standalone Components and Lazy Loading
|
|
100
|
+
Angular 17+ defaults to standalone components (`standalone: true` is now implicit). No NgModules means the `imports` array on each component is its explicit dependency list — this is a design asset, not boilerplate. It makes lazy-loaded routes trivial (`loadComponent: () => import('./page').then(m => m.PageComponent)`), keeps bundle splitting automatic, and keeps the mental model simple. Never reintroduce NgModules unless integrating a library that still requires them.
|
|
101
|
+
|
|
102
|
+
### Performance as UX
|
|
103
|
+
`@defer` blocks, `NgOptimizedImage`, and route-level code splitting are UX decisions as much as engineering ones. A dashboard that defers its chart section until it enters the viewport delivers perceived performance that no visual design trick can replicate. `NgOptimizedImage` automatically generates `srcset`, enforces `width`/`height` to prevent layout shift, and lazy-loads non-priority images. SSR hydration (Angular Universal / `provideClientHydration()`) is the right choice for any content-heavy or SEO-sensitive page. These are design choices that should be made in the planning phase, not retrofitted.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Section 3: Top 20 Angular Material 3 Components
|
|
108
|
+
|
|
109
|
+
| Component | Selector | Best Used For | Don't Use When |
|
|
110
|
+
|---|---|---|---|
|
|
111
|
+
| Filled Button | `mat-flat-button` | Primary CTA, form submit, main action | Secondary actions — use `mat-button` or `mat-stroked-button` |
|
|
112
|
+
| Text Button | `mat-button` | Low-emphasis actions, inline links, cancel | The only action on screen — use filled for primary |
|
|
113
|
+
| Icon Button | `mat-icon-button` | Toolbar actions, compact controls, toggles | The action needs a visible label for clarity |
|
|
114
|
+
| FAB | `mat-fab` / `mat-mini-fab` | Single primary action per screen (add, compose) | Multiple competing CTAs exist; use filled button instead |
|
|
115
|
+
| Card | `mat-card` | Grouping related content, entity summaries | Laying out a page — use structural layout, not cards for everything |
|
|
116
|
+
| List | `mat-list` / `mat-nav-list` | Vertical item sequences, navigation drawers | Tabular data with multiple columns — use `mat-table` |
|
|
117
|
+
| Table | `mat-table` | Structured tabular data with sorting/filtering | Simple key-value display — use `mat-list` or definition list |
|
|
118
|
+
| Paginator | `mat-paginator` | Paging large mat-table datasets | Small datasets under 20 items — show all and use filter |
|
|
119
|
+
| Form Field | `mat-form-field` | Wrapping all text inputs, selects, autocompletes | Checkboxes, radios, toggles — those are standalone |
|
|
120
|
+
| Input | `matInput` directive | Text, number, email, search fields inside form field | Multi-line — use `textarea matInput` with `cdkTextareaAutosize` |
|
|
121
|
+
| Select | `mat-select` | Choosing one (or many) from a fixed short list | Lists over ~8 items — use `mat-autocomplete` instead |
|
|
122
|
+
| Autocomplete | `mat-autocomplete` | Searchable select, tag input, typeahead | Fixed short lists — `mat-select` is simpler |
|
|
123
|
+
| Checkbox | `mat-checkbox` | Multi-select, boolean toggles in lists | Confirming destructive actions — use a dialog with buttons |
|
|
124
|
+
| Radio Group | `mat-radio-group` | Mutually exclusive options (3–5 choices) | Binary yes/no — use `mat-slide-toggle` |
|
|
125
|
+
| Slide Toggle | `mat-slide-toggle` | Binary settings that take effect immediately | Actions requiring confirmation — use checkbox + button |
|
|
126
|
+
| Dialog | `MatDialog` | Confirmations, focused sub-tasks, complex forms | Simple alerts or notifications — use `mat-snack-bar` |
|
|
127
|
+
| Snackbar | `MatSnackBar` | Transient success/error feedback, undo prompts | Errors requiring user action — use inline error or dialog |
|
|
128
|
+
| Progress Bar | `mat-progress-bar` | Page-level loading, step progress, file upload | Indeterminate local widget loading — use `mat-spinner` |
|
|
129
|
+
| Spinner | `mat-spinner` | Inline / button loading states, small areas | Full-page loading — use `mat-progress-bar` at top |
|
|
130
|
+
| Sidenav | `mat-sidenav` | Persistent navigation drawer, responsive shell | Simple page layouts — overhead is not worth it |
|
|
131
|
+
| Toolbar | `mat-toolbar` | App header, page-level title + action bar | Section headers inside content — use `<h2>` or card header |
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Section 4: Angular-Specific UX Anti-Patterns
|
|
136
|
+
|
|
137
|
+
### Rule 1: Don't nest `@if` more than 2 levels deep
|
|
138
|
+
**DON'T:**
|
|
139
|
+
```html
|
|
140
|
+
@if (user()) {
|
|
141
|
+
@if (user()!.isAdmin) {
|
|
142
|
+
@if (user()!.permissions.includes('edit')) {
|
|
143
|
+
<button>Edit</button>
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
**DO:** Extract a `canEdit = computed(() => ...)` signal and use it in a single `@if`, or extract the inner content to a sub-component that receives the resolved data as an input.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### Rule 2: Don't `subscribe()` in the component body without cleanup
|
|
153
|
+
**DON'T:**
|
|
154
|
+
```typescript
|
|
155
|
+
ngOnInit() {
|
|
156
|
+
this.userService.getUser().subscribe(u => this.user = u); // memory leak
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
**DO:** Use `toSignal()` for simple cases, or `takeUntilDestroyed()` for complex pipelines:
|
|
160
|
+
```typescript
|
|
161
|
+
readonly user = toSignal(this.userService.getUser());
|
|
162
|
+
// OR, when you need the Observable pipeline:
|
|
163
|
+
private destroyRef = inject(DestroyRef);
|
|
164
|
+
ngOnInit() {
|
|
165
|
+
this.userService.getUser()
|
|
166
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
167
|
+
.subscribe(u => this.user.set(u));
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### Rule 3: Don't use `ViewChild` to read state
|
|
174
|
+
**DON'T:**
|
|
175
|
+
```typescript
|
|
176
|
+
@ViewChild('myInput') inputRef!: ElementRef;
|
|
177
|
+
getInputValue() { return this.inputRef.nativeElement.value; }
|
|
178
|
+
```
|
|
179
|
+
**DO:** Bind to a `FormControl` or `signal()` — let Angular own the state, not the DOM:
|
|
180
|
+
```typescript
|
|
181
|
+
readonly inputValue = signal('');
|
|
182
|
+
// In template: <input [value]="inputValue()" (input)="inputValue.set($event.target.value)" />
|
|
183
|
+
// Or with reactive forms: this.form.controls.field.value
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
### Rule 4: Don't use default (Zone.js) change detection for new components
|
|
189
|
+
**DON'T:**
|
|
190
|
+
```typescript
|
|
191
|
+
@Component({ selector: 'app-card', ... })
|
|
192
|
+
export class CardComponent { ... } // defaults to CheckAlways
|
|
193
|
+
```
|
|
194
|
+
**DO:**
|
|
195
|
+
```typescript
|
|
196
|
+
@Component({
|
|
197
|
+
selector: 'app-card',
|
|
198
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
199
|
+
...
|
|
200
|
+
})
|
|
201
|
+
export class CardComponent { ... }
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### Rule 5: Don't use inline styles for theming
|
|
207
|
+
**DON'T:**
|
|
208
|
+
```html
|
|
209
|
+
<button mat-flat-button style="background-color: #2563EB; color: white;">Save</button>
|
|
210
|
+
```
|
|
211
|
+
**DO:** Use Angular Material's CSS custom properties or the theming API:
|
|
212
|
+
```scss
|
|
213
|
+
// In component SCSS:
|
|
214
|
+
:host {
|
|
215
|
+
--mat-filled-button-container-color: var(--primary-brand);
|
|
216
|
+
}
|
|
217
|
+
// Or override globally in styles.scss:
|
|
218
|
+
html {
|
|
219
|
+
--mat-toolbar-container-background-color: #1e293b;
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
### Rule 6: Don't block routing with synchronous guards
|
|
226
|
+
**DON'T:**
|
|
227
|
+
```typescript
|
|
228
|
+
canActivate(): boolean {
|
|
229
|
+
return this.authService.isLoggedIn; // sync property read — fragile
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
**DO:** Use functional guards with async resolution:
|
|
233
|
+
```typescript
|
|
234
|
+
export const authGuard = () => {
|
|
235
|
+
const auth = inject(AuthService);
|
|
236
|
+
const router = inject(Router);
|
|
237
|
+
return auth.isAuthenticated$.pipe(
|
|
238
|
+
take(1),
|
|
239
|
+
map(ok => ok ? true : router.createUrlTree(['/login']))
|
|
240
|
+
);
|
|
241
|
+
};
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
### Rule 7: Don't omit `track` in `@for` with mutable lists
|
|
247
|
+
**DON'T:**
|
|
248
|
+
```html
|
|
249
|
+
@for (item of items()) {
|
|
250
|
+
<app-item [item]="item" />
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
**DO:**
|
|
254
|
+
```html
|
|
255
|
+
@for (item of items(); track item.id) {
|
|
256
|
+
<app-item [item]="item" />
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
Without `track`, Angular destroys and recreates DOM nodes on every list mutation, causing flicker and destroying component state (form values, animation state, focus).
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
### Rule 8: Don't use `::ng-deep` for component styling
|
|
264
|
+
**DON'T:**
|
|
265
|
+
```scss
|
|
266
|
+
::ng-deep .mat-mdc-form-field-subscript-wrapper { display: none; }
|
|
267
|
+
```
|
|
268
|
+
**DO:** Use Angular Material's CSS custom properties, the `MAT_FORM_FIELD_DEFAULT_OPTIONS` injection token, or wrap the component in a host element with a class and target that:
|
|
269
|
+
```scss
|
|
270
|
+
// styles.scss — globally, intentionally:
|
|
271
|
+
html {
|
|
272
|
+
--mat-form-field-subscript-overflow: hidden;
|
|
273
|
+
}
|
|
274
|
+
// Or in component SCSS with a host selector:
|
|
275
|
+
:host {
|
|
276
|
+
--mat-form-field-container-height: 48px;
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
`::ng-deep` is deprecated and will be removed. It also breaks style encapsulation silently.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Section 5: Style Recommendations by App Category
|
|
284
|
+
|
|
285
|
+
| App Type | Primary Style | Secondary Style | Color Focus | Recommended Library |
|
|
286
|
+
|---|---|---|---|---|
|
|
287
|
+
| SaaS / B2B Dashboard | Flat + Material | Glassmorphism cards | Trust blue / Indigo (#2563EB) | Angular Material |
|
|
288
|
+
| Enterprise Admin Panel | Data-Dense Material | Minimal borders | Neutral grey + brand accent | NG-ZORRO or Material |
|
|
289
|
+
| Analytics Dashboard | Dark Material | Data-dense grids | Dark bg (#0F172A) + vivid accents | Material + CDK virtual scroll |
|
|
290
|
+
| E-commerce Storefront | Vibrant + Block | Aurora gradient hero | Brand primary + success green | PrimeNG or Material |
|
|
291
|
+
| Developer Tool / IDE | Minimalist / Monochrome | Dark mode first | Monochrome + single accent | Material or fully custom |
|
|
292
|
+
| Consumer Mobile (PWA) | Material You / Soft UI | Rounded, tactile | M3 dynamic color | Angular Material (M3) |
|
|
293
|
+
| Healthcare / Medical | Clean + Trustworthy | High contrast | Green (#059669) + blue | Angular Material |
|
|
294
|
+
| Fintech / Banking | Formal + Structured | Dark sidebar | Dark navy (#1E3A5F) + teal | Angular Material or custom |
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Section 6: Angular Material 3 Color Palettes
|
|
299
|
+
|
|
300
|
+
These palettes are starting points for `mat.define-theme()`. Use the `mat.$*-palette` named palettes or supply a custom tonal palette with `mat.define-palette()`.
|
|
301
|
+
|
|
302
|
+
| App Type | Primary | Secondary | Tertiary | Surface | Notes |
|
|
303
|
+
|---|---|---|---|---|---|
|
|
304
|
+
| SaaS / B2B | #2563EB (Indigo 600) | #6366F1 (Violet) | #EA580C (Orange CTA) | #F8FAFC | Use `mat.$azure-palette` as primary |
|
|
305
|
+
| Healthcare | #059669 (Emerald) | #0891B2 (Cyan) | #7C3AED (Purple) | #F0FDF4 | Conveys calm, trust, health |
|
|
306
|
+
| Fintech / Banking | #1E3A5F (Dark Navy) | #0F766E (Teal 700) | #B45309 (Amber) | #F1F5F9 | Formal, stable, trustworthy |
|
|
307
|
+
| E-commerce | #16A34A (Green) | #EA580C (Orange) | #7C3AED (Purple) | #FFFFFF | Energetic; orange = sale/CTA |
|
|
308
|
+
| Developer Tool | #6B7280 (Grey) | #374151 (Dark Grey) | #3B82F6 (Blue accent) | #111827 | Dark mode first; minimal saturation |
|
|
309
|
+
| Analytics / BI | #6366F1 (Violet) | #06B6D4 (Cyan) | #F59E0B (Amber) | #0F172A | Dark surface; vivid data colors |
|
|
310
|
+
| Consumer / Social | #EC4899 (Pink) | #8B5CF6 (Purple) | #06B6D4 (Cyan) | #FAFAFA | Playful; Material You dynamic color |
|
|
311
|
+
| Education | #2563EB (Blue) | #16A34A (Green) | #F59E0B (Amber) | #EFF6FF | Primary blue = trust; amber = gamification |
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Section 7: Angular Material 3 Theming Quick Reference
|
|
316
|
+
|
|
317
|
+
### Core Theming API
|
|
318
|
+
|
|
319
|
+
```scss
|
|
320
|
+
// styles.scss
|
|
321
|
+
@use '@angular/material' as mat;
|
|
322
|
+
|
|
323
|
+
// Include core styles once (resets, tokens)
|
|
324
|
+
@include mat.core();
|
|
325
|
+
|
|
326
|
+
// Define the theme
|
|
327
|
+
$theme: mat.define-theme((
|
|
328
|
+
color: (
|
|
329
|
+
theme-type: light,
|
|
330
|
+
primary: mat.$azure-palette, // M3 tonal palette
|
|
331
|
+
tertiary: mat.$orange-palette, // Accent/CTA color role
|
|
332
|
+
),
|
|
333
|
+
typography: (
|
|
334
|
+
brand-family: 'Inter, system-ui, sans-serif',
|
|
335
|
+
plain-family: 'Inter, system-ui, sans-serif',
|
|
336
|
+
bold-weight: 700,
|
|
337
|
+
medium-weight: 500,
|
|
338
|
+
regular-weight: 400,
|
|
339
|
+
),
|
|
340
|
+
density: (
|
|
341
|
+
scale: 0, // 0 = default, -1 = compact, -2 = very compact
|
|
342
|
+
),
|
|
343
|
+
));
|
|
344
|
+
|
|
345
|
+
// Apply to root
|
|
346
|
+
html {
|
|
347
|
+
@include mat.all-component-themes($theme);
|
|
348
|
+
// Or selectively:
|
|
349
|
+
// @include mat.button-theme($theme);
|
|
350
|
+
// @include mat.form-field-theme($theme);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Dark mode via class toggle
|
|
354
|
+
.dark-theme {
|
|
355
|
+
$dark-theme: mat.define-theme((
|
|
356
|
+
color: (
|
|
357
|
+
theme-type: dark,
|
|
358
|
+
primary: mat.$azure-palette,
|
|
359
|
+
tertiary: mat.$orange-palette,
|
|
360
|
+
),
|
|
361
|
+
));
|
|
362
|
+
@include mat.all-component-colors($dark-theme);
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Available Named M3 Palettes
|
|
367
|
+
`mat.$red-palette`, `mat.$pink-palette`, `mat.$purple-palette`, `mat.$violet-palette`,
|
|
368
|
+
`mat.$indigo-palette`, `mat.$blue-palette`, `mat.$azure-palette`, `mat.$cyan-palette`,
|
|
369
|
+
`mat.$teal-palette`, `mat.$green-palette`, `mat.$olive-palette`, `mat.$yellow-palette`,
|
|
370
|
+
`mat.$orange-palette`, `mat.$brown-palette`, `mat.$rose-palette`, `mat.$chartreuse-palette`
|
|
371
|
+
|
|
372
|
+
### CSS Custom Property Overrides
|
|
373
|
+
Use `--mat-*` properties for targeted overrides without re-defining the full theme:
|
|
374
|
+
|
|
375
|
+
```scss
|
|
376
|
+
html {
|
|
377
|
+
// Toolbar
|
|
378
|
+
--mat-toolbar-container-background-color: #1e293b;
|
|
379
|
+
--mat-toolbar-container-text-color: #f8fafc;
|
|
380
|
+
|
|
381
|
+
// Buttons
|
|
382
|
+
--mat-filled-button-container-color: #2563eb;
|
|
383
|
+
--mat-filled-button-label-text-color: #ffffff;
|
|
384
|
+
|
|
385
|
+
// Cards
|
|
386
|
+
--mat-card-elevated-container-color: #ffffff;
|
|
387
|
+
--mat-card-elevated-container-elevation: 0 1px 3px rgba(0,0,0,0.1);
|
|
388
|
+
|
|
389
|
+
// Form fields
|
|
390
|
+
--mat-form-field-container-height: 52px;
|
|
391
|
+
|
|
392
|
+
// Sidenav
|
|
393
|
+
--mat-sidenav-container-width: 260px;
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Angular Material Typography Scale (M3)
|
|
398
|
+
```scss
|
|
399
|
+
$theme: mat.define-theme((
|
|
400
|
+
typography: (
|
|
401
|
+
brand-family: 'Geist, Inter, sans-serif',
|
|
402
|
+
plain-family: 'Geist, Inter, sans-serif',
|
|
403
|
+
// M3 type scale roles: display-large → label-small
|
|
404
|
+
// Override individual roles:
|
|
405
|
+
),
|
|
406
|
+
));
|
|
407
|
+
```
|
|
408
|
+
Reference type roles: `display-large`, `display-medium`, `display-small`,
|
|
409
|
+
`headline-large`, `headline-medium`, `headline-small`,
|
|
410
|
+
`title-large`, `title-medium`, `title-small`,
|
|
411
|
+
`body-large`, `body-medium`, `body-small`,
|
|
412
|
+
`label-large`, `label-medium`, `label-small`
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## Section 8: Signals-First UI Patterns
|
|
417
|
+
|
|
418
|
+
### Local UI State
|
|
419
|
+
```typescript
|
|
420
|
+
import { Component, signal, computed, ChangeDetectionStrategy } from '@angular/core';
|
|
421
|
+
|
|
422
|
+
@Component({
|
|
423
|
+
selector: 'app-counter',
|
|
424
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
425
|
+
template: `
|
|
426
|
+
<p>Count: {{ count() }} — Doubled: {{ doubled() }}</p>
|
|
427
|
+
<button mat-stroked-button (click)="increment()">+1</button>
|
|
428
|
+
<button mat-stroked-button (click)="reset()">Reset</button>
|
|
429
|
+
`,
|
|
430
|
+
})
|
|
431
|
+
export class CounterComponent {
|
|
432
|
+
readonly count = signal(0);
|
|
433
|
+
readonly doubled = computed(() => this.count() * 2);
|
|
434
|
+
|
|
435
|
+
increment() { this.count.update(n => n + 1); }
|
|
436
|
+
reset() { this.count.set(0); }
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Async Loading State Pattern
|
|
441
|
+
```typescript
|
|
442
|
+
import { Component, signal, inject, ChangeDetectionStrategy } from '@angular/core';
|
|
443
|
+
import { ItemService } from './item.service';
|
|
444
|
+
|
|
445
|
+
interface Item { id: number; name: string; }
|
|
446
|
+
|
|
447
|
+
@Component({
|
|
448
|
+
selector: 'app-item-list',
|
|
449
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
450
|
+
template: `
|
|
451
|
+
@if (loading()) {
|
|
452
|
+
<mat-progress-bar mode="indeterminate" />
|
|
453
|
+
} @else if (error()) {
|
|
454
|
+
<p class="error">{{ error() }}</p>
|
|
455
|
+
<button mat-button (click)="load()">Retry</button>
|
|
456
|
+
} @else {
|
|
457
|
+
@for (item of data(); track item.id) {
|
|
458
|
+
<app-item-card [item]="item" />
|
|
459
|
+
} @empty {
|
|
460
|
+
<p class="empty-state">No items found.</p>
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
`,
|
|
464
|
+
})
|
|
465
|
+
export class ItemListComponent {
|
|
466
|
+
private service = inject(ItemService);
|
|
467
|
+
|
|
468
|
+
readonly loading = signal(false);
|
|
469
|
+
readonly error = signal<string | null>(null);
|
|
470
|
+
readonly data = signal<Item[]>([]);
|
|
471
|
+
|
|
472
|
+
async load() {
|
|
473
|
+
this.loading.set(true);
|
|
474
|
+
this.error.set(null);
|
|
475
|
+
try {
|
|
476
|
+
this.data.set(await this.service.getItems());
|
|
477
|
+
} catch (e) {
|
|
478
|
+
this.error.set('Failed to load items. Please try again.');
|
|
479
|
+
} finally {
|
|
480
|
+
this.loading.set(false);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Converting Observable to Signal (toSignal)
|
|
487
|
+
```typescript
|
|
488
|
+
import { toSignal } from '@angular/core/rxjs-interop';
|
|
489
|
+
import { inject } from '@angular/core';
|
|
490
|
+
|
|
491
|
+
@Component({ ... })
|
|
492
|
+
export class UserComponent {
|
|
493
|
+
private userService = inject(UserService);
|
|
494
|
+
|
|
495
|
+
// Automatically subscribes and unsubscribes; initial value is undefined
|
|
496
|
+
readonly user = toSignal(this.userService.currentUser$);
|
|
497
|
+
|
|
498
|
+
// With initial value to avoid undefined checks:
|
|
499
|
+
readonly items = toSignal(this.itemService.items$, { initialValue: [] as Item[] });
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Optimistic UI Updates
|
|
504
|
+
```typescript
|
|
505
|
+
async toggleItem(item: Item) {
|
|
506
|
+
const previous = this.items(); // snapshot for rollback
|
|
507
|
+
|
|
508
|
+
// Immediately update UI
|
|
509
|
+
this.items.update(list =>
|
|
510
|
+
list.map(i => i.id === item.id ? { ...i, done: !i.done } : i)
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
await this.api.toggleItem(item.id);
|
|
515
|
+
} catch {
|
|
516
|
+
// Rollback on failure
|
|
517
|
+
this.items.set(previous);
|
|
518
|
+
this.snackBar.open('Update failed — changes reverted', 'Dismiss', { duration: 4000 });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Derived State with computed()
|
|
524
|
+
```typescript
|
|
525
|
+
readonly searchQuery = signal('');
|
|
526
|
+
readonly allItems = signal<Item[]>([]);
|
|
527
|
+
readonly statusFilter = signal<'all' | 'active' | 'done'>('all');
|
|
528
|
+
|
|
529
|
+
readonly filteredItems = computed(() => {
|
|
530
|
+
const q = this.searchQuery().toLowerCase();
|
|
531
|
+
const status = this.statusFilter();
|
|
532
|
+
return this.allItems()
|
|
533
|
+
.filter(item => !q || item.name.toLowerCase().includes(q))
|
|
534
|
+
.filter(item => status === 'all' || (status === 'done' ? item.done : !item.done));
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
readonly resultCount = computed(() => this.filteredItems().length);
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Input Signals (Angular 17.1+)
|
|
541
|
+
```typescript
|
|
542
|
+
import { input, output, model } from '@angular/core';
|
|
543
|
+
|
|
544
|
+
@Component({ selector: 'app-item-card', ... })
|
|
545
|
+
export class ItemCardComponent {
|
|
546
|
+
// Required input — type-safe, signal-based
|
|
547
|
+
readonly item = input.required<Item>();
|
|
548
|
+
|
|
549
|
+
// Optional input with default
|
|
550
|
+
readonly variant = input<'compact' | 'full'>('full');
|
|
551
|
+
|
|
552
|
+
// Two-way binding (model signal)
|
|
553
|
+
readonly selected = model(false);
|
|
554
|
+
|
|
555
|
+
// Output
|
|
556
|
+
readonly deleted = output<Item>();
|
|
557
|
+
|
|
558
|
+
delete() { this.deleted.emit(this.item()); }
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Usage:
|
|
562
|
+
// <app-item-card [item]="item" [(selected)]="isSelected" (deleted)="onDelete($event)" />
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
## Section 9: Angular Form UX
|
|
568
|
+
|
|
569
|
+
### Typed Reactive Forms
|
|
570
|
+
```typescript
|
|
571
|
+
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
|
572
|
+
import { FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
|
|
573
|
+
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
574
|
+
import { MatInputModule } from '@angular/material/input';
|
|
575
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
576
|
+
|
|
577
|
+
interface SignupForm {
|
|
578
|
+
email: FormControl<string>;
|
|
579
|
+
password: FormControl<string>;
|
|
580
|
+
displayName: FormControl<string>;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
@Component({
|
|
584
|
+
selector: 'app-signup-form',
|
|
585
|
+
standalone: true,
|
|
586
|
+
imports: [ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatButtonModule],
|
|
587
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
588
|
+
template: `
|
|
589
|
+
<form [formGroup]="form" (ngSubmit)="submit()">
|
|
590
|
+
<mat-form-field appearance="outline">
|
|
591
|
+
<mat-label>Email</mat-label>
|
|
592
|
+
<input matInput [formControl]="form.controls.email" type="email" autocomplete="email" />
|
|
593
|
+
<mat-error>{{ emailError }}</mat-error>
|
|
594
|
+
</mat-form-field>
|
|
595
|
+
|
|
596
|
+
<mat-form-field appearance="outline">
|
|
597
|
+
<mat-label>Password</mat-label>
|
|
598
|
+
<input matInput [formControl]="form.controls.password" type="password" />
|
|
599
|
+
<mat-hint>At least 8 characters</mat-hint>
|
|
600
|
+
<mat-error>{{ passwordError }}</mat-error>
|
|
601
|
+
</mat-form-field>
|
|
602
|
+
|
|
603
|
+
<mat-form-field appearance="outline">
|
|
604
|
+
<mat-label>Display Name</mat-label>
|
|
605
|
+
<input matInput [formControl]="form.controls.displayName" autocomplete="name" />
|
|
606
|
+
<mat-error *ngIf="form.controls.displayName.hasError('required')">
|
|
607
|
+
Name is required
|
|
608
|
+
</mat-error>
|
|
609
|
+
</mat-form-field>
|
|
610
|
+
|
|
611
|
+
<button mat-flat-button type="submit" [disabled]="form.invalid || submitting()">
|
|
612
|
+
@if (submitting()) {
|
|
613
|
+
<mat-spinner diameter="20" />
|
|
614
|
+
} @else {
|
|
615
|
+
Create Account
|
|
616
|
+
}
|
|
617
|
+
</button>
|
|
618
|
+
</form>
|
|
619
|
+
`,
|
|
620
|
+
})
|
|
621
|
+
export class SignupFormComponent {
|
|
622
|
+
submitting = signal(false);
|
|
623
|
+
|
|
624
|
+
readonly form = new FormGroup<SignupForm>({
|
|
625
|
+
email: new FormControl('', {
|
|
626
|
+
nonNullable: true,
|
|
627
|
+
validators: [Validators.required, Validators.email],
|
|
628
|
+
updateOn: 'blur', // Validate on blur, not on every keystroke
|
|
629
|
+
}),
|
|
630
|
+
password: new FormControl('', {
|
|
631
|
+
nonNullable: true,
|
|
632
|
+
validators: [Validators.required, Validators.minLength(8)],
|
|
633
|
+
updateOn: 'blur',
|
|
634
|
+
}),
|
|
635
|
+
displayName: new FormControl('', {
|
|
636
|
+
nonNullable: true,
|
|
637
|
+
validators: [Validators.required],
|
|
638
|
+
updateOn: 'blur',
|
|
639
|
+
}),
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
get emailError(): string {
|
|
643
|
+
const ctrl = this.form.controls.email;
|
|
644
|
+
if (!ctrl.touched) return '';
|
|
645
|
+
if (ctrl.hasError('required')) return 'Email is required';
|
|
646
|
+
if (ctrl.hasError('email')) return 'Enter a valid email address';
|
|
647
|
+
return '';
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
get passwordError(): string {
|
|
651
|
+
const ctrl = this.form.controls.password;
|
|
652
|
+
if (!ctrl.touched) return '';
|
|
653
|
+
if (ctrl.hasError('required')) return 'Password is required';
|
|
654
|
+
if (ctrl.hasError('minlength')) return 'Password must be at least 8 characters';
|
|
655
|
+
return '';
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async submit() {
|
|
659
|
+
if (this.form.invalid) { this.form.markAllAsTouched(); return; }
|
|
660
|
+
this.submitting.set(true);
|
|
661
|
+
try {
|
|
662
|
+
await this.authService.signup(this.form.getRawValue());
|
|
663
|
+
} finally {
|
|
664
|
+
this.submitting.set(false);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
### Form UX Rules
|
|
671
|
+
1. Use `updateOn: 'blur'` — never validate on every keystroke for non-search fields.
|
|
672
|
+
2. Call `markAllAsTouched()` on submit attempt to surface all errors at once.
|
|
673
|
+
3. Always show `<mat-hint>` for format expectations before the user types, `<mat-error>` after touched.
|
|
674
|
+
4. Disable the submit button while `submitting()` is true and show a spinner inside it.
|
|
675
|
+
5. Use `nonNullable: true` on FormControl to get properly typed `.value` (non-undefined).
|
|
676
|
+
6. Prefer `FormGroup<T>` typed form groups for type-safe `.controls` access.
|
|
677
|
+
7. For long multi-step forms, use Angular CDK Stepper (`mat-stepper`) and validate per-step.
|
|
678
|
+
8. Autocomplete attributes (`autocomplete="email"`, `autocomplete="current-password"`) are UX, not optional.
|
|
679
|
+
|
|
680
|
+
### Search / Filter UX with Debounce
|
|
681
|
+
```typescript
|
|
682
|
+
readonly searchControl = new FormControl('', { nonNullable: true });
|
|
683
|
+
|
|
684
|
+
// In constructor or ngOnInit:
|
|
685
|
+
this.searchControl.valueChanges.pipe(
|
|
686
|
+
debounceTime(300),
|
|
687
|
+
distinctUntilChanged(),
|
|
688
|
+
takeUntilDestroyed(),
|
|
689
|
+
).subscribe(query => this.searchQuery.set(query));
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
## Section 10: Performance Design Checklist
|
|
695
|
+
|
|
696
|
+
- [ ] **OnPush on every component** — set `changeDetection: ChangeDetectionStrategy.OnPush` at component creation
|
|
697
|
+
- [ ] **`track item.id` in every `@for`** — prevents full DOM reconstruction on list mutation
|
|
698
|
+
- [ ] **`@defer (on viewport)`** for below-fold sections — e.g. charts, secondary panels, comment sections
|
|
699
|
+
- [ ] **`NgOptimizedImage`** on all `<img>` tags — add `priority` attribute to the LCP image
|
|
700
|
+
- [ ] **Lazy-loaded routes** with `loadComponent: () => import('./page').then(m => m.PageComponent)`
|
|
701
|
+
- [ ] **`toSignal()`** instead of `.subscribe()` in components — automatic teardown, no leaks
|
|
702
|
+
- [ ] **SSR-aware code** — no `window`/`document`/`localStorage` in constructors; use `afterRender()` or `isPlatformBrowser()`
|
|
703
|
+
- [ ] **`@defer (on idle)`** for non-critical UI loaded after page is interactive
|
|
704
|
+
- [ ] **`provideClientHydration()`** in app.config.ts for SSR + hydration
|
|
705
|
+
- [ ] **`trackBy` on mat-table** — provide `trackBy` function to `<mat-table [dataSource]>`
|
|
706
|
+
- [ ] **Virtual scrolling** for long lists (500+ items) — use `cdk-virtual-scroll-viewport`
|
|
707
|
+
- [ ] **Bundle size** — check `ng build --stats-json` + `webpack-bundle-analyzer`; no library imported in root if only used in one lazy route
|
|
708
|
+
|
|
709
|
+
### @defer Patterns
|
|
710
|
+
|
|
711
|
+
```html
|
|
712
|
+
<!-- Defer chart section until it enters viewport -->
|
|
713
|
+
@defer (on viewport) {
|
|
714
|
+
<app-analytics-chart [data]="chartData()" />
|
|
715
|
+
} @placeholder {
|
|
716
|
+
<div class="chart-skeleton" style="height: 300px; background: #f1f5f9; border-radius: 8px;"></div>
|
|
717
|
+
} @loading (minimum 500ms) {
|
|
718
|
+
<mat-progress-bar mode="indeterminate" />
|
|
719
|
+
} @error {
|
|
720
|
+
<p>Chart failed to load.</p>
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
<!-- Defer non-critical panel until browser is idle -->
|
|
724
|
+
@defer (on idle) {
|
|
725
|
+
<app-recommendations-panel />
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
<!-- Conditional defer — only load when user triggers it -->
|
|
729
|
+
@defer (on interaction(triggerEl)) {
|
|
730
|
+
<app-heavy-modal />
|
|
731
|
+
}
|
|
732
|
+
<button #triggerEl mat-button>Show Details</button>
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
### NgOptimizedImage
|
|
736
|
+
```typescript
|
|
737
|
+
// In component imports:
|
|
738
|
+
import { NgOptimizedImage } from '@angular/common';
|
|
739
|
+
|
|
740
|
+
// In template:
|
|
741
|
+
// LCP image — add priority:
|
|
742
|
+
<img ngSrc="/hero.webp" width="1200" height="600" priority alt="Hero banner" />
|
|
743
|
+
|
|
744
|
+
// Standard lazy-loaded image:
|
|
745
|
+
<img ngSrc="/thumbnail.webp" width="300" height="200" alt="Product photo" />
|
|
746
|
+
|
|
747
|
+
// Dynamic from CDN with loader:
|
|
748
|
+
<img [ngSrc]="product.imageUrl" width="400" height="400" alt="{{ product.name }}" />
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
---
|
|
752
|
+
|
|
753
|
+
## Section 11: Reference Files
|
|
754
|
+
|
|
755
|
+
These files contain expanded catalogs, code patterns, and decision matrices. Instruct Claude to read them when the task requires deeper detail:
|
|
756
|
+
|
|
757
|
+
| File | When to Read |
|
|
758
|
+
|---|---|
|
|
759
|
+
| `references/components.md` | Full Angular Material 3 + CDK component catalog with annotated code examples, inputs/outputs, and common patterns for each component |
|
|
760
|
+
| `references/theming.md` | Complete M3 theming system: custom tonal palettes, dark/light switching, per-component theme overrides, CSS custom property reference, typography scale, density |
|
|
761
|
+
| `references/patterns.md` | Signals patterns (effect, resource, linkedSignal), smart/dumb component split, routing UX (skeleton screens, route transitions, breadcrumbs), state management patterns |
|
|
762
|
+
| `references/animations.md` | Angular Animations module, route transition animations, list stagger/reorder, micro-interactions, `@keyframes` vs Angular `animate()`, reduced-motion media query |
|
|
763
|
+
| `references/performance.md` | `@defer` reference, `NgOptimizedImage` loader setup, SSR hydration patterns, virtual scrolling, bundle splitting, Core Web Vitals measurement in Angular |
|
|
764
|
+
| `references/accessibility.md` | CDK `A11yModule` (LiveAnnouncer, FocusTrap, FocusMonitor), ARIA roles with Angular Material, keyboard navigation patterns, color contrast in M3, screen reader testing |
|
|
765
|
+
| `references/alt-libraries.md` | NG-ZORRO vs PrimeNG vs Angular Material decision matrix: when each wins, migration notes, bundle size comparison, Angular version compatibility |
|
|
766
|
+
|
|
767
|
+
### Quick Access Prompts
|
|
768
|
+
When helping with a specific task, pre-load context with:
|
|
769
|
+
- `Read references/components.md for the mat-table section` — for sortable/filterable table UX
|
|
770
|
+
- `Read references/theming.md for the dark mode section` — for dark/light toggle implementation
|
|
771
|
+
- `Read references/patterns.md for the smart/dumb split` — for component decomposition guidance
|
|
772
|
+
- `Read references/animations.md for route transitions` — for page-to-page motion design
|
|
773
|
+
- `Read references/accessibility.md for keyboard nav` — for ARIA and CDK FocusTrap patterns
|
|
774
|
+
|
|
775
|
+
---
|
|
776
|
+
|
|
777
|
+
## Quick Reference: Angular 17+ Standalone Component Template
|
|
778
|
+
|
|
779
|
+
```typescript
|
|
780
|
+
import {
|
|
781
|
+
Component,
|
|
782
|
+
ChangeDetectionStrategy,
|
|
783
|
+
signal,
|
|
784
|
+
computed,
|
|
785
|
+
input,
|
|
786
|
+
output,
|
|
787
|
+
inject,
|
|
788
|
+
} from '@angular/core';
|
|
789
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
790
|
+
import { MatCardModule } from '@angular/material/card';
|
|
791
|
+
|
|
792
|
+
@Component({
|
|
793
|
+
selector: 'app-my-component',
|
|
794
|
+
standalone: true,
|
|
795
|
+
imports: [MatButtonModule, MatCardModule],
|
|
796
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
797
|
+
host: { class: 'app-my-component' },
|
|
798
|
+
styles: `
|
|
799
|
+
:host {
|
|
800
|
+
display: block;
|
|
801
|
+
}
|
|
802
|
+
`,
|
|
803
|
+
template: `
|
|
804
|
+
<mat-card>
|
|
805
|
+
<mat-card-header>
|
|
806
|
+
<mat-card-title>{{ title() }}</mat-card-title>
|
|
807
|
+
</mat-card-header>
|
|
808
|
+
<mat-card-content>
|
|
809
|
+
@if (loading()) {
|
|
810
|
+
<mat-progress-bar mode="indeterminate" />
|
|
811
|
+
} @else {
|
|
812
|
+
<p>{{ content() }}</p>
|
|
813
|
+
}
|
|
814
|
+
</mat-card-content>
|
|
815
|
+
<mat-card-actions>
|
|
816
|
+
<button mat-flat-button (click)="confirm.emit()">Confirm</button>
|
|
817
|
+
<button mat-button (click)="cancel.emit()">Cancel</button>
|
|
818
|
+
</mat-card-actions>
|
|
819
|
+
</mat-card>
|
|
820
|
+
`,
|
|
821
|
+
})
|
|
822
|
+
export class MyComponent {
|
|
823
|
+
// Inputs
|
|
824
|
+
readonly title = input.required<string>();
|
|
825
|
+
readonly content = input<string>('');
|
|
826
|
+
|
|
827
|
+
// Outputs
|
|
828
|
+
readonly confirm = output<void>();
|
|
829
|
+
readonly cancel = output<void>();
|
|
830
|
+
|
|
831
|
+
// Local state
|
|
832
|
+
readonly loading = signal(false);
|
|
833
|
+
|
|
834
|
+
// Derived state
|
|
835
|
+
readonly hasContent = computed(() => this.content().length > 0);
|
|
836
|
+
|
|
837
|
+
// Services
|
|
838
|
+
private myService = inject(MyService);
|
|
839
|
+
}
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
---
|
|
843
|
+
|
|
844
|
+
*This skill covers Angular 17+ with standalone components, signals, and Angular Material 3. For Angular 14–16 NgModule-based patterns, note that the core UX guidance remains valid but syntax differs (use `ngOnDestroy` + `Subject` for cleanup, `*ngIf`/`*ngFor` directives, and `@NgModule` imports arrays).*
|