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.
@@ -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
+ }