popeye-cli 1.5.0 → 1.7.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 (195) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +184 -31
  3. package/dist/cli/commands/create.d.ts.map +1 -1
  4. package/dist/cli/commands/create.js +54 -4
  5. package/dist/cli/commands/create.js.map +1 -1
  6. package/dist/cli/interactive.d.ts +29 -0
  7. package/dist/cli/interactive.d.ts.map +1 -1
  8. package/dist/cli/interactive.js +90 -7
  9. package/dist/cli/interactive.js.map +1 -1
  10. package/dist/generators/all.d.ts +4 -1
  11. package/dist/generators/all.d.ts.map +1 -1
  12. package/dist/generators/all.js +36 -316
  13. package/dist/generators/all.js.map +1 -1
  14. package/dist/generators/doc-parser.d.ts +18 -3
  15. package/dist/generators/doc-parser.d.ts.map +1 -1
  16. package/dist/generators/doc-parser.js +81 -10
  17. package/dist/generators/doc-parser.js.map +1 -1
  18. package/dist/generators/frontend-design-analyzer.d.ts +30 -0
  19. package/dist/generators/frontend-design-analyzer.d.ts.map +1 -0
  20. package/dist/generators/frontend-design-analyzer.js +208 -0
  21. package/dist/generators/frontend-design-analyzer.js.map +1 -0
  22. package/dist/generators/shared-packages.d.ts +45 -0
  23. package/dist/generators/shared-packages.d.ts.map +1 -0
  24. package/dist/generators/shared-packages.js +456 -0
  25. package/dist/generators/shared-packages.js.map +1 -0
  26. package/dist/generators/templates/index.d.ts +4 -0
  27. package/dist/generators/templates/index.d.ts.map +1 -1
  28. package/dist/generators/templates/index.js +4 -0
  29. package/dist/generators/templates/index.js.map +1 -1
  30. package/dist/generators/templates/website-components.d.ts.map +1 -1
  31. package/dist/generators/templates/website-components.js +36 -11
  32. package/dist/generators/templates/website-components.js.map +1 -1
  33. package/dist/generators/templates/website-config.d.ts +15 -1
  34. package/dist/generators/templates/website-config.d.ts.map +1 -1
  35. package/dist/generators/templates/website-config.js +155 -13
  36. package/dist/generators/templates/website-config.js.map +1 -1
  37. package/dist/generators/templates/website-landing.d.ts +24 -0
  38. package/dist/generators/templates/website-landing.d.ts.map +1 -0
  39. package/dist/generators/templates/website-landing.js +276 -0
  40. package/dist/generators/templates/website-landing.js.map +1 -0
  41. package/dist/generators/templates/website-layout.d.ts +42 -0
  42. package/dist/generators/templates/website-layout.d.ts.map +1 -0
  43. package/dist/generators/templates/website-layout.js +408 -0
  44. package/dist/generators/templates/website-layout.js.map +1 -0
  45. package/dist/generators/templates/website-pricing.d.ts +11 -0
  46. package/dist/generators/templates/website-pricing.d.ts.map +1 -0
  47. package/dist/generators/templates/website-pricing.js +313 -0
  48. package/dist/generators/templates/website-pricing.js.map +1 -0
  49. package/dist/generators/templates/website-sections.d.ts +102 -0
  50. package/dist/generators/templates/website-sections.d.ts.map +1 -0
  51. package/dist/generators/templates/website-sections.js +444 -0
  52. package/dist/generators/templates/website-sections.js.map +1 -0
  53. package/dist/generators/templates/website.d.ts +10 -50
  54. package/dist/generators/templates/website.d.ts.map +1 -1
  55. package/dist/generators/templates/website.js +12 -788
  56. package/dist/generators/templates/website.js.map +1 -1
  57. package/dist/generators/website-content-scanner.d.ts +37 -0
  58. package/dist/generators/website-content-scanner.d.ts.map +1 -0
  59. package/dist/generators/website-content-scanner.js +165 -0
  60. package/dist/generators/website-content-scanner.js.map +1 -0
  61. package/dist/generators/website-context.d.ts +38 -2
  62. package/dist/generators/website-context.d.ts.map +1 -1
  63. package/dist/generators/website-context.js +179 -19
  64. package/dist/generators/website-context.js.map +1 -1
  65. package/dist/generators/website-debug.d.ts +68 -0
  66. package/dist/generators/website-debug.d.ts.map +1 -0
  67. package/dist/generators/website-debug.js +93 -0
  68. package/dist/generators/website-debug.js.map +1 -0
  69. package/dist/generators/website.d.ts +2 -0
  70. package/dist/generators/website.d.ts.map +1 -1
  71. package/dist/generators/website.js +66 -4
  72. package/dist/generators/website.js.map +1 -1
  73. package/dist/generators/workspace-root.d.ts +27 -0
  74. package/dist/generators/workspace-root.d.ts.map +1 -0
  75. package/dist/generators/workspace-root.js +100 -0
  76. package/dist/generators/workspace-root.js.map +1 -0
  77. package/dist/state/index.d.ts +8 -0
  78. package/dist/state/index.d.ts.map +1 -1
  79. package/dist/state/index.js +11 -0
  80. package/dist/state/index.js.map +1 -1
  81. package/dist/types/consensus.d.ts +3 -0
  82. package/dist/types/consensus.d.ts.map +1 -1
  83. package/dist/types/consensus.js +1 -0
  84. package/dist/types/consensus.js.map +1 -1
  85. package/dist/types/index.d.ts +1 -0
  86. package/dist/types/index.d.ts.map +1 -1
  87. package/dist/types/index.js +2 -0
  88. package/dist/types/index.js.map +1 -1
  89. package/dist/types/tester.d.ts +138 -0
  90. package/dist/types/tester.d.ts.map +1 -0
  91. package/dist/types/tester.js +110 -0
  92. package/dist/types/tester.js.map +1 -0
  93. package/dist/types/workflow.d.ts +151 -0
  94. package/dist/types/workflow.d.ts.map +1 -1
  95. package/dist/types/workflow.js +14 -0
  96. package/dist/types/workflow.js.map +1 -1
  97. package/dist/upgrade/handlers.d.ts +15 -0
  98. package/dist/upgrade/handlers.d.ts.map +1 -1
  99. package/dist/upgrade/handlers.js +52 -0
  100. package/dist/upgrade/handlers.js.map +1 -1
  101. package/dist/workflow/auto-fix-bundler.d.ts +37 -0
  102. package/dist/workflow/auto-fix-bundler.d.ts.map +1 -0
  103. package/dist/workflow/auto-fix-bundler.js +320 -0
  104. package/dist/workflow/auto-fix-bundler.js.map +1 -0
  105. package/dist/workflow/auto-fix.d.ts.map +1 -1
  106. package/dist/workflow/auto-fix.js +10 -3
  107. package/dist/workflow/auto-fix.js.map +1 -1
  108. package/dist/workflow/execution-mode.js +2 -2
  109. package/dist/workflow/execution-mode.js.map +1 -1
  110. package/dist/workflow/index.d.ts +2 -0
  111. package/dist/workflow/index.d.ts.map +1 -1
  112. package/dist/workflow/index.js +13 -0
  113. package/dist/workflow/index.js.map +1 -1
  114. package/dist/workflow/overview.d.ts.map +1 -1
  115. package/dist/workflow/overview.js +4 -0
  116. package/dist/workflow/overview.js.map +1 -1
  117. package/dist/workflow/plan-mode.d.ts +4 -3
  118. package/dist/workflow/plan-mode.d.ts.map +1 -1
  119. package/dist/workflow/plan-mode.js +69 -5
  120. package/dist/workflow/plan-mode.js.map +1 -1
  121. package/dist/workflow/task-workflow.d.ts +5 -0
  122. package/dist/workflow/task-workflow.d.ts.map +1 -1
  123. package/dist/workflow/task-workflow.js +172 -6
  124. package/dist/workflow/task-workflow.js.map +1 -1
  125. package/dist/workflow/tester.d.ts +120 -0
  126. package/dist/workflow/tester.d.ts.map +1 -0
  127. package/dist/workflow/tester.js +589 -0
  128. package/dist/workflow/tester.js.map +1 -0
  129. package/dist/workflow/website-strategy.d.ts +9 -0
  130. package/dist/workflow/website-strategy.d.ts.map +1 -1
  131. package/dist/workflow/website-strategy.js +73 -1
  132. package/dist/workflow/website-strategy.js.map +1 -1
  133. package/dist/workflow/website-updater.d.ts.map +1 -1
  134. package/dist/workflow/website-updater.js +15 -4
  135. package/dist/workflow/website-updater.js.map +1 -1
  136. package/dist/workflow/workflow-logger.d.ts +1 -1
  137. package/dist/workflow/workflow-logger.d.ts.map +1 -1
  138. package/dist/workflow/workflow-logger.js.map +1 -1
  139. package/package.json +1 -1
  140. package/src/cli/commands/create.ts +58 -4
  141. package/src/cli/interactive.ts +96 -7
  142. package/src/generators/all.ts +44 -332
  143. package/src/generators/doc-parser.ts +87 -10
  144. package/src/generators/frontend-design-analyzer.ts +261 -0
  145. package/src/generators/shared-packages.ts +500 -0
  146. package/src/generators/templates/index.ts +4 -0
  147. package/src/generators/templates/website-components.ts +36 -11
  148. package/src/generators/templates/website-config.ts +166 -13
  149. package/src/generators/templates/website-landing.ts +331 -0
  150. package/src/generators/templates/website-layout.ts +443 -0
  151. package/src/generators/templates/website-pricing.ts +330 -0
  152. package/src/generators/templates/website-sections.ts +541 -0
  153. package/src/generators/templates/website.ts +38 -851
  154. package/src/generators/website-content-scanner.ts +208 -0
  155. package/src/generators/website-context.ts +248 -20
  156. package/src/generators/website-debug.ts +130 -0
  157. package/src/generators/website.ts +71 -3
  158. package/src/generators/workspace-root.ts +113 -0
  159. package/src/state/index.ts +15 -0
  160. package/src/types/consensus.ts +3 -0
  161. package/src/types/index.ts +21 -0
  162. package/src/types/tester.ts +136 -0
  163. package/src/types/workflow.ts +32 -0
  164. package/src/upgrade/handlers.ts +65 -0
  165. package/src/workflow/auto-fix-bundler.ts +392 -0
  166. package/src/workflow/auto-fix.ts +11 -3
  167. package/src/workflow/execution-mode.ts +2 -2
  168. package/src/workflow/index.ts +13 -0
  169. package/src/workflow/overview.ts +6 -0
  170. package/src/workflow/plan-mode.ts +81 -7
  171. package/src/workflow/task-workflow.ts +227 -5
  172. package/src/workflow/tester.ts +723 -0
  173. package/src/workflow/website-strategy.ts +75 -1
  174. package/src/workflow/website-updater.ts +17 -6
  175. package/src/workflow/workflow-logger.ts +2 -0
  176. package/tests/cli/project-naming.test.ts +136 -0
  177. package/tests/generators/doc-parser.test.ts +121 -0
  178. package/tests/generators/frontend-design-analyzer.test.ts +90 -0
  179. package/tests/generators/quality-gate.test.ts +183 -0
  180. package/tests/generators/shared-packages.test.ts +83 -0
  181. package/tests/generators/website-components.test.ts +1 -1
  182. package/tests/generators/website-config.test.ts +84 -0
  183. package/tests/generators/website-content-scanner.test.ts +181 -0
  184. package/tests/generators/website-context.test.ts +109 -0
  185. package/tests/generators/website-debug.test.ts +77 -0
  186. package/tests/generators/website-landing.test.ts +188 -0
  187. package/tests/generators/website-pricing.test.ts +98 -0
  188. package/tests/generators/website-sections.test.ts +245 -0
  189. package/tests/generators/workspace-root.test.ts +105 -0
  190. package/tests/types/tester.test.ts +174 -0
  191. package/tests/upgrade/handlers.test.ts +162 -0
  192. package/tests/workflow/auto-fix-bundler.test.ts +242 -0
  193. package/tests/workflow/plan-mode.test.ts +111 -1
  194. package/tests/workflow/tester.test.ts +401 -0
  195. package/tests/workflow/website-strategy.test.ts +55 -0
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Pricing page generator with monthly/annual toggle, feature comparison,
3
+ * pricing FAQ, and enterprise CTA section
4
+ */
5
+
6
+ import type { WebsiteContentContext } from '../website-context.js';
7
+
8
+ /**
9
+ * Escape a string for safe use inside JSX template literals
10
+ */
11
+ function escapeJsx(str: string): string {
12
+ return str
13
+ .replace(/\\/g, '\\\\')
14
+ .replace(/'/g, "\\'")
15
+ .replace(/`/g, '\\`')
16
+ .replace(/\$/g, '\\$');
17
+ }
18
+
19
+ /**
20
+ * Generate pricing page with optional context-driven tiers, comparison, and FAQ
21
+ * Rendered as client component for monthly/annual toggle
22
+ */
23
+ export function generateWebsitePricingPage(
24
+ _projectName: string,
25
+ context?: WebsiteContentContext
26
+ ): string {
27
+ const strategy = context?.strategy;
28
+ const tiers = context?.pricing && context.pricing.length > 0
29
+ ? context.pricing
30
+ : null;
31
+
32
+ // Build tiers array
33
+ const tiersBlock = tiers
34
+ ? tiers.map((t) => {
35
+ const featuresStr = t.features.map((f) => ` '${escapeJsx(f)}'`).join(',\n');
36
+ return ` {
37
+ name: '${escapeJsx(t.name)}',
38
+ price: '${escapeJsx(t.price)}',
39
+ period: '${t.period ? escapeJsx(t.period) : '/month'}',
40
+ description: '${escapeJsx(t.description)}',
41
+ features: [
42
+ ${featuresStr}
43
+ ],
44
+ cta: '${escapeJsx(t.cta)}',
45
+ featured: ${t.featured ? 'true' : 'false'},
46
+ }`;
47
+ }).join(',\n')
48
+ : ` {
49
+ name: 'Starter',
50
+ price: 'Free',
51
+ period: '',
52
+ description: 'Perfect for getting started',
53
+ features: ['Core features', 'Community support', 'Basic analytics'],
54
+ cta: 'Get started',
55
+ featured: false,
56
+ },
57
+ {
58
+ name: 'Pro',
59
+ price: '$29',
60
+ period: '/month',
61
+ description: 'For growing teams',
62
+ features: ['Everything in Starter', 'Priority support', 'Advanced analytics', 'Custom integrations'],
63
+ cta: 'Start free trial',
64
+ featured: true,
65
+ },
66
+ {
67
+ name: 'Enterprise',
68
+ price: 'Custom',
69
+ period: '',
70
+ description: 'For large organizations',
71
+ features: ['Everything in Pro', 'Dedicated support', 'SLA guarantee', 'Custom contracts'],
72
+ cta: 'Contact sales',
73
+ featured: false,
74
+ }`;
75
+
76
+ // Enterprise CTA from strategy
77
+ const enterpriseCtaText = strategy?.conversionStrategy.primaryCta.text || 'Contact Sales';
78
+
79
+ // Build feature comparison data if tiers have features
80
+ const hasComparison = tiers && tiers.some(t => t.features.length > 0);
81
+ const allFeatureNames = new Set<string>();
82
+ if (tiers) {
83
+ for (const tier of tiers) {
84
+ for (const feature of tier.features) {
85
+ // Normalize feature name (remove "FeatureName: value" to just "FeatureName")
86
+ const name = feature.includes(':') ? feature.split(':')[0].trim() : feature;
87
+ allFeatureNames.add(name);
88
+ }
89
+ }
90
+ }
91
+
92
+ const comparisonFeatures = Array.from(allFeatureNames).slice(0, 10);
93
+ const comparisonBlock = hasComparison ? buildComparisonTable(comparisonFeatures, tiers!) : '';
94
+
95
+ // Pricing FAQ
96
+ const pricingFaq = [
97
+ { q: 'Can I switch plans later?', a: 'Yes, you can upgrade or downgrade your plan at any time. Changes take effect on your next billing cycle.' },
98
+ { q: 'Is there a free trial?', a: 'Yes, all paid plans come with a 14-day free trial. No credit card required.' },
99
+ { q: 'What payment methods do you accept?', a: 'We accept all major credit cards, PayPal, and bank transfers for enterprise plans.' },
100
+ ];
101
+
102
+ const faqStr = pricingFaq
103
+ .map(item => ` { question: '${escapeJsx(item.q)}', answer: '${escapeJsx(item.a)}' }`)
104
+ .join(',\n');
105
+
106
+ return `'use client';
107
+
108
+ import { useState } from 'react';
109
+ import Link from 'next/link';
110
+ import { Check, ChevronDown } from 'lucide-react';
111
+ import Header from '@/components/Header';
112
+ import Footer from '@/components/Footer';
113
+
114
+ const tiers = [
115
+ ${tiersBlock}
116
+ ];
117
+
118
+ const pricingFaq = [
119
+ ${faqStr}
120
+ ];
121
+
122
+ function FaqItem({ question, answer }: { question: string; answer: string }) {
123
+ const [open, setOpen] = useState(false);
124
+
125
+ return (
126
+ <div className="py-4">
127
+ <button
128
+ type="button"
129
+ className="flex w-full items-center justify-between text-left"
130
+ onClick={() => setOpen(!open)}
131
+ aria-expanded={open}
132
+ >
133
+ <span className="text-base font-medium text-foreground">{question}</span>
134
+ <ChevronDown className={\`h-5 w-5 text-muted-foreground transition-transform \${open ? 'rotate-180' : ''}\`} />
135
+ </button>
136
+ {open && (
137
+ <p className="mt-3 text-muted-foreground">{answer}</p>
138
+ )}
139
+ </div>
140
+ );
141
+ }
142
+
143
+ export default function PricingPage() {
144
+ const [annual, setAnnual] = useState(false);
145
+
146
+ return (
147
+ <>
148
+ <Header />
149
+ <main className="py-20 sm:py-32">
150
+ <div className="container">
151
+ {/* Header */}
152
+ <div className="mx-auto max-w-2xl text-center">
153
+ <h1 className="text-4xl font-bold tracking-tight text-foreground sm:text-5xl">
154
+ Simple, transparent pricing
155
+ </h1>
156
+ <p className="mt-6 text-lg text-muted-foreground">
157
+ Choose the plan that works best for you. All plans include a 14-day free trial.
158
+ </p>
159
+
160
+ {/* Monthly/Annual Toggle */}
161
+ <div className="mt-8 flex items-center justify-center gap-3">
162
+ <span className={\`text-sm font-medium \${!annual ? 'text-foreground' : 'text-muted-foreground'}\`}>
163
+ Monthly
164
+ </span>
165
+ <button
166
+ type="button"
167
+ role="switch"
168
+ aria-checked={annual}
169
+ onClick={() => setAnnual(!annual)}
170
+ className={\`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors \${
171
+ annual ? 'bg-primary-600' : 'bg-muted'
172
+ }\`}
173
+ >
174
+ <span className={\`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition-transform \${
175
+ annual ? 'translate-x-5' : 'translate-x-0'
176
+ }\`} />
177
+ </button>
178
+ <span className={\`text-sm font-medium \${annual ? 'text-foreground' : 'text-muted-foreground'}\`}>
179
+ Annual
180
+ <span className="ml-1.5 inline-block rounded-full bg-primary-100 px-2 py-0.5 text-xs font-semibold text-primary-700">
181
+ Save 20%
182
+ </span>
183
+ </span>
184
+ </div>
185
+ </div>
186
+
187
+ {/* Tier Cards */}
188
+ <div className="mx-auto mt-16 grid max-w-lg grid-cols-1 gap-8 lg:max-w-5xl lg:grid-cols-3">
189
+ {tiers.map((tier) => (
190
+ <div
191
+ key={tier.name}
192
+ className={\`relative rounded-2xl p-8 transition-shadow hover:shadow-lg \${
193
+ tier.featured
194
+ ? 'border-2 border-primary-600 bg-card shadow-lg'
195
+ : 'border border-border bg-card'
196
+ }\`}
197
+ >
198
+ {tier.featured && (
199
+ <div className="absolute -top-4 left-1/2 -translate-x-1/2">
200
+ <span className="rounded-full bg-primary-600 px-4 py-1 text-sm font-semibold text-white">
201
+ Most Popular
202
+ </span>
203
+ </div>
204
+ )}
205
+ <h2 className="text-lg font-semibold text-foreground">
206
+ {tier.name}
207
+ </h2>
208
+ <p className="mt-2 text-sm text-muted-foreground">
209
+ {tier.description}
210
+ </p>
211
+ <p className="mt-6">
212
+ <span className="text-4xl font-bold text-foreground">
213
+ {tier.price === 'Custom' || tier.price === 'Free'
214
+ ? tier.price
215
+ : annual
216
+ ? \`$\${Math.round(parseFloat(tier.price.replace('$', '')) * 0.8)}\`
217
+ : tier.price}
218
+ </span>
219
+ {tier.period && tier.price !== 'Custom' && tier.price !== 'Free' && (
220
+ <span className="text-sm text-muted-foreground">
221
+ {annual ? '/month, billed annually' : tier.period}
222
+ </span>
223
+ )}
224
+ </p>
225
+ <ul className="mt-8 space-y-4">
226
+ {tier.features.map((feature) => (
227
+ <li key={feature} className="flex text-sm text-muted-foreground">
228
+ <Check className="h-5 w-5 flex-shrink-0 text-primary-600" />
229
+ <span className="ml-3">{feature}</span>
230
+ </li>
231
+ ))}
232
+ </ul>
233
+ <button
234
+ className={\`mt-8 w-full rounded-lg px-4 py-2.5 text-sm font-semibold transition-colors \${
235
+ tier.featured
236
+ ? 'bg-primary-600 text-white hover:bg-primary-500 shadow-lg shadow-primary-600/25'
237
+ : 'bg-card text-foreground border border-border hover:bg-muted'
238
+ }\`}
239
+ >
240
+ {tier.cta}
241
+ </button>
242
+ </div>
243
+ ))}
244
+ </div>
245
+
246
+ ${comparisonBlock}
247
+
248
+ {/* Pricing FAQ */}
249
+ <div className="mx-auto mt-20 max-w-3xl">
250
+ <h2 className="text-2xl font-bold text-foreground text-center">
251
+ Pricing FAQ
252
+ </h2>
253
+ <div className="mt-8 divide-y divide-border">
254
+ {pricingFaq.map((item, i) => (
255
+ <FaqItem key={i} question={item.question} answer={item.answer} />
256
+ ))}
257
+ </div>
258
+ </div>
259
+
260
+ {/* Enterprise CTA */}
261
+ <div className="mx-auto mt-20 max-w-2xl rounded-2xl bg-muted/50 p-8 text-center sm:p-12">
262
+ <h2 className="text-2xl font-bold text-foreground">
263
+ Need a custom plan?
264
+ </h2>
265
+ <p className="mt-4 text-muted-foreground">
266
+ Contact our sales team for enterprise pricing, custom contracts, and dedicated support.
267
+ </p>
268
+ <Link
269
+ href="/contact"
270
+ className="mt-6 inline-block rounded-lg border border-primary-600 px-6 py-3 text-sm font-semibold text-primary-600 hover:bg-primary-50 transition-colors"
271
+ >
272
+ ${escapeJsx(enterpriseCtaText)}
273
+ </Link>
274
+ </div>
275
+ </div>
276
+ </main>
277
+ <Footer />
278
+ </>
279
+ );
280
+ }
281
+ `;
282
+ }
283
+
284
+ /**
285
+ * Build feature comparison table section
286
+ */
287
+ function buildComparisonTable(
288
+ featureNames: string[],
289
+ tiers: NonNullable<WebsiteContentContext['pricing']>
290
+ ): string {
291
+ if (featureNames.length === 0) return '';
292
+
293
+ return `
294
+ {/* Feature Comparison */}
295
+ <div className="mx-auto mt-20 max-w-5xl">
296
+ <h2 className="text-2xl font-bold text-foreground text-center">
297
+ Compare plans
298
+ </h2>
299
+ <div className="mt-8 overflow-x-auto">
300
+ <table className="w-full border-collapse text-sm">
301
+ <thead>
302
+ <tr className="border-b border-border">
303
+ <th className="py-4 pr-4 text-left font-medium text-muted-foreground">Feature</th>
304
+ ${tiers.map(t => ` <th className="px-4 py-4 text-center font-medium text-foreground">${escapeJsx(t.name)}</th>`).join('\n')}
305
+ </tr>
306
+ </thead>
307
+ <tbody>
308
+ ${featureNames.map(feature => {
309
+ const cells = tiers.map(t => {
310
+ const hasFeature = t.features.some(f => f.startsWith(feature));
311
+ const featureValue = t.features.find(f => f.startsWith(feature));
312
+ const value = featureValue && featureValue.includes(':')
313
+ ? featureValue.split(':')[1].trim()
314
+ : hasFeature ? 'check' : 'no';
315
+ return value === 'check'
316
+ ? ' <td className="px-4 py-3 text-center"><Check className="mx-auto h-5 w-5 text-primary-600" /></td>'
317
+ : value === 'no'
318
+ ? ' <td className="px-4 py-3 text-center text-muted-foreground">&mdash;</td>'
319
+ : ` <td className="px-4 py-3 text-center text-foreground">${escapeJsx(value)}</td>`;
320
+ });
321
+ return ` <tr className="border-b border-border">
322
+ <td className="py-3 pr-4 font-medium text-foreground">${escapeJsx(feature)}</td>
323
+ ${cells.join('\n')}
324
+ </tr>`;
325
+ }).join('\n')}
326
+ </tbody>
327
+ </table>
328
+ </div>
329
+ </div>`;
330
+ }