guardian-framework 0.1.23 → 0.1.24
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/dist/cli.js +1 -1
- package/dist/exports.js +1 -1
- package/package.json +1 -1
- package/templates/pi/preflight_report.json +1 -1
- package/templates/pi/skills/angular-enterprise-codegen.md +330 -78
- package/templates/pi/skills/design-system-enterprise-codegen.md +347 -0
- package/templates/pi/skills/nextjs-enterprise-codegen.md +319 -74
package/dist/cli.js
CHANGED
|
@@ -1335,7 +1335,7 @@ __export(exports_package, {
|
|
|
1335
1335
|
bin: () => bin,
|
|
1336
1336
|
author: () => author
|
|
1337
1337
|
});
|
|
1338
|
-
var name = "guardian-framework", version = "0.1.
|
|
1338
|
+
var name = "guardian-framework", version = "0.1.24", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
|
|
1339
1339
|
var init_package = __esm(() => {
|
|
1340
1340
|
exports = {
|
|
1341
1341
|
".": {
|
package/dist/exports.js
CHANGED
|
@@ -995,7 +995,7 @@ __export(exports_package, {
|
|
|
995
995
|
bin: () => bin,
|
|
996
996
|
author: () => author
|
|
997
997
|
});
|
|
998
|
-
var name = "guardian-framework", version = "0.1.
|
|
998
|
+
var name = "guardian-framework", version = "0.1.24", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
|
|
999
999
|
var init_package = __esm(() => {
|
|
1000
1000
|
exports = {
|
|
1001
1001
|
".": {
|
package/package.json
CHANGED
|
@@ -1,158 +1,410 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: angular-enterprise-codegen
|
|
3
|
-
description: Full reference for Angular enterprise code generation with DDD + Clean Architecture. Covers module structure,
|
|
3
|
+
description: Full reference for Angular enterprise code generation with DDD + Clean Architecture. Covers module structure, standalone components, Signal-based state, RxJS patterns, design system integration, lazy loading, testing, and performance. Loaded on-demand via agents/angular-codegen.md skill.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Angular Enterprise Code Generation — DDD + Clean Architecture
|
|
7
7
|
|
|
8
8
|
> Canonical skill for generating production-grade Angular code with DDD patterns.
|
|
9
9
|
> All code MUST follow these patterns. Validators enforce compliance.
|
|
10
|
+
>
|
|
11
|
+
> For design system and styling patterns, see: `.pi/skills/design-system-enterprise-codegen.md`
|
|
10
12
|
|
|
11
13
|
---
|
|
12
14
|
|
|
13
|
-
## 1. Project Structure
|
|
15
|
+
## 1. Project Structure — DDD with Angular Standalone Components
|
|
14
16
|
|
|
15
17
|
```
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
src/
|
|
19
|
+
module/ # One bounded context
|
|
20
|
+
domain/ # Pure business logic, zero Angular imports
|
|
21
|
+
entity.ts # Domain entities with encapsulated state
|
|
22
|
+
value.ts # Immutable value objects
|
|
23
|
+
events.ts # Domain events
|
|
24
|
+
errors.ts # Typed error classes
|
|
25
|
+
application/ # Application logic
|
|
26
|
+
store.ts # Signal-based state management
|
|
27
|
+
service.ts # @Injectable use case service
|
|
28
|
+
facade.ts # Facade pattern (service → store)
|
|
29
|
+
dto.ts # Command/Query DTOs
|
|
30
|
+
infrastructure/ # External adapters
|
|
31
|
+
api-client.ts # HttpClient wrapper with interceptors
|
|
32
|
+
auth.ts # Auth provider SDK wrapper
|
|
33
|
+
analytics.ts # Analytics SDK wrapper
|
|
34
|
+
interfaces/ # UI layer
|
|
35
|
+
page.component.ts # Route component (standalone)
|
|
36
|
+
list.component.ts # Smart component
|
|
37
|
+
item.component.ts # Presentational component
|
|
38
|
+
form.component.ts # Form with Reactive Forms
|
|
39
|
+
shared/ # Cross-module shared code
|
|
40
|
+
ui/ # Design system components
|
|
41
|
+
button/
|
|
42
|
+
card/
|
|
43
|
+
modal/
|
|
44
|
+
lib/ # Shared utilities
|
|
45
|
+
pipes/ # Shared Angular pipes
|
|
31
46
|
```
|
|
32
47
|
|
|
33
48
|
### Dependency Rule
|
|
34
49
|
```
|
|
35
50
|
domain → application → infrastructure → interfaces
|
|
51
|
+
↕
|
|
52
|
+
shared/ (no framework deps)
|
|
36
53
|
```
|
|
37
54
|
|
|
55
|
+
- **domain/** — zero Angular imports. Pure TypeScript. No decorators.
|
|
56
|
+
- **application/** — imports from domain. Uses `@Injectable` for DI.
|
|
57
|
+
- **infrastructure/** — imports from domain + application. Uses Angular `HttpClient`.
|
|
58
|
+
- **interfaces/** — imports from application. Standalone components with `@Component`.
|
|
59
|
+
|
|
38
60
|
---
|
|
39
61
|
|
|
40
|
-
## 2.
|
|
62
|
+
## 2. Component Architecture
|
|
41
63
|
|
|
42
|
-
|
|
43
|
-
// domain/entity.ts — No Angular imports
|
|
44
|
-
export class Order {
|
|
45
|
-
private readonly _id: string;
|
|
46
|
-
private _status: OrderStatus = OrderStatus.PENDING;
|
|
47
|
-
|
|
48
|
-
constructor(id?: string) {
|
|
49
|
-
this._id = id ?? crypto.randomUUID();
|
|
50
|
-
}
|
|
64
|
+
### Smart vs Presentational Components
|
|
51
65
|
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
| Type | Location | Responsibility |
|
|
67
|
+
|------|----------|---------------|
|
|
68
|
+
| Smart (Page) | `interfaces/page.component.ts` | Route handler, orchestrates data flow |
|
|
69
|
+
| Smart (List) | `interfaces/list.component.ts` | Uses store/service, passes data down |
|
|
70
|
+
| Presentational | `interfaces/item.component.ts` | Pure @Input/@Output, no DI |
|
|
71
|
+
| Form | `interfaces/form.component.ts` | Reactive Forms, validation |
|
|
54
72
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
73
|
+
```typescript
|
|
74
|
+
// interfaces/list.component.ts — Smart component
|
|
75
|
+
@Component({
|
|
76
|
+
selector: 'app-order-list',
|
|
77
|
+
standalone: true,
|
|
78
|
+
imports: [OrderItemComponent, AsyncPipe],
|
|
79
|
+
template: `
|
|
80
|
+
@for (order of orders$ | async; track order.id) {
|
|
81
|
+
<app-order-item [order]="order" (select)="onSelect($event)" />
|
|
82
|
+
} @empty {
|
|
83
|
+
<empty-state />
|
|
58
84
|
}
|
|
59
|
-
|
|
85
|
+
`,
|
|
86
|
+
})
|
|
87
|
+
export class OrderListComponent {
|
|
88
|
+
private readonly service = inject(OrderService);
|
|
89
|
+
readonly orders$ = this.service.orders$;
|
|
90
|
+
|
|
91
|
+
onSelect(order: Order): void {
|
|
92
|
+
this.service.selectOrder(order.id);
|
|
60
93
|
}
|
|
61
94
|
}
|
|
95
|
+
|
|
96
|
+
// interfaces/item.component.ts — Presentational component
|
|
97
|
+
@Component({
|
|
98
|
+
selector: 'app-order-item',
|
|
99
|
+
standalone: true,
|
|
100
|
+
template: `
|
|
101
|
+
<div (click)="select.emit(order.id)">
|
|
102
|
+
<h3>{{ order.id }}</h3>
|
|
103
|
+
<span [class]="statusClass(order.status)">{{ order.status }}</span>
|
|
104
|
+
</div>
|
|
105
|
+
`,
|
|
106
|
+
})
|
|
107
|
+
export class OrderItemComponent {
|
|
108
|
+
@Input({ required: true }) order!: Order;
|
|
109
|
+
@Output() select = new EventEmitter<string>();
|
|
110
|
+
}
|
|
62
111
|
```
|
|
63
112
|
|
|
64
113
|
---
|
|
65
114
|
|
|
66
|
-
## 3.
|
|
115
|
+
## 3. State Management with Signals
|
|
67
116
|
|
|
68
117
|
```typescript
|
|
69
|
-
// application/
|
|
70
|
-
import {
|
|
71
|
-
import { Order } from '../domain/entity';
|
|
118
|
+
// application/store.ts — Signal-based state
|
|
119
|
+
import { signal, computed, Injectable } from '@angular/core';
|
|
120
|
+
import { Order, OrderStatus } from '../domain/entity';
|
|
72
121
|
|
|
73
122
|
@Injectable({ providedIn: 'root' })
|
|
74
|
-
export class
|
|
123
|
+
export class OrderStore {
|
|
75
124
|
private readonly orders = signal<Order[]>([]);
|
|
125
|
+
private readonly selectedId = signal<string | null>(null);
|
|
126
|
+
private readonly isLoading = signal(false);
|
|
127
|
+
|
|
76
128
|
readonly orders$ = this.orders.asReadonly();
|
|
129
|
+
readonly selectedOrder = computed(() => {
|
|
130
|
+
const id = this.selectedId();
|
|
131
|
+
return id ? this.orders().find(o => o.id === id) ?? null : null;
|
|
132
|
+
});
|
|
133
|
+
readonly isLoading$ = this.isLoading.asReadonly();
|
|
134
|
+
readonly pendingCount = computed(() =>
|
|
135
|
+
this.orders().filter(o => o.status === OrderStatus.PENDING).length
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
async loadOrders(): Promise<void> {
|
|
139
|
+
this.isLoading.set(true);
|
|
140
|
+
try {
|
|
141
|
+
const orders = await this.api.fetchOrders();
|
|
142
|
+
this.orders.set(orders);
|
|
143
|
+
} finally {
|
|
144
|
+
this.isLoading.set(false);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
77
147
|
|
|
78
|
-
|
|
79
|
-
this.
|
|
80
|
-
const order = items.find(o => o.id === id);
|
|
81
|
-
if (!order) throw new DomainError('NOT_FOUND', 'Order not found');
|
|
82
|
-
order.confirm();
|
|
83
|
-
return [...items];
|
|
84
|
-
});
|
|
148
|
+
selectOrder(id: string): void {
|
|
149
|
+
this.selectedId.set(id);
|
|
85
150
|
}
|
|
86
151
|
}
|
|
87
152
|
```
|
|
88
153
|
|
|
154
|
+
### Rules
|
|
155
|
+
- ✅ Use `signal()` over `BehaviorSubject` for state (Angular 17+)
|
|
156
|
+
- ✅ Use `computed()` for derived state — no manual subscriptions
|
|
157
|
+
- ✅ Use `effect()` only for side effects (logging, syncing to localStorage)
|
|
158
|
+
- ❌ Don't mix Signals and RxJS in the same store — pick one pattern
|
|
159
|
+
- ❌ Don't use `async` pipe with Signals — Signals work with `@for` directly
|
|
160
|
+
|
|
89
161
|
---
|
|
90
162
|
|
|
91
|
-
## 4.
|
|
163
|
+
## 4. Data Flow with RxJS
|
|
92
164
|
|
|
93
165
|
```typescript
|
|
94
|
-
//
|
|
95
|
-
import { HttpClient } from '@angular/common/http';
|
|
96
|
-
import { Injectable, inject } from '@angular/core';
|
|
97
|
-
|
|
166
|
+
// application/service.ts — RxJS for async operations
|
|
98
167
|
@Injectable({ providedIn: 'root' })
|
|
99
|
-
export class
|
|
168
|
+
export class OrderService {
|
|
100
169
|
private readonly http = inject(HttpClient);
|
|
170
|
+
private readonly store = inject(OrderStore);
|
|
171
|
+
|
|
172
|
+
// Stream: debounced search → API call → update store
|
|
173
|
+
readonly search = new Subject<string>();
|
|
174
|
+
|
|
175
|
+
constructor() {
|
|
176
|
+
this.search.pipe(
|
|
177
|
+
debounceTime(300),
|
|
178
|
+
distinctUntilChanged(),
|
|
179
|
+
switchMap(query => this.http.get<Order[]>(`/api/orders?q=${query}`)),
|
|
180
|
+
catchError(err => {
|
|
181
|
+
console.error('Search failed', err);
|
|
182
|
+
return of([]);
|
|
183
|
+
}),
|
|
184
|
+
).subscribe(orders => this.store.setOrders(orders));
|
|
185
|
+
}
|
|
101
186
|
|
|
102
|
-
|
|
103
|
-
|
|
187
|
+
// Server mutation with optimistic update
|
|
188
|
+
confirmOrder(id: string): void {
|
|
189
|
+
const previous = this.store.orders$();
|
|
190
|
+
this.store.updateOrder(id, OrderStatus.CONFIRMED); // Optimistic
|
|
191
|
+
|
|
192
|
+
this.http.post(`/api/orders/${id}/confirm`, {}).pipe(
|
|
193
|
+
catchError(err => {
|
|
194
|
+
this.store.setOrders(previous); // Rollback
|
|
195
|
+
throw err;
|
|
196
|
+
}),
|
|
197
|
+
).subscribe();
|
|
104
198
|
}
|
|
105
199
|
}
|
|
106
200
|
```
|
|
107
201
|
|
|
202
|
+
### Rules
|
|
203
|
+
- ✅ Services handle async (HTTP, timers), Stores hold synchronous state
|
|
204
|
+
- ✅ Use `switchMap` for search/autocomplete (cancel previous)
|
|
205
|
+
- ✅ Use `exhaustMap` for mutations (ignore while in-flight)
|
|
206
|
+
- ✅ Always `catchError` in service streams — never let errors propagate unhandled
|
|
207
|
+
- ❌ Never subscribe in components — use `async` pipe or `@for` with Signals
|
|
208
|
+
- ❌ Never put business logic in RxJS pipes — that's the domain layer's job
|
|
209
|
+
|
|
108
210
|
---
|
|
109
211
|
|
|
110
|
-
## 5.
|
|
212
|
+
## 5. Design System & CSS Architecture
|
|
111
213
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
214
|
+
### Structure
|
|
215
|
+
```
|
|
216
|
+
src/
|
|
217
|
+
shared/
|
|
218
|
+
ui/ # Design system components
|
|
219
|
+
tokens/ # Design tokens
|
|
220
|
+
_colors.scss
|
|
221
|
+
_typography.scss
|
|
222
|
+
_spacing.scss
|
|
223
|
+
button/
|
|
224
|
+
button.component.ts
|
|
225
|
+
button.component.scss
|
|
226
|
+
button.test.ts
|
|
227
|
+
card/
|
|
228
|
+
form/
|
|
229
|
+
input/
|
|
230
|
+
select/
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Styling Strategy
|
|
234
|
+
|
|
235
|
+
| Approach | When to use |
|
|
236
|
+
|----------|-------------|
|
|
237
|
+
| Component SCSS (`@Component styleUrls`) | Component-scoped styles |
|
|
238
|
+
| Global SCSS (`styles.scss`) | CSS reset, typography, CSS custom properties |
|
|
239
|
+
| CSS Custom Properties | Design tokens, theming |
|
|
240
|
+
| Tailwind (via ngClass) | Layout, one-off adjustments |
|
|
241
|
+
|
|
242
|
+
```scss
|
|
243
|
+
// shared/ui/tokens/_colors.scss
|
|
244
|
+
:root {
|
|
245
|
+
--color-primary: #0f766e;
|
|
246
|
+
--color-primary-foreground: #ffffff;
|
|
247
|
+
--color-secondary: #64748b;
|
|
248
|
+
--color-destructive: #dc2626;
|
|
249
|
+
--color-background: #ffffff;
|
|
250
|
+
--color-foreground: #0f172a;
|
|
251
|
+
}
|
|
116
252
|
|
|
253
|
+
// button.component.scss
|
|
254
|
+
@use '../../tokens/colors' as *;
|
|
255
|
+
|
|
256
|
+
.button {
|
|
257
|
+
display: inline-flex;
|
|
258
|
+
align-items: center;
|
|
259
|
+
justify-content: center;
|
|
260
|
+
border-radius: 6px;
|
|
261
|
+
font-size: 0.875rem;
|
|
262
|
+
font-weight: 500;
|
|
263
|
+
transition: all 0.2s;
|
|
264
|
+
|
|
265
|
+
&--primary {
|
|
266
|
+
background: var(--color-primary);
|
|
267
|
+
color: var(--color-primary-foreground);
|
|
268
|
+
}
|
|
269
|
+
&--secondary {
|
|
270
|
+
background: var(--color-secondary);
|
|
271
|
+
color: #fff;
|
|
272
|
+
}
|
|
273
|
+
&--sm { height: 2.25rem; padding: 0 0.75rem; }
|
|
274
|
+
&--md { height: 2.5rem; padding: 0 1rem; }
|
|
275
|
+
&--lg { height: 2.75rem; padding: 0 2rem; }
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// button.component.ts
|
|
117
281
|
@Component({
|
|
118
|
-
selector: 'app-
|
|
282
|
+
selector: 'app-button',
|
|
119
283
|
standalone: true,
|
|
120
284
|
template: `
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
</div>
|
|
285
|
+
<button [class]="'button button--' + variant() + ' button--' + size()">
|
|
286
|
+
<ng-content />
|
|
287
|
+
</button>
|
|
126
288
|
`,
|
|
127
289
|
})
|
|
128
|
-
export class
|
|
129
|
-
readonly
|
|
290
|
+
export class ButtonComponent {
|
|
291
|
+
readonly variant = input<'primary' | 'secondary'>('primary');
|
|
292
|
+
readonly size = input<'sm' | 'md' | 'lg'>('md');
|
|
130
293
|
}
|
|
131
294
|
```
|
|
132
295
|
|
|
133
296
|
---
|
|
134
297
|
|
|
135
|
-
## 6.
|
|
298
|
+
## 6. Lazy Loading & Routing
|
|
136
299
|
|
|
137
300
|
```typescript
|
|
138
|
-
|
|
139
|
-
|
|
301
|
+
// app.routes.ts — Lazy-loaded modules
|
|
302
|
+
export const routes: Routes = [
|
|
303
|
+
{
|
|
304
|
+
path: 'orders',
|
|
305
|
+
loadChildren: () => import('./orders/orders.routes'),
|
|
306
|
+
canActivate: [AuthGuard],
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
path: 'checkout',
|
|
310
|
+
loadComponent: () => import('./checkout/page.component'),
|
|
311
|
+
},
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
// orders/orders.routes.ts
|
|
315
|
+
export default [
|
|
316
|
+
{ path: '', component: OrderListComponent },
|
|
317
|
+
{ path: ':id', component: OrderDetailComponent },
|
|
318
|
+
] satisfies Routes;
|
|
319
|
+
```
|
|
140
320
|
|
|
141
|
-
|
|
142
|
-
let service: OrderService;
|
|
321
|
+
---
|
|
143
322
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
323
|
+
## 7. Error Handling
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
// domain/errors.ts
|
|
327
|
+
export class DomainError extends Error {
|
|
328
|
+
constructor(
|
|
329
|
+
public readonly code: string,
|
|
330
|
+
message: string,
|
|
331
|
+
public readonly httpStatus: number = 400,
|
|
332
|
+
) {
|
|
333
|
+
super(message);
|
|
334
|
+
this.name = 'DomainError';
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// infrastructure/api-client.ts — HTTP interceptor
|
|
339
|
+
@Injectable()
|
|
340
|
+
export class ErrorInterceptor implements HttpInterceptor {
|
|
341
|
+
intercept(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
|
|
342
|
+
return next(req).pipe(
|
|
343
|
+
catchError((error: HttpErrorResponse) => {
|
|
344
|
+
const domainError = new DomainError(
|
|
345
|
+
error.error?.code ?? 'UNKNOWN',
|
|
346
|
+
error.error?.message ?? 'An unexpected error occurred',
|
|
347
|
+
error.status,
|
|
348
|
+
);
|
|
349
|
+
console.error(`[${domainError.code}] ${domainError.message}`);
|
|
350
|
+
return throwError(() => domainError);
|
|
351
|
+
}),
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
```
|
|
148
356
|
|
|
149
|
-
|
|
150
|
-
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## 8. Testing Patterns
|
|
360
|
+
|
|
361
|
+
| Test type | Tool | What to test |
|
|
362
|
+
|-----------|------|-------------|
|
|
363
|
+
| Unit | Jest / Vitest | `domain/` entities, value objects |
|
|
364
|
+
| Component | Angular TestBed | `interfaces/` components |
|
|
365
|
+
| Service | TestBed + HttpTestingController | HTTP interactions |
|
|
366
|
+
| E2E | Playwright | Full user flows |
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
// domain/entity.test.ts
|
|
370
|
+
describe('Order', () => {
|
|
371
|
+
it('transitions from pending to confirmed', () => {
|
|
372
|
+
const order = new Order();
|
|
373
|
+
order.confirm();
|
|
374
|
+
expect(order.status).toBe(OrderStatus.CONFIRMED);
|
|
151
375
|
});
|
|
152
376
|
});
|
|
153
377
|
```
|
|
154
378
|
|
|
155
379
|
---
|
|
156
380
|
|
|
157
|
-
|
|
381
|
+
## 9. Anti-Patterns — NEVER DO
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// ❌ Business logic in components
|
|
385
|
+
export class OrderComponent {
|
|
386
|
+
calculateTotal(items: Item[]): number { ... } // BAD — belongs in domain/
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ❌ Subscribing in components without cleanup
|
|
390
|
+
ngOnInit() { this.service.orders$.subscribe(); } // BAD — use async pipe
|
|
391
|
+
|
|
392
|
+
// ❌ Mutable state without Signals
|
|
393
|
+
public orders: Order[] = []; // BAD — use signal()
|
|
394
|
+
|
|
395
|
+
// ❌ Direct HTTP calls in components
|
|
396
|
+
this.http.get('/api/orders'); // BAD — wrap in infrastructure/
|
|
397
|
+
|
|
398
|
+
// ❌ Domain entities with Angular decorators
|
|
399
|
+
export class Order {
|
|
400
|
+
@Input() id: string; // BAD — domain is Angular-free
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ❌ CSS in component TS
|
|
404
|
+
styles: [':host { display: block; }'] // BAD — use SCSS files
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
*Version: 1.1.0*
|
|
158
410
|
*Last updated: 2026-07-03*
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: design-system-enterprise-codegen
|
|
3
|
+
description: Full reference for design system architecture with DDD + component-driven development. Covers design tokens, CSS architecture, component API design, theming, accessibility, and testing. Framework-agnostic (works with React, Angular, Vue). Use alongside framework-specific codegen skills.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Design System & CSS Architecture — DDD Patterns
|
|
7
|
+
|
|
8
|
+
> Canonical skill for building enterprise design systems using DDD principles.
|
|
9
|
+
> Framework-agnostic — works with Next.js, Angular, Vue, or any component framework.
|
|
10
|
+
> Use alongside `.pi/skills/agents/nextjs-codegen.md` or `.pi/skills/agents/angular-codegen.md`.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. Design System Structure (DDD Pattern)
|
|
15
|
+
|
|
16
|
+
A design system is itself a bounded context within the DDD module structure:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
shared/
|
|
20
|
+
ui/ # Design system module
|
|
21
|
+
domain/ # Component API contracts, design tokens
|
|
22
|
+
tokens/ # Design tokens (the "why")
|
|
23
|
+
colors.ts # Color palette as domain values
|
|
24
|
+
typography.ts # Type scale as domain values
|
|
25
|
+
spacing.ts # Spacing scale as domain values
|
|
26
|
+
types.ts # Shared component prop types
|
|
27
|
+
component-api.ts # Base component interfaces
|
|
28
|
+
application/ # Component composition, state logic
|
|
29
|
+
variant-engine.ts # Variant resolution (cva pattern)
|
|
30
|
+
theme-service.ts # Theme switching logic
|
|
31
|
+
component-registry.ts # Component registration for theming
|
|
32
|
+
infrastructure/ # CSS engine adapters
|
|
33
|
+
css-adapter.ts # CSS Modules / Tailwind bridge
|
|
34
|
+
scss-adapter.ts # SCSS variable injection
|
|
35
|
+
theme-adapter.ts # CSS custom properties injection
|
|
36
|
+
interfaces/ # Actual rendered components
|
|
37
|
+
button/
|
|
38
|
+
button.tsx # Component
|
|
39
|
+
button.module.css # Styles
|
|
40
|
+
button.test.tsx # Tests
|
|
41
|
+
button.stories.tsx # Storybook stories
|
|
42
|
+
card/
|
|
43
|
+
modal/
|
|
44
|
+
form/
|
|
45
|
+
input/
|
|
46
|
+
select/
|
|
47
|
+
checkbox/
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Why DDD for a Design System?
|
|
51
|
+
|
|
52
|
+
| Concern | Without DDD | With DDD |
|
|
53
|
+
|---------|-------------|----------|
|
|
54
|
+
| Design tokens | Scattered as CSS variables | Typed domain values in `domain/tokens/` |
|
|
55
|
+
| Component API | Implicit, documented elsewhere | Explicit interfaces in `domain/types.ts` |
|
|
56
|
+
| Variant logic | Inline in component | Shared `application/variant-engine.ts` |
|
|
57
|
+
| CSS framework | Tightly coupled | Adaptable via `infrastructure/css-adapter.ts` |
|
|
58
|
+
| Theming | Hardcoded colors | Injected through `infrastructure/theme-adapter.ts` |
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 2. Design Tokens as Domain Values
|
|
63
|
+
|
|
64
|
+
Design tokens are domain value objects — they define the raw materials of your UI.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// shared/ui/domain/tokens/colors.ts
|
|
68
|
+
export class ColorToken {
|
|
69
|
+
private constructor(
|
|
70
|
+
public readonly name: string,
|
|
71
|
+
public readonly value: string,
|
|
72
|
+
public readonly description: string,
|
|
73
|
+
) {}
|
|
74
|
+
|
|
75
|
+
static readonly PRIMARY = new ColorToken('primary', '#0f766e', 'Primary brand color');
|
|
76
|
+
static readonly PRIMARY_HOVER = new ColorToken('primary-hover', '#0d5e56', 'Primary hover state');
|
|
77
|
+
static readonly DESTRUCTIVE = new ColorToken('destructive', '#dc2626', 'Destructive actions');
|
|
78
|
+
static readonly BACKGROUND = new ColorToken('background', '#ffffff', 'Page background');
|
|
79
|
+
static readonly FOREGROUND = new ColorToken('foreground', '#0f172a', 'Default text color');
|
|
80
|
+
|
|
81
|
+
static all(): ColorToken[] {
|
|
82
|
+
return [this.PRIMARY, this.PRIMARY_HOVER, this.DESTRUCTIVE, this.BACKGROUND, this.FOREGROUND];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// shared/ui/domain/tokens/spacing.ts
|
|
87
|
+
export class SpacingToken {
|
|
88
|
+
private constructor(
|
|
89
|
+
public readonly name: string,
|
|
90
|
+
public readonly value: string, // e.g. '0.25rem'
|
|
91
|
+
public readonly px: number, // e.g. 4
|
|
92
|
+
) {}
|
|
93
|
+
|
|
94
|
+
static readonly XS = new SpacingToken('xs', '0.25rem', 4);
|
|
95
|
+
static readonly SM = new SpacingToken('sm', '0.5rem', 8);
|
|
96
|
+
static readonly MD = new SpacingToken('md', '1rem', 16);
|
|
97
|
+
static readonly LG = new SpacingToken('lg', '1.5rem', 24);
|
|
98
|
+
static readonly XL = new SpacingToken('xl', '2rem', 32);
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Component API Contracts
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// shared/ui/domain/component-api.ts
|
|
106
|
+
export interface ComponentVariant {
|
|
107
|
+
name: string;
|
|
108
|
+
description: string;
|
|
109
|
+
properties: Record<string, string>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ComponentAPI {
|
|
113
|
+
name: string;
|
|
114
|
+
description: string;
|
|
115
|
+
props: ComponentProp[];
|
|
116
|
+
variants: ComponentVariant[];
|
|
117
|
+
accessibility: AccessibilityGuide;
|
|
118
|
+
examples: CodeExample[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ComponentProp {
|
|
122
|
+
name: string;
|
|
123
|
+
type: string;
|
|
124
|
+
required: boolean;
|
|
125
|
+
default?: unknown;
|
|
126
|
+
description: string;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 3. Variant Engine (Application Layer)
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// shared/ui/application/variant-engine.ts
|
|
136
|
+
export type VariantConfig = Record<string, Record<string, string>>;
|
|
137
|
+
|
|
138
|
+
export class VariantEngine {
|
|
139
|
+
constructor(private config: VariantConfig) {}
|
|
140
|
+
|
|
141
|
+
resolve(variant: string, size: string): string {
|
|
142
|
+
const variantClasses = this.config.variants?.[variant] ?? '';
|
|
143
|
+
const sizeClasses = this.config.sizes?.[size] ?? '';
|
|
144
|
+
return [variantClasses, sizeClasses].filter(Boolean).join(' ');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
static cva(base: string, config: {
|
|
148
|
+
variants?: Record<string, Record<string, string>>;
|
|
149
|
+
sizes?: Record<string, string>;
|
|
150
|
+
defaults?: { variant?: string; size?: string };
|
|
151
|
+
}): (props: Record<string, string>) => string {
|
|
152
|
+
const engine = new VariantEngine(config);
|
|
153
|
+
return (props) => {
|
|
154
|
+
const v = props.variant ?? config.defaults?.variant ?? 'primary';
|
|
155
|
+
const s = props.size ?? config.defaults?.size ?? 'md';
|
|
156
|
+
return [base, engine.resolve(v, s)].filter(Boolean).join(' ');
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 4. CSS Architecture
|
|
165
|
+
|
|
166
|
+
### Layer Stack
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
1. CSS Reset / Normalize (global)
|
|
170
|
+
2. Design Tokens (custom props) (global)
|
|
171
|
+
3. Utility classes (Tailwind) (global)
|
|
172
|
+
4. Component styles (Modules) (scoped)
|
|
173
|
+
5. Page-specific styles (scoped)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Output: CSS Custom Properties
|
|
177
|
+
|
|
178
|
+
```scss
|
|
179
|
+
// infrastructure/_tokens.scss — Generated from domain/tokens/
|
|
180
|
+
:root {
|
|
181
|
+
--color-primary: #0f766e;
|
|
182
|
+
--color-primary-hover: #0d5e56;
|
|
183
|
+
--color-destructive: #dc2626;
|
|
184
|
+
--color-background: #ffffff;
|
|
185
|
+
--color-foreground: #0f172a;
|
|
186
|
+
--spacing-xs: 0.25rem;
|
|
187
|
+
--spacing-sm: 0.5rem;
|
|
188
|
+
--spacing-md: 1rem;
|
|
189
|
+
--radius-sm: 4px;
|
|
190
|
+
--radius-md: 8px;
|
|
191
|
+
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
|
|
192
|
+
--shadow-md: 0 4px 6px rgba(0,0,0,0.1);
|
|
193
|
+
--font-sans: 'Inter', system-ui, sans-serif;
|
|
194
|
+
--font-mono: 'JetBrains Mono', monospace;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
[data-theme='dark'] {
|
|
198
|
+
--color-background: #0f172a;
|
|
199
|
+
--color-foreground: #f8fafc;
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Component Style Pattern
|
|
204
|
+
|
|
205
|
+
```scss
|
|
206
|
+
// interfaces/button/button.module.scss
|
|
207
|
+
@use '../../infrastructure/tokens' as *;
|
|
208
|
+
|
|
209
|
+
.button {
|
|
210
|
+
display: inline-flex;
|
|
211
|
+
align-items: center;
|
|
212
|
+
justify-content: center;
|
|
213
|
+
border-radius: var(--radius-md); // Reference tokens via CSS vars
|
|
214
|
+
font-family: var(--font-sans);
|
|
215
|
+
font-weight: 500;
|
|
216
|
+
transition: all 0.2s;
|
|
217
|
+
cursor: pointer;
|
|
218
|
+
|
|
219
|
+
&:focus-visible {
|
|
220
|
+
outline: 2px solid var(--color-primary);
|
|
221
|
+
outline-offset: 2px;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
&:disabled {
|
|
225
|
+
opacity: 0.5;
|
|
226
|
+
cursor: not-allowed;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.primary {
|
|
231
|
+
background: var(--color-primary);
|
|
232
|
+
color: white;
|
|
233
|
+
&:hover { background: var(--color-primary-hover); }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.secondary {
|
|
237
|
+
background: transparent;
|
|
238
|
+
border: 1px solid var(--color-primary);
|
|
239
|
+
color: var(--color-primary);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.sm { height: 2rem; padding: 0 var(--spacing-sm); font-size: 0.75rem; }
|
|
243
|
+
.md { height: 2.5rem; padding: 0 var(--spacing-md); }
|
|
244
|
+
.lg { height: 3rem; padding: 0 var(--spacing-lg); font-size: 1rem; }
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### CSS Architecture Rules
|
|
248
|
+
- ✅ Use CSS Custom Properties for ALL design tokens — enables theming without recompilation
|
|
249
|
+
- ✅ Use CSS Modules for component-scoped styles — no leakage
|
|
250
|
+
- ✅ Use `:focus-visible` for keyboard focus — never `:focus` (which shows on click)
|
|
251
|
+
- ✅ Define breakpoints as CSS custom properties, not in JS
|
|
252
|
+
- ❌ No `!important` — ever. Restructure specificity instead
|
|
253
|
+
- ❌ No nested selectors deeper than 3 levels
|
|
254
|
+
- ❌ No color literals in component styles — always reference tokens
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## 5. Accessibility (Built into Domain)
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
// shared/ui/domain/component-api.ts
|
|
262
|
+
export interface AccessibilityGuide {
|
|
263
|
+
role: string;
|
|
264
|
+
aria: Record<string, string>;
|
|
265
|
+
keyboard: KeyboardInteraction[];
|
|
266
|
+
focusOrder: number;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export interface KeyboardInteraction {
|
|
270
|
+
key: string;
|
|
271
|
+
action: string;
|
|
272
|
+
description: string;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Button accessibility contract
|
|
276
|
+
export const BUTTON_ACCESSIBILITY: AccessibilityGuide = {
|
|
277
|
+
role: 'button',
|
|
278
|
+
aria: {
|
|
279
|
+
'aria-disabled': 'When button is disabled',
|
|
280
|
+
'aria-label': 'When icon-only button',
|
|
281
|
+
},
|
|
282
|
+
keyboard: [
|
|
283
|
+
{ key: 'Enter', action: 'activate', description: 'Activates the button' },
|
|
284
|
+
{ key: 'Space', action: 'activate', description: 'Activates the button' },
|
|
285
|
+
],
|
|
286
|
+
focusOrder: 1,
|
|
287
|
+
};
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## 6. Testing the Design System
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
// Domain token tests
|
|
296
|
+
describe('ColorToken', () => {
|
|
297
|
+
it('all tokens have unique names', () => {
|
|
298
|
+
const names = ColorToken.all().map(t => t.name);
|
|
299
|
+
expect(new Set(names).size).toBe(names.length);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Component tests
|
|
304
|
+
describe('Button', () => {
|
|
305
|
+
it('renders with variant classes', () => {
|
|
306
|
+
render(<Button variant="primary">Click</Button>);
|
|
307
|
+
expect(screen.getByRole('button')).toHaveClass('primary');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('is accessible when disabled', () => {
|
|
311
|
+
render(<Button disabled>Click</Button>);
|
|
312
|
+
expect(screen.getByRole('button')).toBeDisabled();
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Visual regression tests (Storybook + Chromatic)
|
|
317
|
+
// stories/button.stories.ts
|
|
318
|
+
export const Primary: Story = {
|
|
319
|
+
args: { variant: 'primary', children: 'Click me' },
|
|
320
|
+
};
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## 7. Anti-Patterns — NEVER DO
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
// ❌ Magic values
|
|
329
|
+
<Button color="#0f766e"> // BAD — use semantic tokens
|
|
330
|
+
|
|
331
|
+
// ❌ Framework-locked design system
|
|
332
|
+
import { ThemeProvider } from 'next-themes'; // BAD — use CSS custom properties
|
|
333
|
+
|
|
334
|
+
// ❌ Inline styles for layout
|
|
335
|
+
<div style={{ marginTop: 16 }}> // BAD — use spacing tokens
|
|
336
|
+
|
|
337
|
+
// ❌ Component with implicit API
|
|
338
|
+
<Button primary large /> // BAD — use explicit variant/size props
|
|
339
|
+
|
|
340
|
+
// ❌ Global CSS for one-off components
|
|
341
|
+
.hero-section { background: blue; } // BAD — use tokens + CSS Modules
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
*Version: 1.0.0*
|
|
347
|
+
*Last updated: 2026-07-03*
|
|
@@ -1,138 +1,383 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nextjs-enterprise-codegen
|
|
3
|
-
description: Full reference for Next.js enterprise code generation with DDD + Clean Architecture. Covers module structure, server
|
|
3
|
+
description: Full reference for Next.js enterprise code generation with DDD + Clean Architecture. Covers module structure, server/client boundary, data fetching, state management, design system integration, error handling, testing, security, performance, and accessibility. Loaded on-demand via agents/nextjs-codegen.md skill.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Next.js Enterprise Code Generation — DDD + Clean Architecture
|
|
7
7
|
|
|
8
8
|
> Canonical skill for generating production-grade Next.js code with DDD patterns.
|
|
9
9
|
> All code MUST follow these patterns. Validators enforce compliance.
|
|
10
|
+
>
|
|
11
|
+
> For design system and styling patterns, see: `.pi/skills/design-system-enterprise-codegen.md`
|
|
10
12
|
|
|
11
13
|
---
|
|
12
14
|
|
|
13
|
-
## 1. Project Structure
|
|
15
|
+
## 1. Project Structure — DDD with Next.js App Router
|
|
14
16
|
|
|
15
17
|
```
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
18
|
+
src/
|
|
19
|
+
module/ # One bounded context
|
|
20
|
+
domain/ # Pure business logic, zero framework imports
|
|
21
|
+
entity.ts # Domain entities with encapsulated state
|
|
22
|
+
value.ts # Immutable value objects
|
|
23
|
+
events.ts # Domain events
|
|
24
|
+
errors.ts # Typed error classes
|
|
25
|
+
application/ # Application logic
|
|
26
|
+
store.ts # Zustand / Jotai state (client-only)
|
|
27
|
+
actions.ts # Server Actions (server-only)
|
|
28
|
+
queries.ts # React Query hooks for data fetching
|
|
29
|
+
dto.ts # Command/Query DTOs
|
|
30
|
+
infrastructure/ # External adapters
|
|
31
|
+
api-client.ts # fetch wrapper, error handling, retry
|
|
32
|
+
auth.ts # Auth provider SDK wrapper
|
|
33
|
+
analytics.ts # Analytics SDK wrapper
|
|
34
|
+
interfaces/ # UI layer
|
|
35
|
+
page.tsx # App Router page (default: Server Component)
|
|
36
|
+
component.client.tsx # Client Component with 'use client'
|
|
37
|
+
layout.tsx # Layout wrapper
|
|
38
|
+
shared/ # Cross-module shared code
|
|
39
|
+
ui/ # Design system components
|
|
40
|
+
button/
|
|
41
|
+
card/
|
|
42
|
+
modal/
|
|
43
|
+
lib/ # Shared utilities
|
|
44
|
+
hooks/ # Shared React hooks
|
|
32
45
|
```
|
|
33
46
|
|
|
34
47
|
### Dependency Rule
|
|
35
48
|
```
|
|
36
49
|
domain → application → infrastructure → interfaces
|
|
50
|
+
↕
|
|
51
|
+
shared/ (no framework deps)
|
|
37
52
|
```
|
|
38
53
|
|
|
54
|
+
- **domain/** — zero React/Next.js imports. Pure TypeScript.
|
|
55
|
+
- **application/** — imports from domain. May import from `shared/lib/`.
|
|
56
|
+
- **infrastructure/** — imports from domain + application. Wraps external SDKs.
|
|
57
|
+
- **interfaces/** — imports from application. Contains all React/Next.js code.
|
|
58
|
+
|
|
39
59
|
---
|
|
40
60
|
|
|
41
|
-
## 2.
|
|
61
|
+
## 2. Server/Client Boundary
|
|
62
|
+
|
|
63
|
+
This is the most critical architectural decision in Next.js. Get it wrong and you lose the benefits of both paradigms.
|
|
64
|
+
|
|
65
|
+
### What goes where
|
|
66
|
+
|
|
67
|
+
| Layer | Renders on | 'use client'? | Can use |
|
|
68
|
+
|-------|-----------|---------------|---------|
|
|
69
|
+
| `interfaces/page.tsx` | Server | No | `async`, `cookies()`, `headers()`, direct DB |
|
|
70
|
+
| `interfaces/*.client.tsx` | Client | Yes | `useState`, `useEffect`, `onClick`, browser APIs |
|
|
71
|
+
| `application/store.ts` | Client | Depends | Zustand, Jotai |
|
|
72
|
+
| `application/actions.ts` | Server | No | `'use server'`, `revalidatePath()` |
|
|
73
|
+
| `application/queries.ts` | Client | Yes | React Query, SWR |
|
|
74
|
+
| `domain/`, `infrastructure/` | Both | No | Pure TS, fetch |
|
|
75
|
+
|
|
76
|
+
### Pattern: Island Architecture
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
┌─────────────────────────────────────────┐
|
|
80
|
+
│ page.tsx (Server Component) │
|
|
81
|
+
│ <header>Static header</header> │
|
|
82
|
+
│ <ProductList> │
|
|
83
|
+
│ {products.map(p => <ProductCard/>)} │ ← Server-rendered
|
|
84
|
+
│ </ProductList> │
|
|
85
|
+
│ <CheckoutButton.client.tsx> │ ← Client island
|
|
86
|
+
│ <AddToCartButton/> │
|
|
87
|
+
│ <PriceSummary/> │
|
|
88
|
+
│ </CheckoutButton.client.tsx> │
|
|
89
|
+
└─────────────────────────────────────────┘
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Rules
|
|
93
|
+
- ✅ Default to Server Components. Add `'use client'` only when you need interactivity.
|
|
94
|
+
- ✅ Push client state DOWN to the leaf components that need it.
|
|
95
|
+
- ✅ Use Server Actions for mutations, client handlers for optimistic updates.
|
|
96
|
+
- ❌ Never put `'use client'` on a page wrapper — only on interactive islands.
|
|
97
|
+
- ❌ Never import server-only code (actions, DB queries) in client components.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 3. Data Fetching Patterns
|
|
102
|
+
|
|
103
|
+
### Server Component Fetch (Default)
|
|
42
104
|
|
|
43
105
|
```typescript
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
private readonly _id: string;
|
|
47
|
-
private _items: CartItem[] = [];
|
|
106
|
+
// interfaces/products/page.tsx — Server Component, async
|
|
107
|
+
import { ProductService } from '../../application/actions';
|
|
48
108
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
109
|
+
export default async function ProductsPage() {
|
|
110
|
+
const products = await ProductService.list(); // Direct call, no API route
|
|
52
111
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
existing.increaseQuantity(quantity);
|
|
61
|
-
} else {
|
|
62
|
-
this._items.push(new CartItem(product, quantity));
|
|
63
|
-
}
|
|
64
|
-
}
|
|
112
|
+
return (
|
|
113
|
+
<div>
|
|
114
|
+
{products.map(p => (
|
|
115
|
+
<ProductCard key={p.id} product={p} /> // Server-rendered
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
65
119
|
}
|
|
66
120
|
```
|
|
67
121
|
|
|
122
|
+
### Client Data Fetching (React Query)
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// application/queries.ts
|
|
126
|
+
import { useQuery } from '@tanstack/react-query';
|
|
127
|
+
import { apiClient } from '../infrastructure/api-client';
|
|
128
|
+
|
|
129
|
+
export function useProducts() {
|
|
130
|
+
return useQuery({
|
|
131
|
+
queryKey: ['products'],
|
|
132
|
+
queryFn: () => apiClient.get('/api/products'),
|
|
133
|
+
staleTime: 30_000, // 30s before refetch
|
|
134
|
+
gcTime: 5 * 60 * 1000, // 5min cache
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// interfaces/product-list.client.tsx
|
|
139
|
+
'use client';
|
|
140
|
+
export function ProductList() {
|
|
141
|
+
const { data, isLoading } = useProducts();
|
|
142
|
+
if (isLoading) return <Skeleton />;
|
|
143
|
+
return data.map(p => <ProductCard key={p.id} product={p} />);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Server Actions (Mutations)
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// application/actions.ts
|
|
151
|
+
'use server';
|
|
152
|
+
import { revalidatePath } from 'next/cache';
|
|
153
|
+
import { Cart } from '../domain/entity';
|
|
154
|
+
|
|
155
|
+
export async function addToCart(productId: string, quantity: number) {
|
|
156
|
+
const cart = await loadCart();
|
|
157
|
+
cart.addItem(productId, quantity);
|
|
158
|
+
await saveCart(cart);
|
|
159
|
+
revalidatePath('/cart'); // Revalidate without full page reload
|
|
160
|
+
return { success: true };
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Rules
|
|
165
|
+
- ✅ Server Components for initial data — zero client JS for the first render
|
|
166
|
+
- ✅ React Query for client-side data that needs realtime/stale-while-revalidate
|
|
167
|
+
- ✅ Server Actions for mutations — no API route boilerplate
|
|
168
|
+
- ❌ Never fetch in Server Components for data that needs realtime updates
|
|
169
|
+
- ❌ Never use `useEffect` for data fetching — use React Query or Server Actions
|
|
170
|
+
|
|
68
171
|
---
|
|
69
172
|
|
|
70
|
-
##
|
|
173
|
+
## 4. State Management
|
|
174
|
+
|
|
175
|
+
| State type | Tool | Location |
|
|
176
|
+
|-----------|------|----------|
|
|
177
|
+
| Server state (data) | React Query / SWR | `application/queries.ts` |
|
|
178
|
+
| Client state (UI) | Zustand / Jotai | `application/store.ts` |
|
|
179
|
+
| URL state (filters, page) | `useSearchParams` | `interfaces/*.client.tsx` |
|
|
180
|
+
| Form state | React Hook Form + Zod | `interfaces/*.client.tsx` |
|
|
181
|
+
| Server mutations | Server Actions | `application/actions.ts` |
|
|
71
182
|
|
|
72
183
|
```typescript
|
|
73
184
|
// application/store.ts — Zustand store
|
|
74
185
|
import { create } from 'zustand';
|
|
186
|
+
import { devtools } from 'zustand/middleware';
|
|
75
187
|
import { Cart } from '../domain/entity';
|
|
76
188
|
|
|
77
|
-
interface
|
|
189
|
+
interface CartState {
|
|
78
190
|
cart: Cart | null;
|
|
79
|
-
|
|
80
|
-
addItem: (productId: string,
|
|
191
|
+
isOpen: boolean; // UI state — cart drawer toggle
|
|
192
|
+
addItem: (productId: string, qty: number) => Promise<void>;
|
|
193
|
+
toggleCart: () => void;
|
|
81
194
|
}
|
|
82
195
|
|
|
83
|
-
export const useCartStore = create<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}))
|
|
196
|
+
export const useCartStore = create<CartState>()(
|
|
197
|
+
devtools(
|
|
198
|
+
(set) => ({
|
|
199
|
+
cart: null,
|
|
200
|
+
isOpen: false,
|
|
201
|
+
addItem: async (productId, qty) => {
|
|
202
|
+
// Call Server Action, update local state optimistically
|
|
203
|
+
},
|
|
204
|
+
toggleCart: () => set(s => ({ isOpen: !s.isOpen })),
|
|
205
|
+
}),
|
|
206
|
+
{ name: 'cart-store' },
|
|
207
|
+
),
|
|
208
|
+
);
|
|
92
209
|
```
|
|
93
210
|
|
|
211
|
+
### Rules
|
|
212
|
+
- ✅ Separate server state (React Query) from client state (Zustand)
|
|
213
|
+
- ✅ Keep UI-only state (modals, toggles) local with `useState` when possible
|
|
214
|
+
- ✅ Use Zustand slices for complex domains
|
|
215
|
+
- ❌ Don't put server data in Zustand — that's what React Query is for
|
|
216
|
+
|
|
94
217
|
---
|
|
95
218
|
|
|
96
|
-
##
|
|
219
|
+
## 5. Design System & CSS Architecture
|
|
220
|
+
|
|
221
|
+
### Structure
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
shared/
|
|
225
|
+
ui/ # Design system components
|
|
226
|
+
tokens/ # Design tokens
|
|
227
|
+
colors.ts
|
|
228
|
+
typography.ts
|
|
229
|
+
spacing.ts
|
|
230
|
+
button/
|
|
231
|
+
button.tsx # Component
|
|
232
|
+
button.module.css # Styles
|
|
233
|
+
button.test.tsx # Tests
|
|
234
|
+
card/
|
|
235
|
+
modal/
|
|
236
|
+
form/
|
|
237
|
+
input.tsx
|
|
238
|
+
select.tsx
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Styling Strategy
|
|
242
|
+
|
|
243
|
+
| Approach | When to use |
|
|
244
|
+
|----------|-------------|
|
|
245
|
+
| CSS Modules (`*.module.css`) | Component-scoped styles in design system |
|
|
246
|
+
| Tailwind utility classes | Layout, spacing, one-off adjustments |
|
|
247
|
+
| CSS Variables (`var(--color-primary)`) | Design tokens, theming |
|
|
248
|
+
| `clsx` / `tailwind-merge` | Conditional class composition |
|
|
97
249
|
|
|
98
250
|
```typescript
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
251
|
+
// shared/ui/button/button.tsx
|
|
252
|
+
import { type VariantProps, cva } from 'class-variance-authority';
|
|
253
|
+
import { cn } from '../../lib/utils';
|
|
254
|
+
|
|
255
|
+
const buttonVariants = cva(
|
|
256
|
+
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors',
|
|
257
|
+
{
|
|
258
|
+
variants: {
|
|
259
|
+
variant: {
|
|
260
|
+
primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
261
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
262
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
263
|
+
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
264
|
+
},
|
|
265
|
+
size: {
|
|
266
|
+
sm: 'h-9 px-3',
|
|
267
|
+
md: 'h-10 px-4 py-2',
|
|
268
|
+
lg: 'h-11 px-8',
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
defaultVariants: { variant: 'primary', size: 'md' },
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
interface ButtonProps
|
|
276
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
277
|
+
VariantProps<typeof buttonVariants> {}
|
|
278
|
+
|
|
279
|
+
export function Button({ className, variant, size, ...props }: ButtonProps) {
|
|
280
|
+
return <button className={cn(buttonVariants({ variant, size }), className)} {...props} />;
|
|
106
281
|
}
|
|
107
282
|
```
|
|
108
283
|
|
|
284
|
+
### Design System Rules
|
|
285
|
+
- ✅ Design tokens in `shared/ui/tokens/` — single source of truth for colors, spacing, typography
|
|
286
|
+
- ✅ Components use `cva` (class-variance-authority) for variant props
|
|
287
|
+
- ✅ CSS Modules for component styles, Tailwind for layout
|
|
288
|
+
- ✅ Dark mode via `class` strategy on `<html>` element
|
|
289
|
+
- ❌ No inline styles (`style={{}}`) except for dynamic values (animations, transforms)
|
|
290
|
+
- ❌ No CSS-in-JS libraries (styled-components, emotion) — they break Server Components
|
|
291
|
+
|
|
109
292
|
---
|
|
110
293
|
|
|
111
|
-
##
|
|
294
|
+
## 6. Error Handling
|
|
112
295
|
|
|
113
296
|
```typescript
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
297
|
+
// domain/errors.ts — Typed domain errors
|
|
298
|
+
export class DomainError extends Error {
|
|
299
|
+
constructor(
|
|
300
|
+
public readonly code: string,
|
|
301
|
+
message: string,
|
|
302
|
+
public readonly status: number = 400,
|
|
303
|
+
) {
|
|
304
|
+
super(message);
|
|
305
|
+
this.name = 'DomainError';
|
|
306
|
+
}
|
|
120
307
|
|
|
121
|
-
|
|
122
|
-
|
|
308
|
+
static notFound(id: string) {
|
|
309
|
+
return new DomainError('NOT_FOUND', `Resource ${id} not found`, 404);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
123
312
|
|
|
313
|
+
// interfaces/error.tsx — Next.js error boundary
|
|
314
|
+
'use client';
|
|
315
|
+
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
|
124
316
|
return (
|
|
125
|
-
<div>
|
|
126
|
-
<
|
|
127
|
-
{
|
|
128
|
-
<CartItem key={item.id} item={item} />
|
|
129
|
-
))}
|
|
317
|
+
<div role="alert">
|
|
318
|
+
<h2>Something went wrong</h2>
|
|
319
|
+
<button onClick={() => reset()}>Try again</button>
|
|
130
320
|
</div>
|
|
131
321
|
);
|
|
132
322
|
}
|
|
323
|
+
|
|
324
|
+
// interfaces/not-found.tsx — Next.js 404 boundary
|
|
325
|
+
export default function NotFound() {
|
|
326
|
+
return <div>Page not found</div>;
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## 7. Testing Patterns
|
|
333
|
+
|
|
334
|
+
| Test type | Tool | What to test |
|
|
335
|
+
|-----------|------|-------------|
|
|
336
|
+
| Unit | Vitest | `domain/` entities, value objects |
|
|
337
|
+
| Component | Vitest + Testing Library | `interfaces/` components |
|
|
338
|
+
| Integration | Vitest + MSW | API client with mocked server |
|
|
339
|
+
| E2E | Playwright | Full user flows |
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
// domain/entity.test.ts — Pure unit test, no React
|
|
343
|
+
import { Cart } from './entity';
|
|
344
|
+
|
|
345
|
+
describe('Cart', () => {
|
|
346
|
+
it('adds item', () => {
|
|
347
|
+
const cart = new Cart();
|
|
348
|
+
cart.addItem({ id: '1', name: 'Test', price: 100 }, 2);
|
|
349
|
+
expect(cart.total).toBe(200);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## 8. Anti-Patterns — NEVER DO
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
// ❌ Business logic in components
|
|
360
|
+
export default function Page() {
|
|
361
|
+
const [cart, setCart] = useState([]); // BAD — belongs in domain/
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ❌ Data fetching in client components without React Query
|
|
365
|
+
useEffect(() => { fetch('/api/data').then(setData); }, []); // BAD
|
|
366
|
+
|
|
367
|
+
// ❌ 'use client' on the entire page
|
|
368
|
+
'use client'; // BAD — only add to interactive islands
|
|
369
|
+
|
|
370
|
+
// ❌ Inline styles
|
|
371
|
+
<div style={{ marginLeft: '16px' }}> // BAD — use Tailwind or CSS Modules
|
|
372
|
+
|
|
373
|
+
// ❌ Mixing server/client concerns
|
|
374
|
+
import { cookies } from 'next/headers'; // BAD in client component
|
|
375
|
+
|
|
376
|
+
// ❌ Global CSS for component styles
|
|
377
|
+
h1 { color: blue; } // BAD — use CSS Modules or Tailwind
|
|
133
378
|
```
|
|
134
379
|
|
|
135
380
|
---
|
|
136
381
|
|
|
137
|
-
*Version: 1.
|
|
382
|
+
*Version: 1.1.0*
|
|
138
383
|
*Last updated: 2026-07-03*
|