opencode-skills-antigravity 0.0.7 → 0.0.8

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.
@@ -0,0 +1,568 @@
1
+ #!/usr/bin/env python3
2
+ """Landing Page Scaffolder — Generate landing pages as HTML or Next.js TSX from config.
3
+
4
+ Creates production-ready landing pages with hero sections, features,
5
+ testimonials, pricing, CTAs, and responsive design.
6
+
7
+ Usage:
8
+ python landing_page_scaffolder.py config.json --format html --output page.html
9
+ python landing_page_scaffolder.py config.json --format tsx --output LandingPage.tsx
10
+ python landing_page_scaffolder.py config.json --format json
11
+ """
12
+
13
+ import argparse
14
+ import json
15
+ import sys
16
+ from typing import Dict, List, Any, Optional
17
+ from datetime import datetime
18
+ import html as html_module
19
+
20
+
21
+ def escape(text: str) -> str:
22
+ """HTML-escape text."""
23
+ return html_module.escape(str(text))
24
+
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Tailwind style mappings for TSX output
28
+ # ---------------------------------------------------------------------------
29
+
30
+ DESIGN_STYLES = {
31
+ "dark-saas": {
32
+ "bg": "bg-gray-950", "text": "text-white",
33
+ "accent": "violet", "card_bg": "bg-gray-900 border border-gray-800",
34
+ "btn": "bg-violet-600 hover:bg-violet-500 text-white",
35
+ "btn_secondary": "border border-gray-700 text-gray-300 hover:bg-gray-800",
36
+ "section_alt": "bg-gray-900/50", "muted": "text-gray-400",
37
+ "border": "border-gray-800",
38
+ },
39
+ "clean-minimal": {
40
+ "bg": "bg-white", "text": "text-gray-900",
41
+ "accent": "blue", "card_bg": "bg-gray-50 border border-gray-200 rounded-2xl",
42
+ "btn": "bg-blue-600 hover:bg-blue-700 text-white",
43
+ "btn_secondary": "border border-gray-300 text-gray-700 hover:bg-gray-50",
44
+ "section_alt": "bg-gray-50", "muted": "text-gray-500",
45
+ "border": "border-gray-200",
46
+ },
47
+ "bold-startup": {
48
+ "bg": "bg-white", "text": "text-gray-900",
49
+ "accent": "orange", "card_bg": "shadow-xl rounded-3xl bg-white",
50
+ "btn": "bg-orange-500 hover:bg-orange-600 text-white",
51
+ "btn_secondary": "border-2 border-orange-500 text-orange-600 hover:bg-orange-50",
52
+ "section_alt": "bg-orange-50/30", "muted": "text-gray-500",
53
+ "border": "border-gray-200",
54
+ },
55
+ "enterprise": {
56
+ "bg": "bg-slate-50", "text": "text-slate-900",
57
+ "accent": "slate", "card_bg": "bg-white border border-slate-200 shadow-sm",
58
+ "btn": "bg-slate-900 hover:bg-slate-800 text-white",
59
+ "btn_secondary": "border border-slate-300 text-slate-700 hover:bg-slate-100",
60
+ "section_alt": "bg-white", "muted": "text-slate-500",
61
+ "border": "border-slate-200",
62
+ },
63
+ }
64
+
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # TSX generators
68
+ # ---------------------------------------------------------------------------
69
+
70
+ def tsx_nav(config: Dict[str, Any], style: Dict[str, str]) -> str:
71
+ brand = config.get("brand", "Brand")
72
+ nav_links = config.get("nav_links", [])
73
+ cta = config.get("nav_cta", {"text": "Get Started", "url": "#"})
74
+ links_jsx = "\n ".join(
75
+ f'<a href="{l.get("url", "#")}" className="{style["muted"]} hover:{style["text"]} font-medium transition-colors">{l.get("text", "")}</a>'
76
+ for l in nav_links
77
+ )
78
+ return f'''function Navbar() {{
79
+ return (
80
+ <nav className="sticky top-0 z-50 {style["bg"]} border-b {style["border"]} backdrop-blur-sm">
81
+ <div className="mx-auto flex max-w-7xl items-center justify-between px-6 py-4">
82
+ <a href="#" className="text-xl font-bold {style["text"]}">{brand}</a>
83
+ <div className="hidden items-center gap-8 md:flex">
84
+ {links_jsx}
85
+ <a href="{cta.get("url", "#")}" className="rounded-lg {style["btn"]} px-5 py-2.5 text-sm font-semibold transition-colors">
86
+ {cta.get("text", "Get Started")}
87
+ </a>
88
+ </div>
89
+ </div>
90
+ </nav>
91
+ );
92
+ }}'''
93
+
94
+
95
+ def tsx_hero(hero: Dict[str, Any], style: Dict[str, str]) -> str:
96
+ h1 = hero.get("headline", "Your Headline Here")
97
+ sub = hero.get("subheadline", "")
98
+ primary_cta = hero.get("primary_cta", {"text": "Get Started", "url": "#"})
99
+ secondary_cta = hero.get("secondary_cta", None)
100
+ secondary_jsx = ""
101
+ if secondary_cta:
102
+ secondary_jsx = f'''
103
+ <a href="{secondary_cta.get("url", "#")}" className="rounded-lg {style["btn_secondary"]} px-8 py-3 text-lg font-semibold transition-colors">
104
+ {secondary_cta.get("text", "Learn More")}
105
+ </a>'''
106
+ return f'''function Hero() {{
107
+ return (
108
+ <section className="flex min-h-[80vh] flex-col items-center justify-center px-6 py-24 text-center {style["bg"]}">
109
+ <div className="mx-auto max-w-4xl">
110
+ <h1 className="mb-6 text-5xl font-bold tracking-tight {style["text"]} md:text-7xl">
111
+ {h1}
112
+ </h1>
113
+ <p className="mx-auto mb-10 max-w-2xl text-xl {style["muted"]}">
114
+ {sub}
115
+ </p>
116
+ <div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
117
+ <a href="{primary_cta.get("url", "#")}" className="rounded-lg {style["btn"]} px-8 py-3 text-lg font-semibold transition-colors">
118
+ {primary_cta.get("text", "Get Started")}
119
+ </a>{secondary_jsx}
120
+ </div>
121
+ </div>
122
+ </section>
123
+ );
124
+ }}'''
125
+
126
+
127
+ def tsx_features(features: Dict[str, Any], style: Dict[str, str]) -> str:
128
+ title = features.get("title", "Features")
129
+ subtitle = features.get("subtitle", "")
130
+ items = features.get("items", [])
131
+ cards_jsx = "\n ".join(
132
+ f'''<div className="{style["card_bg"]} rounded-xl p-8">
133
+ <div className="mb-4 text-3xl">{f.get("icon", "")}</div>
134
+ <h3 className="mb-3 text-xl font-semibold {style["text"]}">{f.get("title", "")}</h3>
135
+ <p className="{style["muted"]}">{f.get("description", "")}</p>
136
+ </div>'''
137
+ for f in items
138
+ )
139
+ return f'''function Features() {{
140
+ return (
141
+ <section className="{style["section_alt"]} px-6 py-24">
142
+ <div className="mx-auto max-w-7xl">
143
+ <h2 className="mb-4 text-center text-4xl font-bold {style["text"]}">{title}</h2>
144
+ <p className="mx-auto mb-16 max-w-2xl text-center text-lg {style["muted"]}">{subtitle}</p>
145
+ <div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
146
+ {cards_jsx}
147
+ </div>
148
+ </div>
149
+ </section>
150
+ );
151
+ }}'''
152
+
153
+
154
+ def tsx_testimonials(testimonials: Dict[str, Any], style: Dict[str, str]) -> str:
155
+ title = testimonials.get("title", "What Our Customers Say")
156
+ items = testimonials.get("items", [])
157
+ if not items:
158
+ return ""
159
+ cards_jsx = "\n ".join(
160
+ f'''<div className="rounded-xl border {style["border"]} p-8">
161
+ <p className="mb-6 text-lg italic {style["muted"]}">"{t.get("quote", "")}"</p>
162
+ <div>
163
+ <p className="font-semibold {style["text"]}">{t.get("name", "")}</p>
164
+ <p className="text-sm {style["muted"]}">{t.get("title", "")}, {t.get("company", "")}</p>
165
+ </div>
166
+ </div>'''
167
+ for t in items
168
+ )
169
+ return f'''function Testimonials() {{
170
+ return (
171
+ <section className="px-6 py-24 {style["bg"]}">
172
+ <div className="mx-auto max-w-7xl">
173
+ <h2 className="mb-16 text-center text-4xl font-bold {style["text"]}">{title}</h2>
174
+ <div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
175
+ {cards_jsx}
176
+ </div>
177
+ </div>
178
+ </section>
179
+ );
180
+ }}'''
181
+
182
+
183
+ def tsx_pricing(pricing: Dict[str, Any], style: Dict[str, str]) -> str:
184
+ title = pricing.get("title", "Pricing")
185
+ plans = pricing.get("plans", [])
186
+ if not plans:
187
+ return ""
188
+ accent = style["accent"]
189
+ cards = []
190
+ for p in plans:
191
+ featured = p.get("featured", False)
192
+ border_cls = f"border-2 border-{accent}-500 ring-4 ring-{accent}-500/20" if featured else f"border {style['border']}"
193
+ badge = f'\n <div className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-{accent}-600 px-4 py-1 text-xs font-semibold text-white">Most Popular</div>' if featured else ""
194
+ features_jsx = "\n ".join(
195
+ f'<li className="flex items-center gap-2 py-2"><span className="text-{accent}-500 font-bold">&#10003;</span> {feat}</li>'
196
+ for feat in p.get("features", [])
197
+ )
198
+ cards.append(f'''<div className="relative rounded-2xl {border_cls} {style["card_bg"]} p-8 text-center">{badge}
199
+ <h3 className="mb-2 text-xl font-semibold {style["text"]}">{p.get("name", "")}</h3>
200
+ <div className="my-6 text-5xl font-extrabold {style["text"]}">${p.get("price", "0")}<span className="text-base font-normal {style["muted"]}">/mo</span></div>
201
+ <p className="{style["muted"]} mb-6">{p.get("description", "")}</p>
202
+ <ul className="mb-8 space-y-1 text-left {style["muted"]}">
203
+ {features_jsx}
204
+ </ul>
205
+ <a href="{p.get("cta_url", "#")}" className="block w-full rounded-lg {style["btn"]} py-3 text-center font-semibold transition-colors">
206
+ {p.get("cta_text", "Choose Plan")}
207
+ </a>
208
+ </div>''')
209
+ cards_jsx = "\n ".join(cards)
210
+ return f'''function Pricing() {{
211
+ return (
212
+ <section className="{style["section_alt"]} px-6 py-24">
213
+ <div className="mx-auto max-w-5xl">
214
+ <h2 className="mb-16 text-center text-4xl font-bold {style["text"]}">{title}</h2>
215
+ <div className="grid gap-8 lg:grid-cols-{min(len(plans), 3)}">
216
+ {cards_jsx}
217
+ </div>
218
+ </div>
219
+ </section>
220
+ );
221
+ }}'''
222
+
223
+
224
+ def tsx_cta(cta: Dict[str, Any], style: Dict[str, str]) -> str:
225
+ accent = style["accent"]
226
+ return f'''function CTASection() {{
227
+ return (
228
+ <section className="bg-{accent}-600 px-6 py-24 text-center text-white">
229
+ <div className="mx-auto max-w-3xl">
230
+ <h2 className="mb-4 text-4xl font-bold">{cta.get("headline", "Ready to get started?")}</h2>
231
+ <p className="mb-10 text-xl opacity-90">{cta.get("subheadline", "")}</p>
232
+ <a href="{cta.get("url", "#")}" className="rounded-lg bg-white px-8 py-3 text-lg font-semibold text-{accent}-600 transition-colors hover:bg-gray-100">
233
+ {cta.get("text", "Start Free Trial")}
234
+ </a>
235
+ </div>
236
+ </section>
237
+ );
238
+ }}'''
239
+
240
+
241
+ def tsx_footer(config: Dict[str, Any], style: Dict[str, str]) -> str:
242
+ brand = config.get("brand", "Company")
243
+ year = datetime.now().year
244
+ footer_text = config.get("footer_text", f"{year} {brand}. All rights reserved.")
245
+ return f'''function Footer() {{
246
+ return (
247
+ <footer className="border-t {style["border"]} {style["bg"]} px-6 py-10 text-center {style["muted"]}">
248
+ <p>&copy; {footer_text}</p>
249
+ </footer>
250
+ );
251
+ }}'''
252
+
253
+
254
+ def generate_tsx(config: Dict[str, Any]) -> str:
255
+ """Generate complete Next.js/React TSX landing page with Tailwind CSS."""
256
+ style_name = config.get("design_style", "clean-minimal")
257
+ style = DESIGN_STYLES.get(style_name, DESIGN_STYLES["clean-minimal"])
258
+
259
+ components = []
260
+ component_names = []
261
+
262
+ components.append(tsx_nav(config, style))
263
+ component_names.append("Navbar")
264
+
265
+ if config.get("hero"):
266
+ components.append(tsx_hero(config["hero"], style))
267
+ component_names.append("Hero")
268
+
269
+ if config.get("features"):
270
+ components.append(tsx_features(config["features"], style))
271
+ component_names.append("Features")
272
+
273
+ if config.get("testimonials") and config["testimonials"].get("items"):
274
+ components.append(tsx_testimonials(config["testimonials"], style))
275
+ component_names.append("Testimonials")
276
+
277
+ if config.get("pricing") and config["pricing"].get("plans"):
278
+ components.append(tsx_pricing(config["pricing"], style))
279
+ component_names.append("Pricing")
280
+
281
+ if config.get("cta"):
282
+ components.append(tsx_cta(config["cta"], style))
283
+ component_names.append("CTASection")
284
+
285
+ components.append(tsx_footer(config, style))
286
+ component_names.append("Footer")
287
+
288
+ title = config.get("title", "Landing Page")
289
+ meta_desc = config.get("meta_description", "")
290
+
291
+ page_body = "\n ".join(f"<{name} />" for name in component_names)
292
+ all_components = "\n\n".join(components)
293
+
294
+ return f'''// Generated by Landing Page Scaffolder — {datetime.now().strftime("%Y-%m-%d")}
295
+ // Stack: Next.js 14+ App Router, React, Tailwind CSS
296
+ // Design style: {style_name}
297
+
298
+ import type {{ Metadata }} from "next";
299
+
300
+ export const metadata: Metadata = {{
301
+ title: "{title}",
302
+ description: "{meta_desc}",
303
+ openGraph: {{
304
+ title: "{title}",
305
+ description: "{meta_desc}",
306
+ type: "website",
307
+ }},
308
+ }};
309
+
310
+ {all_components}
311
+
312
+ export default function LandingPage() {{
313
+ return (
314
+ <main>
315
+ {page_body}
316
+ </main>
317
+ );
318
+ }}
319
+ '''
320
+
321
+
322
+ # ---------------------------------------------------------------------------
323
+ # HTML generators (existing)
324
+ # ---------------------------------------------------------------------------
325
+
326
+ def generate_css(config: Dict[str, Any]) -> str:
327
+ """Generate responsive CSS from config theme."""
328
+ theme = config.get("theme", {})
329
+ primary = theme.get("primary_color", "#2563eb")
330
+ secondary = theme.get("secondary_color", "#1e40af")
331
+ bg = theme.get("background", "#ffffff")
332
+ text_color = theme.get("text_color", "#1f2937")
333
+ font = theme.get("font", "Inter, system-ui, -apple-system, sans-serif")
334
+
335
+ return f"""
336
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
337
+ body {{ font-family: {font}; color: {text_color}; background: {bg}; line-height: 1.6; }}
338
+ .container {{ max-width: 1200px; margin: 0 auto; padding: 0 24px; }}
339
+ nav {{ padding: 16px 0; border-bottom: 1px solid #e5e7eb; position: sticky; top: 0; background: {bg}; z-index: 100; }}
340
+ nav .container {{ display: flex; justify-content: space-between; align-items: center; }}
341
+ .nav-logo {{ font-size: 1.5rem; font-weight: 700; color: {primary}; text-decoration: none; }}
342
+ .nav-links {{ display: flex; gap: 24px; list-style: none; }}
343
+ .nav-links a {{ text-decoration: none; color: {text_color}; font-weight: 500; }}
344
+ .nav-cta {{ background: {primary}; color: white; padding: 8px 20px; border-radius: 6px; text-decoration: none; font-weight: 600; }}
345
+ .hero {{ padding: 80px 0; text-align: center; }}
346
+ .hero h1 {{ font-size: 3.5rem; font-weight: 800; line-height: 1.1; margin-bottom: 24px; max-width: 800px; margin-left: auto; margin-right: auto; }}
347
+ .hero p {{ font-size: 1.25rem; color: #6b7280; max-width: 600px; margin: 0 auto 32px; }}
348
+ .hero-cta {{ display: inline-flex; gap: 16px; }}
349
+ .btn-primary {{ background: {primary}; color: white; padding: 14px 32px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 1.1rem; }}
350
+ .btn-secondary {{ background: transparent; color: {primary}; padding: 14px 32px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 1.1rem; border: 2px solid {primary}; }}
351
+ .features {{ padding: 80px 0; background: #f9fafb; }}
352
+ .section-title {{ text-align: center; font-size: 2.5rem; font-weight: 700; margin-bottom: 16px; }}
353
+ .section-subtitle {{ text-align: center; color: #6b7280; font-size: 1.1rem; margin-bottom: 48px; max-width: 600px; margin-left: auto; margin-right: auto; }}
354
+ .features-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px; }}
355
+ .feature-card {{ background: white; padding: 32px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }}
356
+ .feature-icon {{ font-size: 2rem; margin-bottom: 16px; }}
357
+ .feature-card h3 {{ font-size: 1.25rem; margin-bottom: 12px; }}
358
+ .feature-card p {{ color: #6b7280; }}
359
+ .testimonials {{ padding: 80px 0; }}
360
+ .testimonials-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 24px; }}
361
+ .testimonial-card {{ padding: 32px; border: 1px solid #e5e7eb; border-radius: 12px; }}
362
+ .testimonial-text {{ font-size: 1.1rem; font-style: italic; margin-bottom: 20px; }}
363
+ .testimonial-author {{ display: flex; align-items: center; gap: 12px; }}
364
+ .author-info strong {{ display: block; }}
365
+ .author-info span {{ color: #6b7280; font-size: 0.9rem; }}
366
+ .pricing {{ padding: 80px 0; background: #f9fafb; }}
367
+ .pricing-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 24px; max-width: 900px; margin: 0 auto; }}
368
+ .pricing-card {{ background: white; padding: 32px; border-radius: 12px; border: 2px solid #e5e7eb; text-align: center; }}
369
+ .pricing-card.featured {{ border-color: {primary}; position: relative; }}
370
+ .pricing-card.featured::before {{ content: "Most Popular"; position: absolute; top: -12px; left: 50%; transform: translateX(-50%); background: {primary}; color: white; padding: 4px 16px; border-radius: 20px; font-size: 0.8rem; font-weight: 600; }}
371
+ .pricing-name {{ font-size: 1.25rem; font-weight: 600; margin-bottom: 8px; }}
372
+ .pricing-price {{ font-size: 3rem; font-weight: 800; margin: 16px 0; }}
373
+ .pricing-price span {{ font-size: 1rem; font-weight: 400; color: #6b7280; }}
374
+ .pricing-features {{ list-style: none; text-align: left; margin: 24px 0; }}
375
+ .pricing-features li {{ padding: 8px 0; border-bottom: 1px solid #f3f4f6; }}
376
+ .pricing-features li::before {{ content: "\\2713 "; color: {primary}; font-weight: 700; }}
377
+ .cta-section {{ padding: 80px 0; text-align: center; background: {primary}; color: white; }}
378
+ .cta-section h2 {{ font-size: 2.5rem; margin-bottom: 16px; }}
379
+ .cta-section p {{ font-size: 1.1rem; opacity: 0.9; margin-bottom: 32px; }}
380
+ .btn-white {{ background: white; color: {primary}; padding: 14px 32px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 1.1rem; }}
381
+ footer {{ padding: 40px 0; border-top: 1px solid #e5e7eb; color: #6b7280; text-align: center; }}
382
+ @media (max-width: 768px) {{
383
+ .hero h1 {{ font-size: 2.25rem; }}
384
+ .hero-cta {{ flex-direction: column; align-items: center; }}
385
+ .nav-links {{ display: none; }}
386
+ .features-grid {{ grid-template-columns: 1fr; }}
387
+ .pricing-grid {{ grid-template-columns: 1fr; }}
388
+ }}
389
+ """
390
+
391
+
392
+ def render_nav(config: Dict[str, Any]) -> str:
393
+ brand = escape(config.get("brand", "Brand"))
394
+ nav_links = config.get("nav_links", [])
395
+ cta = config.get("nav_cta", {"text": "Get Started", "url": "#"})
396
+ links = "\n".join(
397
+ f'<li><a href="{escape(l.get("url", "#"))}">{escape(l.get("text", ""))}</a></li>'
398
+ for l in nav_links
399
+ )
400
+ return f"""
401
+ <nav><div class="container">
402
+ <a href="#" class="nav-logo">{brand}</a>
403
+ <ul class="nav-links">{links}</ul>
404
+ <a href="{escape(cta.get('url', '#'))}" class="nav-cta">{escape(cta.get('text', 'Get Started'))}</a>
405
+ </div></nav>"""
406
+
407
+
408
+ def render_hero(hero: Dict[str, Any]) -> str:
409
+ h1 = escape(hero.get("headline", "Your Headline Here"))
410
+ sub = escape(hero.get("subheadline", ""))
411
+ primary_cta = hero.get("primary_cta", {"text": "Get Started", "url": "#"})
412
+ secondary_cta = hero.get("secondary_cta", None)
413
+ cta_html = f'<a href="{escape(primary_cta.get("url", "#"))}" class="btn-primary">{escape(primary_cta.get("text", "Get Started"))}</a>'
414
+ if secondary_cta:
415
+ cta_html += f'\n<a href="{escape(secondary_cta.get("url", "#"))}" class="btn-secondary">{escape(secondary_cta.get("text", "Learn More"))}</a>'
416
+ return f"""
417
+ <section class="hero"><div class="container">
418
+ <h1>{h1}</h1>
419
+ <p>{sub}</p>
420
+ <div class="hero-cta">{cta_html}</div>
421
+ </div></section>"""
422
+
423
+
424
+ def render_features(features: Dict[str, Any]) -> str:
425
+ title = escape(features.get("title", "Features"))
426
+ subtitle = escape(features.get("subtitle", ""))
427
+ items = features.get("items", [])
428
+ cards = "\n".join(f"""
429
+ <div class="feature-card">
430
+ <div class="feature-icon">{escape(f.get('icon', ''))}</div>
431
+ <h3>{escape(f.get('title', ''))}</h3>
432
+ <p>{escape(f.get('description', ''))}</p>
433
+ </div>""" for f in items)
434
+ return f"""
435
+ <section class="features"><div class="container">
436
+ <h2 class="section-title">{title}</h2>
437
+ <p class="section-subtitle">{subtitle}</p>
438
+ <div class="features-grid">{cards}</div>
439
+ </div></section>"""
440
+
441
+
442
+ def render_testimonials(testimonials: Dict[str, Any]) -> str:
443
+ title = escape(testimonials.get("title", "What Our Customers Say"))
444
+ items = testimonials.get("items", [])
445
+ if not items:
446
+ return ""
447
+ cards = "\n".join(f"""
448
+ <div class="testimonial-card">
449
+ <p class="testimonial-text">"{escape(t.get('quote', ''))}"</p>
450
+ <div class="testimonial-author">
451
+ <div class="author-info">
452
+ <strong>{escape(t.get('name', ''))}</strong>
453
+ <span>{escape(t.get('title', ''))}, {escape(t.get('company', ''))}</span>
454
+ </div>
455
+ </div>
456
+ </div>""" for t in items)
457
+ return f"""
458
+ <section class="testimonials"><div class="container">
459
+ <h2 class="section-title">{title}</h2>
460
+ <div class="testimonials-grid">{cards}</div>
461
+ </div></section>"""
462
+
463
+
464
+ def render_pricing(pricing: Dict[str, Any]) -> str:
465
+ title = escape(pricing.get("title", "Pricing"))
466
+ plans = pricing.get("plans", [])
467
+ if not plans:
468
+ return ""
469
+ cards = "\n".join(f"""
470
+ <div class="pricing-card {'featured' if p.get('featured') else ''}">
471
+ <div class="pricing-name">{escape(p.get('name', ''))}</div>
472
+ <div class="pricing-price">${escape(str(p.get('price', '0')))}<span>/mo</span></div>
473
+ <p>{escape(p.get('description', ''))}</p>
474
+ <ul class="pricing-features">
475
+ {"".join(f'<li>{escape(f)}</li>' for f in p.get('features', []))}
476
+ </ul>
477
+ <a href="{escape(p.get('cta_url', '#'))}" class="btn-primary">{escape(p.get('cta_text', 'Choose Plan'))}</a>
478
+ </div>""" for p in plans)
479
+ return f"""
480
+ <section class="pricing"><div class="container">
481
+ <h2 class="section-title">{title}</h2>
482
+ <div class="pricing-grid">{cards}</div>
483
+ </div></section>"""
484
+
485
+
486
+ def render_cta(cta: Dict[str, Any]) -> str:
487
+ return f"""
488
+ <section class="cta-section"><div class="container">
489
+ <h2>{escape(cta.get('headline', 'Ready to get started?'))}</h2>
490
+ <p>{escape(cta.get('subheadline', ''))}</p>
491
+ <a href="{escape(cta.get('url', '#'))}" class="btn-white">{escape(cta.get('text', 'Start Free Trial'))}</a>
492
+ </div></section>"""
493
+
494
+
495
+ def generate_html(config: Dict[str, Any]) -> str:
496
+ """Generate complete HTML landing page."""
497
+ title = escape(config.get("title", "Landing Page"))
498
+ css = generate_css(config)
499
+ sections = []
500
+ sections.append(render_nav(config))
501
+ if config.get("hero"):
502
+ sections.append(render_hero(config["hero"]))
503
+ if config.get("features"):
504
+ sections.append(render_features(config["features"]))
505
+ if config.get("testimonials"):
506
+ sections.append(render_testimonials(config["testimonials"]))
507
+ if config.get("pricing"):
508
+ sections.append(render_pricing(config["pricing"]))
509
+ if config.get("cta"):
510
+ sections.append(render_cta(config["cta"]))
511
+ sections.append(f"""
512
+ <footer><div class="container">
513
+ <p>{escape(config.get('footer_text', f'{datetime.now().year} {config.get("brand", "Company")}. All rights reserved.'))}</p>
514
+ </div></footer>""")
515
+ return f"""<!DOCTYPE html>
516
+ <html lang="en">
517
+ <head>
518
+ <meta charset="UTF-8">
519
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
520
+ <title>{title}</title>
521
+ <meta name="description" content="{escape(config.get('meta_description', ''))}">
522
+ <style>{css}</style>
523
+ </head>
524
+ <body>
525
+ {"".join(sections)}
526
+ </body>
527
+ </html>"""
528
+
529
+
530
+ def main():
531
+ parser = argparse.ArgumentParser(
532
+ description="Generate landing pages as HTML or Next.js TSX with Tailwind CSS"
533
+ )
534
+ parser.add_argument("input", help="Path to page config JSON")
535
+ parser.add_argument(
536
+ "--format", choices=["html", "tsx", "json"], default="tsx",
537
+ help="Output format: tsx (Next.js + Tailwind), html (standalone), json (metadata)"
538
+ )
539
+ parser.add_argument("--output", type=str, default=None, help="Output file path")
540
+
541
+ args = parser.parse_args()
542
+
543
+ with open(args.input) as f:
544
+ config = json.load(f)
545
+
546
+ if args.format == "json":
547
+ output = json.dumps({
548
+ "generated_at": datetime.now().isoformat(),
549
+ "config": config,
550
+ "formats_available": ["html", "tsx"],
551
+ "sections": [k for k in ["nav", "hero", "features", "testimonials", "pricing", "cta", "footer"]
552
+ if config.get(k) or k in ("nav", "footer")]
553
+ }, indent=2)
554
+ elif args.format == "tsx":
555
+ output = generate_tsx(config)
556
+ else:
557
+ output = generate_html(config)
558
+
559
+ if args.output:
560
+ with open(args.output, "w") as f:
561
+ f.write(output)
562
+ print(f"Landing page written to {args.output}")
563
+ else:
564
+ print(output)
565
+
566
+
567
+ if __name__ == "__main__":
568
+ main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-skills-antigravity",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "OpenCode CLI plugin that automatically downloads and keeps Antigravity Awesome Skills up to date.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",