popeye-cli 1.4.7 → 1.6.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 (214) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +264 -63
  3. package/dist/adapters/gemini.d.ts +1 -0
  4. package/dist/adapters/gemini.d.ts.map +1 -1
  5. package/dist/adapters/gemini.js +9 -4
  6. package/dist/adapters/gemini.js.map +1 -1
  7. package/dist/adapters/grok.d.ts +1 -0
  8. package/dist/adapters/grok.d.ts.map +1 -1
  9. package/dist/adapters/grok.js +9 -4
  10. package/dist/adapters/grok.js.map +1 -1
  11. package/dist/adapters/openai.d.ts +1 -1
  12. package/dist/adapters/openai.d.ts.map +1 -1
  13. package/dist/adapters/openai.js +35 -9
  14. package/dist/adapters/openai.js.map +1 -1
  15. package/dist/cli/commands/create.d.ts.map +1 -1
  16. package/dist/cli/commands/create.js +54 -4
  17. package/dist/cli/commands/create.js.map +1 -1
  18. package/dist/cli/interactive.d.ts +29 -0
  19. package/dist/cli/interactive.d.ts.map +1 -1
  20. package/dist/cli/interactive.js +132 -7
  21. package/dist/cli/interactive.js.map +1 -1
  22. package/dist/generators/all.d.ts +8 -2
  23. package/dist/generators/all.d.ts.map +1 -1
  24. package/dist/generators/all.js +37 -316
  25. package/dist/generators/all.js.map +1 -1
  26. package/dist/generators/doc-parser.d.ts +64 -0
  27. package/dist/generators/doc-parser.d.ts.map +1 -0
  28. package/dist/generators/doc-parser.js +407 -0
  29. package/dist/generators/doc-parser.js.map +1 -0
  30. package/dist/generators/frontend-design-analyzer.d.ts +30 -0
  31. package/dist/generators/frontend-design-analyzer.d.ts.map +1 -0
  32. package/dist/generators/frontend-design-analyzer.js +208 -0
  33. package/dist/generators/frontend-design-analyzer.js.map +1 -0
  34. package/dist/generators/shared-packages.d.ts +45 -0
  35. package/dist/generators/shared-packages.d.ts.map +1 -0
  36. package/dist/generators/shared-packages.js +456 -0
  37. package/dist/generators/shared-packages.js.map +1 -0
  38. package/dist/generators/templates/index.d.ts +8 -0
  39. package/dist/generators/templates/index.d.ts.map +1 -1
  40. package/dist/generators/templates/index.js +8 -0
  41. package/dist/generators/templates/index.js.map +1 -1
  42. package/dist/generators/templates/website-components.d.ts +33 -0
  43. package/dist/generators/templates/website-components.d.ts.map +1 -0
  44. package/dist/generators/templates/website-components.js +303 -0
  45. package/dist/generators/templates/website-components.js.map +1 -0
  46. package/dist/generators/templates/website-config.d.ts +55 -0
  47. package/dist/generators/templates/website-config.d.ts.map +1 -0
  48. package/dist/generators/templates/website-config.js +425 -0
  49. package/dist/generators/templates/website-config.js.map +1 -0
  50. package/dist/generators/templates/website-conversion.d.ts +27 -0
  51. package/dist/generators/templates/website-conversion.d.ts.map +1 -0
  52. package/dist/generators/templates/website-conversion.js +326 -0
  53. package/dist/generators/templates/website-conversion.js.map +1 -0
  54. package/dist/generators/templates/website-landing.d.ts +24 -0
  55. package/dist/generators/templates/website-landing.d.ts.map +1 -0
  56. package/dist/generators/templates/website-landing.js +276 -0
  57. package/dist/generators/templates/website-landing.js.map +1 -0
  58. package/dist/generators/templates/website-layout.d.ts +42 -0
  59. package/dist/generators/templates/website-layout.d.ts.map +1 -0
  60. package/dist/generators/templates/website-layout.js +408 -0
  61. package/dist/generators/templates/website-layout.js.map +1 -0
  62. package/dist/generators/templates/website-pricing.d.ts +11 -0
  63. package/dist/generators/templates/website-pricing.d.ts.map +1 -0
  64. package/dist/generators/templates/website-pricing.js +313 -0
  65. package/dist/generators/templates/website-pricing.js.map +1 -0
  66. package/dist/generators/templates/website-sections.d.ts +102 -0
  67. package/dist/generators/templates/website-sections.d.ts.map +1 -0
  68. package/dist/generators/templates/website-sections.js +444 -0
  69. package/dist/generators/templates/website-sections.js.map +1 -0
  70. package/dist/generators/templates/website-seo.d.ts +76 -0
  71. package/dist/generators/templates/website-seo.d.ts.map +1 -0
  72. package/dist/generators/templates/website-seo.js +326 -0
  73. package/dist/generators/templates/website-seo.js.map +1 -0
  74. package/dist/generators/templates/website.d.ts +10 -83
  75. package/dist/generators/templates/website.d.ts.map +1 -1
  76. package/dist/generators/templates/website.js +12 -875
  77. package/dist/generators/templates/website.js.map +1 -1
  78. package/dist/generators/website-content-scanner.d.ts +37 -0
  79. package/dist/generators/website-content-scanner.d.ts.map +1 -0
  80. package/dist/generators/website-content-scanner.js +165 -0
  81. package/dist/generators/website-content-scanner.js.map +1 -0
  82. package/dist/generators/website-context.d.ts +119 -0
  83. package/dist/generators/website-context.d.ts.map +1 -0
  84. package/dist/generators/website-context.js +350 -0
  85. package/dist/generators/website-context.js.map +1 -0
  86. package/dist/generators/website-debug.d.ts +68 -0
  87. package/dist/generators/website-debug.d.ts.map +1 -0
  88. package/dist/generators/website-debug.js +93 -0
  89. package/dist/generators/website-debug.js.map +1 -0
  90. package/dist/generators/website.d.ts +5 -0
  91. package/dist/generators/website.d.ts.map +1 -1
  92. package/dist/generators/website.js +136 -11
  93. package/dist/generators/website.js.map +1 -1
  94. package/dist/generators/workspace-root.d.ts +27 -0
  95. package/dist/generators/workspace-root.d.ts.map +1 -0
  96. package/dist/generators/workspace-root.js +100 -0
  97. package/dist/generators/workspace-root.js.map +1 -0
  98. package/dist/state/index.d.ts +35 -0
  99. package/dist/state/index.d.ts.map +1 -1
  100. package/dist/state/index.js +40 -0
  101. package/dist/state/index.js.map +1 -1
  102. package/dist/types/consensus.d.ts +3 -0
  103. package/dist/types/consensus.d.ts.map +1 -1
  104. package/dist/types/consensus.js +1 -0
  105. package/dist/types/consensus.js.map +1 -1
  106. package/dist/types/website-strategy.d.ts +263 -0
  107. package/dist/types/website-strategy.d.ts.map +1 -0
  108. package/dist/types/website-strategy.js +105 -0
  109. package/dist/types/website-strategy.js.map +1 -0
  110. package/dist/types/workflow.d.ts +21 -0
  111. package/dist/types/workflow.d.ts.map +1 -1
  112. package/dist/types/workflow.js +8 -0
  113. package/dist/types/workflow.js.map +1 -1
  114. package/dist/upgrade/handlers.d.ts +15 -0
  115. package/dist/upgrade/handlers.d.ts.map +1 -1
  116. package/dist/upgrade/handlers.js +52 -0
  117. package/dist/upgrade/handlers.js.map +1 -1
  118. package/dist/workflow/auto-fix-bundler.d.ts +37 -0
  119. package/dist/workflow/auto-fix-bundler.d.ts.map +1 -0
  120. package/dist/workflow/auto-fix-bundler.js +320 -0
  121. package/dist/workflow/auto-fix-bundler.js.map +1 -0
  122. package/dist/workflow/auto-fix.d.ts.map +1 -1
  123. package/dist/workflow/auto-fix.js +10 -3
  124. package/dist/workflow/auto-fix.js.map +1 -1
  125. package/dist/workflow/consensus.d.ts.map +1 -1
  126. package/dist/workflow/consensus.js +2 -0
  127. package/dist/workflow/consensus.js.map +1 -1
  128. package/dist/workflow/execution-mode.d.ts.map +1 -1
  129. package/dist/workflow/execution-mode.js +18 -0
  130. package/dist/workflow/execution-mode.js.map +1 -1
  131. package/dist/workflow/index.d.ts +4 -0
  132. package/dist/workflow/index.d.ts.map +1 -1
  133. package/dist/workflow/index.js +37 -0
  134. package/dist/workflow/index.js.map +1 -1
  135. package/dist/workflow/overview.d.ts +89 -0
  136. package/dist/workflow/overview.d.ts.map +1 -0
  137. package/dist/workflow/overview.js +358 -0
  138. package/dist/workflow/overview.js.map +1 -0
  139. package/dist/workflow/plan-mode.d.ts +6 -4
  140. package/dist/workflow/plan-mode.d.ts.map +1 -1
  141. package/dist/workflow/plan-mode.js +148 -6
  142. package/dist/workflow/plan-mode.js.map +1 -1
  143. package/dist/workflow/website-strategy.d.ts +79 -0
  144. package/dist/workflow/website-strategy.d.ts.map +1 -0
  145. package/dist/workflow/website-strategy.js +310 -0
  146. package/dist/workflow/website-strategy.js.map +1 -0
  147. package/dist/workflow/website-updater.d.ts +17 -0
  148. package/dist/workflow/website-updater.d.ts.map +1 -0
  149. package/dist/workflow/website-updater.js +116 -0
  150. package/dist/workflow/website-updater.js.map +1 -0
  151. package/dist/workflow/workflow-logger.d.ts +1 -1
  152. package/dist/workflow/workflow-logger.d.ts.map +1 -1
  153. package/dist/workflow/workflow-logger.js.map +1 -1
  154. package/package.json +1 -1
  155. package/src/adapters/gemini.ts +10 -4
  156. package/src/adapters/grok.ts +10 -4
  157. package/src/adapters/openai.ts +38 -6
  158. package/src/cli/commands/create.ts +58 -4
  159. package/src/cli/interactive.ts +143 -7
  160. package/src/generators/all.ts +49 -332
  161. package/src/generators/doc-parser.ts +449 -0
  162. package/src/generators/frontend-design-analyzer.ts +261 -0
  163. package/src/generators/shared-packages.ts +500 -0
  164. package/src/generators/templates/index.ts +8 -0
  165. package/src/generators/templates/website-components.ts +330 -0
  166. package/src/generators/templates/website-config.ts +444 -0
  167. package/src/generators/templates/website-conversion.ts +341 -0
  168. package/src/generators/templates/website-landing.ts +331 -0
  169. package/src/generators/templates/website-layout.ts +443 -0
  170. package/src/generators/templates/website-pricing.ts +330 -0
  171. package/src/generators/templates/website-sections.ts +541 -0
  172. package/src/generators/templates/website-seo.ts +370 -0
  173. package/src/generators/templates/website.ts +38 -905
  174. package/src/generators/website-content-scanner.ts +208 -0
  175. package/src/generators/website-context.ts +493 -0
  176. package/src/generators/website-debug.ts +130 -0
  177. package/src/generators/website.ts +178 -20
  178. package/src/generators/workspace-root.ts +113 -0
  179. package/src/state/index.ts +56 -0
  180. package/src/types/consensus.ts +3 -0
  181. package/src/types/website-strategy.ts +243 -0
  182. package/src/types/workflow.ts +21 -0
  183. package/src/upgrade/handlers.ts +65 -0
  184. package/src/workflow/auto-fix-bundler.ts +392 -0
  185. package/src/workflow/auto-fix.ts +11 -3
  186. package/src/workflow/consensus.ts +2 -0
  187. package/src/workflow/execution-mode.ts +21 -0
  188. package/src/workflow/index.ts +37 -0
  189. package/src/workflow/overview.ts +475 -0
  190. package/src/workflow/plan-mode.ts +193 -8
  191. package/src/workflow/website-strategy.ts +379 -0
  192. package/src/workflow/website-updater.ts +142 -0
  193. package/src/workflow/workflow-logger.ts +1 -0
  194. package/tests/adapters/persona-switching.test.ts +63 -0
  195. package/tests/cli/project-naming.test.ts +136 -0
  196. package/tests/generators/doc-parser.test.ts +121 -0
  197. package/tests/generators/frontend-design-analyzer.test.ts +90 -0
  198. package/tests/generators/quality-gate.test.ts +183 -0
  199. package/tests/generators/shared-packages.test.ts +83 -0
  200. package/tests/generators/website-components.test.ts +159 -0
  201. package/tests/generators/website-config.test.ts +84 -0
  202. package/tests/generators/website-content-scanner.test.ts +181 -0
  203. package/tests/generators/website-context.test.ts +331 -0
  204. package/tests/generators/website-debug.test.ts +77 -0
  205. package/tests/generators/website-landing.test.ts +188 -0
  206. package/tests/generators/website-pricing.test.ts +98 -0
  207. package/tests/generators/website-sections.test.ts +245 -0
  208. package/tests/generators/website-seo-quality.test.ts +246 -0
  209. package/tests/generators/workspace-root.test.ts +105 -0
  210. package/tests/upgrade/handlers.test.ts +162 -0
  211. package/tests/workflow/auto-fix-bundler.test.ts +242 -0
  212. package/tests/workflow/overview.test.ts +392 -0
  213. package/tests/workflow/plan-mode.test.ts +111 -1
  214. package/tests/workflow/website-strategy.test.ts +246 -0
@@ -0,0 +1,541 @@
1
+ /**
2
+ * Reusable website section generators
3
+ * Pain Points, Value Proposition, How It Works, Stats/Proof Points, FAQ
4
+ * Each section is data-driven with graceful skip when data is missing
5
+ */
6
+
7
+ import type { WebsiteContentContext } from '../website-context.js';
8
+ import type { WebsiteStrategyDocument } from '../../types/website-strategy.js';
9
+
10
+ /**
11
+ * Escape a string for safe use inside JSX template literals
12
+ */
13
+ function escapeJsx(str: string): string {
14
+ return str
15
+ .replace(/\\/g, '\\\\')
16
+ .replace(/'/g, "\\'")
17
+ .replace(/`/g, '\\`')
18
+ .replace(/\$/g, '\\$');
19
+ }
20
+
21
+ /**
22
+ * Section render metadata for debug tracing
23
+ */
24
+ export interface SectionRenderInfo {
25
+ name: string;
26
+ dataSource: 'strategy' | 'docs' | 'defaults' | 'skipped';
27
+ itemCount: number;
28
+ }
29
+
30
+ /**
31
+ * Map a feature title to a lucide-react icon name by keyword matching
32
+ */
33
+ export function mapFeatureIcon(title: string): string {
34
+ const lower = title.toLowerCase();
35
+ const iconMap: Array<[RegExp, string]> = [
36
+ [/secur|auth|permission|access|lock|encrypt/, 'Shield'],
37
+ [/speed|fast|perform|optim/, 'Zap'],
38
+ [/api|integrat|connect|plugin/, 'Plug'],
39
+ [/search|find|discover|retriev/, 'Search'],
40
+ [/analyt|metric|monitor|dashboard/, 'BarChart3'],
41
+ [/data|database|storage|vector/, 'Database'],
42
+ [/team|collaborat|share|user/, 'Users'],
43
+ [/automat|workflow|pipeline/, 'GitBranch'],
44
+ [/cloud|deploy|server|host/, 'Cloud'],
45
+ [/scale|grow|expand/, 'TrendingUp'],
46
+ [/custom|config|setting/, 'Settings'],
47
+ [/document|doc|file|content/, 'FileText'],
48
+ [/test|quality|check|verify/, 'CheckCircle'],
49
+ [/ai|machine|learn|model|neural/, 'Brain'],
50
+ [/code|develop|build|engineer/, 'Code'],
51
+ [/email|message|notif|alert/, 'Bell'],
52
+ [/time|schedule|calendar|clock/, 'Clock'],
53
+ [/money|pay|bill|cost|pric/, 'CreditCard'],
54
+ [/global|world|international/, 'Globe'],
55
+ [/support|help|service/, 'Headphones'],
56
+ ];
57
+ for (const [pattern, icon] of iconMap) {
58
+ if (pattern.test(lower)) return icon;
59
+ }
60
+ return 'Star';
61
+ }
62
+
63
+ /**
64
+ * Check if a proof point string contains a numeric metric
65
+ * Numeric metrics are safe to display as stats; qualitative ones become badges
66
+ */
67
+ export function isNumericMetric(point: string): boolean {
68
+ return /\d+[%+KMB]|\d{2,}/.test(point);
69
+ }
70
+
71
+ /**
72
+ * Generate Pain Points section
73
+ * Data: strategy.icp.painPoints
74
+ * Skip: if painPoints is empty
75
+ */
76
+ export function generatePainPointsSection(
77
+ strategy?: WebsiteStrategyDocument
78
+ ): { jsx: string; info: SectionRenderInfo } {
79
+ const painPoints = strategy?.icp.painPoints || [];
80
+ if (painPoints.length === 0) {
81
+ return {
82
+ jsx: '',
83
+ info: { name: 'PainPoints', dataSource: 'skipped', itemCount: 0 },
84
+ };
85
+ }
86
+
87
+ const items = painPoints.slice(0, 3);
88
+ const icons = ['AlertTriangle', 'XCircle', 'AlertOctagon'];
89
+ const itemsStr = items
90
+ .map(
91
+ (point, i) =>
92
+ ` <div key="${i}" className="rounded-2xl bg-card p-8 text-center">
93
+ <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
94
+ <${icons[i % icons.length]} className="h-6 w-6 text-red-600" />
95
+ </div>
96
+ <p className="text-foreground font-medium">${escapeJsx(point)}</p>
97
+ </div>`
98
+ )
99
+ .join('\n');
100
+
101
+ const jsx = `
102
+ {/* Pain Points */}
103
+ <section className="bg-muted/50 py-20 sm:py-28">
104
+ <div className="container">
105
+ <h2 className="text-center text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
106
+ Sound familiar?
107
+ </h2>
108
+ <p className="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">
109
+ Common challenges that hold teams back
110
+ </p>
111
+ <div className="mx-auto mt-12 grid max-w-5xl grid-cols-1 gap-8 md:grid-cols-3">
112
+ ${itemsStr}
113
+ </div>
114
+ </div>
115
+ </section>
116
+ `;
117
+
118
+ return {
119
+ jsx,
120
+ info: { name: 'PainPoints', dataSource: 'strategy', itemCount: items.length },
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Generate Value Proposition / Differentiators section
126
+ * Data: strategy.positioning.differentiators + valueProposition
127
+ * Skip: if no differentiators
128
+ */
129
+ export function generateDifferentiatorsSection(
130
+ strategy?: WebsiteStrategyDocument
131
+ ): { jsx: string; info: SectionRenderInfo } {
132
+ const differentiators = strategy?.positioning.differentiators || [];
133
+ const valueProp = strategy?.positioning.valueProposition;
134
+ if (differentiators.length === 0 && !valueProp) {
135
+ return {
136
+ jsx: '',
137
+ info: { name: 'Differentiators', dataSource: 'skipped', itemCount: 0 },
138
+ };
139
+ }
140
+
141
+ const heading = valueProp
142
+ ? escapeJsx(valueProp)
143
+ : 'Why choose us';
144
+
145
+ const itemsStr = differentiators
146
+ .slice(0, 6)
147
+ .map(
148
+ (diff) =>
149
+ ` <div className="flex items-start gap-3">
150
+ <CheckCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-primary-600" />
151
+ <p className="text-foreground">${escapeJsx(diff)}</p>
152
+ </div>`
153
+ )
154
+ .join('\n');
155
+
156
+ const jsx = `
157
+ {/* Value Proposition */}
158
+ <section className="py-20 sm:py-28">
159
+ <div className="container">
160
+ <div className="mx-auto max-w-3xl text-center">
161
+ <h2 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
162
+ ${heading}
163
+ </h2>
164
+ </div>
165
+ <div className="mx-auto mt-12 grid max-w-3xl grid-cols-1 gap-6 sm:grid-cols-2">
166
+ ${itemsStr}
167
+ </div>
168
+ </div>
169
+ </section>
170
+ `;
171
+
172
+ return {
173
+ jsx,
174
+ info: { name: 'Differentiators', dataSource: 'strategy', itemCount: differentiators.length },
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Generate How It Works section
180
+ * Data: strategy siteArchitecture.pages[0].sections or defaults
181
+ * Always rendered (with defaults if no strategy)
182
+ */
183
+ export function generateHowItWorksSection(
184
+ strategy?: WebsiteStrategyDocument
185
+ ): { jsx: string; info: SectionRenderInfo } {
186
+ const defaultSteps = [
187
+ { title: 'Sign Up', description: 'Create your account in seconds' },
188
+ { title: 'Configure', description: 'Set up your workspace to match your needs' },
189
+ { title: 'Deploy', description: 'Go live and start seeing results' },
190
+ ];
191
+
192
+ const hasSections = strategy?.siteArchitecture.pages[0]?.sections;
193
+ const steps = hasSections && hasSections.length >= 3
194
+ ? hasSections.slice(0, 3).map((s, i) => ({
195
+ title: s.replace(/^(hero|features|cta|pricing|faq|testimonials)/i, '').trim() || defaultSteps[i].title,
196
+ description: defaultSteps[i].description,
197
+ }))
198
+ : defaultSteps;
199
+
200
+ const dataSource = hasSections ? 'strategy' : 'defaults';
201
+
202
+ const stepsStr = steps
203
+ .map(
204
+ (step, i) =>
205
+ ` <div className="relative text-center">
206
+ <div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary-600 text-lg font-bold text-white">
207
+ ${i + 1}
208
+ </div>
209
+ <h3 className="mt-4 text-lg font-semibold text-foreground">${escapeJsx(step.title)}</h3>
210
+ <p className="mt-2 text-muted-foreground">${escapeJsx(step.description)}</p>
211
+ </div>`
212
+ )
213
+ .join('\n');
214
+
215
+ const jsx = `
216
+ {/* How It Works */}
217
+ <section className="bg-muted/50 py-20 sm:py-28">
218
+ <div className="container">
219
+ <h2 className="text-center text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
220
+ How it works
221
+ </h2>
222
+ <div className="mx-auto mt-16 grid max-w-4xl grid-cols-1 gap-12 md:grid-cols-3">
223
+ ${stepsStr}
224
+ </div>
225
+ </div>
226
+ </section>
227
+ `;
228
+
229
+ return {
230
+ jsx,
231
+ info: { name: 'HowItWorks', dataSource: dataSource as 'strategy' | 'defaults', itemCount: steps.length },
232
+ };
233
+ }
234
+
235
+ /**
236
+ * Generate Stats / Proof Points section
237
+ * CRITICAL: Only show numeric metrics if they appear literally in docs/strategy.
238
+ * Qualitative points render as badges, NOT fake numbers.
239
+ */
240
+ export function generateStatsSection(
241
+ strategy?: WebsiteStrategyDocument
242
+ ): { jsx: string; info: SectionRenderInfo } {
243
+ const proofPoints = strategy?.positioning.proofPoints || [];
244
+ if (proofPoints.length === 0) {
245
+ return {
246
+ jsx: '',
247
+ info: { name: 'Stats', dataSource: 'skipped', itemCount: 0 },
248
+ };
249
+ }
250
+
251
+ const numericPoints = proofPoints.filter(isNumericMetric);
252
+ const qualitativePoints = proofPoints.filter((p) => !isNumericMetric(p));
253
+
254
+ let statsContent = '';
255
+
256
+ if (numericPoints.length > 0) {
257
+ const statsStr = numericPoints
258
+ .slice(0, 4)
259
+ .map(
260
+ (point) =>
261
+ ` <div className="text-center">
262
+ <p className="text-4xl font-bold text-primary-600">${escapeJsx(point)}</p>
263
+ </div>`
264
+ )
265
+ .join('\n');
266
+
267
+ statsContent += ` <div className="mx-auto mt-12 grid max-w-4xl grid-cols-2 gap-8 md:grid-cols-${Math.min(numericPoints.length, 4)}">
268
+ ${statsStr}
269
+ </div>\n`;
270
+ }
271
+
272
+ if (qualitativePoints.length > 0) {
273
+ const badgesStr = qualitativePoints
274
+ .slice(0, 6)
275
+ .map(
276
+ (point) =>
277
+ ` <span className="inline-flex items-center gap-1.5 rounded-full bg-primary-50 px-4 py-2 text-sm font-medium text-primary-700">
278
+ <CheckCircle className="h-4 w-4" />
279
+ ${escapeJsx(point)}
280
+ </span>`
281
+ )
282
+ .join('\n');
283
+
284
+ statsContent += ` <div className="mx-auto mt-8 flex max-w-4xl flex-wrap items-center justify-center gap-3">
285
+ ${badgesStr}
286
+ </div>\n`;
287
+ }
288
+
289
+ const jsx = `
290
+ {/* Proof Points */}
291
+ <section className="py-20 sm:py-28">
292
+ <div className="container">
293
+ <h2 className="text-center text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
294
+ Built for production
295
+ </h2>
296
+ ${statsContent}
297
+ </div>
298
+ </section>
299
+ `;
300
+
301
+ return {
302
+ jsx,
303
+ info: { name: 'Stats', dataSource: 'strategy', itemCount: proofPoints.length },
304
+ };
305
+ }
306
+
307
+ /**
308
+ * Generate Social Proof section
309
+ * Data: strategy.conversionStrategy.socialProof
310
+ * Skip: if empty
311
+ */
312
+ export function generateSocialProofSection(
313
+ strategy?: WebsiteStrategyDocument
314
+ ): { jsx: string; info: SectionRenderInfo } {
315
+ const socialProof = strategy?.conversionStrategy.socialProof || [];
316
+ if (socialProof.length === 0) {
317
+ return {
318
+ jsx: '',
319
+ info: { name: 'SocialProof', dataSource: 'skipped', itemCount: 0 },
320
+ };
321
+ }
322
+
323
+ const quotesStr = socialProof
324
+ .slice(0, 4)
325
+ .map(
326
+ (quote, i) =>
327
+ ` <blockquote key={${i}} className="rounded-2xl border border-border bg-card p-6 shadow-sm">
328
+ <div className="mb-4 text-4xl text-primary-300">&ldquo;</div>
329
+ <p className="text-foreground">${escapeJsx(quote)}</p>
330
+ <div className="mt-4 flex items-center gap-3">
331
+ <div className="h-10 w-10 rounded-full bg-muted" />
332
+ <div className="text-sm text-muted-foreground">Verified User</div>
333
+ </div>
334
+ </blockquote>`
335
+ )
336
+ .join('\n');
337
+
338
+ const jsx = `
339
+ {/* Social Proof */}
340
+ <section className="py-20 sm:py-28">
341
+ <div className="container">
342
+ <h2 className="text-center text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
343
+ Trusted by teams everywhere
344
+ </h2>
345
+ <div className="mx-auto mt-12 grid max-w-4xl grid-cols-1 gap-8 md:grid-cols-2">
346
+ ${quotesStr}
347
+ </div>
348
+ </div>
349
+ </section>
350
+ `;
351
+
352
+ return {
353
+ jsx,
354
+ info: { name: 'SocialProof', dataSource: 'strategy', itemCount: socialProof.length },
355
+ };
356
+ }
357
+
358
+ /**
359
+ * Generate Pricing Teaser section for landing page
360
+ * Data: context.pricing (tier names + starting prices)
361
+ * Skip: if no pricing tiers
362
+ */
363
+ export function generatePricingTeaserSection(
364
+ context?: WebsiteContentContext
365
+ ): { jsx: string; info: SectionRenderInfo } {
366
+ const tiers = context?.pricing;
367
+ if (!tiers || tiers.length === 0) {
368
+ return {
369
+ jsx: '',
370
+ info: { name: 'PricingTeaser', dataSource: 'skipped', itemCount: 0 },
371
+ };
372
+ }
373
+
374
+ const tiersStr = tiers
375
+ .slice(0, 3)
376
+ .map(
377
+ (tier) =>
378
+ ` <div className="rounded-2xl border ${tier.featured ? 'border-primary-600 ring-2 ring-primary-600' : 'border-border'} bg-card p-6 text-center">
379
+ <h3 className="text-lg font-semibold text-foreground">${escapeJsx(tier.name)}</h3>
380
+ <p className="mt-2 text-3xl font-bold text-foreground">${escapeJsx(tier.price)}</p>
381
+ ${tier.period ? `<p className="text-sm text-muted-foreground">${escapeJsx(tier.period)}</p>` : ''}
382
+ <p className="mt-2 text-sm text-muted-foreground">${escapeJsx(tier.description)}</p>
383
+ </div>`
384
+ )
385
+ .join('\n');
386
+
387
+ const jsx = `
388
+ {/* Pricing Teaser */}
389
+ <section className="bg-muted/50 py-20 sm:py-28">
390
+ <div className="container">
391
+ <h2 className="text-center text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
392
+ Simple, transparent pricing
393
+ </h2>
394
+ <div className="mx-auto mt-12 grid max-w-4xl grid-cols-1 gap-8 md:grid-cols-3">
395
+ ${tiersStr}
396
+ </div>
397
+ <div className="mt-8 text-center">
398
+ <Link
399
+ href="/pricing"
400
+ className="text-sm font-semibold text-primary-600 hover:text-primary-500"
401
+ >
402
+ View full pricing <span aria-hidden="true">&rarr;</span>
403
+ </Link>
404
+ </div>
405
+ </div>
406
+ </section>
407
+ `;
408
+
409
+ return {
410
+ jsx,
411
+ info: { name: 'PricingTeaser', dataSource: 'docs', itemCount: tiers.length },
412
+ };
413
+ }
414
+
415
+ /**
416
+ * Generate FAQ section from strategy objections
417
+ * Data: strategy.icp.objections rephrased as Q&A
418
+ * Skip: if no objections
419
+ * Note: Renders as client component with useState accordion
420
+ */
421
+ export function generateFaqSection(
422
+ strategy?: WebsiteStrategyDocument
423
+ ): { jsx: string; info: SectionRenderInfo; needsClientDirective: boolean } {
424
+ const objections = strategy?.icp.objections || [];
425
+ if (objections.length === 0) {
426
+ return {
427
+ jsx: '',
428
+ info: { name: 'FAQ', dataSource: 'skipped', itemCount: 0 },
429
+ needsClientDirective: false,
430
+ };
431
+ }
432
+
433
+ const faqItems = objections.slice(0, 6).map((obj) => {
434
+ // Convert objection to Q&A format
435
+ const question = obj.endsWith('?') ? obj : `${obj}?`;
436
+ return { question, answer: `We take this seriously. ${obj.replace(/\?$/, '')} is addressed through our robust platform design and industry best practices.` };
437
+ });
438
+
439
+ const jsx = `
440
+ {/* FAQ */}
441
+ <section id="faq" className="py-20 sm:py-28">
442
+ <div className="container">
443
+ <h2 className="text-center text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
444
+ Frequently asked questions
445
+ </h2>
446
+ <div className="mx-auto mt-12 max-w-3xl divide-y divide-border">
447
+ {faqItems.map((item, index) => (
448
+ <FaqItem key={index} question={item.question} answer={item.answer} />
449
+ ))}
450
+ </div>
451
+ </div>
452
+ </section>
453
+ `;
454
+
455
+ return {
456
+ jsx,
457
+ info: { name: 'FAQ', dataSource: 'strategy', itemCount: faqItems.length },
458
+ needsClientDirective: true,
459
+ };
460
+ }
461
+
462
+ /**
463
+ * Build FAQ items array declaration for the page component
464
+ */
465
+ export function buildFaqItemsDeclaration(
466
+ strategy?: WebsiteStrategyDocument
467
+ ): string {
468
+ const objections = strategy?.icp.objections || [];
469
+ if (objections.length === 0) return '';
470
+
471
+ const faqItems = objections.slice(0, 6).map((obj) => {
472
+ const question = obj.endsWith('?') ? obj : `${obj}?`;
473
+ return { question, answer: `We take this seriously. ${obj.replace(/\?$/, '')} is addressed through our robust platform design and industry best practices.` };
474
+ });
475
+
476
+ const itemsStr = faqItems
477
+ .map(
478
+ (item) =>
479
+ ` { question: '${escapeJsx(item.question)}', answer: '${escapeJsx(item.answer)}' }`
480
+ )
481
+ .join(',\n');
482
+
483
+ return `const faqItems = [\n${itemsStr}\n];\n`;
484
+ }
485
+
486
+ /**
487
+ * Generate the FaqItem client component code
488
+ */
489
+ export function generateFaqItemComponent(): string {
490
+ return `function FaqItem({ question, answer }: { question: string; answer: string }) {
491
+ const [open, setOpen] = useState(false);
492
+
493
+ return (
494
+ <div className="py-4">
495
+ <button
496
+ type="button"
497
+ className="flex w-full items-center justify-between text-left"
498
+ onClick={() => setOpen(!open)}
499
+ aria-expanded={open}
500
+ >
501
+ <span className="text-base font-medium text-foreground">{question}</span>
502
+ <ChevronDown className={\`h-5 w-5 text-muted-foreground transition-transform \${open ? 'rotate-180' : ''}\`} />
503
+ </button>
504
+ {open && (
505
+ <p className="mt-3 text-muted-foreground">{answer}</p>
506
+ )}
507
+ </div>
508
+ );
509
+ }`;
510
+ }
511
+
512
+ /**
513
+ * Build JSON-LD FAQ schema for SEO
514
+ */
515
+ export function buildFaqSchema(
516
+ strategy?: WebsiteStrategyDocument
517
+ ): string {
518
+ const objections = strategy?.icp.objections || [];
519
+ if (objections.length === 0) return '';
520
+
521
+ const faqItems = objections.slice(0, 6).map((obj) => {
522
+ const question = obj.endsWith('?') ? obj : `${obj}?`;
523
+ const answer = `We take this seriously. ${obj.replace(/\?$/, '')} is addressed through our robust platform design and industry best practices.`;
524
+ return { question, answer };
525
+ });
526
+
527
+ return `const FAQ_SCHEMA = {
528
+ '@context': 'https://schema.org',
529
+ '@type': 'FAQPage',
530
+ mainEntity: [
531
+ ${faqItems.map(item => ` {
532
+ '@type': 'Question',
533
+ name: '${escapeJsx(item.question)}',
534
+ acceptedAnswer: {
535
+ '@type': 'Answer',
536
+ text: '${escapeJsx(item.answer)}',
537
+ },
538
+ }`).join(',\n')}
539
+ ],
540
+ };`;
541
+ }