@easybits.cloud/html-tailwind-generator 0.1.6 → 0.2.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 (48) hide show
  1. package/{src/images/enrichImages.ts → dist/chunk-5HSVOF2J.js} +65 -53
  2. package/dist/chunk-5HSVOF2J.js.map +1 -0
  3. package/{src/iframeScript.ts → dist/chunk-5TYGSZAF.js} +259 -10
  4. package/dist/chunk-5TYGSZAF.js.map +1 -0
  5. package/{src/refine.ts → dist/chunk-GMJR2GXL.js} +30 -60
  6. package/dist/chunk-GMJR2GXL.js.map +1 -0
  7. package/dist/chunk-LQ65H4AO.js +41 -0
  8. package/dist/chunk-LQ65H4AO.js.map +1 -0
  9. package/{src/generate.ts → dist/chunk-PK26CWDO.js} +67 -108
  10. package/dist/chunk-PK26CWDO.js.map +1 -0
  11. package/dist/chunk-RTGCZUNJ.js +1 -0
  12. package/dist/chunk-RTGCZUNJ.js.map +1 -0
  13. package/dist/chunk-XM3D3TTJ.js +852 -0
  14. package/dist/chunk-XM3D3TTJ.js.map +1 -0
  15. package/dist/components.d.ts +57 -0
  16. package/dist/components.js +14 -0
  17. package/dist/components.js.map +1 -0
  18. package/dist/deploy.d.ts +39 -0
  19. package/dist/deploy.js +10 -0
  20. package/dist/deploy.js.map +1 -0
  21. package/dist/generate.d.ts +41 -0
  22. package/dist/generate.js +14 -0
  23. package/dist/generate.js.map +1 -0
  24. package/dist/images.d.ts +30 -0
  25. package/dist/images.js +14 -0
  26. package/dist/images.js.map +1 -0
  27. package/dist/index.d.ts +30 -0
  28. package/dist/index.js +64 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/refine.d.ts +32 -0
  31. package/dist/refine.js +10 -0
  32. package/dist/refine.js.map +1 -0
  33. package/dist/themes-DOoj19c8.d.ts +35 -0
  34. package/dist/types-Flpl4wDs.d.ts +31 -0
  35. package/package.json +53 -50
  36. package/src/buildHtml.ts +0 -78
  37. package/src/components/Canvas.tsx +0 -162
  38. package/src/components/CodeEditor.tsx +0 -239
  39. package/src/components/FloatingToolbar.tsx +0 -350
  40. package/src/components/SectionList.tsx +0 -217
  41. package/src/components/index.ts +0 -4
  42. package/src/deploy.ts +0 -73
  43. package/src/images/dalleImages.ts +0 -29
  44. package/src/images/index.ts +0 -3
  45. package/src/images/pexels.ts +0 -27
  46. package/src/index.ts +0 -58
  47. package/src/themes.ts +0 -204
  48. package/src/types.ts +0 -30
@@ -1,350 +0,0 @@
1
- import React, { useState, useRef, useEffect } from "react";
2
- import { HiSparkles } from "react-icons/hi2";
3
- import type { IframeMessage } from "../types";
4
-
5
- const STYLE_PRESETS = [
6
- { label: "Minimal", icon: "○", instruction: "Redisena esta seccion con estetica minimal: mucho espacio en blanco, tipografia limpia, sin bordes ni sombras innecesarias. Manten el mismo contenido." },
7
- { label: "Cards", icon: "▦", instruction: "Redisena esta seccion usando layout de cards en grid: cada item en su propia card con padding, sombra sutil y bordes redondeados. Manten el mismo contenido." },
8
- { label: "Bold", icon: "■", instruction: "Redisena esta seccion con estilo bold/brutalist: tipografia grande y gruesa, colores de alto contraste, bordes solidos, sin gradientes. Manten el mismo contenido." },
9
- { label: "Glass", icon: "◇", instruction: "Redisena esta seccion con glassmorphism: fondos translucidos con backdrop-blur, bordes sutiles blancos, sombras suaves. Usa un fondo oscuro o con gradiente detras. Manten el mismo contenido." },
10
- { label: "Dark", icon: "●", instruction: "Redisena esta seccion con fondo oscuro (#111 o similar), texto claro, acentos de color vibrantes. Manten el mismo contenido." },
11
- ];
12
-
13
- interface FloatingToolbarProps {
14
- selection: IframeMessage | null;
15
- iframeRect: DOMRect | null;
16
- onRefine: (instruction: string, referenceImage?: string) => void;
17
- onMoveUp: () => void;
18
- onMoveDown: () => void;
19
- onDelete: () => void;
20
- onClose: () => void;
21
- onViewCode: () => void;
22
- onUpdateAttribute?: (sectionId: string, elementPath: string, attr: string, value: string) => void;
23
- isRefining: boolean;
24
- }
25
-
26
- export function FloatingToolbar({
27
- selection,
28
- iframeRect,
29
- onRefine,
30
- onMoveUp,
31
- onMoveDown,
32
- onDelete,
33
- onClose,
34
- onViewCode,
35
- onUpdateAttribute,
36
- isRefining,
37
- }: FloatingToolbarProps) {
38
- const [prompt, setPrompt] = useState("");
39
- const [showCode, setShowCode] = useState(false);
40
- const [refImage, setRefImage] = useState<string | null>(null);
41
- const [refImageName, setRefImageName] = useState<string | null>(null);
42
- const inputRef = useRef<HTMLInputElement>(null);
43
- const fileInputRef = useRef<HTMLInputElement>(null);
44
- const toolbarRef = useRef<HTMLDivElement>(null);
45
-
46
- // Local attr editing state
47
- const [imgSrc, setImgSrc] = useState("");
48
- const [imgAlt, setImgAlt] = useState("");
49
- const [linkHref, setLinkHref] = useState("");
50
-
51
- useEffect(() => {
52
- setPrompt("");
53
- setShowCode(false);
54
- setRefImage(null);
55
- setRefImageName(null);
56
- }, [selection?.sectionId]);
57
-
58
- // Sync attr inputs when selection changes
59
- useEffect(() => {
60
- if (selection?.attrs) {
61
- setImgSrc(selection.attrs.src || "");
62
- setImgAlt(selection.attrs.alt || "");
63
- setLinkHref(selection.attrs.href || "");
64
- }
65
- }, [selection?.attrs, selection?.elementPath]);
66
-
67
- // ESC closes toolbar
68
- useEffect(() => {
69
- function handleKey(e: KeyboardEvent) {
70
- if (e.key === "Escape") onClose();
71
- }
72
- document.addEventListener("keydown", handleKey);
73
- return () => document.removeEventListener("keydown", handleKey);
74
- }, [onClose]);
75
-
76
- if (!selection || !selection.rect || !iframeRect) return null;
77
-
78
- const toolbarWidth = toolbarRef.current?.offsetWidth || 480;
79
- const toolbarHeight = toolbarRef.current?.offsetHeight || 60;
80
- const top = iframeRect.top + selection.rect.top + selection.rect.height + 8;
81
- const left = iframeRect.left + selection.rect.left;
82
- const clampedLeft = Math.max(8, Math.min(left, window.innerWidth - toolbarWidth - 8));
83
- const showAbove = top + toolbarHeight + 8 > window.innerHeight;
84
- const finalTop = Math.max(8, showAbove
85
- ? iframeRect.top + selection.rect.top - toolbarHeight - 8
86
- : top);
87
-
88
- function handleSubmit(e: React.FormEvent) {
89
- e.preventDefault();
90
- if (!prompt.trim() || isRefining) return;
91
- onRefine(prompt.trim(), refImage || undefined);
92
- setPrompt("");
93
- setRefImage(null);
94
- setRefImageName(null);
95
- }
96
-
97
- function handleFileSelect(e: React.ChangeEvent<HTMLInputElement>) {
98
- const file = e.target.files?.[0];
99
- if (!file) return;
100
- setRefImageName(file.name);
101
- const reader = new FileReader();
102
- reader.onload = () => {
103
- setRefImage(reader.result as string);
104
- };
105
- reader.readAsDataURL(file);
106
- e.target.value = "";
107
- }
108
-
109
- function handleSetAttr(attr: string, value: string) {
110
- if (!selection?.sectionId || !selection?.elementPath || !onUpdateAttribute) return;
111
- onUpdateAttribute(selection.sectionId, selection.elementPath, attr, value);
112
- }
113
-
114
- const isImg = selection.tagName === "IMG";
115
- const isLink = selection.tagName === "A";
116
- const hasAttrEditing = (isImg || isLink) && onUpdateAttribute;
117
-
118
- return (
119
- <div
120
- ref={toolbarRef}
121
- className="fixed z-50 flex flex-col gap-1.5 bg-gray-900 text-white rounded-xl shadow-2xl px-2 py-1.5 border border-gray-700"
122
- style={{ top: finalTop, left: clampedLeft, maxWidth: "min(480px, calc(100vw - 16px))" }}
123
- >
124
- {/* Main row */}
125
- <div className="flex items-center gap-1.5">
126
- {/* Tag badge */}
127
- {selection.tagName && (
128
- <span className="px-2 py-0.5 rounded-md bg-blue-600 text-[10px] font-mono font-bold uppercase tracking-wider shrink-0">
129
- {selection.tagName.toLowerCase()}
130
- </span>
131
- )}
132
-
133
- {/* AI prompt input */}
134
- <form onSubmit={handleSubmit} className="flex items-center gap-1 flex-1">
135
- <input
136
- ref={inputRef}
137
- type="text"
138
- value={prompt}
139
- onChange={(e) => setPrompt(e.target.value)}
140
- placeholder={refImage ? "Instruccion + imagen..." : "Editar con AI..."}
141
- disabled={isRefining}
142
- className="bg-transparent text-sm text-white placeholder:text-gray-500 outline-none w-40 md:w-56 px-2 py-1"
143
- />
144
- {/* Image attach button */}
145
- <button
146
- type="button"
147
- onClick={() => fileInputRef.current?.click()}
148
- disabled={isRefining}
149
- className={`w-7 h-7 flex items-center justify-center rounded-lg transition-colors shrink-0 ${
150
- refImage
151
- ? "bg-blue-600 text-white"
152
- : "hover:bg-gray-800 text-gray-400 hover:text-white"
153
- }`}
154
- title={refImage ? `Imagen: ${refImageName}` : "Adjuntar imagen de referencia"}
155
- >
156
- <svg className="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor">
157
- <path fillRule="evenodd" d="M1 5.25A2.25 2.25 0 013.25 3h13.5A2.25 2.25 0 0119 5.25v9.5A2.25 2.25 0 0116.75 17H3.25A2.25 2.25 0 011 14.75v-9.5zm1.5 5.81V14.75c0 .414.336.75.75.75h13.5a.75.75 0 00.75-.75v-2.06l-2.22-2.22a.75.75 0 00-1.06 0L8.56 16.1l-3.28-3.28a.75.75 0 00-1.06 0l-1.72 1.72zm12-4.06a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0z" clipRule="evenodd"/>
158
- </svg>
159
- </button>
160
- <input
161
- ref={fileInputRef}
162
- type="file"
163
- accept="image/*"
164
- onChange={handleFileSelect}
165
- className="hidden"
166
- />
167
- <button
168
- type="submit"
169
- disabled={!prompt.trim() || isRefining}
170
- className="w-7 h-7 flex items-center justify-center rounded-lg bg-blue-500 hover:bg-blue-600 disabled:opacity-30 transition-colors"
171
- >
172
- {isRefining ? (
173
- <span className="w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
174
- ) : (
175
- <HiSparkles className="w-3.5 h-3.5" />
176
- )}
177
- </button>
178
- </form>
179
-
180
- {/* Variante button */}
181
- <div className="w-px h-5 bg-gray-700" />
182
- <button
183
- onClick={() => {
184
- const tag = selection.tagName?.toLowerCase();
185
- const text = selection.text?.substring(0, 80);
186
- const prompt = selection.isSectionRoot
187
- ? "Genera una variante completamente diferente de esta seccion. Manten el mismo contenido/informacion pero cambia radicalmente el layout, la estructura visual, y el estilo. Sorprendeme con un diseno creativo e inesperado."
188
- : `Modifica SOLO el elemento <${tag}> que contiene "${text}". Genera una variante visual diferente de ESE elemento (diferente estilo, layout, tipografia). NO modifiques ningun otro elemento de la seccion.`;
189
- onRefine(prompt, refImage || undefined);
190
- }}
191
- disabled={isRefining}
192
- className="px-2.5 py-1 text-[11px] font-bold rounded-lg bg-blue-600 hover:bg-blue-500 disabled:opacity-30 transition-colors whitespace-nowrap shrink-0"
193
- title="Generar variante"
194
- >
195
- ✦ Variante
196
- </button>
197
-
198
- {/* Section-level actions (move/delete) */}
199
- {selection.isSectionRoot && (
200
- <>
201
- <div className="w-px h-5 bg-gray-700" />
202
- <button
203
- onClick={onMoveUp}
204
- className="w-7 h-7 flex items-center justify-center rounded-lg hover:bg-gray-800 transition-colors text-xs"
205
- title="Mover arriba"
206
- >
207
-
208
- </button>
209
- <button
210
- onClick={onMoveDown}
211
- className="w-7 h-7 flex items-center justify-center rounded-lg hover:bg-gray-800 transition-colors text-xs"
212
- title="Mover abajo"
213
- >
214
-
215
- </button>
216
- </>
217
- )}
218
-
219
- {/* View code */}
220
- <button
221
- onClick={onViewCode}
222
- className="w-7 h-7 flex items-center justify-center rounded-lg hover:bg-gray-800 transition-colors text-xs font-mono text-gray-400 hover:text-white"
223
- title="Ver codigo"
224
- >
225
- &lt;/&gt;
226
- </button>
227
-
228
- {selection.isSectionRoot && (
229
- <>
230
- <div className="w-px h-5 bg-gray-700" />
231
- <button
232
- onClick={onDelete}
233
- className="w-7 h-7 flex items-center justify-center rounded-lg hover:bg-red-900/50 text-red-400 transition-colors"
234
- title="Eliminar seccion"
235
- >
236
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
237
- <polyline points="3 6 5 6 21 6" />
238
- <path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2" />
239
- </svg>
240
- </button>
241
- </>
242
- )}
243
-
244
- {/* Close button */}
245
- <div className="w-px h-5 bg-gray-700" />
246
- <button
247
- onClick={onClose}
248
- className="w-7 h-7 flex items-center justify-center rounded-lg hover:bg-gray-800 text-gray-400 hover:text-white transition-colors"
249
- title="Cerrar (ESC)"
250
- >
251
-
252
- </button>
253
- </div>
254
-
255
- {/* Reference image preview */}
256
- {refImage && (
257
- <div className="flex items-center gap-2 pt-0.5 pb-0.5 border-t border-gray-700/50">
258
- <img src={refImage} alt="Referencia" className="w-10 h-10 rounded object-cover border border-gray-600" />
259
- <span className="text-[10px] text-gray-400 truncate flex-1">{refImageName}</span>
260
- <button
261
- onClick={() => { setRefImage(null); setRefImageName(null); }}
262
- className="text-[10px] text-gray-500 hover:text-white px-1"
263
- >
264
-
265
- </button>
266
- </div>
267
- )}
268
-
269
- {/* Style presets row — only for section roots */}
270
- {selection.isSectionRoot && (
271
- <div className="flex items-center gap-1 pt-0.5 pb-0.5 border-t border-gray-700/50">
272
- <span className="text-[10px] text-gray-500 uppercase tracking-wider mr-1 shrink-0">Estilo</span>
273
- {STYLE_PRESETS.map((preset) => (
274
- <button
275
- key={preset.label}
276
- onClick={() => onRefine(preset.instruction)}
277
- disabled={isRefining}
278
- className="px-2 py-0.5 text-[11px] font-medium rounded-md bg-gray-800 hover:bg-gray-700 disabled:opacity-30 transition-colors whitespace-nowrap"
279
- title={preset.label}
280
- >
281
- <span className="mr-1">{preset.icon}</span>
282
- {preset.label}
283
- </button>
284
- ))}
285
- </div>
286
- )}
287
-
288
- {/* Image attr editing */}
289
- {isImg && hasAttrEditing && (
290
- <div className="flex flex-col gap-1 pt-0.5 pb-0.5 border-t border-gray-700/50">
291
- <div className="flex items-center gap-1">
292
- <span className="text-[10px] text-gray-500 uppercase tracking-wider w-8 shrink-0">src</span>
293
- <input
294
- type="text"
295
- value={imgSrc}
296
- onChange={(e) => setImgSrc(e.target.value)}
297
- onKeyDown={(e) => { if (e.key === "Enter") handleSetAttr("src", imgSrc); }}
298
- className="flex-1 bg-gray-800 text-xs text-white rounded px-2 py-1 outline-none min-w-0"
299
- placeholder="URL de imagen..."
300
- />
301
- <button
302
- onClick={() => handleSetAttr("src", imgSrc)}
303
- className="px-2 py-1 text-[10px] font-bold rounded bg-blue-500 hover:bg-blue-600 transition-colors shrink-0"
304
- >
305
- Set
306
- </button>
307
- </div>
308
- <div className="flex items-center gap-1">
309
- <span className="text-[10px] text-gray-500 uppercase tracking-wider w-8 shrink-0">alt</span>
310
- <input
311
- type="text"
312
- value={imgAlt}
313
- onChange={(e) => setImgAlt(e.target.value)}
314
- onKeyDown={(e) => { if (e.key === "Enter") handleSetAttr("alt", imgAlt); }}
315
- className="flex-1 bg-gray-800 text-xs text-white rounded px-2 py-1 outline-none min-w-0"
316
- placeholder="Alt text..."
317
- />
318
- <button
319
- onClick={() => handleSetAttr("alt", imgAlt)}
320
- className="px-2 py-1 text-[10px] font-bold rounded bg-blue-500 hover:bg-blue-600 transition-colors shrink-0"
321
- >
322
- Set
323
- </button>
324
- </div>
325
- </div>
326
- )}
327
-
328
- {/* Link attr editing */}
329
- {isLink && hasAttrEditing && (
330
- <div className="flex items-center gap-1 pt-0.5 pb-0.5 border-t border-gray-700/50">
331
- <span className="text-[10px] text-gray-500 uppercase tracking-wider w-8 shrink-0">href</span>
332
- <input
333
- type="text"
334
- value={linkHref}
335
- onChange={(e) => setLinkHref(e.target.value)}
336
- onKeyDown={(e) => { if (e.key === "Enter") handleSetAttr("href", linkHref); }}
337
- className="flex-1 bg-gray-800 text-xs text-white rounded px-2 py-1 outline-none min-w-0"
338
- placeholder="URL del enlace..."
339
- />
340
- <button
341
- onClick={() => handleSetAttr("href", linkHref)}
342
- className="px-2 py-1 text-[10px] font-bold rounded bg-blue-500 hover:bg-blue-600 transition-colors shrink-0"
343
- >
344
- Set
345
- </button>
346
- </div>
347
- )}
348
- </div>
349
- );
350
- }
@@ -1,217 +0,0 @@
1
- import React, { useRef, useState } from "react";
2
- import type { Section3 } from "../types";
3
- import { LANDING_THEMES, type CustomColors } from "../themes";
4
-
5
- interface SectionListProps {
6
- sections: Section3[];
7
- selectedSectionId: string | null;
8
- theme: string;
9
- customColors?: CustomColors;
10
- onThemeChange: (themeId: string) => void;
11
- onCustomColorChange?: (colors: Partial<CustomColors>) => void;
12
- onSelect: (id: string) => void;
13
- onOpenCode: (id: string) => void;
14
- onReorder: (fromIndex: number, toIndex: number) => void;
15
- onDelete: (id: string) => void;
16
- onRename: (id: string, label: string) => void;
17
- onAdd: () => void;
18
- }
19
-
20
- export function SectionList({
21
- sections,
22
- selectedSectionId,
23
- theme,
24
- customColors,
25
- onThemeChange,
26
- onCustomColorChange,
27
- onSelect,
28
- onOpenCode,
29
- onReorder,
30
- onDelete,
31
- onRename,
32
- onAdd,
33
- }: SectionListProps) {
34
- const sorted = [...sections].sort((a, b) => a.order - b.order);
35
- const colorInputRef = useRef<HTMLInputElement>(null);
36
- const [editingId, setEditingId] = useState<string | null>(null);
37
- const [editingLabel, setEditingLabel] = useState("");
38
-
39
- return (
40
- <div className="w-56 shrink-0 flex flex-col bg-white border-r-2 border-gray-200 overflow-y-auto">
41
- <div className="p-3 border-b border-gray-200">
42
- <h3 className="text-xs font-black uppercase tracking-wider text-gray-500 mb-2">
43
- Tema
44
- </h3>
45
- <div className="flex gap-1.5 flex-wrap">
46
- {LANDING_THEMES.map((t) => (
47
- <button
48
- key={t.id}
49
- onClick={() => onThemeChange(t.id)}
50
- title={t.label}
51
- className={`w-6 h-6 rounded-full border-2 transition-all ${
52
- theme === t.id
53
- ? "border-black scale-110 shadow-sm"
54
- : "border-gray-300 hover:border-gray-400"
55
- }`}
56
- style={{ backgroundColor: t.colors.primary }}
57
- />
58
- ))}
59
- {/* Custom color picker */}
60
- <button
61
- onClick={() => colorInputRef.current?.click()}
62
- title="Color personalizado"
63
- className={`w-6 h-6 rounded-full border-2 transition-all relative overflow-hidden ${
64
- theme === "custom"
65
- ? "border-black scale-110 shadow-sm"
66
- : "border-gray-300 hover:border-gray-400"
67
- }`}
68
- style={theme === "custom" && customColors?.primary ? { backgroundColor: customColors.primary } : undefined}
69
- >
70
- {theme !== "custom" && (
71
- <span className="absolute inset-0 rounded-full"
72
- style={{ background: "conic-gradient(#ef4444, #eab308, #22c55e, #3b82f6, #a855f7, #ef4444)" }}
73
- />
74
- )}
75
- </button>
76
- <input
77
- ref={colorInputRef}
78
- type="color"
79
- value={customColors?.primary || "#6366f1"}
80
- onChange={(e) => onCustomColorChange?.({ primary: e.target.value })}
81
- className="sr-only"
82
- />
83
- </div>
84
- {/* Multi-color pickers when custom theme is active */}
85
- {theme === "custom" && (
86
- <div className="flex items-center gap-2 mt-2">
87
- {([
88
- { key: "primary" as const, label: "Pri", fallback: "#6366f1" },
89
- { key: "secondary" as const, label: "Sec", fallback: "#f59e0b" },
90
- { key: "accent" as const, label: "Acc", fallback: "#06b6d4" },
91
- { key: "surface" as const, label: "Sur", fallback: "#ffffff" },
92
- ]).map((c) => (
93
- <label key={c.key} className="flex flex-col items-center gap-0.5 cursor-pointer">
94
- <input
95
- type="color"
96
- value={customColors?.[c.key] || c.fallback}
97
- onChange={(e) => onCustomColorChange?.({ [c.key]: e.target.value })}
98
- className="w-5 h-5 rounded border border-gray-300 cursor-pointer p-0 [&::-webkit-color-swatch-wrapper]:p-0 [&::-webkit-color-swatch]:border-none [&::-webkit-color-swatch]:rounded"
99
- />
100
- <span className="text-[9px] font-bold text-gray-400 uppercase">{c.label}</span>
101
- </label>
102
- ))}
103
- </div>
104
- )}
105
- </div>
106
- <div className="p-3 border-b border-gray-200">
107
- <h3 className="text-xs font-black uppercase tracking-wider text-gray-500">
108
- Secciones
109
- </h3>
110
- </div>
111
-
112
- <div className="flex-1 overflow-y-auto py-1">
113
- {sorted.map((section, i) => (
114
- <div
115
- key={section.id}
116
- onClick={() => onSelect(section.id)}
117
- className={`group flex items-center gap-2 px-3 py-2 cursor-pointer transition-colors ${
118
- selectedSectionId === section.id
119
- ? "bg-blue-50 border-l-2 border-blue-500"
120
- : "hover:bg-gray-50 border-l-2 border-transparent"
121
- }`}
122
- >
123
- <span className="text-[10px] font-mono text-gray-400 w-4 text-right">
124
- {i + 1}
125
- </span>
126
- {editingId === section.id ? (
127
- <input
128
- type="text"
129
- value={editingLabel}
130
- onChange={(e) => setEditingLabel(e.target.value)}
131
- onBlur={() => {
132
- if (editingLabel.trim()) onRename(section.id, editingLabel.trim());
133
- setEditingId(null);
134
- }}
135
- onKeyDown={(e) => {
136
- if (e.key === "Enter") {
137
- if (editingLabel.trim()) onRename(section.id, editingLabel.trim());
138
- setEditingId(null);
139
- } else if (e.key === "Escape") {
140
- setEditingId(null);
141
- }
142
- }}
143
- className="text-sm font-bold flex-1 min-w-0 bg-transparent border-b border-blue-500 outline-none px-0 py-0"
144
- autoFocus
145
- onClick={(e) => e.stopPropagation()}
146
- />
147
- ) : (
148
- <span
149
- className="text-sm font-bold truncate flex-1"
150
- onDoubleClick={(e) => {
151
- e.stopPropagation();
152
- setEditingId(section.id);
153
- setEditingLabel(section.label);
154
- }}
155
- >
156
- {section.label}
157
- </span>
158
- )}
159
- <div className="opacity-0 group-hover:opacity-100 flex gap-0.5">
160
- <button
161
- onClick={(e) => {
162
- e.stopPropagation();
163
- onOpenCode(section.id);
164
- }}
165
- className="w-5 h-5 flex items-center justify-center rounded text-gray-400 hover:text-gray-700 hover:bg-gray-200"
166
- title="Editar HTML"
167
- >
168
- <svg className="w-3 h-3" viewBox="0 0 16 16" fill="currentColor"><path d="M5.854 4.854a.5.5 0 1 0-.708-.708l-3.5 3.5a.5.5 0 0 0 0 .708l3.5 3.5a.5.5 0 0 0 .708-.708L2.707 8l3.147-3.146zm4.292 0a.5.5 0 0 1 .708-.708l3.5 3.5a.5.5 0 0 1 0 .708l-3.5 3.5a.5.5 0 0 1-.708-.708L13.293 8l-3.147-3.146z"/></svg>
169
- </button>
170
- {i > 0 && (
171
- <button
172
- onClick={(e) => {
173
- e.stopPropagation();
174
- onReorder(i, i - 1);
175
- }}
176
- className="w-5 h-5 flex items-center justify-center rounded text-gray-400 hover:text-gray-700 hover:bg-gray-200 text-[10px]"
177
- >
178
-
179
- </button>
180
- )}
181
- {i < sorted.length - 1 && (
182
- <button
183
- onClick={(e) => {
184
- e.stopPropagation();
185
- onReorder(i, i + 1);
186
- }}
187
- className="w-5 h-5 flex items-center justify-center rounded text-gray-400 hover:text-gray-700 hover:bg-gray-200 text-[10px]"
188
- >
189
-
190
- </button>
191
- )}
192
- <button
193
- onClick={(e) => {
194
- e.stopPropagation();
195
- onDelete(section.id);
196
- }}
197
- className="w-5 h-5 flex items-center justify-center rounded text-gray-400 hover:text-red-600 hover:bg-red-50 text-[10px]"
198
- title="Eliminar seccion"
199
- >
200
-
201
- </button>
202
- </div>
203
- </div>
204
- ))}
205
- </div>
206
-
207
- <div className="p-3 border-t border-gray-200">
208
- <button
209
- onClick={onAdd}
210
- className="w-full text-center py-2 text-sm font-bold text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
211
- >
212
- + Agregar seccion
213
- </button>
214
- </div>
215
- </div>
216
- );
217
- }
@@ -1,4 +0,0 @@
1
- export { Canvas, type CanvasHandle } from "./Canvas";
2
- export { SectionList } from "./SectionList";
3
- export { FloatingToolbar } from "./FloatingToolbar";
4
- export { CodeEditor } from "./CodeEditor";
package/src/deploy.ts DELETED
@@ -1,73 +0,0 @@
1
- import { buildDeployHtml } from "./buildHtml";
2
- import type { Section3 } from "./types";
3
- import type { CustomColors } from "./themes";
4
-
5
- export interface DeployToS3Options {
6
- /** The sections to deploy */
7
- sections: Section3[];
8
- /** Theme ID */
9
- theme?: string;
10
- /** Custom colors (when theme is "custom") */
11
- customColors?: CustomColors;
12
- /** S3-compatible upload function. Receives the HTML string, returns the URL */
13
- upload: (html: string) => Promise<string>;
14
- }
15
-
16
- /**
17
- * Deploy a landing page to any S3-compatible storage.
18
- * The consumer provides their own upload function.
19
- */
20
- export async function deployToS3(options: DeployToS3Options): Promise<string> {
21
- const { sections, theme, customColors, upload } = options;
22
- const html = buildDeployHtml(sections, theme, customColors);
23
- return upload(html);
24
- }
25
-
26
- export interface DeployToEasyBitsOptions {
27
- /** EasyBits API key */
28
- apiKey: string;
29
- /** Website slug (e.g. "my-landing" → my-landing.easybits.cloud) */
30
- slug: string;
31
- /** The sections to deploy */
32
- sections: Section3[];
33
- /** Theme ID */
34
- theme?: string;
35
- /** Custom colors (when theme is "custom") */
36
- customColors?: CustomColors;
37
- /** EasyBits API base URL (default: https://easybits.cloud) */
38
- baseUrl?: string;
39
- }
40
-
41
- /**
42
- * Deploy a landing page to EasyBits hosting (slug.easybits.cloud).
43
- * Uses the EasyBits API to create/update a website.
44
- */
45
- export async function deployToEasyBits(options: DeployToEasyBitsOptions): Promise<string> {
46
- const {
47
- apiKey,
48
- slug,
49
- sections,
50
- theme,
51
- customColors,
52
- baseUrl = "https://easybits.cloud",
53
- } = options;
54
-
55
- const html = buildDeployHtml(sections, theme, customColors);
56
-
57
- const res = await fetch(`${baseUrl}/api/v2/websites`, {
58
- method: "POST",
59
- headers: {
60
- "Content-Type": "application/json",
61
- Authorization: `Bearer ${apiKey}`,
62
- },
63
- body: JSON.stringify({ slug, html }),
64
- });
65
-
66
- if (!res.ok) {
67
- const error = await res.json().catch(() => ({ error: "Deploy failed" }));
68
- throw new Error(error.error || "Deploy failed");
69
- }
70
-
71
- const data = await res.json();
72
- return data.url || `https://${slug}.easybits.cloud`;
73
- }
@@ -1,29 +0,0 @@
1
- /**
2
- * Generate an image using DALL-E 3 API.
3
- */
4
- export async function generateImage(
5
- query: string,
6
- openaiApiKey: string
7
- ): Promise<string> {
8
- const res = await fetch("https://api.openai.com/v1/images/generations", {
9
- method: "POST",
10
- headers: {
11
- Authorization: `Bearer ${openaiApiKey}`,
12
- "Content-Type": "application/json",
13
- },
14
- body: JSON.stringify({
15
- model: "dall-e-3",
16
- prompt: query,
17
- n: 1,
18
- size: "1792x1024",
19
- }),
20
- });
21
-
22
- if (!res.ok) {
23
- const err = await res.text().catch(() => "Unknown error");
24
- throw new Error(`DALL-E API error ${res.status}: ${err}`);
25
- }
26
-
27
- const data = await res.json();
28
- return data.data[0].url;
29
- }
@@ -1,3 +0,0 @@
1
- export { searchImage, type PexelsResult } from "./pexels";
2
- export { enrichImages, findImageSlots } from "./enrichImages";
3
- export { generateImage } from "./dalleImages";