@polymorphism-tech/morph-spec 4.12.0 → 4.13.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 (314) hide show
  1. package/README.md +21 -29
  2. package/bin/morph-spec.js +41 -24
  3. package/bin/task-manager.js +962 -1341
  4. package/bin/validate.js +1 -1
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +10 -11
  7. package/docs/QUICKSTART.md +15 -15
  8. package/framework/CLAUDE.md +5 -2
  9. package/framework/agents/README.md +3 -4
  10. package/framework/agents/ai-agents/ai-system-architect.md +6 -10
  11. package/framework/agents/architecture/seo-growth-hacker.md +166 -326
  12. package/framework/agents/architecture/standards-architect.md +21 -31
  13. package/framework/agents/backend/dotnet-senior.md +160 -86
  14. package/framework/agents/backend/ef-modeler.md +2 -6
  15. package/framework/agents/backend/hangfire-orchestrator.md +8 -9
  16. package/framework/agents/backend/ms-agent-expert.md +1 -1
  17. package/framework/agents/frontend/nextjs-expert.md +198 -133
  18. package/framework/agents/frontend/ui-ux-designer.md +226 -267
  19. package/framework/agents/infrastructure/infra-architect.md +191 -27
  20. package/framework/agents/integrations/clerk-auth.md +19 -26
  21. package/framework/agents/integrations/hangfire-integration.md +1 -4
  22. package/framework/agents.json +215 -1453
  23. package/framework/commands/morph-apply.md +4 -4
  24. package/framework/commands/morph-preflight.md +4 -26
  25. package/framework/commands/morph-proposal.md +19 -19
  26. package/framework/commands/morph-status.md +2 -2
  27. package/framework/commands/morph-troubleshoot.md +6 -13
  28. package/framework/hooks/README.md +1 -6
  29. package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +4 -6
  30. package/framework/hooks/claude-code/pre-compact/save-morph-context.js +12 -7
  31. package/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +2 -2
  32. package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +2 -2
  33. package/framework/hooks/claude-code/pre-tool-use/task-tracking-guard.js +5 -10
  34. package/framework/hooks/claude-code/session-start/inject-morph-context.js +63 -30
  35. package/framework/hooks/claude-code/session-start/resume-context.js +77 -0
  36. package/framework/hooks/claude-code/statusline.py +4 -5
  37. package/framework/hooks/claude-code/stop/validate-completion.js +4 -4
  38. package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +8 -8
  39. package/framework/hooks/shared/compact-restore.js +28 -5
  40. package/framework/hooks/shared/phase-utils.js +4 -8
  41. package/framework/hooks/shared/state-reader.js +34 -3
  42. package/framework/phases.json +7 -45
  43. package/framework/rules/ai-agents-standards.md +15 -0
  44. package/framework/rules/asaas-standards.md +13 -0
  45. package/framework/rules/clerk-standards.md +13 -0
  46. package/framework/rules/csharp-standards.md +8 -4
  47. package/framework/rules/data-standards.md +18 -0
  48. package/framework/rules/dotnet-templates.md +87 -0
  49. package/framework/rules/frontend-standards.md +13 -13
  50. package/framework/rules/hangfire-standards.md +13 -0
  51. package/framework/rules/infrastructure-standards.md +27 -16
  52. package/framework/rules/morph-workflow.md +73 -13
  53. package/framework/rules/nextjs-standards.md +1 -0
  54. package/framework/rules/nextjs-templates.md +40 -0
  55. package/framework/rules/resend-standards.md +12 -0
  56. package/framework/rules/supabase-standards.md +29 -0
  57. package/framework/skills/README.md +6 -8
  58. package/framework/skills/level-0-meta/mcp-registry.json +21 -99
  59. package/framework/{standards/ai-agents/team-orchestration.md → skills/level-0-meta/morph-agent-teams/SKILL.md} +472 -479
  60. package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +28 -1
  61. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +2 -2
  62. package/framework/skills/level-0-meta/morph-code-review/SKILL.md +1 -5
  63. package/framework/skills/level-0-meta/morph-code-review/scripts/scan-csharp.mjs +1 -1
  64. package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +23 -41
  65. package/framework/skills/level-0-meta/morph-init/SKILL.md +18 -14
  66. package/framework/skills/level-0-meta/morph-parallel-dispatch/SKILL.md +225 -0
  67. package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +20 -0
  68. package/framework/skills/level-0-meta/morph-tool-usage-guide/SKILL.md +4 -8
  69. package/framework/skills/level-0-meta/morph-tool-usage-guide/references/tools-per-phase.md +15 -59
  70. package/framework/skills/level-0-meta/morph-verification-before-completion/SKILL.md +7 -19
  71. package/framework/skills/level-0-meta/morph-verification-before-completion/scripts/check-phase-outputs.mjs +8 -8
  72. package/framework/skills/level-1-workflows/README.md +1 -1
  73. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +49 -50
  74. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +58 -124
  75. package/framework/skills/level-1-workflows/morph-phase-design/references/architecture-analysis-guide.md +5 -50
  76. package/framework/skills/level-1-workflows/morph-phase-design/references/spec-example.md +1 -1
  77. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +127 -159
  78. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +4 -3
  79. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/spec-reviewer-prompt.md +2 -2
  80. package/framework/skills/level-1-workflows/morph-phase-implement/references/recap-example.md +1 -1
  81. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +17 -13
  82. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +22 -40
  83. package/framework/skills/level-1-workflows/morph-phase-tasks/references/task-planning-patterns.md +0 -2
  84. package/framework/skills/level-1-workflows/morph-phase-tasks/references/tasks-example.md +5 -5
  85. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +57 -77
  86. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
  87. package/framework/squad-templates/backend-only.json +6 -6
  88. package/framework/squad-templates/frontend-only.json +17 -8
  89. package/framework/squad-templates/full-stack.json +29 -24
  90. package/framework/standards/STANDARDS.json +1021 -2369
  91. package/framework/standards/backend/dotnet/fluent-validation-vsa.md +200 -0
  92. package/framework/standards/backend/dotnet/result-pattern.md +213 -0
  93. package/framework/standards/backend/dotnet/scalar-ui.md +172 -0
  94. package/framework/standards/backend/dotnet/scrutor.md +158 -0
  95. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +274 -0
  96. package/framework/standards/frontend/nextjs/motion-patterns.md +1 -1
  97. package/framework/standards/infrastructure/supabase/mcp-setup.md +83 -193
  98. package/framework/standards/integration/mcp/mcp-tools.md +91 -77
  99. package/framework/templates/IDE-SNIPPETS.md +2 -6
  100. package/framework/templates/README.md +709 -814
  101. package/framework/templates/REGISTRY.json +223 -762
  102. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -0
  103. package/framework/templates/code/dotnet/abstractions/error.cs +60 -0
  104. package/framework/templates/code/dotnet/abstractions/handler.cs +12 -0
  105. package/framework/templates/code/dotnet/abstractions/result.cs +74 -0
  106. package/framework/templates/code/dotnet/backend/repository.cs +148 -141
  107. package/framework/templates/code/dotnet/constants/api-tags.cs +6 -0
  108. package/framework/templates/code/dotnet/exceptions/exception-handler.cs +32 -0
  109. package/framework/templates/code/dotnet/extensions/database-extensions.cs +20 -0
  110. package/framework/templates/code/dotnet/extensions/handler-registration.cs +38 -0
  111. package/framework/templates/code/dotnet/extensions/health-checks.cs +24 -0
  112. package/framework/templates/code/dotnet/extensions/map-endpoints.cs +35 -0
  113. package/framework/templates/code/dotnet/extensions/result-extensions.cs +29 -0
  114. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -0
  115. package/framework/templates/code/dotnet/feature/entity.cs.hbs +8 -0
  116. package/framework/templates/code/dotnet/feature/errors.cs.hbs +8 -0
  117. package/framework/templates/code/dotnet/feature/handler.cs.hbs +24 -0
  118. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -0
  119. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -0
  120. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -0
  121. package/framework/templates/code/dotnet/jobs/job.cs +21 -17
  122. package/framework/templates/code/dotnet/pipelines/logging-decorator.cs +31 -0
  123. package/framework/templates/code/dotnet/pipelines/validation-decorator.cs +97 -0
  124. package/framework/templates/code/dotnet/test.cs +192 -106
  125. package/framework/templates/context/ARCHITECTURE.md +89 -0
  126. package/framework/templates/context/CONTEXT-FEATURE.md +2 -2
  127. package/framework/templates/context/CONTEXT.md +180 -181
  128. package/framework/templates/context/design-system.md +1 -5
  129. package/framework/templates/docs/onboarding.md +2 -3
  130. package/framework/templates/docs/proposal.md +171 -182
  131. package/framework/templates/docs/spec.md +1 -3
  132. package/framework/templates/docs/ui-components.md +0 -72
  133. package/framework/templates/examples/design-system-examples.md +4 -4
  134. package/framework/templates/feature/decisions.md +0 -66
  135. package/framework/templates/feature/recap.md +1 -2
  136. package/framework/templates/infrastructure/github/README.md +56 -593
  137. package/framework/templates/integrations/clerk-config.cs +0 -41
  138. package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +2 -2
  139. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -2
  140. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +83 -126
  141. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +2 -2
  142. package/framework/templates/project/validate.js +2 -4
  143. package/framework/templates/ui/design-system.css +1 -1
  144. package/framework/workflows/README.md +8 -8
  145. package/framework/workflows/configs/fast-track.json +51 -0
  146. package/framework/workflows/configs/full.json +68 -0
  147. package/framework/workflows/configs/standard.json +7 -3
  148. package/framework/workflows/docs/STORY-DRIVEN-DEVELOPMENT.md +1 -1
  149. package/framework/workflows/docs/enforcement-pipeline.md +18 -19
  150. package/framework/workflows/docs/full-morph.md +69 -76
  151. package/framework/workflows/docs/standard.md +49 -44
  152. package/framework/workflows/docs/ui-refresh.md +1 -1
  153. package/package.json +3 -3
  154. package/src/commands/agents/dispatch-agents.js +55 -116
  155. package/src/commands/context/context-refresh.js +79 -0
  156. package/src/commands/phase/phase-reset.js +1 -1
  157. package/src/commands/project/doctor.js +10 -32
  158. package/src/commands/project/init.js +12 -0
  159. package/src/commands/project/status.js +6 -12
  160. package/src/commands/scaffold/scaffold.js +131 -0
  161. package/src/commands/scope/escalate.js +1 -1
  162. package/src/commands/state/advance-phase.js +15 -29
  163. package/src/commands/state/state.js +20 -5
  164. package/src/commands/tasks/task.js +0 -30
  165. package/src/commands/validation/validate-feature.js +3 -10
  166. package/src/core/paths/output-schema.js +12 -13
  167. package/src/core/state/phase-state-machine.js +211 -217
  168. package/src/core/state/state-manager.js +9 -9
  169. package/src/lib/agents/agents-manager.js +2 -2
  170. package/src/lib/agents/output-collector.js +1 -2
  171. package/src/lib/detectors/design-system-detector.js +4 -4
  172. package/src/lib/detectors/index.js +1 -11
  173. package/src/lib/generators/architecture-scanner.js +640 -0
  174. package/src/lib/generators/context-generator.js +3 -3
  175. package/src/lib/generators/design-system-generator.js +508 -587
  176. package/src/lib/generators/index.js +12 -10
  177. package/src/lib/generators/metadata-extractor.js +386 -387
  178. package/src/lib/generators/recap-generator.js +4 -4
  179. package/src/lib/generators/scaffold-generator.js +317 -0
  180. package/src/lib/generators/settings-generator.js +0 -1
  181. package/src/lib/phase-chain/phase-validator.js +64 -42
  182. package/src/lib/stack/stack-profile.js +2 -2
  183. package/src/lib/standards/digest-builder.js +72 -285
  184. package/src/lib/tasks/task-parser.js +11 -5
  185. package/src/lib/validators/architecture/architecture-validator.js +259 -387
  186. package/src/lib/validators/content/content-validator.js +287 -287
  187. package/src/lib/validators/css/css-validator.js +340 -352
  188. package/src/lib/validators/packages/package-validator.js +347 -360
  189. package/src/lib/validators/shared/issue-counter.js +14 -18
  190. package/src/lib/validators/validation-runner.js +3 -19
  191. package/src/scripts/setup-infra.js +1 -1
  192. package/src/utils/agents-installer.js +24 -3
  193. package/src/utils/file-copier.js +1 -0
  194. package/src/utils/hooks-installer.js +11 -19
  195. package/src/utils/skills-installer.js +178 -178
  196. package/framework/agents/architecture/prompt-engineer.md +0 -195
  197. package/framework/agents/backend/api-designer.md +0 -106
  198. package/framework/agents/frontend/blazor-builder.md +0 -229
  199. package/framework/agents/frontend/nextjs-ui-specialist.md +0 -218
  200. package/framework/agents/infrastructure/azure-architect.md +0 -150
  201. package/framework/agents/infrastructure/azure-deploy-specialist.md +0 -707
  202. package/framework/agents/infrastructure/bicep-architect.md +0 -132
  203. package/framework/agents/infrastructure/container-specialist.md +0 -137
  204. package/framework/agents/infrastructure/devops-engineer.md +0 -126
  205. package/framework/agents/integrations/azure-identity.md +0 -148
  206. package/framework/agents/quality/code-analyzer.md +0 -239
  207. package/framework/agents/quality/testing-specialist.md +0 -130
  208. package/framework/hooks/claude-code/notification/approval-reminder.js +0 -57
  209. package/framework/hooks/claude-code/post-tool-use/context-refresh.js +0 -109
  210. package/framework/hooks/claude-code/post-tool-use/skill-reminder.js +0 -78
  211. package/framework/index/troubleshooting-index.json +0 -184
  212. package/framework/rules/testing-standards.md +0 -14
  213. package/framework/skills/level-0-meta/morph-replicate/SKILL.md +0 -230
  214. package/framework/skills/level-0-meta/morph-replicate/references/blazor-html-mapping.md +0 -70
  215. package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +0 -256
  216. package/framework/skills/level-1-workflows/morph-phase-clarify/references/clarifications-example.md +0 -117
  217. package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +0 -269
  218. package/framework/standards/ai-agents/blazor-ui.md +0 -364
  219. package/framework/standards/architecture/ddd/aggregates.md +0 -120
  220. package/framework/standards/architecture/ddd/bounded-contexts.md +0 -105
  221. package/framework/standards/architecture/ddd/complexity-levels.md +0 -108
  222. package/framework/standards/architecture/ddd/entities.md +0 -99
  223. package/framework/standards/architecture/ddd/ubiquitous-language.md +0 -58
  224. package/framework/standards/architecture/ddd/value-objects.md +0 -124
  225. package/framework/standards/backend/api/minimal-api.md +0 -494
  226. package/framework/standards/backend/api/rest.md +0 -492
  227. package/framework/standards/backend/authentication/passkeys.md +0 -428
  228. package/framework/standards/backend/database/ef-core.md +0 -199
  229. package/framework/standards/backend/database/migrations.md +0 -393
  230. package/framework/standards/backend/database/repository-patterns.md +0 -528
  231. package/framework/standards/backend/dotnet/async.md +0 -366
  232. package/framework/standards/backend/dotnet/core.md +0 -117
  233. package/framework/standards/backend/dotnet/di.md +0 -439
  234. package/framework/standards/backend/dotnet/program-cs-checklist.md +0 -92
  235. package/framework/standards/context/analytics.md +0 -96
  236. package/framework/standards/context/bundles.md +0 -110
  237. package/framework/standards/context/priming.md +0 -78
  238. package/framework/standards/core/architecture.md +0 -185
  239. package/framework/standards/core/coding.md +0 -214
  240. package/framework/standards/core/git-branching-strategy.md +0 -403
  241. package/framework/standards/core/git.md +0 -185
  242. package/framework/standards/core/testing.md +0 -295
  243. package/framework/standards/data/nosql/blob-storage.md +0 -102
  244. package/framework/standards/data/vector-search/azure-ai-search.md +0 -121
  245. package/framework/standards/frontend/blazor/design-checklist.md +0 -222
  246. package/framework/standards/frontend/blazor/fluent-ui-setup.md +0 -595
  247. package/framework/standards/frontend/blazor/fluent-ui.md +0 -137
  248. package/framework/standards/frontend/blazor/html-conversion.md +0 -184
  249. package/framework/standards/frontend/blazor/lifecycle.md +0 -195
  250. package/framework/standards/frontend/blazor/pitfalls.md +0 -198
  251. package/framework/standards/frontend/blazor/state.md +0 -191
  252. package/framework/standards/frontend/design-system/aesthetic-direction.md +0 -266
  253. package/framework/standards/frontend/design-system/design-trends.md +0 -171
  254. package/framework/standards/infrastructure/azure/azure.md +0 -624
  255. package/framework/standards/infrastructure/azure/bicep/bicep-patterns.md +0 -422
  256. package/framework/standards/infrastructure/azure/devops/azure-devops-setup.md +0 -516
  257. package/framework/standards/infrastructure/azure/devops/local-development.md +0 -520
  258. package/framework/standards/infrastructure/azure/services/functions.md +0 -486
  259. package/framework/standards/infrastructure/azure/services/service-bus.md +0 -459
  260. package/framework/standards/infrastructure/azure/services/storage.md +0 -407
  261. package/framework/standards/infrastructure/vercel/vercel-database.md +0 -106
  262. package/framework/standards/integration/api/graphql.md +0 -91
  263. package/framework/standards/integration/api/grpc.md +0 -114
  264. package/framework/standards/integration/api/rest-design.md +0 -95
  265. package/framework/standards/integration/event-driven/cqrs.md +0 -101
  266. package/framework/standards/integration/event-driven/event-sourcing.md +0 -124
  267. package/framework/standards/integration/event-driven/service-bus.md +0 -95
  268. package/framework/standards/observability/logging.md +0 -131
  269. package/framework/standards/observability/metrics.md +0 -121
  270. package/framework/standards/observability/monitoring.md +0 -114
  271. package/framework/standards/observability/tracing.md +0 -132
  272. package/framework/standards/workflows/parallel-execution.md +0 -112
  273. package/framework/standards/workflows/thread-management.md +0 -113
  274. package/framework/templates/code/dotnet/backend/service.cs +0 -139
  275. package/framework/templates/code/dotnet/contracts/Commands.cs +0 -74
  276. package/framework/templates/code/dotnet/contracts/Entities.cs +0 -25
  277. package/framework/templates/code/dotnet/contracts/Queries.cs +0 -74
  278. package/framework/templates/code/dotnet/contracts/api-contracts.cs +0 -173
  279. package/framework/templates/code/dotnet/contracts/contracts-level1.cs +0 -69
  280. package/framework/templates/code/dotnet/contracts/contracts-level2.cs +0 -86
  281. package/framework/templates/code/dotnet/contracts/contracts-level3.cs +0 -41
  282. package/framework/templates/code/dotnet/frontend/component.razor +0 -239
  283. package/framework/templates/infrastructure/azure/Dockerfile.example +0 -82
  284. package/framework/templates/infrastructure/azure/README.md +0 -286
  285. package/framework/templates/infrastructure/azure/app-insights.bicep +0 -63
  286. package/framework/templates/infrastructure/azure/app-service.bicep +0 -164
  287. package/framework/templates/infrastructure/azure/container-app-env.bicep +0 -49
  288. package/framework/templates/infrastructure/azure/container-app.bicep +0 -156
  289. package/framework/templates/infrastructure/azure/deploy-checklist.md +0 -426
  290. package/framework/templates/infrastructure/azure/deploy.ps1 +0 -229
  291. package/framework/templates/infrastructure/azure/deploy.sh +0 -208
  292. package/framework/templates/infrastructure/azure/key-vault.bicep +0 -91
  293. package/framework/templates/infrastructure/azure/main.bicep +0 -189
  294. package/framework/templates/infrastructure/azure/parameters.dev.json +0 -29
  295. package/framework/templates/infrastructure/azure/parameters.prod.json +0 -29
  296. package/framework/templates/infrastructure/azure/parameters.staging.json +0 -29
  297. package/framework/templates/infrastructure/azure/sql-database.bicep +0 -103
  298. package/framework/templates/infrastructure/azure/storage.bicep +0 -106
  299. package/framework/templates/infrastructure/github/actions/azure-auth/action.yml.hbs +0 -22
  300. package/framework/templates/infrastructure/github/workflows/deploy-azure-app-service.yml.hbs +0 -61
  301. package/framework/templates/integrations/azure-identity-config.cs +0 -288
  302. package/framework/templates/project-structure/dotnet-ddd.md +0 -70
  303. package/framework/templates/saas/subscription.cs +0 -347
  304. package/framework/templates/saas/tenant.cs +0 -338
  305. package/framework/templates/ui/FluentDesignTheme.cs +0 -149
  306. package/framework/templates/ui/MudTheme.cs +0 -281
  307. package/framework/workflows/configs/nodejs-cli.json +0 -40
  308. package/src/commands/standards/context-budget.js +0 -89
  309. package/src/lib/standards/index.js +0 -7
  310. package/src/lib/standards/standards-context-injector.js +0 -302
  311. package/src/lib/validators/blazor/blazor-concurrency-analyzer.js +0 -316
  312. package/src/lib/validators/blazor/blazor-state-validator.js +0 -279
  313. package/src/lib/validators/blazor/blazor-validator.js +0 -369
  314. package/src/lib/validators/blazor/index.js +0 -9
@@ -1,587 +1,508 @@
1
- /**
2
- * MORPH-SPEC Design System Generator Library
3
- *
4
- * Reads design-system.md (or ui-design-system.md) and generates:
5
- * - wwwroot/css/design-system.css (CSS variables)
6
- * - Themes/FluentDesignTheme.cs (Fluent UI theme)
7
- * - Themes/MudTheme.cs (MudBlazor theme)
8
- *
9
- * Supports BOTH:
10
- * - New CSS :root format (design-system.md template v2)
11
- * - Old markdown list format (backward compat)
12
- *
13
- * Used both by CLI commands and internal automation.
14
- */
15
-
16
- import { readFileSync, existsSync } from 'fs';
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
-
51
- // ============================================================================
52
- // Parsing Functions
53
- // ============================================================================
54
-
55
- /**
56
- * Parse colors from markdown — supports CSS :root format and old markdown format
57
- * @param {string} markdown - Markdown content
58
- * @returns {Object} Colors object with primary, accent, secondary, neutral, semantic, surface, text, other
59
- */
60
- export function parseColors(markdown) {
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
- }
119
-
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
126
- const colorRegex = /(?:^|\n)[-*]\s+\*\*([^*]+)\*\*[^\(]*\(`(#[0-9A-Fa-f]{6})`\)/gm;
127
-
128
- let match;
129
- while ((match = colorRegex.exec(markdown)) !== null) {
130
- const [, name, hex] = match;
131
-
132
- if (name.includes('Primary')) {
133
- const variant = name.match(/\d+/)?.[0] || 'default';
134
- colors.primary[variant] = hex;
135
- } else if (name.includes('Secondary')) {
136
- const variant = name.match(/\d+/)?.[0] || 'default';
137
- colors.secondary[variant] = hex;
138
- } else if (name.includes('Neutral') || name.includes('Gray')) {
139
- const variant = name.match(/\d+/)?.[0] || 'default';
140
- colors.neutral[variant] = hex;
141
- } else if (name.includes('Success') || name.includes('Error') || name.includes('Warning') || name.includes('Info')) {
142
- const type = name.toLowerCase().replace(/\s+/g, '-');
143
- colors.semantic[type] = hex;
144
- }
145
- }
146
-
147
- return colors;
148
- }
149
-
150
- /**
151
- * Parse typography from markdown — supports CSS :root format and old markdown format
152
- * @param {string} markdown - Markdown content
153
- * @returns {Object} Typography with fontFamily, fontSize, fontWeight, lineHeight
154
- */
155
- export function parseTypography(markdown) {
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
- }
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
201
- // Match font family
202
- const fontFamilyRegex = /(?:font[- ]?family|typeface)[:\s]+([^\n]+)/gi;
203
- const fontMatch = fontFamilyRegex.exec(markdown);
204
- if (fontMatch) {
205
- typography.fontFamily.primary = fontMatch[1].trim();
206
- }
207
-
208
- // Match font sizes (but exclude spacing-related)
209
- const fontSizeRegex = /(?:^|\n)[-*]\s+\*\*([^*]+)\*\*[^:]*:\s*(\d+(?:\.\d+)?(?:rem|px|em))/gm;
210
- let match;
211
- while ((match = fontSizeRegex.exec(markdown)) !== null) {
212
- const [, name, size] = match;
213
- const lowerName = name.toLowerCase();
214
- // Skip if it's spacing-related
215
- if (lowerName.includes('spacing') || lowerName.includes('gap') || lowerName.includes('margin') || lowerName.includes('padding')) {
216
- continue;
217
- }
218
- const cleanName = name.trim().toLowerCase().replace(/\s+/g, '-');
219
- typography.fontSize[cleanName] = size;
220
- }
221
-
222
- return typography;
223
- }
224
-
225
- /**
226
- * Parse spacing from markdown — supports CSS :root format and old markdown format
227
- * @param {string} markdown - Markdown content
228
- * @returns {Object} Spacing values
229
- */
230
- export function parseSpacing(markdown) {
231
- const spacing = {};
232
-
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
253
- const spacingRegex = /(?:^|\n)[-*]\s+\*\*([^*]+)\*\*[^:]*:\s*(\d+(?:\.\d+)?(?:rem|px|em))/gm;
254
- let match;
255
- while ((match = spacingRegex.exec(markdown)) !== null) {
256
- const [, name, value] = match;
257
- if (name.toLowerCase().includes('spacing') || name.toLowerCase().includes('gap') || name.toLowerCase().includes('margin') || name.toLowerCase().includes('padding')) {
258
- const cleanName = name.trim().toLowerCase().replace(/\s+/g, '-');
259
- spacing[cleanName] = value;
260
- }
261
- }
262
-
263
- return spacing;
264
- }
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
-
340
- /**
341
- * Parse all design system properties from markdown
342
- * @param {string} markdownPath - Path to design-system.md file
343
- * @returns {Object} Parsed design system with colors, typography, spacing, motion, effects
344
- */
345
- export function parseDesignSystem(markdownPath) {
346
- if (!existsSync(markdownPath)) {
347
- throw new Error(`File not found: ${markdownPath}`);
348
- }
349
-
350
- const markdown = readFileSync(markdownPath, 'utf-8');
351
-
352
- return {
353
- colors: parseColors(markdown),
354
- typography: parseTypography(markdown),
355
- spacing: parseSpacing(markdown),
356
- motion: parseMotion(markdown),
357
- effects: parseAdvancedEffects(markdown)
358
- };
359
- }
360
-
361
- // ============================================================================
362
- // Generation Functions
363
- // ============================================================================
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
-
384
- /**
385
- * Generate CSS variables file
386
- * @param {Object} colors - Colors object
387
- * @param {Object} typography - Typography object
388
- * @param {Object} spacing - Spacing object
389
- * @returns {string} CSS content
390
- */
391
- export function generateCSS(colors, typography, spacing) {
392
- let css = `/* Auto-generated by MORPH-SPEC Design System Generator */\n`;
393
- css += `/* Do not edit manually - regenerate from ui-design-system.md */\n\n`;
394
- css += `:root {\n`;
395
-
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');
405
- }
406
-
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');
411
- }
412
-
413
- if (Object.keys(colors.text || {}).length > 0) {
414
- css += emitColorCategory(colors.text, 'text', 'Text Colors');
415
- }
416
-
417
- if (Object.keys(colors.semantic || {}).length > 0) {
418
- css += ` /* Semantic Colors */\n`;
419
- Object.entries(colors.semantic).forEach(([type, hex]) => {
420
- css += ` --color-${type}: ${hex};\n`;
421
- });
422
- css += `\n`;
423
- }
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
-
433
- // Typography
434
- const fontFamilies = typography.fontFamily || {};
435
- if (fontFamilies.display || fontFamilies.body || fontFamilies.mono || fontFamilies.primary) {
436
- css += ` /* Typography */\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`;
441
- css += `\n`;
442
- }
443
-
444
- if (Object.keys(typography.fontSize).length > 0) {
445
- css += ` /* Font Sizes */\n`;
446
- Object.entries(typography.fontSize).forEach(([name, size]) => {
447
- css += ` --font-size-${name}: ${size};\n`;
448
- });
449
- css += `\n`;
450
- }
451
-
452
- // Spacing
453
- if (Object.keys(spacing).length > 0) {
454
- css += ` /* Spacing */\n`;
455
- Object.entries(spacing).forEach(([name, value]) => {
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
- }
462
- });
463
- }
464
-
465
- css += `}\n`;
466
-
467
- return css;
468
- }
469
-
470
- /**
471
- * Generate Fluent UI theme (C#)
472
- * @param {Object} colors - Colors object
473
- * @param {Object} typography - Typography object
474
- * @param {string} namespace - C# namespace (default: YourProject.Themes)
475
- * @returns {string} C# code
476
- */
477
- export function generateFluentTheme(colors, typography, namespace = 'YourProject.Themes') {
478
- // Support both new format (primary.default) and old format (primary['500'])
479
- const primary = colors.primary.default || colors.primary['500'] || '#0078d4';
480
-
481
- let csharp = `// Auto-generated by MORPH-SPEC Design System Generator\n`;
482
- csharp += `// Do not edit manually - regenerate from ui-design-system.md\n\n`;
483
- csharp += `using Microsoft.FluentUI.AspNetCore.Components;\n\n`;
484
- csharp += `namespace ${namespace};\n\n`;
485
- csharp += `public static class FluentDesignTheme\n{\n`;
486
- csharp += ` public static DesignThemePalette GetPalette()\n`;
487
- csharp += ` {\n`;
488
- csharp += ` return new DesignThemePalette\n`;
489
- csharp += ` {\n`;
490
- csharp += ` Accent = "${primary}",\n`;
491
- csharp += ` // Add more theme properties as needed\n`;
492
- csharp += ` };\n`;
493
- csharp += ` }\n`;
494
- csharp += `}\n`;
495
-
496
- return csharp;
497
- }
498
-
499
- /**
500
- * Generate MudBlazor theme (C#)
501
- * @param {Object} colors - Colors object
502
- * @param {Object} typography - Typography object
503
- * @param {string} namespace - C# namespace (default: YourProject.Themes)
504
- * @returns {string} C# code
505
- */
506
- export function generateMudTheme(colors, typography, namespace = 'YourProject.Themes') {
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';
511
-
512
- let csharp = `// Auto-generated by MORPH-SPEC Design System Generator\n`;
513
- csharp += `// Do not edit manually - regenerate from ui-design-system.md\n\n`;
514
- csharp += `using MudBlazor;\n\n`;
515
- csharp += `namespace ${namespace};\n\n`;
516
- csharp += `public static class MudDesignTheme\n{\n`;
517
- csharp += ` public static MudTheme GetTheme()\n`;
518
- csharp += ` {\n`;
519
- csharp += ` return new MudTheme\n`;
520
- csharp += ` {\n`;
521
- csharp += ` Palette = new PaletteLight\n`;
522
- csharp += ` {\n`;
523
- csharp += ` Primary = "${primary}",\n`;
524
- csharp += ` Secondary = "${secondary}",\n`;
525
- csharp += ` // Add more palette colors as needed\n`;
526
- csharp += ` }\n`;
527
- csharp += ` };\n`;
528
- csharp += ` }\n`;
529
- csharp += `}\n`;
530
-
531
- return csharp;
532
- }
533
-
534
- /**
535
- * Count non-empty keys in an object
536
- */
537
- function countTokens(obj) {
538
- return Object.keys(obj || {}).length;
539
- }
540
-
541
- /**
542
- * Generate all design system files
543
- * @param {string} designSystemPath - Path to design-system.md
544
- * @param {Object} options - Generation options
545
- * @param {string} options.mode - Generation mode: 'fluent', 'mud', or 'both'
546
- * @param {string} options.namespace - C# namespace
547
- * @returns {Object} Generated files content
548
- */
549
- export function generateDesignSystem(designSystemPath, options = {}) {
550
- const { mode = 'both', namespace = 'YourProject.Themes' } = options;
551
-
552
- // Parse design system
553
- const { colors, typography, spacing, motion, effects } = parseDesignSystem(designSystemPath);
554
-
555
- const generated = {
556
- css: generateCSS(colors, typography, spacing),
557
- stats: {
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)
574
- }
575
- };
576
-
577
- // Generate themes based on mode
578
- if (mode === 'fluent' || mode === 'both') {
579
- generated.fluentTheme = generateFluentTheme(colors, typography, namespace);
580
- }
581
-
582
- if (mode === 'mud' || mode === 'both') {
583
- generated.mudTheme = generateMudTheme(colors, typography, namespace);
584
- }
585
-
586
- return generated;
587
- }
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
+ }