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 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.23", 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;
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.23", 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;
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,6 +1,6 @@
1
1
  {
2
2
  "name": "guardian-framework",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "description": "Token-optimized agentic framework scaffolder with pi-first architecture",
5
5
  "type": "module",
6
6
  "main": "dist/exports.js",
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2026-07-03T07:41:28Z",
2
+ "timestamp": "2026-07-03T07:45:52Z",
3
3
  "mode": "all",
4
4
  "stages_run": [],
5
5
  "summary": {
@@ -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, services, Signal stores, standalone components, RxJS patterns, and testing. Loaded on-demand via agents/angular-codegen.md skill.
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
- module/
17
- ├── domain/ # Pure business logic, no Angular imports
18
- │ ├── entity.ts
19
- │ ├── value.ts
20
- │ └── errors.ts
21
- ├── application/ # Injectable services, Signal stores
22
- │ ├── store.ts # Signal-based state
23
- │ ├── service.ts # @Injectable use case service
24
- │ └── dto.ts
25
- ├── infrastructure/ # HTTP clients, localStorage adapters
26
- │ ├── api-client.ts # HttpClient wrapper
27
- │ └── storage.ts
28
- └── interfaces/ # UI Components (standalone)
29
- ├── component.ts
30
- └── page.component.ts
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. Domain Layer
62
+ ## 2. Component Architecture
41
63
 
42
- ```typescript
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
- get id(): string { return this._id; }
53
- get status(): OrderStatus { return this._status; }
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
- confirm(): void {
56
- if (this._status !== OrderStatus.PENDING) {
57
- throw new DomainError('INVALID_STATE', 'Only pending orders can be confirmed');
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
- this._status = OrderStatus.CONFIRMED;
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. Application Layer
115
+ ## 3. State Management with Signals
67
116
 
68
117
  ```typescript
69
- // application/service.ts
70
- import { Injectable, signal } from '@angular/core';
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 OrderService {
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
- confirmOrder(id: string): void {
79
- this.orders.update(items => {
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. Infrastructure Layer
163
+ ## 4. Data Flow with RxJS
92
164
 
93
165
  ```typescript
94
- // infrastructure/api-client.ts
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 OrderApiClient {
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
- fetchOrders() {
103
- return this.http.get<Order[]>('/api/orders');
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. Interfaces Layer (Components)
212
+ ## 5. Design System & CSS Architecture
111
213
 
112
- ```typescript
113
- // interfaces/order-list.component.ts
114
- import { Component, inject } from '@angular/core';
115
- import { OrderService } from '../application/service';
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-order-list',
282
+ selector: 'app-button',
119
283
  standalone: true,
120
284
  template: `
121
- <div>
122
- @for (order of service.orders$(); track order.id) {
123
- <div>{{ order.id }} - {{ order.status }}</div>
124
- }
125
- </div>
285
+ <button [class]="'button button--' + variant() + ' button--' + size()">
286
+ <ng-content />
287
+ </button>
126
288
  `,
127
289
  })
128
- export class OrderListComponent {
129
- readonly service = inject(OrderService);
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. Testing
298
+ ## 6. Lazy Loading & Routing
136
299
 
137
300
  ```typescript
138
- import { TestBed } from '@angular/core/testing';
139
- import { OrderService } from './service';
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
- describe('OrderService', () => {
142
- let service: OrderService;
321
+ ---
143
322
 
144
- beforeEach(() => {
145
- TestBed.configureTestingModule({});
146
- service = TestBed.inject(OrderService);
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
- it('should confirm order', () => {
150
- // ... test using service.orders$()
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
- *Version: 1.0.0*
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 actions, Zustand stores, React components, and testing patterns. Loaded on-demand via agents/nextjs-codegen.md skill.
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
- module/
17
- ├── domain/ # Pure business logic, no framework dependencies
18
- │ ├── entity.ts
19
- │ ├── value.ts
20
- │ └── errors.ts
21
- ├── application/ # Server actions, state stores, use cases
22
- │ ├── store.ts # Zustand store (client state)
23
- │ ├── actions.ts # Server Actions (Next.js 14+)
24
- │ └── dto.ts
25
- ├── infrastructure/ # API clients, localStorage adapters
26
- │ ├── api-client.ts
27
- │ └── storage.ts
28
- └── interfaces/ # UI Components (Next.js App Router)
29
- ├── page.tsx
30
- ├── component.tsx
31
- └── layout.tsx
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. Domain Layer
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
- // domain/entity.tsPure business logic, no React/Next.js imports
45
- export class Cart {
46
- private readonly _id: string;
47
- private _items: CartItem[] = [];
106
+ // interfaces/products/page.tsxServer Component, async
107
+ import { ProductService } from '../../application/actions';
48
108
 
49
- constructor(id?: string) {
50
- this._id = id ?? crypto.randomUUID();
51
- }
109
+ export default async function ProductsPage() {
110
+ const products = await ProductService.list(); // Direct call, no API route
52
111
 
53
- get id(): string { return this._id; }
54
- get items(): readonly CartItem[] { return this._items; }
55
- get total(): Money { return this._items.reduce((sum, i) => sum.add(i.subtotal), Money.ZERO); }
56
-
57
- addItem(product: Product, quantity: number): void {
58
- const existing = this._items.find(i => i.productId === product.id);
59
- if (existing) {
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
- ## 3. Application Layer
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 CartStore {
189
+ interface CartState {
78
190
  cart: Cart | null;
79
- isLoading: boolean;
80
- addItem: (productId: string, quantity: number) => Promise<void>;
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<CartStore>((set) => ({
84
- cart: null,
85
- isLoading: false,
86
- addItem: async (productId, quantity) => {
87
- set({ isLoading: true });
88
- // ... domain logic
89
- set({ isLoading: false });
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
- ## 4. Infrastructure Layer
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
- // infrastructure/api-client.ts
100
- export class CartApiClient {
101
- async fetchCart(cartId: string): Promise<Cart> {
102
- const res = await fetch(`/api/cart/${cartId}`);
103
- if (!res.ok) throw new ApiError('Failed to fetch cart', res.status);
104
- return res.json();
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
- ## 5. Interfaces Layer (Components)
294
+ ## 6. Error Handling
112
295
 
113
296
  ```typescript
114
- // interfaces/cart-page.tsx
115
- 'use client';
116
- import { useCartStore } from '../application/store';
117
-
118
- export default function CartPage() {
119
- const { cart, isLoading } = useCartStore();
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
- if (isLoading) return <div>Loading...</div>;
122
- if (!cart) return <div>Cart is empty</div>;
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
- <h1>Shopping Cart</h1>
127
- {cart.items.map(item => (
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.0.0*
382
+ *Version: 1.1.0*
138
383
  *Last updated: 2026-07-03*