@polymorphism-tech/morph-spec 4.11.0 → 4.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +106 -2
- package/README.md +2 -2
- package/bin/morph-spec.js +31 -3
- package/bin/task-manager.js +391 -35
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents.json +16 -6
- package/framework/commands/morph-apply.md +21 -1
- package/framework/commands/morph-proposal.md +44 -1
- package/framework/hooks/claude-code/post-tool-use/agent-complete.js +122 -0
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +47 -2
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +4 -3
- package/framework/hooks/shared/dispatch-helpers.js +31 -0
- package/framework/hooks/shared/state-reader.js +7 -3
- package/framework/rules/morph-task-tracking.md +49 -0
- package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +6 -10
- package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +10 -1
- package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +7 -13
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +7 -1
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +9 -12
- package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +7 -10
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +10 -18
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +256 -14
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +31 -0
- package/framework/standards/frontend/design-system/aesthetic-direction.md +60 -7
- package/framework/standards/frontend/design-system/design-trends.md +171 -0
- package/framework/templates/context/CONTEXT.md +2 -2
- package/framework/templates/context/design-system.md +346 -211
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/package.json +1 -1
- package/src/commands/agents/dispatch-agents.js +25 -1
- package/src/commands/mcp/mcp-setup.js +28 -6
- package/src/commands/project/init.js +20 -1
- package/src/commands/tasks/task.js +54 -0
- package/src/lib/build/build-runner.js +99 -0
- package/src/lib/checkpoints/checkpoint-hooks.js +110 -0
- package/src/lib/generators/design-system-generator.js +340 -51
- package/src/lib/generators/recap-generator.js +27 -4
- package/src/lib/stack/stack-profile.js +3 -0
- package/src/lib/tasks/task-parser.js +23 -9
- package/src/lib/validators/design-system/design-system-validator.js +11 -5
- package/src/utils/file-copier.js +1 -0
- package/src/utils/hooks-installer.js +11 -1
- package/framework/hooks/claude-code/pre-tool-use/protect-readonly-files.js +0 -7
|
@@ -1,35 +1,133 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MORPH-SPEC Design System Generator Library
|
|
3
3
|
*
|
|
4
|
-
* Reads ui-design-system.md and generates:
|
|
4
|
+
* Reads design-system.md (or ui-design-system.md) and generates:
|
|
5
5
|
* - wwwroot/css/design-system.css (CSS variables)
|
|
6
6
|
* - Themes/FluentDesignTheme.cs (Fluent UI theme)
|
|
7
7
|
* - Themes/MudTheme.cs (MudBlazor theme)
|
|
8
8
|
*
|
|
9
|
+
* Supports BOTH:
|
|
10
|
+
* - New CSS :root format (design-system.md template v2)
|
|
11
|
+
* - Old markdown list format (backward compat)
|
|
12
|
+
*
|
|
9
13
|
* Used both by CLI commands and internal automation.
|
|
10
14
|
*/
|
|
11
15
|
|
|
12
16
|
import { readFileSync, existsSync } from 'fs';
|
|
13
17
|
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// CSS :root Helpers
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extract all CSS :root blocks from markdown (inside ```css fences)
|
|
24
|
+
* @param {string} markdown
|
|
25
|
+
* @returns {string} Combined CSS content from all :root blocks
|
|
26
|
+
*/
|
|
27
|
+
function extractCSSRootBlocks(markdown) {
|
|
28
|
+
const cssBlocks = [];
|
|
29
|
+
const fenceRegex = /```css\s*\n([\s\S]*?)```/g;
|
|
30
|
+
let match;
|
|
31
|
+
while ((match = fenceRegex.exec(markdown)) !== null) {
|
|
32
|
+
cssBlocks.push(match[1]);
|
|
33
|
+
}
|
|
34
|
+
return cssBlocks.join('\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract CSS custom properties from :root blocks
|
|
39
|
+
* Returns array of { name, value } for all --xxx: yyy; declarations
|
|
40
|
+
*/
|
|
41
|
+
function extractCSSProperties(cssContent) {
|
|
42
|
+
const props = [];
|
|
43
|
+
const propRegex = /(--[\w-]+)\s*:\s*([^;]+);/g;
|
|
44
|
+
let match;
|
|
45
|
+
while ((match = propRegex.exec(cssContent)) !== null) {
|
|
46
|
+
props.push({ name: match[1].trim(), value: match[2].trim() });
|
|
47
|
+
}
|
|
48
|
+
return props;
|
|
49
|
+
}
|
|
50
|
+
|
|
14
51
|
// ============================================================================
|
|
15
52
|
// Parsing Functions
|
|
16
53
|
// ============================================================================
|
|
17
54
|
|
|
18
55
|
/**
|
|
19
|
-
* Parse colors from markdown
|
|
56
|
+
* Parse colors from markdown — supports CSS :root format and old markdown format
|
|
20
57
|
* @param {string} markdown - Markdown content
|
|
21
|
-
* @returns {Object} Colors object with primary, secondary, neutral, semantic
|
|
58
|
+
* @returns {Object} Colors object with primary, accent, secondary, neutral, semantic, surface, text, other
|
|
22
59
|
*/
|
|
23
60
|
export function parseColors(markdown) {
|
|
24
|
-
const colors = {
|
|
61
|
+
const colors = {
|
|
62
|
+
primary: {},
|
|
63
|
+
accent: {},
|
|
64
|
+
secondary: {},
|
|
65
|
+
neutral: {},
|
|
66
|
+
semantic: {},
|
|
67
|
+
surface: {},
|
|
68
|
+
text: {},
|
|
69
|
+
other: {}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Try CSS :root format first
|
|
73
|
+
const cssContent = extractCSSRootBlocks(markdown);
|
|
74
|
+
const props = extractCSSProperties(cssContent);
|
|
75
|
+
|
|
76
|
+
let cssColorCount = 0;
|
|
77
|
+
|
|
78
|
+
for (const { name, value } of props) {
|
|
79
|
+
if (!name.startsWith('--color-')) continue;
|
|
80
|
+
|
|
81
|
+
const suffix = name.slice('--color-'.length); // e.g. "primary", "primary-light", "neutral-500"
|
|
82
|
+
|
|
83
|
+
if (suffix.startsWith('primary-rgb')) {
|
|
84
|
+
// Store RGB metadata as-is
|
|
85
|
+
colors.primary['rgb'] = value;
|
|
86
|
+
cssColorCount++;
|
|
87
|
+
} else if (suffix.startsWith('primary')) {
|
|
88
|
+
const variant = suffix.slice('primary'.length).replace(/^-/, '') || 'default';
|
|
89
|
+
colors.primary[variant] = value;
|
|
90
|
+
cssColorCount++;
|
|
91
|
+
} else if (suffix.startsWith('accent')) {
|
|
92
|
+
const variant = suffix.slice('accent'.length).replace(/^-/, '') || 'default';
|
|
93
|
+
colors.accent[variant] = value;
|
|
94
|
+
cssColorCount++;
|
|
95
|
+
} else if (suffix.startsWith('neutral-')) {
|
|
96
|
+
const variant = suffix.slice('neutral-'.length);
|
|
97
|
+
colors.neutral[variant] = value;
|
|
98
|
+
cssColorCount++;
|
|
99
|
+
} else if (suffix.startsWith('surface-')) {
|
|
100
|
+
const variant = suffix.slice('surface-'.length);
|
|
101
|
+
colors.surface[variant] = value;
|
|
102
|
+
cssColorCount++;
|
|
103
|
+
} else if (suffix.startsWith('text-')) {
|
|
104
|
+
const variant = suffix.slice('text-'.length);
|
|
105
|
+
colors.text[variant] = value;
|
|
106
|
+
cssColorCount++;
|
|
107
|
+
} else if (suffix === 'success' || suffix === 'warning' || suffix === 'error' || suffix === 'info') {
|
|
108
|
+
colors.semantic[suffix] = value;
|
|
109
|
+
cssColorCount++;
|
|
110
|
+
} else if (suffix === 'border') {
|
|
111
|
+
colors.other['border'] = value;
|
|
112
|
+
cssColorCount++;
|
|
113
|
+
} else {
|
|
114
|
+
// Any other --color-xxx token
|
|
115
|
+
colors.other[suffix] = value;
|
|
116
|
+
cssColorCount++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
25
119
|
|
|
26
|
-
//
|
|
120
|
+
// If CSS format found colors, return them
|
|
121
|
+
if (cssColorCount > 0) {
|
|
122
|
+
return colors;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// FALL BACK to old markdown regex format
|
|
27
126
|
const colorRegex = /(?:^|\n)[-*]\s+\*\*([^*]+)\*\*[^\(]*\(`(#[0-9A-Fa-f]{6})`\)/gm;
|
|
28
127
|
|
|
29
128
|
let match;
|
|
30
129
|
while ((match = colorRegex.exec(markdown)) !== null) {
|
|
31
130
|
const [, name, hex] = match;
|
|
32
|
-
const cleanName = name.trim().toLowerCase().replace(/\s+/g, '-');
|
|
33
131
|
|
|
34
132
|
if (name.includes('Primary')) {
|
|
35
133
|
const variant = name.match(/\d+/)?.[0] || 'default';
|
|
@@ -50,13 +148,56 @@ export function parseColors(markdown) {
|
|
|
50
148
|
}
|
|
51
149
|
|
|
52
150
|
/**
|
|
53
|
-
* Parse typography from markdown
|
|
151
|
+
* Parse typography from markdown — supports CSS :root format and old markdown format
|
|
54
152
|
* @param {string} markdown - Markdown content
|
|
55
|
-
* @returns {Object} Typography
|
|
153
|
+
* @returns {Object} Typography with fontFamily, fontSize, fontWeight, lineHeight
|
|
56
154
|
*/
|
|
57
155
|
export function parseTypography(markdown) {
|
|
58
|
-
const typography = {
|
|
156
|
+
const typography = {
|
|
157
|
+
fontFamily: {},
|
|
158
|
+
fontSize: {},
|
|
159
|
+
fontWeight: {},
|
|
160
|
+
lineHeight: {}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Try CSS :root format first
|
|
164
|
+
const cssContent = extractCSSRootBlocks(markdown);
|
|
165
|
+
const props = extractCSSProperties(cssContent);
|
|
166
|
+
|
|
167
|
+
let cssTypoCount = 0;
|
|
168
|
+
|
|
169
|
+
for (const { name, value } of props) {
|
|
170
|
+
if (name === '--font-display') {
|
|
171
|
+
typography.fontFamily.display = value;
|
|
172
|
+
cssTypoCount++;
|
|
173
|
+
} else if (name === '--font-body') {
|
|
174
|
+
typography.fontFamily.body = value;
|
|
175
|
+
cssTypoCount++;
|
|
176
|
+
} else if (name === '--font-mono') {
|
|
177
|
+
typography.fontFamily.mono = value;
|
|
178
|
+
cssTypoCount++;
|
|
179
|
+
} else if (name.startsWith('--text-')) {
|
|
180
|
+
// Font sizes: --text-xs, --text-sm, --text-base, etc.
|
|
181
|
+
const sizeName = name.slice('--text-'.length);
|
|
182
|
+
typography.fontSize[sizeName] = value;
|
|
183
|
+
cssTypoCount++;
|
|
184
|
+
} else if (name.startsWith('--font-weight-')) {
|
|
185
|
+
const weightName = name.slice('--font-weight-'.length);
|
|
186
|
+
typography.fontWeight[weightName] = value;
|
|
187
|
+
cssTypoCount++;
|
|
188
|
+
} else if (name.startsWith('--leading-')) {
|
|
189
|
+
const leadingName = name.slice('--leading-'.length);
|
|
190
|
+
typography.lineHeight[leadingName] = value;
|
|
191
|
+
cssTypoCount++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
59
194
|
|
|
195
|
+
// If CSS format found typography, return it
|
|
196
|
+
if (cssTypoCount > 0) {
|
|
197
|
+
return typography;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// FALL BACK to old markdown format
|
|
60
201
|
// Match font family
|
|
61
202
|
const fontFamilyRegex = /(?:font[- ]?family|typeface)[:\s]+([^\n]+)/gi;
|
|
62
203
|
const fontMatch = fontFamilyRegex.exec(markdown);
|
|
@@ -82,14 +223,33 @@ export function parseTypography(markdown) {
|
|
|
82
223
|
}
|
|
83
224
|
|
|
84
225
|
/**
|
|
85
|
-
* Parse spacing from markdown
|
|
226
|
+
* Parse spacing from markdown — supports CSS :root format and old markdown format
|
|
86
227
|
* @param {string} markdown - Markdown content
|
|
87
|
-
* @returns {Object} Spacing
|
|
228
|
+
* @returns {Object} Spacing values
|
|
88
229
|
*/
|
|
89
230
|
export function parseSpacing(markdown) {
|
|
90
231
|
const spacing = {};
|
|
91
232
|
|
|
92
|
-
//
|
|
233
|
+
// Try CSS :root format first
|
|
234
|
+
const cssContent = extractCSSRootBlocks(markdown);
|
|
235
|
+
const props = extractCSSProperties(cssContent);
|
|
236
|
+
|
|
237
|
+
let cssSpaceCount = 0;
|
|
238
|
+
|
|
239
|
+
for (const { name, value } of props) {
|
|
240
|
+
if (name.startsWith('--space-')) {
|
|
241
|
+
const spaceName = name.slice('--space-'.length);
|
|
242
|
+
spacing[spaceName] = value;
|
|
243
|
+
cssSpaceCount++;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// If CSS format found spacing, return it
|
|
248
|
+
if (cssSpaceCount > 0) {
|
|
249
|
+
return spacing;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// FALL BACK to old markdown format
|
|
93
253
|
const spacingRegex = /(?:^|\n)[-*]\s+\*\*([^*]+)\*\*[^:]*:\s*(\d+(?:\.\d+)?(?:rem|px|em))/gm;
|
|
94
254
|
let match;
|
|
95
255
|
while ((match = spacingRegex.exec(markdown)) !== null) {
|
|
@@ -103,10 +263,84 @@ export function parseSpacing(markdown) {
|
|
|
103
263
|
return spacing;
|
|
104
264
|
}
|
|
105
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Parse motion tokens from markdown (CSS :root format only)
|
|
268
|
+
* @param {string} markdown - Markdown content
|
|
269
|
+
* @returns {Object} { durations, easings, scroll }
|
|
270
|
+
*/
|
|
271
|
+
export function parseMotion(markdown) {
|
|
272
|
+
const motion = {
|
|
273
|
+
durations: {},
|
|
274
|
+
easings: {},
|
|
275
|
+
scroll: {}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const cssContent = extractCSSRootBlocks(markdown);
|
|
279
|
+
const props = extractCSSProperties(cssContent);
|
|
280
|
+
|
|
281
|
+
for (const { name, value } of props) {
|
|
282
|
+
if (name.startsWith('--duration-')) {
|
|
283
|
+
const key = name.slice('--duration-'.length);
|
|
284
|
+
motion.durations[key] = value;
|
|
285
|
+
} else if (name.startsWith('--easing-')) {
|
|
286
|
+
const key = name.slice('--easing-'.length);
|
|
287
|
+
motion.easings[key] = value;
|
|
288
|
+
} else if (name.startsWith('--scroll-')) {
|
|
289
|
+
const key = name.slice('--scroll-'.length);
|
|
290
|
+
motion.scroll[key] = value;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return motion;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Parse advanced effects tokens from markdown (CSS :root format only)
|
|
299
|
+
* @param {string} markdown - Markdown content
|
|
300
|
+
* @returns {Object} { glass, aurora, perspective, container, viewTransition, noise }
|
|
301
|
+
*/
|
|
302
|
+
export function parseAdvancedEffects(markdown) {
|
|
303
|
+
const effects = {
|
|
304
|
+
glass: {},
|
|
305
|
+
aurora: {},
|
|
306
|
+
perspective: {},
|
|
307
|
+
container: {},
|
|
308
|
+
viewTransition: {},
|
|
309
|
+
noise: {}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const cssContent = extractCSSRootBlocks(markdown);
|
|
313
|
+
const props = extractCSSProperties(cssContent);
|
|
314
|
+
|
|
315
|
+
for (const { name, value } of props) {
|
|
316
|
+
if (name.startsWith('--glass-')) {
|
|
317
|
+
const key = name.slice('--glass-'.length);
|
|
318
|
+
effects.glass[key] = value;
|
|
319
|
+
} else if (name.startsWith('--aurora-')) {
|
|
320
|
+
const key = name.slice('--aurora-'.length);
|
|
321
|
+
effects.aurora[key] = value;
|
|
322
|
+
} else if (name.startsWith('--noise-')) {
|
|
323
|
+
const key = name.slice('--noise-'.length);
|
|
324
|
+
effects.noise[key] = value;
|
|
325
|
+
} else if (name.startsWith('--perspective-')) {
|
|
326
|
+
const key = name.slice('--perspective-'.length);
|
|
327
|
+
effects.perspective[key] = value;
|
|
328
|
+
} else if (name.startsWith('--container-')) {
|
|
329
|
+
const key = name.slice('--container-'.length);
|
|
330
|
+
effects.container[key] = value;
|
|
331
|
+
} else if (name.startsWith('--view-transition-')) {
|
|
332
|
+
const key = name.slice('--view-transition-'.length);
|
|
333
|
+
effects.viewTransition[key] = value;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return effects;
|
|
338
|
+
}
|
|
339
|
+
|
|
106
340
|
/**
|
|
107
341
|
* Parse all design system properties from markdown
|
|
108
|
-
* @param {string} markdownPath - Path to
|
|
109
|
-
* @returns {Object} Parsed design system with colors, typography, spacing
|
|
342
|
+
* @param {string} markdownPath - Path to design-system.md file
|
|
343
|
+
* @returns {Object} Parsed design system with colors, typography, spacing, motion, effects
|
|
110
344
|
*/
|
|
111
345
|
export function parseDesignSystem(markdownPath) {
|
|
112
346
|
if (!existsSync(markdownPath)) {
|
|
@@ -118,7 +352,9 @@ export function parseDesignSystem(markdownPath) {
|
|
|
118
352
|
return {
|
|
119
353
|
colors: parseColors(markdown),
|
|
120
354
|
typography: parseTypography(markdown),
|
|
121
|
-
spacing: parseSpacing(markdown)
|
|
355
|
+
spacing: parseSpacing(markdown),
|
|
356
|
+
motion: parseMotion(markdown),
|
|
357
|
+
effects: parseAdvancedEffects(markdown)
|
|
122
358
|
};
|
|
123
359
|
}
|
|
124
360
|
|
|
@@ -126,6 +362,25 @@ export function parseDesignSystem(markdownPath) {
|
|
|
126
362
|
// Generation Functions
|
|
127
363
|
// ============================================================================
|
|
128
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Helper to emit CSS variable entries for a color category
|
|
367
|
+
*/
|
|
368
|
+
function emitColorCategory(colors, prefix, label) {
|
|
369
|
+
if (Object.keys(colors).length === 0) return '';
|
|
370
|
+
let css = ` /* ${label} */\n`;
|
|
371
|
+
Object.entries(colors).forEach(([variant, value]) => {
|
|
372
|
+
if (variant === 'rgb') {
|
|
373
|
+
css += ` --color-${prefix}-rgb: ${value};\n`;
|
|
374
|
+
} else if (variant === 'default') {
|
|
375
|
+
css += ` --color-${prefix}: ${value};\n`;
|
|
376
|
+
} else {
|
|
377
|
+
css += ` --color-${prefix}-${variant}: ${value};\n`;
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
css += `\n`;
|
|
381
|
+
return css;
|
|
382
|
+
}
|
|
383
|
+
|
|
129
384
|
/**
|
|
130
385
|
* Generate CSS variables file
|
|
131
386
|
* @param {Object} colors - Colors object
|
|
@@ -138,32 +393,28 @@ export function generateCSS(colors, typography, spacing) {
|
|
|
138
393
|
css += `/* Do not edit manually - regenerate from ui-design-system.md */\n\n`;
|
|
139
394
|
css += `:root {\n`;
|
|
140
395
|
|
|
141
|
-
// Colors
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
396
|
+
// Colors — handle expanded structure
|
|
397
|
+
css += emitColorCategory(colors.primary, 'primary', 'Primary Colors');
|
|
398
|
+
|
|
399
|
+
// Support both accent (new) and secondary (old)
|
|
400
|
+
if (Object.keys(colors.accent || {}).length > 0) {
|
|
401
|
+
css += emitColorCategory(colors.accent, 'accent', 'Accent Colors');
|
|
402
|
+
}
|
|
403
|
+
if (Object.keys(colors.secondary || {}).length > 0) {
|
|
404
|
+
css += emitColorCategory(colors.secondary, 'secondary', 'Secondary Colors');
|
|
148
405
|
}
|
|
149
406
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
});
|
|
155
|
-
css += `\n`;
|
|
407
|
+
css += emitColorCategory(colors.neutral || {}, 'neutral', 'Neutral Colors');
|
|
408
|
+
|
|
409
|
+
if (Object.keys(colors.surface || {}).length > 0) {
|
|
410
|
+
css += emitColorCategory(colors.surface, 'surface', 'Surface Colors');
|
|
156
411
|
}
|
|
157
412
|
|
|
158
|
-
if (Object.keys(colors.
|
|
159
|
-
css +=
|
|
160
|
-
Object.entries(colors.neutral).forEach(([variant, hex]) => {
|
|
161
|
-
css += ` --color-neutral-${variant}: ${hex};\n`;
|
|
162
|
-
});
|
|
163
|
-
css += `\n`;
|
|
413
|
+
if (Object.keys(colors.text || {}).length > 0) {
|
|
414
|
+
css += emitColorCategory(colors.text, 'text', 'Text Colors');
|
|
164
415
|
}
|
|
165
416
|
|
|
166
|
-
if (Object.keys(colors.semantic).length > 0) {
|
|
417
|
+
if (Object.keys(colors.semantic || {}).length > 0) {
|
|
167
418
|
css += ` /* Semantic Colors */\n`;
|
|
168
419
|
Object.entries(colors.semantic).forEach(([type, hex]) => {
|
|
169
420
|
css += ` --color-${type}: ${hex};\n`;
|
|
@@ -171,10 +422,22 @@ export function generateCSS(colors, typography, spacing) {
|
|
|
171
422
|
css += `\n`;
|
|
172
423
|
}
|
|
173
424
|
|
|
425
|
+
if (Object.keys(colors.other || {}).length > 0) {
|
|
426
|
+
css += ` /* Other Colors */\n`;
|
|
427
|
+
Object.entries(colors.other).forEach(([key, value]) => {
|
|
428
|
+
css += ` --color-${key}: ${value};\n`;
|
|
429
|
+
});
|
|
430
|
+
css += `\n`;
|
|
431
|
+
}
|
|
432
|
+
|
|
174
433
|
// Typography
|
|
175
|
-
|
|
434
|
+
const fontFamilies = typography.fontFamily || {};
|
|
435
|
+
if (fontFamilies.display || fontFamilies.body || fontFamilies.mono || fontFamilies.primary) {
|
|
176
436
|
css += ` /* Typography */\n`;
|
|
177
|
-
css += ` --font-
|
|
437
|
+
if (fontFamilies.display) css += ` --font-display: ${fontFamilies.display};\n`;
|
|
438
|
+
if (fontFamilies.body) css += ` --font-body: ${fontFamilies.body};\n`;
|
|
439
|
+
if (fontFamilies.mono) css += ` --font-mono: ${fontFamilies.mono};\n`;
|
|
440
|
+
if (fontFamilies.primary) css += ` --font-family-primary: ${fontFamilies.primary};\n`;
|
|
178
441
|
css += `\n`;
|
|
179
442
|
}
|
|
180
443
|
|
|
@@ -190,7 +453,12 @@ export function generateCSS(colors, typography, spacing) {
|
|
|
190
453
|
if (Object.keys(spacing).length > 0) {
|
|
191
454
|
css += ` /* Spacing */\n`;
|
|
192
455
|
Object.entries(spacing).forEach(([name, value]) => {
|
|
193
|
-
|
|
456
|
+
// If key is numeric (from --space-N), use --space-N format
|
|
457
|
+
if (/^\d+$/.test(name)) {
|
|
458
|
+
css += ` --space-${name}: ${value};\n`;
|
|
459
|
+
} else {
|
|
460
|
+
css += ` --${name}: ${value};\n`;
|
|
461
|
+
}
|
|
194
462
|
});
|
|
195
463
|
}
|
|
196
464
|
|
|
@@ -207,7 +475,8 @@ export function generateCSS(colors, typography, spacing) {
|
|
|
207
475
|
* @returns {string} C# code
|
|
208
476
|
*/
|
|
209
477
|
export function generateFluentTheme(colors, typography, namespace = 'YourProject.Themes') {
|
|
210
|
-
|
|
478
|
+
// Support both new format (primary.default) and old format (primary['500'])
|
|
479
|
+
const primary = colors.primary.default || colors.primary['500'] || '#0078d4';
|
|
211
480
|
|
|
212
481
|
let csharp = `// Auto-generated by MORPH-SPEC Design System Generator\n`;
|
|
213
482
|
csharp += `// Do not edit manually - regenerate from ui-design-system.md\n\n`;
|
|
@@ -218,7 +487,7 @@ export function generateFluentTheme(colors, typography, namespace = 'YourProject
|
|
|
218
487
|
csharp += ` {\n`;
|
|
219
488
|
csharp += ` return new DesignThemePalette\n`;
|
|
220
489
|
csharp += ` {\n`;
|
|
221
|
-
csharp += ` Accent = "${
|
|
490
|
+
csharp += ` Accent = "${primary}",\n`;
|
|
222
491
|
csharp += ` // Add more theme properties as needed\n`;
|
|
223
492
|
csharp += ` };\n`;
|
|
224
493
|
csharp += ` }\n`;
|
|
@@ -235,8 +504,10 @@ export function generateFluentTheme(colors, typography, namespace = 'YourProject
|
|
|
235
504
|
* @returns {string} C# code
|
|
236
505
|
*/
|
|
237
506
|
export function generateMudTheme(colors, typography, namespace = 'YourProject.Themes') {
|
|
238
|
-
|
|
239
|
-
const
|
|
507
|
+
// Support both new format (primary.default) and old format (primary['500'])
|
|
508
|
+
const primary = colors.primary.default || colors.primary['500'] || '#594ae2';
|
|
509
|
+
// Support accent (new) or secondary (old)
|
|
510
|
+
const secondary = (colors.accent || {}).default || (colors.secondary || {})['500'] || '#ff4081';
|
|
240
511
|
|
|
241
512
|
let csharp = `// Auto-generated by MORPH-SPEC Design System Generator\n`;
|
|
242
513
|
csharp += `// Do not edit manually - regenerate from ui-design-system.md\n\n`;
|
|
@@ -249,8 +520,8 @@ export function generateMudTheme(colors, typography, namespace = 'YourProject.Th
|
|
|
249
520
|
csharp += ` {\n`;
|
|
250
521
|
csharp += ` Palette = new PaletteLight\n`;
|
|
251
522
|
csharp += ` {\n`;
|
|
252
|
-
csharp += ` Primary = "${
|
|
253
|
-
csharp += ` Secondary = "${
|
|
523
|
+
csharp += ` Primary = "${primary}",\n`;
|
|
524
|
+
csharp += ` Secondary = "${secondary}",\n`;
|
|
254
525
|
csharp += ` // Add more palette colors as needed\n`;
|
|
255
526
|
csharp += ` }\n`;
|
|
256
527
|
csharp += ` };\n`;
|
|
@@ -260,9 +531,16 @@ export function generateMudTheme(colors, typography, namespace = 'YourProject.Th
|
|
|
260
531
|
return csharp;
|
|
261
532
|
}
|
|
262
533
|
|
|
534
|
+
/**
|
|
535
|
+
* Count non-empty keys in an object
|
|
536
|
+
*/
|
|
537
|
+
function countTokens(obj) {
|
|
538
|
+
return Object.keys(obj || {}).length;
|
|
539
|
+
}
|
|
540
|
+
|
|
263
541
|
/**
|
|
264
542
|
* Generate all design system files
|
|
265
|
-
* @param {string} designSystemPath - Path to
|
|
543
|
+
* @param {string} designSystemPath - Path to design-system.md
|
|
266
544
|
* @param {Object} options - Generation options
|
|
267
545
|
* @param {string} options.mode - Generation mode: 'fluent', 'mud', or 'both'
|
|
268
546
|
* @param {string} options.namespace - C# namespace
|
|
@@ -272,16 +550,27 @@ export function generateDesignSystem(designSystemPath, options = {}) {
|
|
|
272
550
|
const { mode = 'both', namespace = 'YourProject.Themes' } = options;
|
|
273
551
|
|
|
274
552
|
// Parse design system
|
|
275
|
-
const { colors, typography, spacing } = parseDesignSystem(designSystemPath);
|
|
553
|
+
const { colors, typography, spacing, motion, effects } = parseDesignSystem(designSystemPath);
|
|
276
554
|
|
|
277
555
|
const generated = {
|
|
278
556
|
css: generateCSS(colors, typography, spacing),
|
|
279
557
|
stats: {
|
|
280
|
-
primaryColors:
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
558
|
+
primaryColors: countTokens(colors.primary),
|
|
559
|
+
accentColors: countTokens(colors.accent),
|
|
560
|
+
secondaryColors: countTokens(colors.secondary),
|
|
561
|
+
neutralColors: countTokens(colors.neutral),
|
|
562
|
+
semanticColors: countTokens(colors.semantic),
|
|
563
|
+
surfaceColors: countTokens(colors.surface),
|
|
564
|
+
textColors: countTokens(colors.text),
|
|
565
|
+
fontSizes: countTokens(typography.fontSize),
|
|
566
|
+
fontWeights: countTokens(typography.fontWeight),
|
|
567
|
+
lineHeights: countTokens(typography.lineHeight),
|
|
568
|
+
spacingValues: countTokens(spacing),
|
|
569
|
+
motionDurations: countTokens(motion.durations),
|
|
570
|
+
motionEasings: countTokens(motion.easings),
|
|
571
|
+
effectsGlass: countTokens(effects.glass),
|
|
572
|
+
effectsAurora: countTokens(effects.aurora),
|
|
573
|
+
effectsPerspective: countTokens(effects.perspective)
|
|
285
574
|
}
|
|
286
575
|
};
|
|
287
576
|
|
|
@@ -233,11 +233,34 @@ function buildRecapMarkdown(data) {
|
|
|
233
233
|
|
|
234
234
|
// Tasks Detail
|
|
235
235
|
if (tasksSummary.items.length > 0) {
|
|
236
|
+
const anyNotes = tasksSummary.items.some(t => t.notes?.length > 0);
|
|
236
237
|
md += `## Tasks\n\n`;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
238
|
+
if (anyNotes) {
|
|
239
|
+
md += `| ID | Title | Status | Notes |\n|----|-------|--------|-------|\n`;
|
|
240
|
+
for (const task of tasksSummary.items) {
|
|
241
|
+
const statusIcon = task.status === 'completed' ? '✅' : task.status === 'in_progress' ? '🔄' : '⏳';
|
|
242
|
+
const noteCount = task.notes?.length || 0;
|
|
243
|
+
const notesCell = noteCount > 0 ? `${noteCount} note${noteCount > 1 ? 's' : ''}` : '-';
|
|
244
|
+
md += `| ${task.id} | ${task.title || '-'} | ${statusIcon} ${task.status} | ${notesCell} |\n`;
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
md += `| ID | Title | Status |\n|----|-------|--------|\n`;
|
|
248
|
+
for (const task of tasksSummary.items) {
|
|
249
|
+
const statusIcon = task.status === 'completed' ? '✅' : task.status === 'in_progress' ? '🔄' : '⏳';
|
|
250
|
+
md += `| ${task.id} | ${task.title || '-'} | ${statusIcon} ${task.status} |\n`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
md += `\n`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Deviations from Plan
|
|
257
|
+
const deviations = tasksSummary.items
|
|
258
|
+
.filter(t => t.notes?.some(n => n.type === 'deviation'))
|
|
259
|
+
.flatMap(t => t.notes.filter(n => n.type === 'deviation').map(n => ({ taskId: t.id, text: n.text })));
|
|
260
|
+
if (deviations.length > 0) {
|
|
261
|
+
md += `## Deviations from Plan\n\n`;
|
|
262
|
+
for (const d of deviations) {
|
|
263
|
+
md += `- **${d.taskId}**: ${d.text}\n`;
|
|
241
264
|
}
|
|
242
265
|
md += `\n`;
|
|
243
266
|
}
|
|
@@ -24,6 +24,7 @@ const PROFILES = {
|
|
|
24
24
|
isDotnet: true, isNextjs: false,
|
|
25
25
|
defaultValidators: ['architecture', 'packages'],
|
|
26
26
|
checkpointValidators: ['architecture', 'packages', 'design-system', 'security'],
|
|
27
|
+
buildCommand: 'dotnet build --no-restore',
|
|
27
28
|
contractsOutputType: 'contracts',
|
|
28
29
|
contractsFilename: 'contracts.cs',
|
|
29
30
|
contractsLanguage: 'csharp',
|
|
@@ -32,6 +33,7 @@ const PROFILES = {
|
|
|
32
33
|
isDotnet: false, isNextjs: true,
|
|
33
34
|
defaultValidators: ['nextjs-component'],
|
|
34
35
|
checkpointValidators: ['nextjs-component', 'css'],
|
|
36
|
+
buildCommand: 'npx tsc --noEmit',
|
|
35
37
|
contractsOutputType: 'contractsTs',
|
|
36
38
|
contractsFilename: 'contracts.ts',
|
|
37
39
|
contractsLanguage: 'typescript',
|
|
@@ -40,6 +42,7 @@ const PROFILES = {
|
|
|
40
42
|
isDotnet: false, isNextjs: false,
|
|
41
43
|
defaultValidators: [],
|
|
42
44
|
checkpointValidators: [],
|
|
45
|
+
buildCommand: null,
|
|
43
46
|
contractsOutputType: 'contracts',
|
|
44
47
|
contractsFilename: 'contracts.cs',
|
|
45
48
|
contractsLanguage: 'csharp',
|
|
@@ -122,18 +122,32 @@ export async function parseTasksMd(featureName, cwd = process.cwd()) {
|
|
|
122
122
|
return [];
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
// Phase 1: collect heading positions
|
|
126
126
|
const headingRe = /^###\s+(T\d+)\s*[—–:\-]\s*(.+)$/gm;
|
|
127
|
+
const depRe = /\*\*Dependenc(?:y|ies):\*\*\s*(.+)/i;
|
|
128
|
+
const positions = [];
|
|
127
129
|
let match;
|
|
128
130
|
while ((match = headingRe.exec(content)) !== null) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
positions.push({ id: match[1], title: match[2].trim(), pos: match.index });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Phase 2: extract body and parse dependencies
|
|
135
|
+
const tasks = [];
|
|
136
|
+
for (let i = 0; i < positions.length; i++) {
|
|
137
|
+
const { id, title, pos } = positions[i];
|
|
138
|
+
const end = i + 1 < positions.length ? positions[i + 1].pos : content.length;
|
|
139
|
+
const block = content.slice(pos, end);
|
|
140
|
+
|
|
141
|
+
let dependencies = [];
|
|
142
|
+
const depMatch = depRe.exec(block);
|
|
143
|
+
if (depMatch) {
|
|
144
|
+
const val = depMatch[1].trim();
|
|
145
|
+
if (val.toLowerCase() !== 'none') {
|
|
146
|
+
dependencies = val.split(/[,\s]+/).filter(s => /^T\d+$/.test(s));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
tasks.push({ id, title, status: 'pending', dependencies, files: [], checkpoint: null });
|
|
137
151
|
}
|
|
138
152
|
return tasks;
|
|
139
153
|
}
|
|
@@ -58,12 +58,18 @@ export async function validateDesignSystem(projectPath, featureName = null, opti
|
|
|
58
58
|
return result;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
// Build allowed color set
|
|
61
|
+
// Build allowed color set — iterate all color categories
|
|
62
62
|
const allowedColors = new Set();
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
const colorCategories = ['primary', 'accent', 'secondary', 'neutral', 'semantic', 'surface', 'text', 'other'];
|
|
64
|
+
for (const category of colorCategories) {
|
|
65
|
+
const catColors = designSystem.colors[category] || {};
|
|
66
|
+
Object.values(catColors).forEach(val => {
|
|
67
|
+
// Only add hex-like values (skip rgb tuples, rgba, var() references, etc.)
|
|
68
|
+
if (typeof val === 'string' && val.startsWith('#')) {
|
|
69
|
+
allowedColors.add(val.toLowerCase());
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
67
73
|
|
|
68
74
|
// Common system colors (allowed)
|
|
69
75
|
const systemColors = new Set([
|