guardian-framework 0.1.24 → 0.1.26
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 +333 -29
- package/dist/exports.js +333 -29
- package/package.json +1 -1
- package/templates/pi/architecture/modules/module-template.md +90 -57
- package/templates/pi/extensions/domain-explorer.ts +402 -108
- package/templates/pi/preflight_report.json +1 -1
- package/templates/pi/skills/agents/design-system-codegen.md +41 -0
- package/templates/pi/skills/design-system-enterprise-codegen.md +241 -237
- package/templates/pi/skills/nextjs-enterprise-codegen.md +302 -292
|
@@ -1,94 +1,116 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: design-system-enterprise-codegen
|
|
3
|
-
description: Full reference for design system architecture with DDD +
|
|
3
|
+
description: Full reference for design system architecture with DDD + adapter pattern. Covers design tokens, CSS framework integration (Tailwind/MUI/Material/CSS Modules), component API contracts, theming, accessibility, and testing. Framework-agnostic — use with any component library.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Design System & CSS Architecture — DDD
|
|
6
|
+
# Design System & CSS Architecture — DDD with Adapter Pattern
|
|
7
7
|
|
|
8
8
|
> Canonical skill for building enterprise design systems using DDD principles.
|
|
9
|
-
> Framework-agnostic — works with Next.js, Angular, Vue, or any component framework.
|
|
9
|
+
> **Framework-agnostic** — works with Next.js, Angular, Vue, or any component framework.
|
|
10
|
+
> **CSS-agnostic** — works with Tailwind, MUI, Material, CSS Modules, or any styling approach.
|
|
11
|
+
>
|
|
10
12
|
> Use alongside `.pi/skills/agents/nextjs-codegen.md` or `.pi/skills/agents/angular-codegen.md`.
|
|
11
13
|
|
|
12
14
|
---
|
|
13
15
|
|
|
14
|
-
##
|
|
16
|
+
## The Core Idea: Domain → Infrastructure Adapter
|
|
17
|
+
|
|
18
|
+
A design system shouldn't be coupled to a specific CSS framework. The DDD layer structure enforces this:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
domain/tokens/ ← Pure TypeScript: color values, spacing, typography
|
|
22
|
+
│ (zero CSS imports, zero framework imports)
|
|
23
|
+
▼
|
|
24
|
+
application/ ← Logic: variant resolution, theme switching
|
|
25
|
+
│ (pure functions, no rendering)
|
|
26
|
+
▼
|
|
27
|
+
infrastructure/ ← CSS ADAPTER: translates tokens into Tailwind/MUI/CSS Modules
|
|
28
|
+
│ (THIS is the only layer that knows about your CSS framework)
|
|
29
|
+
▼
|
|
30
|
+
interfaces/button/ ← Components: use the adapter, render the UI
|
|
31
|
+
```
|
|
15
32
|
|
|
16
|
-
|
|
33
|
+
| If you use... | Infrastructure adapter does | Components stay the same? |
|
|
34
|
+
|---------------|---------------------------|---------------------------|
|
|
35
|
+
| **Tailwind** | Generates `bg-primary text-white` class strings from tokens | ✅ Yes |
|
|
36
|
+
| **CSS Modules** | Generates `.module.css` with `var(--color-primary)` | ✅ Yes |
|
|
37
|
+
| **MUI** | Wraps MUI's `<Button>` component, maps tokens to MUI theme | ✅ Yes |
|
|
38
|
+
| **shadcn/ui** | Re-exports with token-based variants | ✅ Yes |
|
|
39
|
+
| **Styled Components** | Generates template literals | ✅ (but breaks RSC) |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 1. Design System Structure (DDD Pattern)
|
|
17
44
|
|
|
18
45
|
```
|
|
19
46
|
shared/
|
|
20
|
-
ui/
|
|
21
|
-
domain/ #
|
|
22
|
-
tokens/ # Design tokens
|
|
23
|
-
colors.ts #
|
|
24
|
-
typography.ts #
|
|
25
|
-
spacing.ts #
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
47
|
+
ui/
|
|
48
|
+
domain/ # Framework-free. Zero CSS imports.
|
|
49
|
+
tokens/ # Design tokens as typed value objects
|
|
50
|
+
colors.ts # ColorToken — typed color palette
|
|
51
|
+
typography.ts # TypeScale — font families, sizes
|
|
52
|
+
spacing.ts # SpacingScale — margin, padding units
|
|
53
|
+
shadows.ts # ShadowToken — elevation levels
|
|
54
|
+
breakpoints.ts # Breakpoint — responsive breakpoints
|
|
55
|
+
types.ts # Component API contracts
|
|
56
|
+
component-api.ts # Base interfaces for all components
|
|
57
|
+
application/ # Logic layer (pure functions)
|
|
58
|
+
variant-engine.ts # Variant resolution (cva-like)
|
|
59
|
+
theme-service.ts # Dark/light theme switching
|
|
60
|
+
token-resolver.ts # Token → value lookup
|
|
61
|
+
infrastructure/ # CSS ADAPTER — swap this per framework
|
|
62
|
+
tailwind-adapter.ts # Tailwind class generation from tokens
|
|
63
|
+
css-modules-adapter.ts # CSS Modules variable injection
|
|
64
|
+
mui-adapter.ts # MUI theme provider wrapper
|
|
65
|
+
material-adapter.ts # Angular Material theme mapping
|
|
66
|
+
interfaces/ # Rendered components
|
|
37
67
|
button/
|
|
38
|
-
button.tsx # Component
|
|
39
|
-
button.module.css # Styles
|
|
68
|
+
button.tsx # Component (imports from application + infrastructure)
|
|
40
69
|
button.test.tsx # Tests
|
|
41
70
|
button.stories.tsx # Storybook stories
|
|
42
71
|
card/
|
|
43
72
|
modal/
|
|
44
73
|
form/
|
|
45
|
-
input/
|
|
46
|
-
select/
|
|
47
|
-
checkbox/
|
|
48
74
|
```
|
|
49
75
|
|
|
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
76
|
---
|
|
61
77
|
|
|
62
|
-
## 2. Design Tokens
|
|
78
|
+
## 2. Domain Layer — Design Tokens (Framework-Free)
|
|
63
79
|
|
|
64
|
-
|
|
80
|
+
This layer has zero CSS, zero framework, zero styling. It's pure TypeScript values.
|
|
65
81
|
|
|
66
82
|
```typescript
|
|
67
|
-
//
|
|
83
|
+
// domain/tokens/colors.ts
|
|
68
84
|
export class ColorToken {
|
|
69
85
|
private constructor(
|
|
70
86
|
public readonly name: string,
|
|
71
|
-
public readonly value: string,
|
|
87
|
+
public readonly value: string, // Hex: '#0f766e'
|
|
72
88
|
public readonly description: string,
|
|
73
89
|
) {}
|
|
74
90
|
|
|
75
91
|
static readonly PRIMARY = new ColorToken('primary', '#0f766e', 'Primary brand color');
|
|
76
|
-
static readonly PRIMARY_HOVER = new ColorToken('primary-hover', '#0d5e56'
|
|
77
|
-
static readonly DESTRUCTIVE = new ColorToken('destructive', '#dc2626'
|
|
78
|
-
static readonly BACKGROUND = new ColorToken('background', '#ffffff'
|
|
79
|
-
static readonly FOREGROUND = new ColorToken('foreground', '#0f172a'
|
|
92
|
+
static readonly PRIMARY_HOVER = new ColorToken('primary-hover', '#0d5e56');
|
|
93
|
+
static readonly DESTRUCTIVE = new ColorToken('destructive', '#dc2626');
|
|
94
|
+
static readonly BACKGROUND = new ColorToken('background', '#ffffff');
|
|
95
|
+
static readonly FOREGROUND = new ColorToken('foreground', '#0f172a');
|
|
96
|
+
|
|
97
|
+
// Dark mode overrides — still pure data, no CSS
|
|
98
|
+
static readonly DARK: Record<string, string> = {
|
|
99
|
+
background: '#0f172a',
|
|
100
|
+
foreground: '#f8fafc',
|
|
101
|
+
};
|
|
80
102
|
|
|
81
103
|
static all(): ColorToken[] {
|
|
82
|
-
return
|
|
104
|
+
return Object.values(this).filter(v => v instanceof ColorToken);
|
|
83
105
|
}
|
|
84
106
|
}
|
|
85
107
|
|
|
86
|
-
//
|
|
108
|
+
// domain/tokens/spacing.ts
|
|
87
109
|
export class SpacingToken {
|
|
88
|
-
|
|
110
|
+
constructor(
|
|
89
111
|
public readonly name: string,
|
|
90
|
-
public readonly value: string, //
|
|
91
|
-
public readonly px: number, //
|
|
112
|
+
public readonly value: string, // CSS value: '0.25rem'
|
|
113
|
+
public readonly px: number, // Design value: 4
|
|
92
114
|
) {}
|
|
93
115
|
|
|
94
116
|
static readonly XS = new SpacingToken('xs', '0.25rem', 4);
|
|
@@ -97,251 +119,233 @@ export class SpacingToken {
|
|
|
97
119
|
static readonly LG = new SpacingToken('lg', '1.5rem', 24);
|
|
98
120
|
static readonly XL = new SpacingToken('xl', '2rem', 32);
|
|
99
121
|
}
|
|
100
|
-
```
|
|
101
122
|
|
|
102
|
-
|
|
123
|
+
// domain/tokens/typography.ts
|
|
124
|
+
export class TypeScale {
|
|
125
|
+
constructor(
|
|
126
|
+
public readonly name: string,
|
|
127
|
+
public readonly fontSize: string,
|
|
128
|
+
public readonly lineHeight: string,
|
|
129
|
+
public readonly fontWeight: number,
|
|
130
|
+
) {}
|
|
103
131
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
description: string;
|
|
109
|
-
properties: Record<string, string>;
|
|
132
|
+
static readonly H1 = new TypeScale('h1', '2.5rem', '1.2', 700);
|
|
133
|
+
static readonly H2 = new TypeScale('h2', '2rem', '1.3', 600);
|
|
134
|
+
static readonly BODY = new TypeScale('body', '1rem', '1.5', 400);
|
|
135
|
+
static readonly SMALL = new TypeScale('small', '0.875rem', '1.5', 400);
|
|
110
136
|
}
|
|
137
|
+
```
|
|
111
138
|
|
|
112
|
-
|
|
113
|
-
name: string;
|
|
114
|
-
description: string;
|
|
115
|
-
props: ComponentProp[];
|
|
116
|
-
variants: ComponentVariant[];
|
|
117
|
-
accessibility: AccessibilityGuide;
|
|
118
|
-
examples: CodeExample[];
|
|
119
|
-
}
|
|
139
|
+
### Why tokens as domain objects?
|
|
120
140
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
default?: unknown;
|
|
126
|
-
description: string;
|
|
127
|
-
}
|
|
128
|
-
```
|
|
141
|
+
- **Type-safe** — `ColorToken.PRIMARY.value` is a `string`, can't pass an invalid color
|
|
142
|
+
- **Documented** — each token has a `description` field, self-documenting
|
|
143
|
+
- **Enumerable** — `ColorToken.all()` can generate CSS variables, Tailwind config, or MUI theme
|
|
144
|
+
- **Framework-independent** — the exact same tokens feed Tailwind, CSS Modules, or MUI
|
|
129
145
|
|
|
130
146
|
---
|
|
131
147
|
|
|
132
|
-
## 3. Variant Engine
|
|
148
|
+
## 3. Application Layer — Variant Engine
|
|
133
149
|
|
|
134
150
|
```typescript
|
|
135
|
-
//
|
|
136
|
-
export type
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
}
|
|
151
|
+
// application/variant-engine.ts
|
|
152
|
+
export type VariantDefinition = {
|
|
153
|
+
base: string;
|
|
154
|
+
variants: Record<string, Record<string, string>>;
|
|
155
|
+
defaults: Record<string, string>;
|
|
156
|
+
};
|
|
146
157
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return [base, engine.resolve(v, s)].filter(Boolean).join(' ');
|
|
157
|
-
};
|
|
158
|
-
}
|
|
158
|
+
export function defineVariants(config: VariantDefinition) {
|
|
159
|
+
return (props: Record<string, string>): string => {
|
|
160
|
+
const classes = [config.base];
|
|
161
|
+
for (const [key, values] of Object.entries(config.variants)) {
|
|
162
|
+
const value = props[key] ?? config.defaults[key];
|
|
163
|
+
if (value && values[value]) classes.push(values[value]);
|
|
164
|
+
}
|
|
165
|
+
return classes.filter(Boolean).join(' ');
|
|
166
|
+
};
|
|
159
167
|
}
|
|
160
168
|
```
|
|
161
169
|
|
|
162
170
|
---
|
|
163
171
|
|
|
164
|
-
## 4. CSS
|
|
172
|
+
## 4. Infrastructure — CSS Framework Adapters
|
|
165
173
|
|
|
166
|
-
###
|
|
174
|
+
### Tailwind Adapter
|
|
167
175
|
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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;
|
|
176
|
+
```typescript
|
|
177
|
+
// infrastructure/tailwind-adapter.ts
|
|
178
|
+
// Translates domain tokens → Tailwind utility classes
|
|
179
|
+
import { ColorToken } from '../domain/tokens/colors';
|
|
180
|
+
import { SpacingToken } from '../domain/tokens/spacing';
|
|
181
|
+
|
|
182
|
+
export function tailwindColor(token: ColorToken): string {
|
|
183
|
+
const map: Record<string, string> = {
|
|
184
|
+
'#0f766e': 'bg-teal-700 text-white',
|
|
185
|
+
'#dc2626': 'bg-red-600 text-white',
|
|
186
|
+
'#ffffff': 'bg-white text-gray-900',
|
|
187
|
+
'#0f172a': 'bg-slate-900 text-white',
|
|
188
|
+
};
|
|
189
|
+
return map[token.value] ?? '';
|
|
195
190
|
}
|
|
196
191
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
192
|
+
export function tailwindSpacing(token: SpacingToken): string {
|
|
193
|
+
const map: Record<number, string> = {
|
|
194
|
+
4: 'p-1', 8: 'p-2', 16: 'p-4', 24: 'p-6', 32: 'p-8',
|
|
195
|
+
};
|
|
196
|
+
return map[token.px] ?? '';
|
|
200
197
|
}
|
|
201
198
|
```
|
|
202
199
|
|
|
203
|
-
###
|
|
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
|
-
}
|
|
200
|
+
### CSS Modules Adapter
|
|
223
201
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
202
|
+
```typescript
|
|
203
|
+
// infrastructure/css-modules-adapter.ts
|
|
204
|
+
// Generates CSS custom properties from domain tokens
|
|
205
|
+
import { ColorToken } from '../domain/tokens/colors';
|
|
206
|
+
import { SpacingToken } from '../domain/tokens/spacing';
|
|
229
207
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
color:
|
|
233
|
-
|
|
234
|
-
}
|
|
208
|
+
export function generateCSSVariables(): string {
|
|
209
|
+
const colors = ColorToken.all().map(t =>
|
|
210
|
+
` --color-${t.name}: ${t.value};`
|
|
211
|
+
).join('\n');
|
|
235
212
|
|
|
236
|
-
|
|
237
|
-
background: transparent;
|
|
238
|
-
border: 1px solid var(--color-primary);
|
|
239
|
-
color: var(--color-primary);
|
|
213
|
+
return `:root {\n${colors}\n}`;
|
|
240
214
|
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### MUI Adapter
|
|
241
218
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
219
|
+
```typescript
|
|
220
|
+
// infrastructure/mui-adapter.ts
|
|
221
|
+
// Maps domain tokens to MUI theme structure
|
|
222
|
+
import { createTheme } from '@mui/material';
|
|
223
|
+
import { ColorToken } from '../domain/tokens/colors';
|
|
224
|
+
|
|
225
|
+
export function createMuiTheme() {
|
|
226
|
+
return createTheme({
|
|
227
|
+
palette: {
|
|
228
|
+
primary: { main: ColorToken.PRIMARY.value },
|
|
229
|
+
background: { default: ColorToken.BACKGROUND.value },
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
}
|
|
245
233
|
```
|
|
246
234
|
|
|
247
|
-
###
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
235
|
+
### Choosing an Adapter
|
|
236
|
+
|
|
237
|
+
| Adapter | When to use | Tradeoffs |
|
|
238
|
+
|---------|-------------|-----------|
|
|
239
|
+
| **Tailwind** | Startups, rapid prototyping, utility-first | Class strings get long; needs `clsx`/`tailwind-merge` |
|
|
240
|
+
| **CSS Modules** | Enterprise, strict design systems, component libraries | More boilerplate; full control over CSS |
|
|
241
|
+
| **MUI** | Need a complete component library out of the box | Bundle size; customization can be complex |
|
|
242
|
+
| **Material (Angular)** | Angular projects needing Material Design | Framework-locked; heavy |
|
|
243
|
+
| **shadcn/ui** | Next.js projects wanting Radix primitives with Tailwind | Requires Tailwind; new ecosystem |
|
|
244
|
+
|
|
245
|
+
**Rule:** Pick one adapter per project. Never mix. The adapter is the only file that changes when you switch CSS frameworks.
|
|
255
246
|
|
|
256
247
|
---
|
|
257
248
|
|
|
258
|
-
## 5.
|
|
249
|
+
## 5. Component Implementation Pattern
|
|
259
250
|
|
|
260
251
|
```typescript
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
252
|
+
// interfaces/button/button.tsx
|
|
253
|
+
import { defineVariants } from '../../application/variant-engine';
|
|
254
|
+
// ↑ import from application, not infrastructure
|
|
255
|
+
// Components DON'T import the adapter directly
|
|
256
|
+
|
|
257
|
+
const buttonVariants = defineVariants({
|
|
258
|
+
base: 'inline-flex items-center justify-center rounded-md transition-colors',
|
|
259
|
+
variants: {
|
|
260
|
+
variant: {
|
|
261
|
+
primary: 'bg-primary text-white hover:bg-primary-hover',
|
|
262
|
+
secondary: 'border border-primary text-primary hover:bg-primary/10',
|
|
263
|
+
ghost: 'hover:bg-accent',
|
|
264
|
+
},
|
|
265
|
+
size: { sm: 'h-9 px-3', md: 'h-10 px-4', lg: 'h-11 px-8' },
|
|
266
|
+
},
|
|
267
|
+
defaults: { variant: 'primary', size: 'md' },
|
|
268
|
+
});
|
|
268
269
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
description: string;
|
|
270
|
+
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
271
|
+
variant?: 'primary' | 'secondary' | 'ghost';
|
|
272
|
+
size?: 'sm' | 'md' | 'lg';
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
{ key: 'Enter', action: 'activate', description: 'Activates the button' },
|
|
284
|
-
{ key: 'Space', action: 'activate', description: 'Activates the button' },
|
|
285
|
-
],
|
|
286
|
-
focusOrder: 1,
|
|
287
|
-
};
|
|
275
|
+
export function Button({ variant, size, className, ...props }: ButtonProps) {
|
|
276
|
+
return (
|
|
277
|
+
<button
|
|
278
|
+
className={[buttonVariants({ variant, size }), className].filter(Boolean).join(' ')}
|
|
279
|
+
{...props}
|
|
280
|
+
/>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
288
283
|
```
|
|
289
284
|
|
|
285
|
+
**Key rule:** Components import from `application/variant-engine.ts`, not from `infrastructure/`. The infrastructure adapter is only used at the app root (theme provider, CSS variable injection).
|
|
286
|
+
|
|
290
287
|
---
|
|
291
288
|
|
|
292
|
-
## 6.
|
|
289
|
+
## 6. Theming (Dark Mode)
|
|
293
290
|
|
|
294
291
|
```typescript
|
|
295
|
-
//
|
|
296
|
-
|
|
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
|
-
});
|
|
292
|
+
// application/theme-service.ts
|
|
293
|
+
import { ColorToken } from '../domain/tokens/colors';
|
|
302
294
|
|
|
303
|
-
|
|
304
|
-
describe('Button', () => {
|
|
305
|
-
it('renders with variant classes', () => {
|
|
306
|
-
render(<Button variant="primary">Click</Button>);
|
|
307
|
-
expect(screen.getByRole('button')).toHaveClass('primary');
|
|
308
|
-
});
|
|
295
|
+
export type Theme = 'light' | 'dark';
|
|
309
296
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
expect(screen.getByRole('button')).toBeDisabled();
|
|
313
|
-
});
|
|
314
|
-
});
|
|
297
|
+
export class ThemeService {
|
|
298
|
+
private current: Theme = 'light';
|
|
315
299
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
300
|
+
get theme(): Theme { return this.current; }
|
|
301
|
+
|
|
302
|
+
setTheme(theme: Theme): void {
|
|
303
|
+
this.current = theme;
|
|
304
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
toggle(): void {
|
|
308
|
+
this.setTheme(this.current === 'light' ? 'dark' : 'light');
|
|
309
|
+
}
|
|
310
|
+
}
|
|
321
311
|
```
|
|
322
312
|
|
|
323
313
|
---
|
|
324
314
|
|
|
325
|
-
## 7.
|
|
315
|
+
## 7. Testing — Framework-Independent
|
|
326
316
|
|
|
327
317
|
```typescript
|
|
328
|
-
//
|
|
329
|
-
|
|
318
|
+
// domain/tokens/colors.test.ts
|
|
319
|
+
describe('ColorToken', () => {
|
|
320
|
+
it('all tokens have valid hex values', () => {
|
|
321
|
+
for (const t of ColorToken.all()) {
|
|
322
|
+
expect(t.value).toMatch(/^#[0-9a-fA-F]{6}$/);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
});
|
|
330
326
|
|
|
331
|
-
//
|
|
332
|
-
|
|
327
|
+
// application/variant-engine.test.ts
|
|
328
|
+
describe('VariantEngine', () => {
|
|
329
|
+
it('resolves variant classes', () => {
|
|
330
|
+
const button = defineVariants({ base: 'btn', variants: { color: { red: 'bg-red' } }, defaults: { color: 'red' } });
|
|
331
|
+
expect(button({})).toBe('btn bg-red');
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
```
|
|
333
335
|
|
|
334
|
-
|
|
335
|
-
<div style={{ marginTop: 16 }}> // BAD — use spacing tokens
|
|
336
|
+
---
|
|
336
337
|
|
|
337
|
-
|
|
338
|
-
<Button primary large /> // BAD — use explicit variant/size props
|
|
338
|
+
## 8. Choosing Your Stack — Quick Reference
|
|
339
339
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
340
|
+
| Project type | Recommended adapter | Why |
|
|
341
|
+
|-------------|-------------------|-----|
|
|
342
|
+
| Next.js app, small team | Tailwind + shadcn/ui | Fastest path, good defaults, RSC-compatible |
|
|
343
|
+
| Next.js app, large team | CSS Modules + custom tokens | Full control, strict design system |
|
|
344
|
+
| Angular app, Material look | Angular Material adapter | Native integration, theming built-in |
|
|
345
|
+
| Angular app, custom design | CSS Modules + SCSS tokens | Full control, SCSS features |
|
|
346
|
+
| Multi-framework design system | CSS Modules + CSS Custom Properties | Works everywhere, zero framework lock-in |
|
|
343
347
|
|
|
344
348
|
---
|
|
345
349
|
|
|
346
|
-
*Version: 1.
|
|
350
|
+
*Version: 1.1.0*
|
|
347
351
|
*Last updated: 2026-07-03*
|