@polymorphism-tech/morph-spec 7.9.0 → 7.9.1
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/README.md +2 -2
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +5 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/commands/commit.md +166 -166
- package/framework/commands/morph-infra.md +215 -215
- package/framework/commands/morph-preflight.md +205 -205
- package/framework/commands/morph-troubleshoot.md +124 -124
- package/framework/hooks/claude-code/statusline.py +679 -679
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/activity-logger.js +160 -160
- package/framework/hooks/shared/hook-response.js +45 -45
- package/framework/hooks/shared/payload-utils.js +78 -78
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/rubrics/backend.json +41 -41
- package/framework/rules/ai-agents-standards.md +45 -45
- package/framework/rules/dotnet-templates.md +86 -86
- package/framework/rules/frontend-standards.md +13 -13
- package/framework/rules/infrastructure-standards.md +27 -27
- package/framework/rules/neon-standards.md +25 -25
- package/framework/rules/nextjs-standards.md +20 -20
- package/framework/rules/nextjs-templates.md +40 -40
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +1 -1
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/ai-agents/agent-session.md +160 -160
- package/framework/standards/ai-agents/batching.md +193 -193
- package/framework/standards/ai-agents/context-providers.md +188 -188
- package/framework/standards/ai-agents/mcp-server.md +250 -250
- package/framework/standards/ai-agents/mcp-tools.md +197 -197
- package/framework/standards/ai-agents/middleware-patterns.md +207 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
- package/framework/standards/ai-agents/modalities-vision.md +201 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
- package/framework/standards/ai-agents/production.md +275 -275
- package/framework/standards/ai-agents/prompts-in-db.md +214 -214
- package/framework/standards/ai-agents/providers/model-registry.md +158 -158
- package/framework/standards/ai-agents/service-tier-flex.md +135 -135
- package/framework/standards/ai-agents/structured-output.md +249 -249
- package/framework/standards/ai-agents/sweet-spot.md +109 -109
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
- package/framework/templates/code/dotnet/database/migration.cs +83 -83
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/code/typescript/contracts.ts +167 -167
- package/framework/templates/docs/clarifications.md +253 -253
- package/framework/templates/docs/schema-analysis.md +119 -119
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/docs/ui-mockups.md +98 -98
- package/framework/templates/feature/decisions.md +122 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +50 -50
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/integrations/asaas-client.cs +387 -387
- package/framework/templates/integrations/asaas-webhook.cs +351 -351
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +113 -113
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/project/validate.js +120 -120
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +2 -1
- package/src/commands/dashboard/dashboard.js +53 -53
- package/src/commands/index.js +13 -13
- package/src/commands/mcp/mcp-setup.js +519 -519
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/update.js +4 -1
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/components/DetailPanel.jsx +11 -11
- package/src/dashboard/app/components/FlowGraph.jsx +17 -17
- package/src/dashboard/app/components/SearchFilter.jsx +22 -22
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +370 -370
- package/src/dashboard/app/pages/HookInspector.jsx +418 -418
- package/src/dashboard/app/pages/LiveState.jsx +239 -239
- package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
- package/src/dashboard/app/pages/RulesMap.jsx +273 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
- package/src/dashboard/app/pages/Standards.jsx +332 -332
- package/src/dashboard/app/styles/index.css +47 -47
- package/src/dashboard/index.html +12 -12
- package/src/dashboard/package.json +24 -24
- package/src/dashboard/scanner/hooks-scanner.js +41 -41
- package/src/dashboard/scanner/phases-scanner.js +15 -15
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/scanner/rules-scanner.js +37 -37
- package/src/dashboard/scanner/skills-scanner.js +43 -43
- package/src/dashboard/scanner/squads-scanner.js +13 -13
- package/src/dashboard/scanner/standards-scanner.js +26 -26
- package/src/dashboard/vite-plugin-morph-data.js +17 -17
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/agents/metrics-aggregator.js +56 -56
- package/src/lib/agents/metrics-calculator.js +125 -125
- package/src/lib/agents/output-collector.js +73 -73
- package/src/lib/detectors/claude-config-detector.js +152 -152
- package/src/lib/detectors/design-system-detector.js +231 -231
- package/src/lib/detectors/index.js +217 -217
- package/src/lib/generators/architecture-scanner.js +639 -639
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/index.js +13 -13
- package/src/lib/generators/settings-generator.js +209 -209
- package/src/lib/hooks/hook-executor.js +167 -167
- package/src/lib/installers/mcp-installer.js +719 -719
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/test-runner.js +81 -81
- package/src/lib/validators/architecture/architecture-validator.js +259 -259
- package/src/lib/validators/checkpoint-persistence.js +39 -39
- package/src/lib/validators/contracts/contract-compliance-validator.js +274 -274
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/packages/package-validator.js +347 -347
- package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
- package/src/lib/validators/shared/issue-counter.js +14 -14
- package/src/lib/validators/validation-runner.js +349 -349
- package/src/scripts/setup-infra.js +385 -385
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/skills-installer.js +174 -174
- package/framework/templates/.idea/morph-templates.xml +0 -92
- package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
|
@@ -1,508 +1,508 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MORPH-SPEC Design System Generator Library
|
|
3
|
-
*
|
|
4
|
-
* Reads design-system.md and generates:
|
|
5
|
-
* - CSS variables (design-system.css)
|
|
6
|
-
*
|
|
7
|
-
* Supports BOTH:
|
|
8
|
-
* - New CSS :root format (design-system.md template v2)
|
|
9
|
-
* - Old markdown list format (backward compat)
|
|
10
|
-
*
|
|
11
|
-
* Used both by CLI commands and internal automation.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { readFileSync, existsSync } from 'fs';
|
|
15
|
-
|
|
16
|
-
// ============================================================================
|
|
17
|
-
// CSS :root Helpers
|
|
18
|
-
// ============================================================================
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Extract all CSS :root blocks from markdown (inside ```css fences)
|
|
22
|
-
* @param {string} markdown
|
|
23
|
-
* @returns {string} Combined CSS content from all :root blocks
|
|
24
|
-
*/
|
|
25
|
-
function extractCSSRootBlocks(markdown) {
|
|
26
|
-
const cssBlocks = [];
|
|
27
|
-
const fenceRegex = /```css\s*\n([\s\S]*?)```/g;
|
|
28
|
-
let match;
|
|
29
|
-
while ((match = fenceRegex.exec(markdown)) !== null) {
|
|
30
|
-
cssBlocks.push(match[1]);
|
|
31
|
-
}
|
|
32
|
-
return cssBlocks.join('\n');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Extract CSS custom properties from :root blocks
|
|
37
|
-
* Returns array of { name, value } for all --xxx: yyy; declarations
|
|
38
|
-
*/
|
|
39
|
-
function extractCSSProperties(cssContent) {
|
|
40
|
-
const props = [];
|
|
41
|
-
const propRegex = /(--[\w-]+)\s*:\s*([^;]+);/g;
|
|
42
|
-
let match;
|
|
43
|
-
while ((match = propRegex.exec(cssContent)) !== null) {
|
|
44
|
-
props.push({ name: match[1].trim(), value: match[2].trim() });
|
|
45
|
-
}
|
|
46
|
-
return props;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ============================================================================
|
|
50
|
-
// Parsing Functions
|
|
51
|
-
// ============================================================================
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Parse colors from markdown — supports CSS :root format and old markdown format
|
|
55
|
-
* @param {string} markdown - Markdown content
|
|
56
|
-
* @returns {Object} Colors object with primary, accent, secondary, neutral, semantic, surface, text, other
|
|
57
|
-
*/
|
|
58
|
-
export function parseColors(markdown) {
|
|
59
|
-
const colors = {
|
|
60
|
-
primary: {},
|
|
61
|
-
accent: {},
|
|
62
|
-
secondary: {},
|
|
63
|
-
neutral: {},
|
|
64
|
-
semantic: {},
|
|
65
|
-
surface: {},
|
|
66
|
-
text: {},
|
|
67
|
-
other: {}
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// Try CSS :root format first
|
|
71
|
-
const cssContent = extractCSSRootBlocks(markdown);
|
|
72
|
-
const props = extractCSSProperties(cssContent);
|
|
73
|
-
|
|
74
|
-
let cssColorCount = 0;
|
|
75
|
-
|
|
76
|
-
for (const { name, value } of props) {
|
|
77
|
-
if (!name.startsWith('--color-')) continue;
|
|
78
|
-
|
|
79
|
-
const suffix = name.slice('--color-'.length); // e.g. "primary", "primary-light", "neutral-500"
|
|
80
|
-
|
|
81
|
-
if (suffix.startsWith('primary-rgb')) {
|
|
82
|
-
// Store RGB metadata as-is
|
|
83
|
-
colors.primary['rgb'] = value;
|
|
84
|
-
cssColorCount++;
|
|
85
|
-
} else if (suffix.startsWith('primary')) {
|
|
86
|
-
const variant = suffix.slice('primary'.length).replace(/^-/, '') || 'default';
|
|
87
|
-
colors.primary[variant] = value;
|
|
88
|
-
cssColorCount++;
|
|
89
|
-
} else if (suffix.startsWith('accent')) {
|
|
90
|
-
const variant = suffix.slice('accent'.length).replace(/^-/, '') || 'default';
|
|
91
|
-
colors.accent[variant] = value;
|
|
92
|
-
cssColorCount++;
|
|
93
|
-
} else if (suffix.startsWith('neutral-')) {
|
|
94
|
-
const variant = suffix.slice('neutral-'.length);
|
|
95
|
-
colors.neutral[variant] = value;
|
|
96
|
-
cssColorCount++;
|
|
97
|
-
} else if (suffix.startsWith('surface-')) {
|
|
98
|
-
const variant = suffix.slice('surface-'.length);
|
|
99
|
-
colors.surface[variant] = value;
|
|
100
|
-
cssColorCount++;
|
|
101
|
-
} else if (suffix.startsWith('text-')) {
|
|
102
|
-
const variant = suffix.slice('text-'.length);
|
|
103
|
-
colors.text[variant] = value;
|
|
104
|
-
cssColorCount++;
|
|
105
|
-
} else if (suffix === 'success' || suffix === 'warning' || suffix === 'error' || suffix === 'info') {
|
|
106
|
-
colors.semantic[suffix] = value;
|
|
107
|
-
cssColorCount++;
|
|
108
|
-
} else if (suffix === 'border') {
|
|
109
|
-
colors.other['border'] = value;
|
|
110
|
-
cssColorCount++;
|
|
111
|
-
} else {
|
|
112
|
-
// Any other --color-xxx token
|
|
113
|
-
colors.other[suffix] = value;
|
|
114
|
-
cssColorCount++;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// If CSS format found colors, return them
|
|
119
|
-
if (cssColorCount > 0) {
|
|
120
|
-
return colors;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// FALL BACK to old markdown regex format
|
|
124
|
-
const colorRegex = /(?:^|\n)[-*]\s+\*\*([^*]+)\*\*[^\(]*\(`(#[0-9A-Fa-f]{6})`\)/gm;
|
|
125
|
-
|
|
126
|
-
let match;
|
|
127
|
-
while ((match = colorRegex.exec(markdown)) !== null) {
|
|
128
|
-
const [, name, hex] = match;
|
|
129
|
-
|
|
130
|
-
if (name.includes('Primary')) {
|
|
131
|
-
const variant = name.match(/\d+/)?.[0] || 'default';
|
|
132
|
-
colors.primary[variant] = hex;
|
|
133
|
-
} else if (name.includes('Secondary')) {
|
|
134
|
-
const variant = name.match(/\d+/)?.[0] || 'default';
|
|
135
|
-
colors.secondary[variant] = hex;
|
|
136
|
-
} else if (name.includes('Neutral') || name.includes('Gray')) {
|
|
137
|
-
const variant = name.match(/\d+/)?.[0] || 'default';
|
|
138
|
-
colors.neutral[variant] = hex;
|
|
139
|
-
} else if (name.includes('Success') || name.includes('Error') || name.includes('Warning') || name.includes('Info')) {
|
|
140
|
-
const type = name.toLowerCase().replace(/\s+/g, '-');
|
|
141
|
-
colors.semantic[type] = hex;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return colors;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Parse typography from markdown — supports CSS :root format and old markdown format
|
|
150
|
-
* @param {string} markdown - Markdown content
|
|
151
|
-
* @returns {Object} Typography with fontFamily, fontSize, fontWeight, lineHeight
|
|
152
|
-
*/
|
|
153
|
-
export function parseTypography(markdown) {
|
|
154
|
-
const typography = {
|
|
155
|
-
fontFamily: {},
|
|
156
|
-
fontSize: {},
|
|
157
|
-
fontWeight: {},
|
|
158
|
-
lineHeight: {}
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
// Try CSS :root format first
|
|
162
|
-
const cssContent = extractCSSRootBlocks(markdown);
|
|
163
|
-
const props = extractCSSProperties(cssContent);
|
|
164
|
-
|
|
165
|
-
let cssTypoCount = 0;
|
|
166
|
-
|
|
167
|
-
for (const { name, value } of props) {
|
|
168
|
-
if (name === '--font-display') {
|
|
169
|
-
typography.fontFamily.display = value;
|
|
170
|
-
cssTypoCount++;
|
|
171
|
-
} else if (name === '--font-body') {
|
|
172
|
-
typography.fontFamily.body = value;
|
|
173
|
-
cssTypoCount++;
|
|
174
|
-
} else if (name === '--font-mono') {
|
|
175
|
-
typography.fontFamily.mono = value;
|
|
176
|
-
cssTypoCount++;
|
|
177
|
-
} else if (name.startsWith('--text-')) {
|
|
178
|
-
// Font sizes: --text-xs, --text-sm, --text-base, etc.
|
|
179
|
-
const sizeName = name.slice('--text-'.length);
|
|
180
|
-
typography.fontSize[sizeName] = value;
|
|
181
|
-
cssTypoCount++;
|
|
182
|
-
} else if (name.startsWith('--font-weight-')) {
|
|
183
|
-
const weightName = name.slice('--font-weight-'.length);
|
|
184
|
-
typography.fontWeight[weightName] = value;
|
|
185
|
-
cssTypoCount++;
|
|
186
|
-
} else if (name.startsWith('--leading-')) {
|
|
187
|
-
const leadingName = name.slice('--leading-'.length);
|
|
188
|
-
typography.lineHeight[leadingName] = value;
|
|
189
|
-
cssTypoCount++;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// If CSS format found typography, return it
|
|
194
|
-
if (cssTypoCount > 0) {
|
|
195
|
-
return typography;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// FALL BACK to old markdown format
|
|
199
|
-
// Match font family
|
|
200
|
-
const fontFamilyRegex = /(?:font[- ]?family|typeface)[:\s]+([^\n]+)/gi;
|
|
201
|
-
const fontMatch = fontFamilyRegex.exec(markdown);
|
|
202
|
-
if (fontMatch) {
|
|
203
|
-
typography.fontFamily.primary = fontMatch[1].trim();
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Match font sizes (but exclude spacing-related)
|
|
207
|
-
const fontSizeRegex = /(?:^|\n)[-*]\s+\*\*([^*]+)\*\*[^:]*:\s*(\d+(?:\.\d+)?(?:rem|px|em))/gm;
|
|
208
|
-
let match;
|
|
209
|
-
while ((match = fontSizeRegex.exec(markdown)) !== null) {
|
|
210
|
-
const [, name, size] = match;
|
|
211
|
-
const lowerName = name.toLowerCase();
|
|
212
|
-
// Skip if it's spacing-related
|
|
213
|
-
if (lowerName.includes('spacing') || lowerName.includes('gap') || lowerName.includes('margin') || lowerName.includes('padding')) {
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
const cleanName = name.trim().toLowerCase().replace(/\s+/g, '-');
|
|
217
|
-
typography.fontSize[cleanName] = size;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return typography;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Parse spacing from markdown — supports CSS :root format and old markdown format
|
|
225
|
-
* @param {string} markdown - Markdown content
|
|
226
|
-
* @returns {Object} Spacing values
|
|
227
|
-
*/
|
|
228
|
-
export function parseSpacing(markdown) {
|
|
229
|
-
const spacing = {};
|
|
230
|
-
|
|
231
|
-
// Try CSS :root format first
|
|
232
|
-
const cssContent = extractCSSRootBlocks(markdown);
|
|
233
|
-
const props = extractCSSProperties(cssContent);
|
|
234
|
-
|
|
235
|
-
let cssSpaceCount = 0;
|
|
236
|
-
|
|
237
|
-
for (const { name, value } of props) {
|
|
238
|
-
if (name.startsWith('--space-')) {
|
|
239
|
-
const spaceName = name.slice('--space-'.length);
|
|
240
|
-
spacing[spaceName] = value;
|
|
241
|
-
cssSpaceCount++;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// If CSS format found spacing, return it
|
|
246
|
-
if (cssSpaceCount > 0) {
|
|
247
|
-
return spacing;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// FALL BACK to old markdown format
|
|
251
|
-
const spacingRegex = /(?:^|\n)[-*]\s+\*\*([^*]+)\*\*[^:]*:\s*(\d+(?:\.\d+)?(?:rem|px|em))/gm;
|
|
252
|
-
let match;
|
|
253
|
-
while ((match = spacingRegex.exec(markdown)) !== null) {
|
|
254
|
-
const [, name, value] = match;
|
|
255
|
-
if (name.toLowerCase().includes('spacing') || name.toLowerCase().includes('gap') || name.toLowerCase().includes('margin') || name.toLowerCase().includes('padding')) {
|
|
256
|
-
const cleanName = name.trim().toLowerCase().replace(/\s+/g, '-');
|
|
257
|
-
spacing[cleanName] = value;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return spacing;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Parse motion tokens from markdown (CSS :root format only)
|
|
266
|
-
* @param {string} markdown - Markdown content
|
|
267
|
-
* @returns {Object} { durations, easings, scroll }
|
|
268
|
-
*/
|
|
269
|
-
export function parseMotion(markdown) {
|
|
270
|
-
const motion = {
|
|
271
|
-
durations: {},
|
|
272
|
-
easings: {},
|
|
273
|
-
scroll: {}
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
const cssContent = extractCSSRootBlocks(markdown);
|
|
277
|
-
const props = extractCSSProperties(cssContent);
|
|
278
|
-
|
|
279
|
-
for (const { name, value } of props) {
|
|
280
|
-
if (name.startsWith('--duration-')) {
|
|
281
|
-
const key = name.slice('--duration-'.length);
|
|
282
|
-
motion.durations[key] = value;
|
|
283
|
-
} else if (name.startsWith('--easing-')) {
|
|
284
|
-
const key = name.slice('--easing-'.length);
|
|
285
|
-
motion.easings[key] = value;
|
|
286
|
-
} else if (name.startsWith('--scroll-')) {
|
|
287
|
-
const key = name.slice('--scroll-'.length);
|
|
288
|
-
motion.scroll[key] = value;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return motion;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Parse advanced effects tokens from markdown (CSS :root format only)
|
|
297
|
-
* @param {string} markdown - Markdown content
|
|
298
|
-
* @returns {Object} { glass, aurora, perspective, container, viewTransition, noise }
|
|
299
|
-
*/
|
|
300
|
-
export function parseAdvancedEffects(markdown) {
|
|
301
|
-
const effects = {
|
|
302
|
-
glass: {},
|
|
303
|
-
aurora: {},
|
|
304
|
-
perspective: {},
|
|
305
|
-
container: {},
|
|
306
|
-
viewTransition: {},
|
|
307
|
-
noise: {}
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
const cssContent = extractCSSRootBlocks(markdown);
|
|
311
|
-
const props = extractCSSProperties(cssContent);
|
|
312
|
-
|
|
313
|
-
for (const { name, value } of props) {
|
|
314
|
-
if (name.startsWith('--glass-')) {
|
|
315
|
-
const key = name.slice('--glass-'.length);
|
|
316
|
-
effects.glass[key] = value;
|
|
317
|
-
} else if (name.startsWith('--aurora-')) {
|
|
318
|
-
const key = name.slice('--aurora-'.length);
|
|
319
|
-
effects.aurora[key] = value;
|
|
320
|
-
} else if (name.startsWith('--noise-')) {
|
|
321
|
-
const key = name.slice('--noise-'.length);
|
|
322
|
-
effects.noise[key] = value;
|
|
323
|
-
} else if (name.startsWith('--perspective-')) {
|
|
324
|
-
const key = name.slice('--perspective-'.length);
|
|
325
|
-
effects.perspective[key] = value;
|
|
326
|
-
} else if (name.startsWith('--container-')) {
|
|
327
|
-
const key = name.slice('--container-'.length);
|
|
328
|
-
effects.container[key] = value;
|
|
329
|
-
} else if (name.startsWith('--view-transition-')) {
|
|
330
|
-
const key = name.slice('--view-transition-'.length);
|
|
331
|
-
effects.viewTransition[key] = value;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return effects;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Parse all design system properties from markdown
|
|
340
|
-
* @param {string} markdownPath - Path to design-system.md file
|
|
341
|
-
* @returns {Object} Parsed design system with colors, typography, spacing, motion, effects
|
|
342
|
-
*/
|
|
343
|
-
export function parseDesignSystem(markdownPath) {
|
|
344
|
-
if (!existsSync(markdownPath)) {
|
|
345
|
-
throw new Error(`File not found: ${markdownPath}`);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const markdown = readFileSync(markdownPath, 'utf-8');
|
|
349
|
-
|
|
350
|
-
return {
|
|
351
|
-
colors: parseColors(markdown),
|
|
352
|
-
typography: parseTypography(markdown),
|
|
353
|
-
spacing: parseSpacing(markdown),
|
|
354
|
-
motion: parseMotion(markdown),
|
|
355
|
-
effects: parseAdvancedEffects(markdown)
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// ============================================================================
|
|
360
|
-
// Generation Functions
|
|
361
|
-
// ============================================================================
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Helper to emit CSS variable entries for a color category
|
|
365
|
-
*/
|
|
366
|
-
function emitColorCategory(colors, prefix, label) {
|
|
367
|
-
if (Object.keys(colors).length === 0) return '';
|
|
368
|
-
let css = ` /* ${label} */\n`;
|
|
369
|
-
Object.entries(colors).forEach(([variant, value]) => {
|
|
370
|
-
if (variant === 'rgb') {
|
|
371
|
-
css += ` --color-${prefix}-rgb: ${value};\n`;
|
|
372
|
-
} else if (variant === 'default') {
|
|
373
|
-
css += ` --color-${prefix}: ${value};\n`;
|
|
374
|
-
} else {
|
|
375
|
-
css += ` --color-${prefix}-${variant}: ${value};\n`;
|
|
376
|
-
}
|
|
377
|
-
});
|
|
378
|
-
css += `\n`;
|
|
379
|
-
return css;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Generate CSS variables file
|
|
384
|
-
* @param {Object} colors - Colors object
|
|
385
|
-
* @param {Object} typography - Typography object
|
|
386
|
-
* @param {Object} spacing - Spacing object
|
|
387
|
-
* @returns {string} CSS content
|
|
388
|
-
*/
|
|
389
|
-
export function generateCSS(colors, typography, spacing) {
|
|
390
|
-
let css = `/* Auto-generated by MORPH-SPEC Design System Generator */\n`;
|
|
391
|
-
css += `/* Do not edit manually - regenerate from design-system.md */\n\n`;
|
|
392
|
-
css += `:root {\n`;
|
|
393
|
-
|
|
394
|
-
// Colors — handle expanded structure
|
|
395
|
-
css += emitColorCategory(colors.primary, 'primary', 'Primary Colors');
|
|
396
|
-
|
|
397
|
-
// Support both accent (new) and secondary (old)
|
|
398
|
-
if (Object.keys(colors.accent || {}).length > 0) {
|
|
399
|
-
css += emitColorCategory(colors.accent, 'accent', 'Accent Colors');
|
|
400
|
-
}
|
|
401
|
-
if (Object.keys(colors.secondary || {}).length > 0) {
|
|
402
|
-
css += emitColorCategory(colors.secondary, 'secondary', 'Secondary Colors');
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
css += emitColorCategory(colors.neutral || {}, 'neutral', 'Neutral Colors');
|
|
406
|
-
|
|
407
|
-
if (Object.keys(colors.surface || {}).length > 0) {
|
|
408
|
-
css += emitColorCategory(colors.surface, 'surface', 'Surface Colors');
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (Object.keys(colors.text || {}).length > 0) {
|
|
412
|
-
css += emitColorCategory(colors.text, 'text', 'Text Colors');
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
if (Object.keys(colors.semantic || {}).length > 0) {
|
|
416
|
-
css += ` /* Semantic Colors */\n`;
|
|
417
|
-
Object.entries(colors.semantic).forEach(([type, hex]) => {
|
|
418
|
-
css += ` --color-${type}: ${hex};\n`;
|
|
419
|
-
});
|
|
420
|
-
css += `\n`;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (Object.keys(colors.other || {}).length > 0) {
|
|
424
|
-
css += ` /* Other Colors */\n`;
|
|
425
|
-
Object.entries(colors.other).forEach(([key, value]) => {
|
|
426
|
-
css += ` --color-${key}: ${value};\n`;
|
|
427
|
-
});
|
|
428
|
-
css += `\n`;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Typography
|
|
432
|
-
const fontFamilies = typography.fontFamily || {};
|
|
433
|
-
if (fontFamilies.display || fontFamilies.body || fontFamilies.mono || fontFamilies.primary) {
|
|
434
|
-
css += ` /* Typography */\n`;
|
|
435
|
-
if (fontFamilies.display) css += ` --font-display: ${fontFamilies.display};\n`;
|
|
436
|
-
if (fontFamilies.body) css += ` --font-body: ${fontFamilies.body};\n`;
|
|
437
|
-
if (fontFamilies.mono) css += ` --font-mono: ${fontFamilies.mono};\n`;
|
|
438
|
-
if (fontFamilies.primary) css += ` --font-family-primary: ${fontFamilies.primary};\n`;
|
|
439
|
-
css += `\n`;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
if (Object.keys(typography.fontSize).length > 0) {
|
|
443
|
-
css += ` /* Font Sizes */\n`;
|
|
444
|
-
Object.entries(typography.fontSize).forEach(([name, size]) => {
|
|
445
|
-
css += ` --font-size-${name}: ${size};\n`;
|
|
446
|
-
});
|
|
447
|
-
css += `\n`;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Spacing
|
|
451
|
-
if (Object.keys(spacing).length > 0) {
|
|
452
|
-
css += ` /* Spacing */\n`;
|
|
453
|
-
Object.entries(spacing).forEach(([name, value]) => {
|
|
454
|
-
// If key is numeric (from --space-N), use --space-N format
|
|
455
|
-
if (/^\d+$/.test(name)) {
|
|
456
|
-
css += ` --space-${name}: ${value};\n`;
|
|
457
|
-
} else {
|
|
458
|
-
css += ` --${name}: ${value};\n`;
|
|
459
|
-
}
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
css += `}\n`;
|
|
464
|
-
|
|
465
|
-
return css;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* Count non-empty keys in an object
|
|
470
|
-
*/
|
|
471
|
-
function countTokens(obj) {
|
|
472
|
-
return Object.keys(obj || {}).length;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Generate all design system files
|
|
477
|
-
* @param {string} designSystemPath - Path to design-system.md
|
|
478
|
-
* @param {Object} options - Generation options
|
|
479
|
-
* @returns {Object} Generated files content
|
|
480
|
-
*/
|
|
481
|
-
export function generateDesignSystem(designSystemPath, options = {}) {
|
|
482
|
-
// Parse design system
|
|
483
|
-
const { colors, typography, spacing, motion, effects } = parseDesignSystem(designSystemPath);
|
|
484
|
-
|
|
485
|
-
const generated = {
|
|
486
|
-
css: generateCSS(colors, typography, spacing),
|
|
487
|
-
stats: {
|
|
488
|
-
primaryColors: countTokens(colors.primary),
|
|
489
|
-
accentColors: countTokens(colors.accent),
|
|
490
|
-
secondaryColors: countTokens(colors.secondary),
|
|
491
|
-
neutralColors: countTokens(colors.neutral),
|
|
492
|
-
semanticColors: countTokens(colors.semantic),
|
|
493
|
-
surfaceColors: countTokens(colors.surface),
|
|
494
|
-
textColors: countTokens(colors.text),
|
|
495
|
-
fontSizes: countTokens(typography.fontSize),
|
|
496
|
-
fontWeights: countTokens(typography.fontWeight),
|
|
497
|
-
lineHeights: countTokens(typography.lineHeight),
|
|
498
|
-
spacingValues: countTokens(spacing),
|
|
499
|
-
motionDurations: countTokens(motion.durations),
|
|
500
|
-
motionEasings: countTokens(motion.easings),
|
|
501
|
-
effectsGlass: countTokens(effects.glass),
|
|
502
|
-
effectsAurora: countTokens(effects.aurora),
|
|
503
|
-
effectsPerspective: countTokens(effects.perspective)
|
|
504
|
-
}
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
return generated;
|
|
508
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* MORPH-SPEC Design System Generator Library
|
|
3
|
+
*
|
|
4
|
+
* Reads design-system.md and generates:
|
|
5
|
+
* - CSS variables (design-system.css)
|
|
6
|
+
*
|
|
7
|
+
* Supports BOTH:
|
|
8
|
+
* - New CSS :root format (design-system.md template v2)
|
|
9
|
+
* - Old markdown list format (backward compat)
|
|
10
|
+
*
|
|
11
|
+
* Used both by CLI commands and internal automation.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, existsSync } from 'fs';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// CSS :root Helpers
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extract all CSS :root blocks from markdown (inside ```css fences)
|
|
22
|
+
* @param {string} markdown
|
|
23
|
+
* @returns {string} Combined CSS content from all :root blocks
|
|
24
|
+
*/
|
|
25
|
+
function extractCSSRootBlocks(markdown) {
|
|
26
|
+
const cssBlocks = [];
|
|
27
|
+
const fenceRegex = /```css\s*\n([\s\S]*?)```/g;
|
|
28
|
+
let match;
|
|
29
|
+
while ((match = fenceRegex.exec(markdown)) !== null) {
|
|
30
|
+
cssBlocks.push(match[1]);
|
|
31
|
+
}
|
|
32
|
+
return cssBlocks.join('\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extract CSS custom properties from :root blocks
|
|
37
|
+
* Returns array of { name, value } for all --xxx: yyy; declarations
|
|
38
|
+
*/
|
|
39
|
+
function extractCSSProperties(cssContent) {
|
|
40
|
+
const props = [];
|
|
41
|
+
const propRegex = /(--[\w-]+)\s*:\s*([^;]+);/g;
|
|
42
|
+
let match;
|
|
43
|
+
while ((match = propRegex.exec(cssContent)) !== null) {
|
|
44
|
+
props.push({ name: match[1].trim(), value: match[2].trim() });
|
|
45
|
+
}
|
|
46
|
+
return props;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Parsing Functions
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse colors from markdown — supports CSS :root format and old markdown format
|
|
55
|
+
* @param {string} markdown - Markdown content
|
|
56
|
+
* @returns {Object} Colors object with primary, accent, secondary, neutral, semantic, surface, text, other
|
|
57
|
+
*/
|
|
58
|
+
export function parseColors(markdown) {
|
|
59
|
+
const colors = {
|
|
60
|
+
primary: {},
|
|
61
|
+
accent: {},
|
|
62
|
+
secondary: {},
|
|
63
|
+
neutral: {},
|
|
64
|
+
semantic: {},
|
|
65
|
+
surface: {},
|
|
66
|
+
text: {},
|
|
67
|
+
other: {}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Try CSS :root format first
|
|
71
|
+
const cssContent = extractCSSRootBlocks(markdown);
|
|
72
|
+
const props = extractCSSProperties(cssContent);
|
|
73
|
+
|
|
74
|
+
let cssColorCount = 0;
|
|
75
|
+
|
|
76
|
+
for (const { name, value } of props) {
|
|
77
|
+
if (!name.startsWith('--color-')) continue;
|
|
78
|
+
|
|
79
|
+
const suffix = name.slice('--color-'.length); // e.g. "primary", "primary-light", "neutral-500"
|
|
80
|
+
|
|
81
|
+
if (suffix.startsWith('primary-rgb')) {
|
|
82
|
+
// Store RGB metadata as-is
|
|
83
|
+
colors.primary['rgb'] = value;
|
|
84
|
+
cssColorCount++;
|
|
85
|
+
} else if (suffix.startsWith('primary')) {
|
|
86
|
+
const variant = suffix.slice('primary'.length).replace(/^-/, '') || 'default';
|
|
87
|
+
colors.primary[variant] = value;
|
|
88
|
+
cssColorCount++;
|
|
89
|
+
} else if (suffix.startsWith('accent')) {
|
|
90
|
+
const variant = suffix.slice('accent'.length).replace(/^-/, '') || 'default';
|
|
91
|
+
colors.accent[variant] = value;
|
|
92
|
+
cssColorCount++;
|
|
93
|
+
} else if (suffix.startsWith('neutral-')) {
|
|
94
|
+
const variant = suffix.slice('neutral-'.length);
|
|
95
|
+
colors.neutral[variant] = value;
|
|
96
|
+
cssColorCount++;
|
|
97
|
+
} else if (suffix.startsWith('surface-')) {
|
|
98
|
+
const variant = suffix.slice('surface-'.length);
|
|
99
|
+
colors.surface[variant] = value;
|
|
100
|
+
cssColorCount++;
|
|
101
|
+
} else if (suffix.startsWith('text-')) {
|
|
102
|
+
const variant = suffix.slice('text-'.length);
|
|
103
|
+
colors.text[variant] = value;
|
|
104
|
+
cssColorCount++;
|
|
105
|
+
} else if (suffix === 'success' || suffix === 'warning' || suffix === 'error' || suffix === 'info') {
|
|
106
|
+
colors.semantic[suffix] = value;
|
|
107
|
+
cssColorCount++;
|
|
108
|
+
} else if (suffix === 'border') {
|
|
109
|
+
colors.other['border'] = value;
|
|
110
|
+
cssColorCount++;
|
|
111
|
+
} else {
|
|
112
|
+
// Any other --color-xxx token
|
|
113
|
+
colors.other[suffix] = value;
|
|
114
|
+
cssColorCount++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// If CSS format found colors, return them
|
|
119
|
+
if (cssColorCount > 0) {
|
|
120
|
+
return colors;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// FALL BACK to old markdown regex format
|
|
124
|
+
const colorRegex = /(?:^|\n)[-*]\s+\*\*([^*]+)\*\*[^\(]*\(`(#[0-9A-Fa-f]{6})`\)/gm;
|
|
125
|
+
|
|
126
|
+
let match;
|
|
127
|
+
while ((match = colorRegex.exec(markdown)) !== null) {
|
|
128
|
+
const [, name, hex] = match;
|
|
129
|
+
|
|
130
|
+
if (name.includes('Primary')) {
|
|
131
|
+
const variant = name.match(/\d+/)?.[0] || 'default';
|
|
132
|
+
colors.primary[variant] = hex;
|
|
133
|
+
} else if (name.includes('Secondary')) {
|
|
134
|
+
const variant = name.match(/\d+/)?.[0] || 'default';
|
|
135
|
+
colors.secondary[variant] = hex;
|
|
136
|
+
} else if (name.includes('Neutral') || name.includes('Gray')) {
|
|
137
|
+
const variant = name.match(/\d+/)?.[0] || 'default';
|
|
138
|
+
colors.neutral[variant] = hex;
|
|
139
|
+
} else if (name.includes('Success') || name.includes('Error') || name.includes('Warning') || name.includes('Info')) {
|
|
140
|
+
const type = name.toLowerCase().replace(/\s+/g, '-');
|
|
141
|
+
colors.semantic[type] = hex;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return colors;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Parse typography from markdown — supports CSS :root format and old markdown format
|
|
150
|
+
* @param {string} markdown - Markdown content
|
|
151
|
+
* @returns {Object} Typography with fontFamily, fontSize, fontWeight, lineHeight
|
|
152
|
+
*/
|
|
153
|
+
export function parseTypography(markdown) {
|
|
154
|
+
const typography = {
|
|
155
|
+
fontFamily: {},
|
|
156
|
+
fontSize: {},
|
|
157
|
+
fontWeight: {},
|
|
158
|
+
lineHeight: {}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Try CSS :root format first
|
|
162
|
+
const cssContent = extractCSSRootBlocks(markdown);
|
|
163
|
+
const props = extractCSSProperties(cssContent);
|
|
164
|
+
|
|
165
|
+
let cssTypoCount = 0;
|
|
166
|
+
|
|
167
|
+
for (const { name, value } of props) {
|
|
168
|
+
if (name === '--font-display') {
|
|
169
|
+
typography.fontFamily.display = value;
|
|
170
|
+
cssTypoCount++;
|
|
171
|
+
} else if (name === '--font-body') {
|
|
172
|
+
typography.fontFamily.body = value;
|
|
173
|
+
cssTypoCount++;
|
|
174
|
+
} else if (name === '--font-mono') {
|
|
175
|
+
typography.fontFamily.mono = value;
|
|
176
|
+
cssTypoCount++;
|
|
177
|
+
} else if (name.startsWith('--text-')) {
|
|
178
|
+
// Font sizes: --text-xs, --text-sm, --text-base, etc.
|
|
179
|
+
const sizeName = name.slice('--text-'.length);
|
|
180
|
+
typography.fontSize[sizeName] = value;
|
|
181
|
+
cssTypoCount++;
|
|
182
|
+
} else if (name.startsWith('--font-weight-')) {
|
|
183
|
+
const weightName = name.slice('--font-weight-'.length);
|
|
184
|
+
typography.fontWeight[weightName] = value;
|
|
185
|
+
cssTypoCount++;
|
|
186
|
+
} else if (name.startsWith('--leading-')) {
|
|
187
|
+
const leadingName = name.slice('--leading-'.length);
|
|
188
|
+
typography.lineHeight[leadingName] = value;
|
|
189
|
+
cssTypoCount++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// If CSS format found typography, return it
|
|
194
|
+
if (cssTypoCount > 0) {
|
|
195
|
+
return typography;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// FALL BACK to old markdown format
|
|
199
|
+
// Match font family
|
|
200
|
+
const fontFamilyRegex = /(?:font[- ]?family|typeface)[:\s]+([^\n]+)/gi;
|
|
201
|
+
const fontMatch = fontFamilyRegex.exec(markdown);
|
|
202
|
+
if (fontMatch) {
|
|
203
|
+
typography.fontFamily.primary = fontMatch[1].trim();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Match font sizes (but exclude spacing-related)
|
|
207
|
+
const fontSizeRegex = /(?:^|\n)[-*]\s+\*\*([^*]+)\*\*[^:]*:\s*(\d+(?:\.\d+)?(?:rem|px|em))/gm;
|
|
208
|
+
let match;
|
|
209
|
+
while ((match = fontSizeRegex.exec(markdown)) !== null) {
|
|
210
|
+
const [, name, size] = match;
|
|
211
|
+
const lowerName = name.toLowerCase();
|
|
212
|
+
// Skip if it's spacing-related
|
|
213
|
+
if (lowerName.includes('spacing') || lowerName.includes('gap') || lowerName.includes('margin') || lowerName.includes('padding')) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const cleanName = name.trim().toLowerCase().replace(/\s+/g, '-');
|
|
217
|
+
typography.fontSize[cleanName] = size;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return typography;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Parse spacing from markdown — supports CSS :root format and old markdown format
|
|
225
|
+
* @param {string} markdown - Markdown content
|
|
226
|
+
* @returns {Object} Spacing values
|
|
227
|
+
*/
|
|
228
|
+
export function parseSpacing(markdown) {
|
|
229
|
+
const spacing = {};
|
|
230
|
+
|
|
231
|
+
// Try CSS :root format first
|
|
232
|
+
const cssContent = extractCSSRootBlocks(markdown);
|
|
233
|
+
const props = extractCSSProperties(cssContent);
|
|
234
|
+
|
|
235
|
+
let cssSpaceCount = 0;
|
|
236
|
+
|
|
237
|
+
for (const { name, value } of props) {
|
|
238
|
+
if (name.startsWith('--space-')) {
|
|
239
|
+
const spaceName = name.slice('--space-'.length);
|
|
240
|
+
spacing[spaceName] = value;
|
|
241
|
+
cssSpaceCount++;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// If CSS format found spacing, return it
|
|
246
|
+
if (cssSpaceCount > 0) {
|
|
247
|
+
return spacing;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// FALL BACK to old markdown format
|
|
251
|
+
const spacingRegex = /(?:^|\n)[-*]\s+\*\*([^*]+)\*\*[^:]*:\s*(\d+(?:\.\d+)?(?:rem|px|em))/gm;
|
|
252
|
+
let match;
|
|
253
|
+
while ((match = spacingRegex.exec(markdown)) !== null) {
|
|
254
|
+
const [, name, value] = match;
|
|
255
|
+
if (name.toLowerCase().includes('spacing') || name.toLowerCase().includes('gap') || name.toLowerCase().includes('margin') || name.toLowerCase().includes('padding')) {
|
|
256
|
+
const cleanName = name.trim().toLowerCase().replace(/\s+/g, '-');
|
|
257
|
+
spacing[cleanName] = value;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return spacing;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Parse motion tokens from markdown (CSS :root format only)
|
|
266
|
+
* @param {string} markdown - Markdown content
|
|
267
|
+
* @returns {Object} { durations, easings, scroll }
|
|
268
|
+
*/
|
|
269
|
+
export function parseMotion(markdown) {
|
|
270
|
+
const motion = {
|
|
271
|
+
durations: {},
|
|
272
|
+
easings: {},
|
|
273
|
+
scroll: {}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const cssContent = extractCSSRootBlocks(markdown);
|
|
277
|
+
const props = extractCSSProperties(cssContent);
|
|
278
|
+
|
|
279
|
+
for (const { name, value } of props) {
|
|
280
|
+
if (name.startsWith('--duration-')) {
|
|
281
|
+
const key = name.slice('--duration-'.length);
|
|
282
|
+
motion.durations[key] = value;
|
|
283
|
+
} else if (name.startsWith('--easing-')) {
|
|
284
|
+
const key = name.slice('--easing-'.length);
|
|
285
|
+
motion.easings[key] = value;
|
|
286
|
+
} else if (name.startsWith('--scroll-')) {
|
|
287
|
+
const key = name.slice('--scroll-'.length);
|
|
288
|
+
motion.scroll[key] = value;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return motion;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Parse advanced effects tokens from markdown (CSS :root format only)
|
|
297
|
+
* @param {string} markdown - Markdown content
|
|
298
|
+
* @returns {Object} { glass, aurora, perspective, container, viewTransition, noise }
|
|
299
|
+
*/
|
|
300
|
+
export function parseAdvancedEffects(markdown) {
|
|
301
|
+
const effects = {
|
|
302
|
+
glass: {},
|
|
303
|
+
aurora: {},
|
|
304
|
+
perspective: {},
|
|
305
|
+
container: {},
|
|
306
|
+
viewTransition: {},
|
|
307
|
+
noise: {}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const cssContent = extractCSSRootBlocks(markdown);
|
|
311
|
+
const props = extractCSSProperties(cssContent);
|
|
312
|
+
|
|
313
|
+
for (const { name, value } of props) {
|
|
314
|
+
if (name.startsWith('--glass-')) {
|
|
315
|
+
const key = name.slice('--glass-'.length);
|
|
316
|
+
effects.glass[key] = value;
|
|
317
|
+
} else if (name.startsWith('--aurora-')) {
|
|
318
|
+
const key = name.slice('--aurora-'.length);
|
|
319
|
+
effects.aurora[key] = value;
|
|
320
|
+
} else if (name.startsWith('--noise-')) {
|
|
321
|
+
const key = name.slice('--noise-'.length);
|
|
322
|
+
effects.noise[key] = value;
|
|
323
|
+
} else if (name.startsWith('--perspective-')) {
|
|
324
|
+
const key = name.slice('--perspective-'.length);
|
|
325
|
+
effects.perspective[key] = value;
|
|
326
|
+
} else if (name.startsWith('--container-')) {
|
|
327
|
+
const key = name.slice('--container-'.length);
|
|
328
|
+
effects.container[key] = value;
|
|
329
|
+
} else if (name.startsWith('--view-transition-')) {
|
|
330
|
+
const key = name.slice('--view-transition-'.length);
|
|
331
|
+
effects.viewTransition[key] = value;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return effects;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Parse all design system properties from markdown
|
|
340
|
+
* @param {string} markdownPath - Path to design-system.md file
|
|
341
|
+
* @returns {Object} Parsed design system with colors, typography, spacing, motion, effects
|
|
342
|
+
*/
|
|
343
|
+
export function parseDesignSystem(markdownPath) {
|
|
344
|
+
if (!existsSync(markdownPath)) {
|
|
345
|
+
throw new Error(`File not found: ${markdownPath}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const markdown = readFileSync(markdownPath, 'utf-8');
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
colors: parseColors(markdown),
|
|
352
|
+
typography: parseTypography(markdown),
|
|
353
|
+
spacing: parseSpacing(markdown),
|
|
354
|
+
motion: parseMotion(markdown),
|
|
355
|
+
effects: parseAdvancedEffects(markdown)
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ============================================================================
|
|
360
|
+
// Generation Functions
|
|
361
|
+
// ============================================================================
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Helper to emit CSS variable entries for a color category
|
|
365
|
+
*/
|
|
366
|
+
function emitColorCategory(colors, prefix, label) {
|
|
367
|
+
if (Object.keys(colors).length === 0) return '';
|
|
368
|
+
let css = ` /* ${label} */\n`;
|
|
369
|
+
Object.entries(colors).forEach(([variant, value]) => {
|
|
370
|
+
if (variant === 'rgb') {
|
|
371
|
+
css += ` --color-${prefix}-rgb: ${value};\n`;
|
|
372
|
+
} else if (variant === 'default') {
|
|
373
|
+
css += ` --color-${prefix}: ${value};\n`;
|
|
374
|
+
} else {
|
|
375
|
+
css += ` --color-${prefix}-${variant}: ${value};\n`;
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
css += `\n`;
|
|
379
|
+
return css;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Generate CSS variables file
|
|
384
|
+
* @param {Object} colors - Colors object
|
|
385
|
+
* @param {Object} typography - Typography object
|
|
386
|
+
* @param {Object} spacing - Spacing object
|
|
387
|
+
* @returns {string} CSS content
|
|
388
|
+
*/
|
|
389
|
+
export function generateCSS(colors, typography, spacing) {
|
|
390
|
+
let css = `/* Auto-generated by MORPH-SPEC Design System Generator */\n`;
|
|
391
|
+
css += `/* Do not edit manually - regenerate from design-system.md */\n\n`;
|
|
392
|
+
css += `:root {\n`;
|
|
393
|
+
|
|
394
|
+
// Colors — handle expanded structure
|
|
395
|
+
css += emitColorCategory(colors.primary, 'primary', 'Primary Colors');
|
|
396
|
+
|
|
397
|
+
// Support both accent (new) and secondary (old)
|
|
398
|
+
if (Object.keys(colors.accent || {}).length > 0) {
|
|
399
|
+
css += emitColorCategory(colors.accent, 'accent', 'Accent Colors');
|
|
400
|
+
}
|
|
401
|
+
if (Object.keys(colors.secondary || {}).length > 0) {
|
|
402
|
+
css += emitColorCategory(colors.secondary, 'secondary', 'Secondary Colors');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
css += emitColorCategory(colors.neutral || {}, 'neutral', 'Neutral Colors');
|
|
406
|
+
|
|
407
|
+
if (Object.keys(colors.surface || {}).length > 0) {
|
|
408
|
+
css += emitColorCategory(colors.surface, 'surface', 'Surface Colors');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (Object.keys(colors.text || {}).length > 0) {
|
|
412
|
+
css += emitColorCategory(colors.text, 'text', 'Text Colors');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (Object.keys(colors.semantic || {}).length > 0) {
|
|
416
|
+
css += ` /* Semantic Colors */\n`;
|
|
417
|
+
Object.entries(colors.semantic).forEach(([type, hex]) => {
|
|
418
|
+
css += ` --color-${type}: ${hex};\n`;
|
|
419
|
+
});
|
|
420
|
+
css += `\n`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (Object.keys(colors.other || {}).length > 0) {
|
|
424
|
+
css += ` /* Other Colors */\n`;
|
|
425
|
+
Object.entries(colors.other).forEach(([key, value]) => {
|
|
426
|
+
css += ` --color-${key}: ${value};\n`;
|
|
427
|
+
});
|
|
428
|
+
css += `\n`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Typography
|
|
432
|
+
const fontFamilies = typography.fontFamily || {};
|
|
433
|
+
if (fontFamilies.display || fontFamilies.body || fontFamilies.mono || fontFamilies.primary) {
|
|
434
|
+
css += ` /* Typography */\n`;
|
|
435
|
+
if (fontFamilies.display) css += ` --font-display: ${fontFamilies.display};\n`;
|
|
436
|
+
if (fontFamilies.body) css += ` --font-body: ${fontFamilies.body};\n`;
|
|
437
|
+
if (fontFamilies.mono) css += ` --font-mono: ${fontFamilies.mono};\n`;
|
|
438
|
+
if (fontFamilies.primary) css += ` --font-family-primary: ${fontFamilies.primary};\n`;
|
|
439
|
+
css += `\n`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (Object.keys(typography.fontSize).length > 0) {
|
|
443
|
+
css += ` /* Font Sizes */\n`;
|
|
444
|
+
Object.entries(typography.fontSize).forEach(([name, size]) => {
|
|
445
|
+
css += ` --font-size-${name}: ${size};\n`;
|
|
446
|
+
});
|
|
447
|
+
css += `\n`;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Spacing
|
|
451
|
+
if (Object.keys(spacing).length > 0) {
|
|
452
|
+
css += ` /* Spacing */\n`;
|
|
453
|
+
Object.entries(spacing).forEach(([name, value]) => {
|
|
454
|
+
// If key is numeric (from --space-N), use --space-N format
|
|
455
|
+
if (/^\d+$/.test(name)) {
|
|
456
|
+
css += ` --space-${name}: ${value};\n`;
|
|
457
|
+
} else {
|
|
458
|
+
css += ` --${name}: ${value};\n`;
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
css += `}\n`;
|
|
464
|
+
|
|
465
|
+
return css;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Count non-empty keys in an object
|
|
470
|
+
*/
|
|
471
|
+
function countTokens(obj) {
|
|
472
|
+
return Object.keys(obj || {}).length;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Generate all design system files
|
|
477
|
+
* @param {string} designSystemPath - Path to design-system.md
|
|
478
|
+
* @param {Object} options - Generation options
|
|
479
|
+
* @returns {Object} Generated files content
|
|
480
|
+
*/
|
|
481
|
+
export function generateDesignSystem(designSystemPath, options = {}) {
|
|
482
|
+
// Parse design system
|
|
483
|
+
const { colors, typography, spacing, motion, effects } = parseDesignSystem(designSystemPath);
|
|
484
|
+
|
|
485
|
+
const generated = {
|
|
486
|
+
css: generateCSS(colors, typography, spacing),
|
|
487
|
+
stats: {
|
|
488
|
+
primaryColors: countTokens(colors.primary),
|
|
489
|
+
accentColors: countTokens(colors.accent),
|
|
490
|
+
secondaryColors: countTokens(colors.secondary),
|
|
491
|
+
neutralColors: countTokens(colors.neutral),
|
|
492
|
+
semanticColors: countTokens(colors.semantic),
|
|
493
|
+
surfaceColors: countTokens(colors.surface),
|
|
494
|
+
textColors: countTokens(colors.text),
|
|
495
|
+
fontSizes: countTokens(typography.fontSize),
|
|
496
|
+
fontWeights: countTokens(typography.fontWeight),
|
|
497
|
+
lineHeights: countTokens(typography.lineHeight),
|
|
498
|
+
spacingValues: countTokens(spacing),
|
|
499
|
+
motionDurations: countTokens(motion.durations),
|
|
500
|
+
motionEasings: countTokens(motion.easings),
|
|
501
|
+
effectsGlass: countTokens(effects.glass),
|
|
502
|
+
effectsAurora: countTokens(effects.aurora),
|
|
503
|
+
effectsPerspective: countTokens(effects.perspective)
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
return generated;
|
|
508
|
+
}
|