@polymorphism-tech/morph-spec 4.11.1 → 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.
Files changed (28) hide show
  1. package/README.md +2 -2
  2. package/claude-plugin.json +1 -1
  3. package/docs/CHEATSHEET.md +1 -1
  4. package/docs/QUICKSTART.md +1 -1
  5. package/framework/agents.json +16 -6
  6. package/framework/hooks/claude-code/post-tool-use/agent-complete.js +122 -0
  7. package/framework/hooks/shared/dispatch-helpers.js +17 -0
  8. package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +1 -1
  9. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +1 -1
  10. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +1 -1
  11. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +1 -1
  12. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +1 -1
  13. package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +1 -1
  14. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +1 -1
  15. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +248 -14
  16. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
  17. package/framework/standards/STANDARDS.json +31 -0
  18. package/framework/standards/frontend/design-system/aesthetic-direction.md +60 -7
  19. package/framework/standards/frontend/design-system/design-trends.md +171 -0
  20. package/framework/templates/context/CONTEXT.md +2 -2
  21. package/framework/templates/context/design-system.md +346 -211
  22. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  23. package/package.json +1 -1
  24. package/src/commands/mcp/mcp-setup.js +28 -6
  25. package/src/commands/project/init.js +20 -1
  26. package/src/lib/generators/design-system-generator.js +340 -51
  27. package/src/lib/validators/design-system/design-system-validator.js +11 -5
  28. package/src/utils/hooks-installer.js +11 -1
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'url';
3
3
  import { homedir } from 'os';
4
4
  import fs from 'fs-extra';
5
5
  import ora from 'ora';
6
+ import { execSync } from 'child_process';
6
7
  import { logger } from '../../utils/logger.js';
7
8
  import {
8
9
  pathExists,
@@ -21,7 +22,8 @@ import {
21
22
  installAutoMcps,
22
23
  installMcpWithCredentials,
23
24
  generateSetupInstructions,
24
- formatMcpStatusTable
25
+ formatMcpStatusTable,
26
+ isRemoteMcp
25
27
  } from '../../lib/installers/mcp-installer.js';
26
28
  import { setupInfra, installRulesForStack } from '../../scripts/setup-infra.js';
27
29
  import { installSkills } from '../../utils/skills-installer.js';
@@ -277,6 +279,23 @@ export async function initCommand(options) {
277
279
  }]);
278
280
 
279
281
  if (setupNow) {
282
+ // Remote MCPs — launch OAuth via claude mcp add
283
+ if (isRemoteMcp(entry.install.config)) {
284
+ const url = entry.install.config.url;
285
+ logger.info(` Launching ${capitalizedName} OAuth authentication...`);
286
+ try {
287
+ execSync(`claude mcp add --transport http ${name} ${url}`, {
288
+ stdio: 'inherit',
289
+ timeout: 120000
290
+ });
291
+ logger.success(` ${capitalizedName} MCP configured`);
292
+ manualInstalled[name] = entry;
293
+ } catch {
294
+ logger.warn(` ${capitalizedName} setup cancelled. Run later: claude mcp add --transport http ${name} ${url}`);
295
+ }
296
+ continue;
297
+ }
298
+
280
299
  // Collect credentials
281
300
  const credentialValues = {};
282
301
  for (const cred of entry.install.credentials) {
@@ -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 = { primary: {}, secondary: {}, neutral: {}, semantic: {} };
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
- // Match color definitions like: - **Primary 500** (`#...`) - ...
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 object with fontFamily, fontSize, fontWeight, lineHeight
153
+ * @returns {Object} Typography with fontFamily, fontSize, fontWeight, lineHeight
56
154
  */
57
155
  export function parseTypography(markdown) {
58
- const typography = { fontFamily: {}, fontSize: {}, fontWeight: {}, lineHeight: {} };
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 object with spacing values
228
+ * @returns {Object} Spacing values
88
229
  */
89
230
  export function parseSpacing(markdown) {
90
231
  const spacing = {};
91
232
 
92
- // Match spacing definitions
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 ui-design-system.md file
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
- if (Object.keys(colors.primary).length > 0) {
143
- css += ` /* Primary Colors */\n`;
144
- Object.entries(colors.primary).forEach(([variant, hex]) => {
145
- css += ` --color-primary-${variant}: ${hex};\n`;
146
- });
147
- css += `\n`;
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
- if (Object.keys(colors.secondary).length > 0) {
151
- css += ` /* Secondary Colors */\n`;
152
- Object.entries(colors.secondary).forEach(([variant, hex]) => {
153
- css += ` --color-secondary-${variant}: ${hex};\n`;
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.neutral).length > 0) {
159
- css += ` /* Neutral Colors */\n`;
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
- if (typography.fontFamily.primary) {
434
+ const fontFamilies = typography.fontFamily || {};
435
+ if (fontFamilies.display || fontFamilies.body || fontFamilies.mono || fontFamilies.primary) {
176
436
  css += ` /* Typography */\n`;
177
- css += ` --font-family-primary: ${typography.fontFamily.primary};\n`;
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
- css += ` --${name}: ${value};\n`;
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
- const primary500 = colors.primary['500'] || '#0078d4';
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 = "${primary500}",\n`;
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
- const primary500 = colors.primary['500'] || '#594ae2';
239
- const secondary500 = colors.secondary['500'] || '#ff4081';
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 = "${primary500}",\n`;
253
- csharp += ` Secondary = "${secondary500}",\n`;
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 ui-design-system.md
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: Object.keys(colors.primary).length,
281
- neutralColors: Object.keys(colors.neutral).length,
282
- semanticColors: Object.keys(colors.semantic).length,
283
- fontSizes: Object.keys(typography.fontSize).length,
284
- spacingValues: Object.keys(spacing).length
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
 
@@ -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
- Object.values(designSystem.colors.primary).forEach(hex => allowedColors.add(hex.toLowerCase()));
64
- Object.values(designSystem.colors.secondary).forEach(hex => allowedColors.add(hex.toLowerCase()));
65
- Object.values(designSystem.colors.neutral).forEach(hex => allowedColors.add(hex.toLowerCase()));
66
- Object.values(designSystem.colors.semantic).forEach(hex => allowedColors.add(hex.toLowerCase()));
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([
@@ -15,7 +15,7 @@ import { homedir } from 'os';
15
15
  import { execSync } from 'child_process';
16
16
 
17
17
  /** Current hooks schema version — bump when hook definitions change */
18
- const HOOKS_VERSION = '2.12.0';
18
+ const HOOKS_VERSION = '2.13.0';
19
19
 
20
20
  /** Marker for old dispatch.js (v1) */
21
21
  const OLD_DISPATCH_COMMAND = 'node framework/hooks/agent-teams/dispatch.js';
@@ -119,6 +119,16 @@ Otherwise respond: {"ok": true}`
119
119
  }]
120
120
  },
121
121
 
122
+ // === PostToolUse: Agent ===
123
+ {
124
+ event: 'PostToolUse',
125
+ matcher: 'Agent',
126
+ hooks: [{
127
+ type: 'command',
128
+ command: 'node framework/hooks/claude-code/post-tool-use/agent-complete.js'
129
+ }]
130
+ },
131
+
122
132
  // === PostToolUse: Bash ===
123
133
  {
124
134
  event: 'PostToolUse',