ebade 0.1.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.
- package/CHANGELOG.md +36 -0
- package/CONTRIBUTING.md +177 -0
- package/LICENSE +21 -0
- package/MANIFESTO.md +170 -0
- package/README.md +263 -0
- package/ROADMAP.md +119 -0
- package/SYNTAX.md +515 -0
- package/benchmarks/RESULTS.md +119 -0
- package/benchmarks/token-benchmark.js +197 -0
- package/cli/scaffold.js +706 -0
- package/docs/GREEN-AI.md +86 -0
- package/examples/ecommerce.ebade.yaml +192 -0
- package/landing/favicon.svg +6 -0
- package/landing/index.html +227 -0
- package/landing/main.js +147 -0
- package/landing/og-image.png +0 -0
- package/landing/style.css +616 -0
- package/package.json +43 -0
- package/packages/mcp-server/README.md +144 -0
- package/packages/mcp-server/package-lock.json +1178 -0
- package/packages/mcp-server/package.json +32 -0
- package/packages/mcp-server/src/index.ts +316 -0
- package/packages/mcp-server/src/tools/compile.ts +269 -0
- package/packages/mcp-server/src/tools/generate.ts +420 -0
- package/packages/mcp-server/src/tools/scaffold.ts +474 -0
- package/packages/mcp-server/src/tools/validate.ts +233 -0
- package/packages/mcp-server/tsconfig.json +16 -0
- package/schema/project.schema.json +195 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate Tool
|
|
3
|
+
*
|
|
4
|
+
* Generates a component from a natural language description.
|
|
5
|
+
* This is the "magic" tool where AI meets ebade.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface GenerateArgs {
|
|
9
|
+
description: string;
|
|
10
|
+
style?: "minimal-modern" | "bold-vibrant" | "dark-premium" | "glassmorphism";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Pattern matching for common component types
|
|
14
|
+
const componentPatterns: Array<{
|
|
15
|
+
keywords: string[];
|
|
16
|
+
type: string;
|
|
17
|
+
template: (desc: string, style: string) => { intent: any; code: string };
|
|
18
|
+
}> = [
|
|
19
|
+
{
|
|
20
|
+
keywords: ["product", "card", "item", "listing"],
|
|
21
|
+
type: "product-card",
|
|
22
|
+
template: (desc, style) => ({
|
|
23
|
+
intent: {
|
|
24
|
+
type: "component",
|
|
25
|
+
name: "product-card",
|
|
26
|
+
displays: ["image", "title", "price", "rating"],
|
|
27
|
+
actions: ["add-to-cart", "add-to-wishlist"],
|
|
28
|
+
style: style,
|
|
29
|
+
},
|
|
30
|
+
code: `"use client";
|
|
31
|
+
|
|
32
|
+
import Image from "next/image";
|
|
33
|
+
import { useState } from "react";
|
|
34
|
+
|
|
35
|
+
interface Product {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
price: number;
|
|
39
|
+
image: string;
|
|
40
|
+
rating?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface ProductCardProps {
|
|
44
|
+
product: Product;
|
|
45
|
+
onAddToCart?: (product: Product) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function ProductCard({ product, onAddToCart }: ProductCardProps) {
|
|
49
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<article
|
|
53
|
+
className="product-card"
|
|
54
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
55
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
56
|
+
>
|
|
57
|
+
<div className="product-image">
|
|
58
|
+
<Image
|
|
59
|
+
src={product.image}
|
|
60
|
+
alt={product.name}
|
|
61
|
+
fill
|
|
62
|
+
className="object-cover"
|
|
63
|
+
/>
|
|
64
|
+
{isHovered && (
|
|
65
|
+
<button
|
|
66
|
+
className="quick-add"
|
|
67
|
+
onClick={() => onAddToCart?.(product)}
|
|
68
|
+
>
|
|
69
|
+
Quick Add
|
|
70
|
+
</button>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
<div className="product-info">
|
|
74
|
+
<h3 className="product-title">{product.name}</h3>
|
|
75
|
+
{product.rating && (
|
|
76
|
+
<div className="product-rating">
|
|
77
|
+
{"★".repeat(Math.floor(product.rating))}
|
|
78
|
+
{"☆".repeat(5 - Math.floor(product.rating))}
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
<p className="product-price">\${product.price.toFixed(2)}</p>
|
|
82
|
+
</div>
|
|
83
|
+
<button
|
|
84
|
+
className="add-to-cart"
|
|
85
|
+
onClick={() => onAddToCart?.(product)}
|
|
86
|
+
>
|
|
87
|
+
Add to Cart
|
|
88
|
+
</button>
|
|
89
|
+
</article>
|
|
90
|
+
);
|
|
91
|
+
}`,
|
|
92
|
+
}),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
keywords: ["hero", "header", "banner", "landing"],
|
|
96
|
+
type: "hero-section",
|
|
97
|
+
template: (desc, style) => ({
|
|
98
|
+
intent: {
|
|
99
|
+
type: "component",
|
|
100
|
+
name: "hero-section",
|
|
101
|
+
displays: ["headline", "subheadline", "cta"],
|
|
102
|
+
style: style,
|
|
103
|
+
},
|
|
104
|
+
code: `"use client";
|
|
105
|
+
|
|
106
|
+
interface HeroSectionProps {
|
|
107
|
+
title?: string;
|
|
108
|
+
subtitle?: string;
|
|
109
|
+
ctaText?: string;
|
|
110
|
+
ctaHref?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function HeroSection({
|
|
114
|
+
title = "Welcome to Our Platform",
|
|
115
|
+
subtitle = "Discover amazing features that will transform your workflow",
|
|
116
|
+
ctaText = "Get Started",
|
|
117
|
+
ctaHref = "/signup",
|
|
118
|
+
}: HeroSectionProps) {
|
|
119
|
+
return (
|
|
120
|
+
<section className="hero-section">
|
|
121
|
+
<div className="hero-content">
|
|
122
|
+
<h1 className="hero-title">{title}</h1>
|
|
123
|
+
<p className="hero-subtitle">{subtitle}</p>
|
|
124
|
+
<div className="hero-actions">
|
|
125
|
+
<a href={ctaHref} className="btn btn-primary btn-lg">
|
|
126
|
+
{ctaText}
|
|
127
|
+
</a>
|
|
128
|
+
<a href="#learn-more" className="btn btn-secondary btn-lg">
|
|
129
|
+
Learn More
|
|
130
|
+
</a>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
<div className="hero-decoration" aria-hidden="true" />
|
|
134
|
+
</section>
|
|
135
|
+
);
|
|
136
|
+
}`,
|
|
137
|
+
}),
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
keywords: ["form", "contact", "input", "submit"],
|
|
141
|
+
type: "contact-form",
|
|
142
|
+
template: (desc, style) => ({
|
|
143
|
+
intent: {
|
|
144
|
+
type: "component",
|
|
145
|
+
name: "contact-form",
|
|
146
|
+
fields: ["name", "email", "message"],
|
|
147
|
+
validation: ["required", "email-format"],
|
|
148
|
+
outcomes: {
|
|
149
|
+
success: "show-toast",
|
|
150
|
+
error: "show-inline",
|
|
151
|
+
},
|
|
152
|
+
style: style,
|
|
153
|
+
},
|
|
154
|
+
code: `"use client";
|
|
155
|
+
|
|
156
|
+
import { useState } from "react";
|
|
157
|
+
|
|
158
|
+
interface FormData {
|
|
159
|
+
name: string;
|
|
160
|
+
email: string;
|
|
161
|
+
message: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function ContactForm() {
|
|
165
|
+
const [formData, setFormData] = useState<FormData>({
|
|
166
|
+
name: "",
|
|
167
|
+
email: "",
|
|
168
|
+
message: "",
|
|
169
|
+
});
|
|
170
|
+
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
|
|
171
|
+
const [errors, setErrors] = useState<Partial<FormData>>({});
|
|
172
|
+
|
|
173
|
+
const validate = (): boolean => {
|
|
174
|
+
const newErrors: Partial<FormData> = {};
|
|
175
|
+
if (!formData.name) newErrors.name = "Name is required";
|
|
176
|
+
if (!formData.email) newErrors.email = "Email is required";
|
|
177
|
+
else if (!/\\S+@\\S+\\.\\S+/.test(formData.email)) newErrors.email = "Invalid email";
|
|
178
|
+
if (!formData.message) newErrors.message = "Message is required";
|
|
179
|
+
setErrors(newErrors);
|
|
180
|
+
return Object.keys(newErrors).length === 0;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
184
|
+
e.preventDefault();
|
|
185
|
+
if (!validate()) return;
|
|
186
|
+
|
|
187
|
+
setStatus("loading");
|
|
188
|
+
try {
|
|
189
|
+
// TODO: API call
|
|
190
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
191
|
+
setStatus("success");
|
|
192
|
+
setFormData({ name: "", email: "", message: "" });
|
|
193
|
+
} catch {
|
|
194
|
+
setStatus("error");
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<form className="contact-form" onSubmit={handleSubmit}>
|
|
200
|
+
<div className="form-group">
|
|
201
|
+
<label htmlFor="name">Name</label>
|
|
202
|
+
<input
|
|
203
|
+
id="name"
|
|
204
|
+
type="text"
|
|
205
|
+
value={formData.name}
|
|
206
|
+
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
207
|
+
className={errors.name ? "error" : ""}
|
|
208
|
+
/>
|
|
209
|
+
{errors.name && <span className="error-message">{errors.name}</span>}
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div className="form-group">
|
|
213
|
+
<label htmlFor="email">Email</label>
|
|
214
|
+
<input
|
|
215
|
+
id="email"
|
|
216
|
+
type="email"
|
|
217
|
+
value={formData.email}
|
|
218
|
+
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
219
|
+
className={errors.email ? "error" : ""}
|
|
220
|
+
/>
|
|
221
|
+
{errors.email && <span className="error-message">{errors.email}</span>}
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div className="form-group">
|
|
225
|
+
<label htmlFor="message">Message</label>
|
|
226
|
+
<textarea
|
|
227
|
+
id="message"
|
|
228
|
+
value={formData.message}
|
|
229
|
+
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
|
|
230
|
+
className={errors.message ? "error" : ""}
|
|
231
|
+
rows={5}
|
|
232
|
+
/>
|
|
233
|
+
{errors.message && <span className="error-message">{errors.message}</span>}
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<button type="submit" disabled={status === "loading"}>
|
|
237
|
+
{status === "loading" ? "Sending..." : "Send Message"}
|
|
238
|
+
</button>
|
|
239
|
+
|
|
240
|
+
{status === "success" && (
|
|
241
|
+
<div className="success-message">Message sent successfully!</div>
|
|
242
|
+
)}
|
|
243
|
+
{status === "error" && (
|
|
244
|
+
<div className="error-message">Failed to send. Please try again.</div>
|
|
245
|
+
)}
|
|
246
|
+
</form>
|
|
247
|
+
);
|
|
248
|
+
}`,
|
|
249
|
+
}),
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
keywords: ["cart", "basket", "shopping"],
|
|
253
|
+
type: "shopping-cart",
|
|
254
|
+
template: (desc, style) => ({
|
|
255
|
+
intent: {
|
|
256
|
+
type: "component",
|
|
257
|
+
name: "shopping-cart",
|
|
258
|
+
displays: ["items", "quantities", "total"],
|
|
259
|
+
actions: ["update-quantity", "remove-item", "checkout"],
|
|
260
|
+
style: style,
|
|
261
|
+
},
|
|
262
|
+
code: `"use client";
|
|
263
|
+
|
|
264
|
+
import { useState } from "react";
|
|
265
|
+
import Image from "next/image";
|
|
266
|
+
|
|
267
|
+
interface CartItem {
|
|
268
|
+
id: string;
|
|
269
|
+
name: string;
|
|
270
|
+
price: number;
|
|
271
|
+
quantity: number;
|
|
272
|
+
image: string;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
interface ShoppingCartProps {
|
|
276
|
+
initialItems?: CartItem[];
|
|
277
|
+
onCheckout?: (items: CartItem[]) => void;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function ShoppingCart({ initialItems = [], onCheckout }: ShoppingCartProps) {
|
|
281
|
+
const [items, setItems] = useState<CartItem[]>(initialItems);
|
|
282
|
+
|
|
283
|
+
const updateQuantity = (id: string, delta: number) => {
|
|
284
|
+
setItems(items.map(item =>
|
|
285
|
+
item.id === id
|
|
286
|
+
? { ...item, quantity: Math.max(0, item.quantity + delta) }
|
|
287
|
+
: item
|
|
288
|
+
).filter(item => item.quantity > 0));
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const removeItem = (id: string) => {
|
|
292
|
+
setItems(items.filter(item => item.id !== id));
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
|
296
|
+
|
|
297
|
+
if (items.length === 0) {
|
|
298
|
+
return (
|
|
299
|
+
<div className="cart-empty">
|
|
300
|
+
<p>Your cart is empty</p>
|
|
301
|
+
<a href="/products" className="btn">Continue Shopping</a>
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<div className="shopping-cart">
|
|
308
|
+
<ul className="cart-items">
|
|
309
|
+
{items.map(item => (
|
|
310
|
+
<li key={item.id} className="cart-item">
|
|
311
|
+
<Image src={item.image} alt={item.name} width={80} height={80} />
|
|
312
|
+
<div className="item-details">
|
|
313
|
+
<h4>{item.name}</h4>
|
|
314
|
+
<p>\${item.price.toFixed(2)}</p>
|
|
315
|
+
</div>
|
|
316
|
+
<div className="quantity-controls">
|
|
317
|
+
<button onClick={() => updateQuantity(item.id, -1)}>-</button>
|
|
318
|
+
<span>{item.quantity}</span>
|
|
319
|
+
<button onClick={() => updateQuantity(item.id, 1)}>+</button>
|
|
320
|
+
</div>
|
|
321
|
+
<button className="remove-btn" onClick={() => removeItem(item.id)}>
|
|
322
|
+
×
|
|
323
|
+
</button>
|
|
324
|
+
</li>
|
|
325
|
+
))}
|
|
326
|
+
</ul>
|
|
327
|
+
<div className="cart-summary">
|
|
328
|
+
<div className="cart-total">
|
|
329
|
+
<span>Total:</span>
|
|
330
|
+
<span>\${total.toFixed(2)}</span>
|
|
331
|
+
</div>
|
|
332
|
+
<button
|
|
333
|
+
className="checkout-btn"
|
|
334
|
+
onClick={() => onCheckout?.(items)}
|
|
335
|
+
>
|
|
336
|
+
Proceed to Checkout
|
|
337
|
+
</button>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
);
|
|
341
|
+
}`,
|
|
342
|
+
}),
|
|
343
|
+
},
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
// Default component template
|
|
347
|
+
const defaultTemplate = (description: string, style: string) => ({
|
|
348
|
+
intent: {
|
|
349
|
+
type: "component",
|
|
350
|
+
name: description.toLowerCase().replace(/\s+/g, "-").substring(0, 30),
|
|
351
|
+
description: description,
|
|
352
|
+
style: style,
|
|
353
|
+
},
|
|
354
|
+
code: `"use client";
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Generated component from description:
|
|
358
|
+
* "${description}"
|
|
359
|
+
*/
|
|
360
|
+
|
|
361
|
+
interface ComponentProps {
|
|
362
|
+
className?: string;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function GeneratedComponent({ className }: ComponentProps) {
|
|
366
|
+
return (
|
|
367
|
+
<div className={\`generated-component \${className || ""}\`}>
|
|
368
|
+
{/*
|
|
369
|
+
TODO: Implement based on description:
|
|
370
|
+
"${description}"
|
|
371
|
+
*/}
|
|
372
|
+
<p>Component placeholder</p>
|
|
373
|
+
</div>
|
|
374
|
+
);
|
|
375
|
+
}`,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
export async function generateComponent(args: GenerateArgs) {
|
|
379
|
+
const { description, style = "minimal-modern" } = args;
|
|
380
|
+
const lowerDesc = description.toLowerCase();
|
|
381
|
+
|
|
382
|
+
// Find matching pattern
|
|
383
|
+
let result;
|
|
384
|
+
for (const pattern of componentPatterns) {
|
|
385
|
+
if (pattern.keywords.some((kw) => lowerDesc.includes(kw))) {
|
|
386
|
+
result = pattern.template(description, style);
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Use default if no pattern matches
|
|
392
|
+
if (!result) {
|
|
393
|
+
result = defaultTemplate(description, style);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
content: [
|
|
398
|
+
{
|
|
399
|
+
type: "text",
|
|
400
|
+
text: `✅ Generated component from description
|
|
401
|
+
|
|
402
|
+
📝 Description: "${description}"
|
|
403
|
+
🎨 Style: ${style}
|
|
404
|
+
|
|
405
|
+
📋 Inferred ebade:
|
|
406
|
+
\`\`\`json
|
|
407
|
+
${JSON.stringify(result.intent, null, 2)}
|
|
408
|
+
\`\`\`
|
|
409
|
+
|
|
410
|
+
💻 Generated Code:
|
|
411
|
+
\`\`\`typescript
|
|
412
|
+
${result.code}
|
|
413
|
+
\`\`\`
|
|
414
|
+
|
|
415
|
+
💡 Tip: You can refine this by providing more specific descriptions or using the ebade_compile tool directly with a custom ebade definition.
|
|
416
|
+
`,
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
};
|
|
420
|
+
}
|