popeye-cli 1.4.7 → 1.5.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 (134) hide show
  1. package/README.md +222 -63
  2. package/dist/adapters/gemini.d.ts +1 -0
  3. package/dist/adapters/gemini.d.ts.map +1 -1
  4. package/dist/adapters/gemini.js +9 -4
  5. package/dist/adapters/gemini.js.map +1 -1
  6. package/dist/adapters/grok.d.ts +1 -0
  7. package/dist/adapters/grok.d.ts.map +1 -1
  8. package/dist/adapters/grok.js +9 -4
  9. package/dist/adapters/grok.js.map +1 -1
  10. package/dist/adapters/openai.d.ts +1 -1
  11. package/dist/adapters/openai.d.ts.map +1 -1
  12. package/dist/adapters/openai.js +35 -9
  13. package/dist/adapters/openai.js.map +1 -1
  14. package/dist/cli/interactive.d.ts.map +1 -1
  15. package/dist/cli/interactive.js +42 -0
  16. package/dist/cli/interactive.js.map +1 -1
  17. package/dist/generators/all.d.ts +4 -1
  18. package/dist/generators/all.d.ts.map +1 -1
  19. package/dist/generators/all.js +2 -1
  20. package/dist/generators/all.js.map +1 -1
  21. package/dist/generators/doc-parser.d.ts +49 -0
  22. package/dist/generators/doc-parser.d.ts.map +1 -0
  23. package/dist/generators/doc-parser.js +336 -0
  24. package/dist/generators/doc-parser.js.map +1 -0
  25. package/dist/generators/templates/index.d.ts +4 -0
  26. package/dist/generators/templates/index.d.ts.map +1 -1
  27. package/dist/generators/templates/index.js +4 -0
  28. package/dist/generators/templates/index.js.map +1 -1
  29. package/dist/generators/templates/website-components.d.ts +33 -0
  30. package/dist/generators/templates/website-components.d.ts.map +1 -0
  31. package/dist/generators/templates/website-components.js +278 -0
  32. package/dist/generators/templates/website-components.js.map +1 -0
  33. package/dist/generators/templates/website-config.d.ts +41 -0
  34. package/dist/generators/templates/website-config.d.ts.map +1 -0
  35. package/dist/generators/templates/website-config.js +283 -0
  36. package/dist/generators/templates/website-config.js.map +1 -0
  37. package/dist/generators/templates/website-conversion.d.ts +27 -0
  38. package/dist/generators/templates/website-conversion.d.ts.map +1 -0
  39. package/dist/generators/templates/website-conversion.js +326 -0
  40. package/dist/generators/templates/website-conversion.js.map +1 -0
  41. package/dist/generators/templates/website-seo.d.ts +76 -0
  42. package/dist/generators/templates/website-seo.d.ts.map +1 -0
  43. package/dist/generators/templates/website-seo.js +326 -0
  44. package/dist/generators/templates/website-seo.js.map +1 -0
  45. package/dist/generators/templates/website.d.ts +14 -47
  46. package/dist/generators/templates/website.d.ts.map +1 -1
  47. package/dist/generators/templates/website.js +412 -499
  48. package/dist/generators/templates/website.js.map +1 -1
  49. package/dist/generators/website-context.d.ts +83 -0
  50. package/dist/generators/website-context.d.ts.map +1 -0
  51. package/dist/generators/website-context.js +190 -0
  52. package/dist/generators/website-context.js.map +1 -0
  53. package/dist/generators/website.d.ts +3 -0
  54. package/dist/generators/website.d.ts.map +1 -1
  55. package/dist/generators/website.js +73 -10
  56. package/dist/generators/website.js.map +1 -1
  57. package/dist/state/index.d.ts +27 -0
  58. package/dist/state/index.d.ts.map +1 -1
  59. package/dist/state/index.js +30 -0
  60. package/dist/state/index.js.map +1 -1
  61. package/dist/types/consensus.d.ts +3 -0
  62. package/dist/types/consensus.d.ts.map +1 -1
  63. package/dist/types/consensus.js +1 -0
  64. package/dist/types/consensus.js.map +1 -1
  65. package/dist/types/website-strategy.d.ts +263 -0
  66. package/dist/types/website-strategy.d.ts.map +1 -0
  67. package/dist/types/website-strategy.js +105 -0
  68. package/dist/types/website-strategy.js.map +1 -0
  69. package/dist/types/workflow.d.ts +15 -0
  70. package/dist/types/workflow.d.ts.map +1 -1
  71. package/dist/types/workflow.js +6 -0
  72. package/dist/types/workflow.js.map +1 -1
  73. package/dist/workflow/consensus.d.ts.map +1 -1
  74. package/dist/workflow/consensus.js +2 -0
  75. package/dist/workflow/consensus.js.map +1 -1
  76. package/dist/workflow/execution-mode.d.ts.map +1 -1
  77. package/dist/workflow/execution-mode.js +18 -0
  78. package/dist/workflow/execution-mode.js.map +1 -1
  79. package/dist/workflow/index.d.ts +3 -0
  80. package/dist/workflow/index.d.ts.map +1 -1
  81. package/dist/workflow/index.js +25 -0
  82. package/dist/workflow/index.js.map +1 -1
  83. package/dist/workflow/overview.d.ts +89 -0
  84. package/dist/workflow/overview.d.ts.map +1 -0
  85. package/dist/workflow/overview.js +354 -0
  86. package/dist/workflow/overview.js.map +1 -0
  87. package/dist/workflow/plan-mode.d.ts +2 -1
  88. package/dist/workflow/plan-mode.d.ts.map +1 -1
  89. package/dist/workflow/plan-mode.js +83 -5
  90. package/dist/workflow/plan-mode.js.map +1 -1
  91. package/dist/workflow/website-strategy.d.ts +70 -0
  92. package/dist/workflow/website-strategy.d.ts.map +1 -0
  93. package/dist/workflow/website-strategy.js +238 -0
  94. package/dist/workflow/website-strategy.js.map +1 -0
  95. package/dist/workflow/website-updater.d.ts +17 -0
  96. package/dist/workflow/website-updater.d.ts.map +1 -0
  97. package/dist/workflow/website-updater.js +105 -0
  98. package/dist/workflow/website-updater.js.map +1 -0
  99. package/dist/workflow/workflow-logger.d.ts +1 -1
  100. package/dist/workflow/workflow-logger.d.ts.map +1 -1
  101. package/dist/workflow/workflow-logger.js.map +1 -1
  102. package/package.json +1 -1
  103. package/src/adapters/gemini.ts +10 -4
  104. package/src/adapters/grok.ts +10 -4
  105. package/src/adapters/openai.ts +38 -6
  106. package/src/cli/interactive.ts +47 -0
  107. package/src/generators/all.ts +6 -1
  108. package/src/generators/doc-parser.ts +372 -0
  109. package/src/generators/templates/index.ts +4 -0
  110. package/src/generators/templates/website-components.ts +305 -0
  111. package/src/generators/templates/website-config.ts +291 -0
  112. package/src/generators/templates/website-conversion.ts +341 -0
  113. package/src/generators/templates/website-seo.ts +370 -0
  114. package/src/generators/templates/website.ts +451 -505
  115. package/src/generators/website-context.ts +265 -0
  116. package/src/generators/website.ts +109 -19
  117. package/src/state/index.ts +42 -0
  118. package/src/types/consensus.ts +3 -0
  119. package/src/types/website-strategy.ts +243 -0
  120. package/src/types/workflow.ts +15 -0
  121. package/src/workflow/consensus.ts +2 -0
  122. package/src/workflow/execution-mode.ts +21 -0
  123. package/src/workflow/index.ts +25 -0
  124. package/src/workflow/overview.ts +469 -0
  125. package/src/workflow/plan-mode.ts +115 -4
  126. package/src/workflow/website-strategy.ts +305 -0
  127. package/src/workflow/website-updater.ts +131 -0
  128. package/src/workflow/workflow-logger.ts +1 -0
  129. package/tests/adapters/persona-switching.test.ts +63 -0
  130. package/tests/generators/website-components.test.ts +159 -0
  131. package/tests/generators/website-context.test.ts +222 -0
  132. package/tests/generators/website-seo-quality.test.ts +246 -0
  133. package/tests/workflow/overview.test.ts +392 -0
  134. package/tests/workflow/website-strategy.test.ts +191 -0
@@ -1,191 +1,35 @@
1
1
  /**
2
- * Website templates for Next.js marketing sites
3
- * Generates SEO-ready Next.js App Router projects
2
+ * Website content templates for Next.js marketing sites
3
+ * Generates SEO-ready content pages with optional project context
4
+ * and strategy-driven marketing content
4
5
  */
5
6
 
6
- /**
7
- * Generate Next.js package.json
8
- */
9
- export function generateWebsitePackageJson(projectName: string): string {
10
- return `{
11
- "name": "${projectName}-website",
12
- "version": "1.0.0",
13
- "private": true,
14
- "scripts": {
15
- "dev": "next dev -p 3001",
16
- "build": "next build",
17
- "start": "next start -p 3001",
18
- "lint": "next lint",
19
- "test": "vitest run",
20
- "test:watch": "vitest",
21
- "typecheck": "tsc --noEmit"
22
- },
23
- "dependencies": {
24
- "next": "^14.1.0",
25
- "react": "^18.2.0",
26
- "react-dom": "^18.2.0",
27
- "lucide-react": "^0.312.0",
28
- "clsx": "^2.1.0",
29
- "tailwind-merge": "^2.2.0"
30
- },
31
- "devDependencies": {
32
- "@types/node": "^20.11.0",
33
- "@types/react": "^18.2.0",
34
- "@types/react-dom": "^18.2.0",
35
- "autoprefixer": "^10.4.17",
36
- "postcss": "^8.4.33",
37
- "tailwindcss": "^3.4.1",
38
- "typescript": "^5.3.3",
39
- "@testing-library/react": "^14.1.2",
40
- "@vitejs/plugin-react": "^4.2.1",
41
- "vitest": "^1.2.0",
42
- "jsdom": "^24.0.0"
43
- }
44
- }
45
- `;
46
- }
47
-
48
- /**
49
- * Generate Next.js config
50
- */
51
- export function generateNextConfig(): string {
52
- return `/** @type {import('next').NextConfig} */
53
- const nextConfig = {
54
- // Enable React Strict Mode for better development
55
- reactStrictMode: true,
56
-
57
- // Image optimization
58
- images: {
59
- domains: [],
60
- formats: ['image/avif', 'image/webp'],
61
- },
62
-
63
- // Disable x-powered-by header
64
- poweredByHeader: false,
65
-
66
- // Trailing slash config
67
- trailingSlash: false,
68
-
69
- // Headers for security
70
- async headers() {
71
- return [
72
- {
73
- source: '/:path*',
74
- headers: [
75
- {
76
- key: 'X-DNS-Prefetch-Control',
77
- value: 'on',
78
- },
79
- {
80
- key: 'X-Content-Type-Options',
81
- value: 'nosniff',
82
- },
83
- ],
84
- },
85
- ];
86
- },
87
- };
88
-
89
- module.exports = nextConfig;
90
- `;
91
- }
92
-
93
- /**
94
- * Generate website tsconfig.json
95
- */
96
- export function generateWebsiteTsconfig(): string {
97
- return `{
98
- "compilerOptions": {
99
- "target": "ES2017",
100
- "lib": ["dom", "dom.iterable", "esnext"],
101
- "allowJs": true,
102
- "skipLibCheck": true,
103
- "strict": true,
104
- "noEmit": true,
105
- "esModuleInterop": true,
106
- "module": "esnext",
107
- "moduleResolution": "bundler",
108
- "resolveJsonModule": true,
109
- "isolatedModules": true,
110
- "jsx": "preserve",
111
- "incremental": true,
112
- "plugins": [
113
- {
114
- "name": "next"
115
- }
116
- ],
117
- "paths": {
118
- "@/*": ["./src/*"]
119
- }
120
- },
121
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
122
- "exclude": ["node_modules"]
123
- }
124
- `;
125
- }
126
-
127
- /**
128
- * Generate Tailwind config for website
129
- */
130
- export function generateWebsiteTailwindConfig(): string {
131
- return `import type { Config } from 'tailwindcss';
132
-
133
- const config: Config = {
134
- content: [
135
- './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
136
- './src/components/**/*.{js,ts,jsx,tsx,mdx}',
137
- './src/app/**/*.{js,ts,jsx,tsx,mdx}',
138
- ],
139
- theme: {
140
- extend: {
141
- colors: {
142
- primary: {
143
- 50: '#f0f9ff',
144
- 100: '#e0f2fe',
145
- 200: '#bae6fd',
146
- 300: '#7dd3fc',
147
- 400: '#38bdf8',
148
- 500: '#0ea5e9',
149
- 600: '#0284c7',
150
- 700: '#0369a1',
151
- 800: '#075985',
152
- 900: '#0c4a6e',
153
- },
154
- },
155
- fontFamily: {
156
- sans: ['var(--font-inter)', 'system-ui', 'sans-serif'],
157
- },
158
- },
159
- },
160
- plugins: [],
161
- };
162
-
163
- export default config;
164
- `;
165
- }
166
-
167
- /**
168
- * Generate PostCSS config for website
169
- */
170
- export function generateWebsitePostcssConfig(): string {
171
- return `module.exports = {
172
- plugins: {
173
- tailwindcss: {},
174
- autoprefixer: {},
175
- },
176
- };
177
- `;
178
- }
7
+ import type { WebsiteContentContext } from '../website-context.js';
8
+ // Strategy data is accessed via context.strategy (WebsiteContentContext includes it)
179
9
 
180
10
  /**
181
11
  * Generate root layout.tsx with metadata
182
12
  */
183
- export function generateWebsiteLayout(projectName: string): string {
13
+ export function generateWebsiteLayout(
14
+ projectName: string,
15
+ context?: WebsiteContentContext
16
+ ): string {
184
17
  const title = projectName
185
18
  .split('-')
186
19
  .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
187
20
  .join(' ');
188
21
 
22
+ const strategy = context?.strategy;
23
+ const displayName = context?.productName || title;
24
+ const desc = strategy?.messaging.longDescription
25
+ || context?.description
26
+ || `${displayName} - Your modern web application`;
27
+
28
+ // SEO keywords from strategy or defaults
29
+ const keywords = strategy?.seoStrategy.primaryKeywords
30
+ ? strategy.seoStrategy.primaryKeywords.map(k => `'${escapeJsx(k)}'`).join(', ')
31
+ : `'${projectName}', 'web app', 'nextjs'`;
32
+
189
33
  return `import type { Metadata } from 'next';
190
34
  import { Inter } from 'next/font/google';
191
35
  import './globals.css';
@@ -195,27 +39,30 @@ const inter = Inter({
195
39
  variable: '--font-inter',
196
40
  });
197
41
 
42
+ const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://${projectName}.com';
43
+
198
44
  export const metadata: Metadata = {
45
+ metadataBase: new URL(BASE_URL),
199
46
  title: {
200
- default: '${title}',
201
- template: '%s | ${title}',
47
+ default: '${escapeJsx(displayName)}',
48
+ template: '%s | ${escapeJsx(displayName)}',
202
49
  },
203
- description: '${title} - Your modern web application',
204
- keywords: ['${projectName}', 'web app', 'nextjs'],
205
- authors: [{ name: '${title} Team' }],
206
- creator: '${title}',
50
+ description: '${escapeJsx(desc)}',
51
+ keywords: [${keywords}],
52
+ authors: [{ name: '${escapeJsx(displayName)} Team' }],
53
+ creator: '${escapeJsx(displayName)}',
207
54
  openGraph: {
208
55
  type: 'website',
209
56
  locale: 'en_US',
210
- url: 'https://${projectName}.com',
211
- siteName: '${title}',
212
- title: '${title}',
213
- description: '${title} - Your modern web application',
57
+ url: BASE_URL,
58
+ siteName: '${escapeJsx(displayName)}',
59
+ title: '${escapeJsx(displayName)}',
60
+ description: '${escapeJsx(desc)}',
214
61
  },
215
62
  twitter: {
216
63
  card: 'summary_large_image',
217
- title: '${title}',
218
- description: '${title} - Your modern web application',
64
+ title: '${escapeJsx(displayName)}',
65
+ description: '${escapeJsx(desc)}',
219
66
  },
220
67
  robots: {
221
68
  index: true,
@@ -240,9 +87,16 @@ export default function RootLayout({
240
87
  }
241
88
 
242
89
  /**
243
- * Generate globals.css
90
+ * Generate globals.css with optional brand colors
244
91
  */
245
- export function generateWebsiteGlobalsCss(): string {
92
+ export function generateWebsiteGlobalsCss(
93
+ context?: WebsiteContentContext
94
+ ): string {
95
+ // Convert hex to HSL for CSS custom properties if brand color provided
96
+ const primaryHsl = context?.brand?.primaryColor
97
+ ? hexToHslString(context.brand.primaryColor)
98
+ : '199 89% 48%';
99
+
246
100
  return `@tailwind base;
247
101
  @tailwind components;
248
102
  @tailwind utilities;
@@ -251,7 +105,7 @@ export function generateWebsiteGlobalsCss(): string {
251
105
  :root {
252
106
  --background: 0 0% 100%;
253
107
  --foreground: 222.2 84% 4.9%;
254
- --primary: 199 89% 48%;
108
+ --primary: ${primaryHsl};
255
109
  --primary-foreground: 210 40% 98%;
256
110
  }
257
111
 
@@ -269,257 +123,417 @@ export function generateWebsiteGlobalsCss(): string {
269
123
  }
270
124
 
271
125
  /**
272
- * Generate landing page.tsx
126
+ * Generate landing page.tsx with optional context-driven content
127
+ * When strategy is available, uses strategy messaging, trust signals, and CTAs
273
128
  */
274
- export function generateWebsiteLandingPage(projectName: string): string {
129
+ export function generateWebsiteLandingPage(
130
+ projectName: string,
131
+ context?: WebsiteContentContext
132
+ ): string {
275
133
  const title = projectName
276
134
  .split('-')
277
135
  .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
278
136
  .join(' ');
279
137
 
138
+ const strategy = context?.strategy;
139
+ const displayName = context?.productName || title;
140
+
141
+ // Strategy-driven or context-driven hero
142
+ const headline = strategy?.messaging.headline || displayName;
143
+ const subheadline = strategy?.messaging.subheadline || '';
144
+ const heroText = strategy?.messaging.longDescription
145
+ ? escapeJsx(strategy.messaging.longDescription)
146
+ : context?.description
147
+ ? escapeJsx(context.description)
148
+ : null;
149
+
150
+ const features = context?.features && context.features.length > 0
151
+ ? context.features.slice(0, 6)
152
+ : null;
153
+
154
+ // CTAs from strategy or defaults
155
+ const primaryCtaText = strategy?.conversionStrategy.primaryCta.text || 'Get started';
156
+ const primaryCtaHref = strategy?.conversionStrategy.primaryCta.href || '/pricing';
157
+ const secondaryCtaText = strategy?.conversionStrategy.secondaryCta.text || 'Learn more';
158
+ const secondaryCtaHref = strategy?.conversionStrategy.secondaryCta.href || '/docs';
159
+
160
+ // Build hero paragraph
161
+ const heroParagraph = heroText
162
+ ? ` ${heroText}`
163
+ : ` {/* TODO: populate from project specification */}`;
164
+
165
+ // Build features array
166
+ const featuresBlock = features
167
+ ? features.map((f) =>
168
+ ` {\n title: '${escapeJsx(f.title)}',\n description: '${escapeJsx(f.description)}',\n }`
169
+ ).join(',\n')
170
+ : ` {\n title: 'Feature 1',\n description: '/* TODO: populate from project specification */',\n },\n {\n title: 'Feature 2',\n description: '/* TODO: populate from project specification */',\n },\n {\n title: 'Feature 3',\n description: '/* TODO: populate from project specification */',\n }`;
171
+
172
+ // Trust signals from strategy
173
+ const trustSignals = strategy?.conversionStrategy.trustSignals || [];
174
+ const trustSignalsBlock = trustSignals.length > 0
175
+ ? trustSignals.map(s => ` '${escapeJsx(s)}'`).join(',\n')
176
+ : '';
177
+
178
+ // Social proof from strategy
179
+ const socialProof = strategy?.conversionStrategy.socialProof || [];
180
+ const socialProofBlock = socialProof.length > 0
181
+ ? socialProof.map(s => ` '${escapeJsx(s)}'`).join(',\n')
182
+ : '';
183
+
184
+ // Build optional sections
185
+ const trustSection = trustSignals.length > 0 ? `
186
+ {/* Trust Signals */}
187
+ <section className="border-y border-gray-100 bg-gray-50 py-12">
188
+ <div className="container">
189
+ <div className="flex flex-wrap items-center justify-center gap-x-8 gap-y-4">
190
+ {[
191
+ ${trustSignalsBlock}
192
+ ].map((signal) => (
193
+ <p key={signal} className="text-sm font-medium text-gray-600">{signal}</p>
194
+ ))}
195
+ </div>
196
+ </div>
197
+ </section>
198
+ ` : '';
199
+
200
+ const socialProofSection = socialProof.length > 0 ? `
201
+ {/* Social Proof */}
202
+ <section className="py-16 sm:py-24">
203
+ <div className="container">
204
+ <h2 className="text-center text-3xl font-bold tracking-tight text-gray-900">
205
+ Trusted by teams everywhere
206
+ </h2>
207
+ <div className="mx-auto mt-12 grid max-w-4xl grid-cols-1 gap-8 md:grid-cols-2">
208
+ {[
209
+ ${socialProofBlock}
210
+ ].map((quote, i) => (
211
+ <blockquote key={i} className="rounded-2xl border border-gray-200 p-6">
212
+ <p className="text-gray-700">&ldquo;{quote}&rdquo;</p>
213
+ </blockquote>
214
+ ))}
215
+ </div>
216
+ </div>
217
+ </section>
218
+ ` : '';
219
+
220
+ // Metadata: strategy-driven or default
221
+ const metaTitle = strategy?.seoStrategy.titleTemplates?.home || 'Welcome';
222
+ const metaDesc = strategy?.seoStrategy.metaDescriptions?.home || `Welcome to ${displayName}`;
223
+
280
224
  return `import type { Metadata } from 'next';
281
225
  import Link from 'next/link';
226
+ import Header from '@/components/Header';
227
+ import Footer from '@/components/Footer';
228
+ import JsonLd from '@/components/JsonLd';
282
229
 
283
230
  export const metadata: Metadata = {
284
- title: 'Welcome',
285
- description: 'Welcome to ${title} - Your modern web application',
231
+ title: '${escapeJsx(metaTitle)}',
232
+ description: '${escapeJsx(metaDesc)}',
233
+ };
234
+
235
+ const ORG_SCHEMA = {
236
+ '@context': 'https://schema.org',
237
+ '@type': 'Organization',
238
+ name: '${escapeJsx(displayName)}',
239
+ url: process.env.NEXT_PUBLIC_SITE_URL || 'https://${projectName}.com',
240
+ };
241
+
242
+ const PRODUCT_SCHEMA = {
243
+ '@context': 'https://schema.org',
244
+ '@type': 'SoftwareApplication',
245
+ name: '${escapeJsx(displayName)}',
246
+ applicationCategory: 'BusinessApplication',
247
+ operatingSystem: 'Web',
286
248
  };
287
249
 
288
250
  export default function HomePage() {
289
251
  return (
290
- <main className="flex min-h-screen flex-col">
291
- {/* Hero Section */}
292
- <section className="relative overflow-hidden bg-gradient-to-b from-primary-50 to-white py-20 sm:py-32">
293
- <div className="container">
294
- <div className="mx-auto max-w-2xl text-center">
295
- <h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
296
- ${title}
297
- </h1>
298
- <p className="mt-6 text-lg leading-8 text-gray-600">
299
- Build something amazing with our modern, scalable platform.
300
- Get started today and see the difference.
301
- </p>
302
- <div className="mt-10 flex items-center justify-center gap-x-6">
303
- <Link
304
- href="/pricing"
305
- className="rounded-md bg-primary-600 px-6 py-3 text-sm font-semibold text-white shadow-sm hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600"
306
- >
307
- Get started
308
- </Link>
309
- <Link
310
- href="/docs"
311
- className="text-sm font-semibold leading-6 text-gray-900 hover:text-primary-600"
312
- >
313
- Learn more <span aria-hidden="true">-&gt;</span>
314
- </Link>
252
+ <>
253
+ <Header />
254
+ <JsonLd schema={ORG_SCHEMA} />
255
+ <JsonLd schema={PRODUCT_SCHEMA} />
256
+ <main className="flex min-h-screen flex-col">
257
+ {/* Hero Section */}
258
+ <section className="relative overflow-hidden bg-gradient-to-b from-primary-50 to-white py-20 sm:py-32">
259
+ <div className="container">
260
+ <div className="mx-auto max-w-2xl text-center">
261
+ <h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
262
+ ${escapeJsx(headline)}
263
+ </h1>
264
+ ${subheadline ? ` <p className="mt-4 text-xl font-medium text-primary-600">\n ${escapeJsx(subheadline)}\n </p>` : ''}
265
+ <p className="mt-6 text-lg leading-8 text-gray-600">
266
+ ${heroParagraph}
267
+ </p>
268
+ <div className="mt-10 flex items-center justify-center gap-x-6">
269
+ <Link
270
+ href="${escapeJsx(primaryCtaHref)}"
271
+ className="rounded-md bg-primary-600 px-6 py-3 text-sm font-semibold text-white shadow-sm hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600"
272
+ >
273
+ ${escapeJsx(primaryCtaText)}
274
+ </Link>
275
+ <Link
276
+ href="${escapeJsx(secondaryCtaHref)}"
277
+ className="text-sm font-semibold leading-6 text-gray-900 hover:text-primary-600"
278
+ >
279
+ ${escapeJsx(secondaryCtaText)} <span aria-hidden="true">-&gt;</span>
280
+ </Link>
281
+ </div>
315
282
  </div>
316
283
  </div>
317
- </div>
318
- </section>
319
-
320
- {/* Features Section */}
321
- <section className="py-20 sm:py-32">
322
- <div className="container">
323
- <div className="mx-auto max-w-2xl text-center">
324
- <h2 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
325
- Everything you need
284
+ </section>
285
+ ${trustSection}
286
+ {/* Features Section */}
287
+ <section id="features" className="py-20 sm:py-32">
288
+ <div className="container">
289
+ <div className="mx-auto max-w-2xl text-center">
290
+ <h2 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
291
+ Everything you need
292
+ </h2>
293
+ <p className="mt-4 text-lg text-gray-600">
294
+ {/* TODO: populate section subtitle from project specification */}
295
+ </p>
296
+ </div>
297
+ <div className="mx-auto mt-16 max-w-5xl">
298
+ <div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
299
+ {[
300
+ ${featuresBlock}
301
+ ].map((feature) => (
302
+ <div
303
+ key={feature.title}
304
+ className="rounded-2xl border border-gray-200 p-8"
305
+ >
306
+ <h3 className="text-lg font-semibold text-gray-900">
307
+ {feature.title}
308
+ </h3>
309
+ <p className="mt-2 text-gray-600">{feature.description}</p>
310
+ </div>
311
+ ))}
312
+ </div>
313
+ </div>
314
+ </div>
315
+ </section>
316
+ ${socialProofSection}
317
+ {/* CTA Section */}
318
+ <section className="bg-primary-600 py-16 sm:py-24">
319
+ <div className="container text-center">
320
+ <h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">
321
+ Ready to get started?
326
322
  </h2>
327
- <p className="mt-4 text-lg text-gray-600">
328
- All the features you need to build amazing products.
323
+ <p className="mt-4 text-lg text-primary-100">
324
+ ${strategy?.messaging.elevatorPitch ? escapeJsx(strategy.messaging.elevatorPitch) : 'Start building today.'}
329
325
  </p>
330
- </div>
331
- <div className="mx-auto mt-16 max-w-5xl">
332
- <div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
333
- {[
334
- {
335
- title: 'Fast',
336
- description: 'Optimized for speed and performance.',
337
- },
338
- {
339
- title: 'Secure',
340
- description: 'Built with security best practices.',
341
- },
342
- {
343
- title: 'Scalable',
344
- description: 'Grows with your business needs.',
345
- },
346
- ].map((feature) => (
347
- <div
348
- key={feature.title}
349
- className="rounded-2xl border border-gray-200 p-8"
350
- >
351
- <h3 className="text-lg font-semibold text-gray-900">
352
- {feature.title}
353
- </h3>
354
- <p className="mt-2 text-gray-600">{feature.description}</p>
355
- </div>
356
- ))}
326
+ <div className="mt-8">
327
+ <Link
328
+ href="${escapeJsx(primaryCtaHref)}"
329
+ className="rounded-md bg-white px-6 py-3 text-sm font-semibold text-primary-600 shadow-sm hover:bg-primary-50"
330
+ >
331
+ ${escapeJsx(primaryCtaText)}
332
+ </Link>
357
333
  </div>
358
334
  </div>
359
- </div>
360
- </section>
361
-
362
- {/* Footer */}
363
- <footer className="border-t border-gray-200 py-12">
364
- <div className="container">
365
- <p className="text-center text-sm text-gray-500">
366
- &copy; {new Date().getFullYear()} ${title}. All rights reserved.
367
- </p>
368
- </div>
369
- </footer>
370
- </main>
335
+ </section>
336
+ </main>
337
+ <Footer />
338
+ </>
371
339
  );
372
340
  }
373
341
  `;
374
342
  }
375
343
 
376
344
  /**
377
- * Generate pricing page
345
+ * Generate pricing page with optional context-driven tiers and FAQ
378
346
  */
379
- export function generateWebsitePricingPage(projectName: string): string {
347
+ export function generateWebsitePricingPage(
348
+ projectName: string,
349
+ context?: WebsiteContentContext
350
+ ): string {
380
351
  const title = projectName
381
352
  .split('-')
382
353
  .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
383
354
  .join(' ');
384
355
 
385
- return `import type { Metadata } from 'next';
386
-
387
- export const metadata: Metadata = {
388
- title: 'Pricing',
389
- description: 'Choose the perfect plan for your needs - ${title}',
390
- };
391
-
392
- const tiers = [
393
- {
394
- name: 'Free',
395
- price: '$0',
396
- description: 'Perfect for getting started',
397
- features: ['Up to 3 projects', 'Basic support', 'Community access'],
356
+ const strategy = context?.strategy;
357
+ const displayName = context?.productName || title;
358
+ const tiers = context?.pricing && context.pricing.length > 0
359
+ ? context.pricing
360
+ : null;
361
+
362
+ // Build tiers array
363
+ const tiersBlock = tiers
364
+ ? tiers.map((t) => {
365
+ const featuresStr = t.features.map((f) => ` '${escapeJsx(f)}'`).join(',\n');
366
+ return ` {
367
+ name: '${escapeJsx(t.name)}',
368
+ price: '${escapeJsx(t.price)}',
369
+ description: '${escapeJsx(t.description)}',
370
+ features: [
371
+ ${featuresStr}
372
+ ],
373
+ cta: '${escapeJsx(t.cta)}',
374
+ featured: ${t.featured ? 'true' : 'false'},
375
+ }`;
376
+ }).join(',\n')
377
+ : ` {
378
+ name: '/* TODO: tier name */',
379
+ price: '/* TODO */',
380
+ description: '/* TODO: populate from project specification */',
381
+ features: ['/* TODO: populate from project specification */'],
398
382
  cta: 'Get started',
399
383
  featured: false,
400
384
  },
401
385
  {
402
- name: 'Pro',
403
- price: '$29',
404
- description: 'For growing teams',
405
- features: [
406
- 'Unlimited projects',
407
- 'Priority support',
408
- 'Advanced analytics',
409
- 'Custom integrations',
410
- ],
386
+ name: '/* TODO: tier name */',
387
+ price: '/* TODO */',
388
+ description: '/* TODO: populate from project specification */',
389
+ features: ['/* TODO: populate from project specification */'],
411
390
  cta: 'Start free trial',
412
391
  featured: true,
413
392
  },
414
393
  {
415
- name: 'Enterprise',
416
- price: 'Custom',
417
- description: 'For large organizations',
418
- features: [
419
- 'Everything in Pro',
420
- 'Dedicated support',
421
- 'SLA guarantee',
422
- 'Custom contracts',
423
- ],
394
+ name: '/* TODO: tier name */',
395
+ price: '/* TODO */',
396
+ description: '/* TODO: populate from project specification */',
397
+ features: ['/* TODO: populate from project specification */'],
424
398
  cta: 'Contact sales',
425
399
  featured: false,
426
- },
400
+ }`;
401
+
402
+ // Pricing metadata from strategy or defaults
403
+ const metaTitle = strategy?.seoStrategy.titleTemplates?.pricing || 'Pricing';
404
+ const metaDesc = strategy?.seoStrategy.metaDescriptions?.pricing || `Choose the perfect plan for your needs - ${displayName}`;
405
+
406
+ // Enterprise CTA from strategy
407
+ const enterpriseCtaText = strategy?.conversionStrategy.primaryCta.text || 'Contact Sales';
408
+
409
+ return `import type { Metadata } from 'next';
410
+ import Link from 'next/link';
411
+ import Header from '@/components/Header';
412
+ import Footer from '@/components/Footer';
413
+
414
+ export const metadata: Metadata = {
415
+ title: '${escapeJsx(metaTitle)}',
416
+ description: '${escapeJsx(metaDesc)}',
417
+ };
418
+
419
+ const tiers = [
420
+ ${tiersBlock}
427
421
  ];
428
422
 
429
423
  export default function PricingPage() {
430
424
  return (
431
- <main className="py-20 sm:py-32">
432
- <div className="container">
433
- <div className="mx-auto max-w-2xl text-center">
434
- <h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl">
435
- Simple, transparent pricing
436
- </h1>
437
- <p className="mt-6 text-lg text-gray-600">
438
- Choose the plan that works best for you.
439
- </p>
440
- </div>
425
+ <>
426
+ <Header />
427
+ <main className="py-20 sm:py-32">
428
+ <div className="container">
429
+ <div className="mx-auto max-w-2xl text-center">
430
+ <h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl">
431
+ Simple, transparent pricing
432
+ </h1>
433
+ <p className="mt-6 text-lg text-gray-600">
434
+ Choose the plan that works best for you.
435
+ </p>
436
+ </div>
441
437
 
442
- <div className="mx-auto mt-16 grid max-w-lg grid-cols-1 gap-8 lg:max-w-5xl lg:grid-cols-3">
443
- {tiers.map((tier) => (
444
- <div
445
- key={tier.name}
446
- className={\`rounded-2xl p-8 \${
447
- tier.featured
448
- ? 'bg-primary-600 text-white ring-2 ring-primary-600'
449
- : 'border border-gray-200 bg-white'
450
- }\`}
451
- >
452
- <h2
453
- className={\`text-lg font-semibold \${
454
- tier.featured ? 'text-white' : 'text-gray-900'
455
- }\`}
456
- >
457
- {tier.name}
458
- </h2>
459
- <p
460
- className={\`mt-2 text-sm \${
461
- tier.featured ? 'text-primary-100' : 'text-gray-600'
438
+ <div className="mx-auto mt-16 grid max-w-lg grid-cols-1 gap-8 lg:max-w-5xl lg:grid-cols-3">
439
+ {tiers.map((tier) => (
440
+ <div
441
+ key={tier.name}
442
+ className={\`rounded-2xl p-8 \${
443
+ tier.featured
444
+ ? 'bg-primary-600 text-white ring-2 ring-primary-600'
445
+ : 'border border-gray-200 bg-white'
462
446
  }\`}
463
447
  >
464
- {tier.description}
465
- </p>
466
- <p className="mt-6">
467
- <span
468
- className={\`text-4xl font-bold \${
448
+ <h2
449
+ className={\`text-lg font-semibold \${
469
450
  tier.featured ? 'text-white' : 'text-gray-900'
470
451
  }\`}
471
452
  >
472
- {tier.price}
473
- </span>
474
- {tier.price !== 'Custom' && (
453
+ {tier.name}
454
+ </h2>
455
+ <p
456
+ className={\`mt-2 text-sm \${
457
+ tier.featured ? 'text-primary-100' : 'text-gray-600'
458
+ }\`}
459
+ >
460
+ {tier.description}
461
+ </p>
462
+ <p className="mt-6">
475
463
  <span
476
- className={\`text-sm \${
477
- tier.featured ? 'text-primary-100' : 'text-gray-600'
464
+ className={\`text-4xl font-bold \${
465
+ tier.featured ? 'text-white' : 'text-gray-900'
478
466
  }\`}
479
467
  >
480
- /month
468
+ {tier.price}
481
469
  </span>
482
- )}
483
- </p>
484
- <ul className="mt-8 space-y-4">
485
- {tier.features.map((feature) => (
486
- <li
487
- key={feature}
488
- className={\`flex text-sm \${
489
- tier.featured ? 'text-primary-100' : 'text-gray-600'
490
- }\`}
491
- >
492
- <svg
493
- className={\`h-5 w-5 flex-shrink-0 \${
494
- tier.featured ? 'text-white' : 'text-primary-600'
470
+ {tier.price !== 'Custom' && (
471
+ <span
472
+ className={\`text-sm \${
473
+ tier.featured ? 'text-primary-100' : 'text-gray-600'
495
474
  }\`}
496
- viewBox="0 0 20 20"
497
- fill="currentColor"
498
475
  >
499
- <path
500
- fillRule="evenodd"
501
- d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
502
- clipRule="evenodd"
503
- />
504
- </svg>
505
- <span className="ml-3">{feature}</span>
506
- </li>
507
- ))}
508
- </ul>
509
- <button
510
- className={\`mt-8 w-full rounded-md px-4 py-2 text-sm font-semibold \${
511
- tier.featured
512
- ? 'bg-white text-primary-600 hover:bg-primary-50'
513
- : 'bg-primary-600 text-white hover:bg-primary-500'
514
- }\`}
515
- >
516
- {tier.cta}
517
- </button>
518
- </div>
519
- ))}
476
+ /month
477
+ </span>
478
+ )}
479
+ </p>
480
+ <ul className="mt-8 space-y-4">
481
+ {tier.features.map((feature) => (
482
+ <li
483
+ key={feature}
484
+ className={\`flex text-sm \${
485
+ tier.featured ? 'text-primary-100' : 'text-gray-600'
486
+ }\`}
487
+ >
488
+ <svg
489
+ className={\`h-5 w-5 flex-shrink-0 \${
490
+ tier.featured ? 'text-white' : 'text-primary-600'
491
+ }\`}
492
+ viewBox="0 0 20 20"
493
+ fill="currentColor"
494
+ >
495
+ <path
496
+ fillRule="evenodd"
497
+ d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
498
+ clipRule="evenodd"
499
+ />
500
+ </svg>
501
+ <span className="ml-3">{feature}</span>
502
+ </li>
503
+ ))}
504
+ </ul>
505
+ <button
506
+ className={\`mt-8 w-full rounded-md px-4 py-2 text-sm font-semibold \${
507
+ tier.featured
508
+ ? 'bg-white text-primary-600 hover:bg-primary-50'
509
+ : 'bg-primary-600 text-white hover:bg-primary-500'
510
+ }\`}
511
+ >
512
+ {tier.cta}
513
+ </button>
514
+ </div>
515
+ ))}
516
+ </div>
517
+
518
+ {/* Enterprise CTA */}
519
+ <div className="mx-auto mt-16 max-w-2xl text-center">
520
+ <h2 className="text-2xl font-bold text-gray-900">
521
+ Need a custom plan?
522
+ </h2>
523
+ <p className="mt-4 text-gray-600">
524
+ Contact our sales team for enterprise pricing and custom solutions.
525
+ </p>
526
+ <Link
527
+ href="/contact"
528
+ className="mt-6 inline-block rounded-md border border-primary-600 px-6 py-3 text-sm font-semibold text-primary-600 hover:bg-primary-50"
529
+ >
530
+ ${escapeJsx(enterpriseCtaText)}
531
+ </Link>
532
+ </div>
520
533
  </div>
521
- </div>
522
- </main>
534
+ </main>
535
+ <Footer />
536
+ </>
523
537
  );
524
538
  }
525
539
  `;
@@ -587,55 +601,6 @@ export default function robots(): MetadataRoute.Robots {
587
601
  `;
588
602
  }
589
603
 
590
- /**
591
- * Generate website Dockerfile
592
- */
593
- export function generateWebsiteDockerfile(): string {
594
- return `# Build stage
595
- FROM node:20-alpine AS builder
596
-
597
- WORKDIR /app
598
-
599
- # Copy package files
600
- COPY package*.json ./
601
-
602
- # Install dependencies
603
- RUN npm ci
604
-
605
- # Copy source
606
- COPY . .
607
-
608
- # Build
609
- RUN npm run build
610
-
611
- # Production stage
612
- FROM node:20-alpine AS runner
613
-
614
- WORKDIR /app
615
-
616
- ENV NODE_ENV=production
617
- ENV NEXT_TELEMETRY_DISABLED=1
618
-
619
- # Create non-root user
620
- RUN addgroup --system --gid 1001 nodejs
621
- RUN adduser --system --uid 1001 nextjs
622
-
623
- # Copy built assets
624
- COPY --from=builder /app/public ./public
625
- COPY --from=builder /app/.next/standalone ./
626
- COPY --from=builder /app/.next/static ./.next/static
627
-
628
- USER nextjs
629
-
630
- EXPOSE 3000
631
-
632
- ENV PORT=3000
633
- ENV HOSTNAME="0.0.0.0"
634
-
635
- CMD ["node", "server.js"]
636
- `;
637
- }
638
-
639
604
  /**
640
605
  * Generate website README
641
606
  */
@@ -700,22 +665,29 @@ content/
700
665
  }
701
666
 
702
667
  /**
703
- * Generate website spec JSON
668
+ * Generate website spec JSON with optional context
704
669
  */
705
- export function generateWebsiteSpec(projectName: string): string {
670
+ export function generateWebsiteSpec(
671
+ projectName: string,
672
+ context?: WebsiteContentContext
673
+ ): string {
706
674
  const title = projectName
707
675
  .split('-')
708
676
  .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
709
677
  .join(' ');
710
678
 
679
+ const displayName = context?.productName || title;
680
+ const tagline = context?.tagline || context?.description || 'Build something amazing';
681
+ const primaryColor = context?.brand?.primaryColor || '#0ea5e9';
682
+
711
683
  return JSON.stringify(
712
684
  {
713
685
  version: '1.0',
714
686
  brand: {
715
- name: title,
716
- tagline: 'Build something amazing',
687
+ name: displayName,
688
+ tagline,
717
689
  colors: {
718
- primary: '#0ea5e9',
690
+ primary: primaryColor,
719
691
  secondary: '#64748b',
720
692
  accent: '#f59e0b',
721
693
  background: '#ffffff',
@@ -727,8 +699,8 @@ export function generateWebsiteSpec(projectName: string): string {
727
699
  },
728
700
  },
729
701
  seo: {
730
- title: title,
731
- description: `${title} - Your modern web application`,
702
+ title: displayName,
703
+ description: context?.description || `${displayName} - Your modern web application`,
732
704
  keywords: [projectName, 'web app', 'nextjs', 'saas'],
733
705
  locale: 'en_US',
734
706
  },
@@ -754,58 +726,6 @@ export function generateWebsiteSpec(projectName: string): string {
754
726
  );
755
727
  }
756
728
 
757
- /**
758
- * Generate vitest config for website
759
- */
760
- export function generateWebsiteVitestConfig(): string {
761
- return `import { defineConfig } from 'vitest/config';
762
- import react from '@vitejs/plugin-react';
763
- import path from 'path';
764
-
765
- export default defineConfig({
766
- plugins: [react()],
767
- test: {
768
- environment: 'jsdom',
769
- include: ['**/*.test.{ts,tsx}'],
770
- globals: true,
771
- setupFiles: ['./tests/setup.ts'],
772
- },
773
- resolve: {
774
- alias: {
775
- '@': path.resolve(__dirname, './src'),
776
- },
777
- },
778
- });
779
- `;
780
- }
781
-
782
- /**
783
- * Generate vitest setup for website
784
- */
785
- export function generateWebsiteVitestSetup(): string {
786
- return `import '@testing-library/jest-dom';
787
-
788
- // Mock next/navigation
789
- vi.mock('next/navigation', () => ({
790
- useRouter: () => ({
791
- push: vi.fn(),
792
- replace: vi.fn(),
793
- prefetch: vi.fn(),
794
- }),
795
- useSearchParams: () => new URLSearchParams(),
796
- usePathname: () => '/',
797
- }));
798
-
799
- // Mock next/image
800
- vi.mock('next/image', () => ({
801
- default: (props: Record<string, unknown>) => {
802
- // eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text
803
- return <img {...props} />;
804
- },
805
- }));
806
- `;
807
- }
808
-
809
729
  /**
810
730
  * Generate sample test for website
811
731
  */
@@ -830,13 +750,6 @@ describe('HomePage', () => {
830
750
  expect(screen.getByRole('link', { name: /get started/i })).toBeInTheDocument();
831
751
  expect(screen.getByRole('link', { name: /learn more/i })).toBeInTheDocument();
832
752
  });
833
-
834
- it('renders feature cards', () => {
835
- render(<HomePage />);
836
- expect(screen.getByText('Fast')).toBeInTheDocument();
837
- expect(screen.getByText('Secure')).toBeInTheDocument();
838
- expect(screen.getByText('Scalable')).toBeInTheDocument();
839
- });
840
753
  });
841
754
  `;
842
755
  }
@@ -894,13 +807,46 @@ export default function BlogPage() {
894
807
  }
895
808
 
896
809
  /**
897
- * Generate Next.js environment declaration
810
+ * Escape a string for safe use inside JSX template literals
898
811
  */
899
- export function generateWebsiteNextEnv(): string {
900
- return `/// <reference types="next" />
901
- /// <reference types="next/image-types/global" />
812
+ function escapeJsx(str: string): string {
813
+ return str
814
+ .replace(/\\/g, '\\\\')
815
+ .replace(/'/g, "\\'")
816
+ .replace(/`/g, '\\`')
817
+ .replace(/\$/g, '\\$');
818
+ }
902
819
 
903
- // NOTE: This file should not be edited
904
- // see https://nextjs.org/docs/basic-features/typescript for more information.
905
- `;
820
+ /**
821
+ * Convert hex color to HSL string for CSS custom properties
822
+ * Returns format: "H S% L%"
823
+ */
824
+ function hexToHslString(hex: string): string {
825
+ // Remove # prefix
826
+ const h = hex.replace('#', '');
827
+ const r = parseInt(h.substring(0, 2), 16) / 255;
828
+ const g = parseInt(h.substring(2, 4), 16) / 255;
829
+ const b = parseInt(h.substring(4, 6), 16) / 255;
830
+
831
+ const max = Math.max(r, g, b);
832
+ const min = Math.min(r, g, b);
833
+ const l = (max + min) / 2;
834
+
835
+ if (max === min) {
836
+ return `0 0% ${Math.round(l * 100)}%`;
837
+ }
838
+
839
+ const d = max - min;
840
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
841
+
842
+ let hue = 0;
843
+ if (max === r) {
844
+ hue = ((g - b) / d + (g < b ? 6 : 0)) / 6;
845
+ } else if (max === g) {
846
+ hue = ((b - r) / d + 2) / 6;
847
+ } else {
848
+ hue = ((r - g) / d + 4) / 6;
849
+ }
850
+
851
+ return `${Math.round(hue * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
906
852
  }